Index: cpp/CONTRIBUTORS.txt
===================================================================
--- cpp/CONTRIBUTORS.txt	(revision 957)
+++ cpp/CONTRIBUTORS.txt	(revision 958)
@@ -12,4 +12,5 @@
 	Marek Kubiak - initial versions of the similarity measure,
 	Agnieszka Mensfelt - the similarity measure and the SVD,
-	Grzegorz Latosinski - genetic encodings: f4, fH, fB, fL.
+	Grzegorz Latosinski - genetic encodings: f4 update, fH, fB, fL,
+	Jakub Sztyma - the fS genetic encoding.
 
Index: cpp/frams/genetics/defgenoconv.cpp
===================================================================
--- cpp/frams/genetics/defgenoconv.cpp	(revision 957)
+++ cpp/frams/genetics/defgenoconv.cpp	(revision 958)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2015  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2020  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -52,4 +52,7 @@
 #include "fL/fL_conv.h"
 #endif
+#ifdef USE_GENCONV_fS0
+#include "fS/fS_conv.h" //solids
+#endif
 
 void DefaultGenoConvManager::addDefaultConverters()
@@ -100,4 +103,7 @@
 	addConverter(new GenoConv_fL0);
 #endif
+#ifdef USE_GENCONV_fS0
+	addConverter(new GenoConv_fS0); //solids
+#endif
 
 	param.updatetab();
Index: cpp/frams/genetics/fS/fS_conv.cpp
===================================================================
--- cpp/frams/genetics/fS/fS_conv.cpp	(revision 958)
+++ cpp/frams/genetics/fS/fS_conv.cpp	(revision 958)
@@ -0,0 +1,32 @@
+// This file is a part of Framsticks SDK.  http://www.framsticks.com/
+// Copyright (C) 2019-2020  Maciej Komosinski and Szymon Ulatowski.
+// See LICENSE.txt for details.
+
+#include "fS_conv.h"
+
+SString GenoConv_fS0::convert(SString &i, MultiMap *map, bool using_checkpoints)
+{
+	fS_Genotype *genotype;
+	try
+	{
+		genotype = new fS_Genotype(i.c_str());
+	}
+	catch (fS_Exception &e)
+	{
+		logPrintf("GenoConv_fS0", "convert", LOG_ERROR, e.what());
+		return SString();
+	}
+
+	Model model;
+	model.open(using_checkpoints);
+	genotype->buildModel(model);
+	model.close();
+	delete genotype;
+
+	if (map)
+	{
+		model.getCurrentToF0Map(*map);
+	}
+
+	return model.getF0Geno().getGenes();
+}
Index: cpp/frams/genetics/fS/fS_conv.h
===================================================================
--- cpp/frams/genetics/fS/fS_conv.h	(revision 958)
+++ cpp/frams/genetics/fS/fS_conv.h	(revision 958)
@@ -0,0 +1,33 @@
+// This file is a part of Framsticks SDK.  http://www.framsticks.com/
+// Copyright (C) 2019-2020  Maciej Komosinski and Szymon Ulatowski.
+// See LICENSE.txt for details.
+
+#ifndef _FS_CONV_H_
+#define _FS_CONV_H_
+
+#include "fS_general.h"
+#include "frams/util/multimap.h"
+
+/**
+ * Genotype converter from fS to f0.
+ */
+class GenoConv_fS0 : public GenoConverter
+{
+public:
+	GenoConv_fS0() : GenoConverter()
+	{
+		name = "Solid encoding";
+
+		in_format = 'S';
+		out_format = '0';
+		mapsupport = 1;
+	}
+
+	/// Return empty string if can not convert
+	SString convert(SString &i, MultiMap *map, bool using_checkpoints);
+
+	~GenoConv_fS0()
+	{};
+};
+
+#endif
Index: cpp/frams/genetics/fS/fS_general.cpp
===================================================================
--- cpp/frams/genetics/fS/fS_general.cpp	(revision 958)
+++ cpp/frams/genetics/fS/fS_general.cpp	(revision 958)
@@ -0,0 +1,1043 @@
+// This file is a part of Framsticks SDK.  http://www.framsticks.com/
+// Copyright (C) 2019-2020  Maciej Komosinski and Szymon Ulatowski.
+// See LICENSE.txt for details.
+
+#include <float.h>
+#include "fS_general.h"
+#include "frams/model/geometry/geometryutils.h"
+#include "frams/genetics/genooperators.h"
+#include "common/Convert.h"
+#include "frams/util/rndutil.h"
+#include "frams/neuro/neurolibrary.h"
+
+
+int fS_Genotype::precision = 4;
+
+
+double round2(double var)
+{
+	double value = (int) (var * 100 + .5);
+	return (double) value / 100;
+}
+
+double fS_stod(const string&  str, int start, size_t* size)
+{
+	try
+	{
+		return std::stod(str, size);
+	}
+	catch(const std::invalid_argument& ex)
+	{
+		throw fS_Exception("Invalid numeric value", start);
+	}
+}
+
+State::State(State *_state)
+{
+	location = Pt3D(_state->location);
+	v = Pt3D(_state->v);
+	fr = _state->fr;
+	s = _state->s;
+}
+
+State::State(Pt3D _location, Pt3D _v)
+{
+	location = Pt3D(_location);
+	v = Pt3D(_v);
+}
+
+void State::addVector(const double length)
+{
+	location += v * length;
+}
+
+void rotateVector(Pt3D &vector, const Pt3D &rotation)
+{
+	Orient rotmatrix = Orient_1;
+	rotmatrix.rotate(Pt3D(
+			Convert::toRadians(rotation.x),
+			Convert::toRadians(rotation.y),
+			Convert::toRadians(rotation.z)
+	));
+	vector = rotmatrix.transform(vector);
+}
+
+void State::rotate(const Pt3D &rotation)
+{
+       rotateVector(v, rotation);
+       v.normalize();
+}
+
+
+fS_Neuron::fS_Neuron(const char *str, int start, int length)
+{
+	if (length == 0)
+		return;
+
+	vector<SString> inputStrings;
+	strSplit(SString(str, length), NEURON_INTERNAL_SEPARATOR, false, inputStrings);
+	if (inputStrings.empty())
+		return;
+
+	int inputStart = 0;
+	SString details = "N";
+
+	SString tmp = inputStrings[0];
+	if(tmp.indexOf(':') != -1)
+		tmp = tmp.substr(0, tmp.indexOf(':'));
+
+	if (NeuroLibrary::staticlibrary.findClassIndex(tmp, true) != -1)
+	{
+		inputStart = 1;
+		details = inputStrings[0];
+	}
+	setDetails(details);
+
+	for (int i = inputStart; i < int(inputStrings.size()); i++)
+	{
+		SString keyValue = inputStrings[i];
+		int separatorIndex = keyValue.indexOf(NEURON_I_W_SEPARATOR);
+		const char *buffer = keyValue.c_str();
+		size_t keyLength;
+		double value;
+		if (separatorIndex == -1)
+		{
+			keyLength = keyValue.len();
+			value = DEFAULT_NEURO_CONNECTION_WEIGHT;
+		} else
+		{
+			keyLength = separatorIndex;
+			size_t valueLength = keyValue.len() - (separatorIndex);
+			value = fS_stod(buffer + separatorIndex + 1, start, &valueLength);
+		}
+		inputs[fS_stod(buffer, start, &keyLength)] = value;
+	}
+}
+
+Node::Node(Substring &restOfGeno, bool _modifierMode, bool _paramMode, bool _cycleMode, Node *_parent)
+{
+	parent = _parent;
+	modifierMode = _modifierMode;
+	paramMode = _paramMode;
+	cycleMode = _cycleMode;
+	partDescription = new Substring(restOfGeno);
+
+	try
+	{
+		extractModifiers(restOfGeno);
+		extractPartType(restOfGeno);
+		extractNeurons(restOfGeno);
+		extractParams(restOfGeno);
+
+		partDescription->shortenBy(restOfGeno.len);
+		if (restOfGeno.len > 0)
+			getChildren(restOfGeno);
+	}
+	catch(fS_Exception &e)
+	{
+		cleanUp();
+		throw e;
+	}
+}
+
+Node::~Node()
+{
+	cleanUp();
+}
+
+void Node::cleanUp()
+{
+	delete partDescription;
+	if (state != nullptr)
+		delete state;
+	for (int i = 0; i < int(neurons.size()); i++)
+		delete neurons[i];
+	for (int i = 0; i < int(children.size()); i++)
+		delete children[i];
+}
+
+int Node::getPartPosition(Substring &restOfGenotype)
+{
+	for (int i = 0; i < restOfGenotype.len; i++)
+	{
+		if (GENE_TO_SHAPETYPE.find(restOfGenotype.at(i)) != GENE_TO_SHAPETYPE.end())
+			return i;
+	}
+	return -1;
+}
+
+void Node::extractModifiers(Substring &restOfGenotype)
+{
+	int partTypePosition = getPartPosition(restOfGenotype);
+	if (partTypePosition == -1)
+		throw fS_Exception("Part type missing", restOfGenotype.start);
+
+	for (int i = 0; i < partTypePosition; i++)
+	{
+		// Extract modifiers and joint
+		char mType = restOfGenotype.at(i);
+		if (JOINTS.find(tolower(mType)) != string::npos)
+			joint = tolower(mType);
+		else if (MODIFIERS.find(toupper(mType)) != string::npos)
+			modifiers[toupper(mType)] += isupper(mType) ? 1 : -1;
+		else
+			throw fS_Exception("Invalid modifier", restOfGenotype.start + i);
+	}
+	restOfGenotype.startFrom(partTypePosition);
+}
+
+void Node::extractPartType(Substring &restOfGenotype)
+{
+	auto itr = GENE_TO_SHAPETYPE.find(restOfGenotype.at(0));
+	if (itr == GENE_TO_SHAPETYPE.end())
+		throw fS_Exception("Invalid part type", restOfGenotype.start);
+
+	partType = itr->second;
+	restOfGenotype.startFrom(1);
+}
+
+vector<int> getSeparatorPositions(const char *str, int len, char separator, char endSign, int &endIndex)
+{
+	endIndex = -1;
+	vector<int> separators {-1};
+	for (int i = 0; i < len; i++)
+	{
+		if (str[i] == separator)
+			separators.push_back(i);
+		else if (str[i] == endSign)
+		{
+			endIndex = i;
+			break;
+		}
+	}
+	separators.push_back(endIndex); // End of string as last separator
+	return separators;
+}
+
+void Node::extractNeurons(Substring &restOfGenotype)
+{
+	if (restOfGenotype.len == 0 || restOfGenotype.at(0) != NEURON_START)
+		return;
+
+	const char *ns = restOfGenotype.c_str() + 1;
+	int neuronsEndIndex;
+	vector<int> separators = getSeparatorPositions(ns, restOfGenotype.len, NEURON_SEPARATOR, NEURON_END, neuronsEndIndex);
+	if(neuronsEndIndex == -1)
+		throw fS_Exception("Lacking neuro end sign", restOfGenotype.start);
+
+	for (int i = 0; i < int(separators.size()) - 1; i++)
+	{
+		int start = separators[i] + 1;
+		int length = separators[i + 1] - start;
+		fS_Neuron *newNeuron = new fS_Neuron(ns + start, restOfGenotype.start + start, length);
+		neurons.push_back(newNeuron);
+	}
+
+	restOfGenotype.startFrom(neuronsEndIndex + 2);
+}
+
+void Node::extractParams(Substring &restOfGenotype)
+{
+	if (restOfGenotype.len == 0 || restOfGenotype.at(0) != PARAM_START)
+		return;
+
+	const char *paramString = restOfGenotype.c_str() + 1;
+
+	// Find the indexes of the parameter separators
+	int paramsEndIndex;
+	vector<int> separators = getSeparatorPositions(paramString, restOfGenotype.len, PARAM_SEPARATOR, PARAM_END, paramsEndIndex);
+	if(paramsEndIndex == -1)
+		throw fS_Exception("Lacking param end sign", restOfGenotype.start);
+	for (int i = 0; i < int(separators.size()) - 1; i++)
+	{
+		int start = separators[i] + 1;
+		int length = separators[i + 1] - start;
+		const char *buffer = paramString + start;
+
+		// Find the index of key-value separator
+		int separatorIndex = -1;
+		for (int i = 0; i < length; i++)
+		{
+			if (buffer[i] == PARAM_KEY_VALUE_SEPARATOR)
+			{
+				separatorIndex = i;
+				break;
+			}
+		}
+		if (-1 == separatorIndex)
+			throw fS_Exception("Parameter separator expected", restOfGenotype.start);
+
+		// Compute the value of parameter and assign it to the key
+		int valueStartIndex = separatorIndex + 1;
+		string key(buffer, separatorIndex);
+		if(std::find(PARAMS.begin(), PARAMS.end(), key) == PARAMS.end())
+			throw fS_Exception("Invalid parameter key", restOfGenotype.start + start);
+
+		const char *val = buffer + valueStartIndex;
+		size_t len = length - valueStartIndex;
+		double value = fS_stod(val, restOfGenotype.start + start + valueStartIndex, &len);
+		if((key==SIZE_X || key==SIZE_Y || key==SIZE_Z) && value <= 0.0)
+			throw fS_Exception("Invalid value of radius parameter", restOfGenotype.start + start + valueStartIndex);
+
+		params[key] = value;
+
+	}
+
+	restOfGenotype.startFrom(paramsEndIndex + 2);
+}
+
+double Node::getParam(string key)
+{
+	auto item = params.find(key);
+	if (item != params.end())
+		return item->second;
+	else
+		return defaultParamValues.at(key);
+}
+
+double avg(double a, double b)
+{
+	return 0.5 * (a + b);
+}
+
+double min3(Pt3D p)
+{
+	double tmp = p.x;
+	if (p.y < tmp)
+		tmp = p.y;
+	if (p.z < tmp)
+		tmp = p.z;
+	return tmp;
+}
+
+double max3(Pt3D p)
+{
+	double tmp = p.x;
+	if (p.y > tmp)
+		tmp = p.y;
+	if (p.z > tmp)
+		tmp = p.z;
+	return tmp;
+}
+
+double getSphereCoordinate(double dimension, double sphereDiameter, double index, int count)
+{
+	if (count == 1)
+		return 0;
+	return (dimension - sphereDiameter) * (index / (count - 1) - 0.5);
+}
+
+Pt3D *findSphereCenters(int &sphereCount, double &sphereRadius, Pt3D radii, Pt3D rotations)
+{
+	double sphereRelativeDistance = SPHERE_RELATIVE_DISTANCE;
+	double minRadius = min3(radii);
+	if(minRadius <= 0)
+	    throw fS_Exception("Invalid part size", 0);
+	double maxRadius = max3(radii);
+	if (MAX_DIAMETER_QUOTIENT > maxRadius / minRadius)
+		sphereRadius = minRadius;
+	else
+	{
+		// When max radius is much bigger than min radius
+		sphereRelativeDistance = 1.0;   // Make the spheres adjacent to speed up the computation
+		sphereRadius = maxRadius / MAX_DIAMETER_QUOTIENT;
+	}
+	double sphereDiameter = 2 * sphereRadius;
+
+	double *diameters = new double[3] {2 * radii.x, 2 * radii.y, 2 * radii.z};
+	int counts[3];
+	for (int i = 0; i < 3; i++)
+	{
+		counts[i] = 1;
+		if (diameters[i] > sphereDiameter)
+			counts[i] += ceil((diameters[i] - sphereDiameter) / sphereDiameter / sphereRelativeDistance);
+	}
+
+	sphereCount = counts[0] * counts[1] * counts[2];
+	double x, y, z;
+	int totalCount = 0;
+	Pt3D *centers = new Pt3D[sphereCount];
+	for (double xi = 0; xi < counts[0]; xi++)
+	{
+		x = getSphereCoordinate(diameters[0], sphereDiameter, xi, counts[0]);
+		for (double yi = 0; yi < counts[1]; yi++)
+		{
+			y = getSphereCoordinate(diameters[1], sphereDiameter, yi, counts[1]);
+			for (double zi = 0; zi < counts[2]; zi++)
+			{
+				z = getSphereCoordinate(diameters[2], sphereDiameter, zi, counts[2]);
+				centers[totalCount] = Pt3D(x, y, z);
+				rotateVector(centers[totalCount], rotations);
+				totalCount++;
+			}
+		}
+	}
+	delete[] diameters;
+	return centers;
+}
+
+int isCollision(Pt3D *centersParent, Pt3D *centers, int parentSphereCount, int sphereCount, Pt3D &vector,
+				double distanceThreshold)
+{
+	double upperThreshold = distanceThreshold;
+	double lowerThreshold = SPHERE_DISTANCE_TOLERANCE * distanceThreshold;
+	double distance;
+	double dx, dy, dz;
+	bool existsAdjacent = false;
+	Pt3D *tmpPoint;
+	for (int sc = 0; sc < sphereCount; sc++)
+	{
+		Pt3D shiftedSphere = Pt3D(centers[sc]);
+		shiftedSphere += vector;
+		for (int psc = 0; psc < parentSphereCount; psc++)
+		{
+			tmpPoint = &centersParent[psc];
+			dx = shiftedSphere.x - tmpPoint->x;
+			dy = shiftedSphere.y - tmpPoint->y;
+			dz = shiftedSphere.z - tmpPoint->z;
+			distance = sqrt(dx * dx + dy * dy + dz * dz);
+
+			if (distance <= upperThreshold)
+			{
+				if (distance >= lowerThreshold)
+					existsAdjacent = true;
+				else
+				{
+					return COLLISION;
+				}
+			}
+		}
+	}
+	if (existsAdjacent)
+		return ADJACENT;
+	else
+		return DISJOINT;
+}
+
+double getDistance(Pt3D radiiParent, Pt3D radii, Pt3D vector, Pt3D rotationParent, Pt3D rotation)
+{
+	int parentSphereCount, sphereCount;
+	double parentSphereRadius, sphereRadius;
+	Pt3D *centersParent = findSphereCenters(parentSphereCount, parentSphereRadius, radiiParent, rotationParent);
+	Pt3D *centers = findSphereCenters(sphereCount, sphereRadius, radii, rotation);
+
+	double distanceThreshold = sphereRadius + parentSphereRadius;
+	double minDistance = 0.0;
+	double maxDistance = 2 * (max3(radiiParent) + max3(radii));
+	double currentDistance = avg(maxDistance, minDistance);
+	int result = -1;
+	while (result != ADJACENT)
+	{
+		Pt3D currentVector = vector * currentDistance;
+		result = isCollision(centersParent, centers, parentSphereCount, sphereCount, currentVector, distanceThreshold);
+		if (result == DISJOINT)
+		{
+			maxDistance = currentDistance;
+			currentDistance = avg(currentDistance, minDistance);
+		} else if (result == COLLISION)
+		{
+			minDistance = currentDistance;
+			currentDistance = avg(maxDistance, currentDistance);
+		}
+		if (currentDistance > maxDistance)
+			throw fS_Exception("Internal error; then maximal distance between parts exceeded.", 0);
+		if (currentDistance < minDistance)
+			throw fS_Exception("Internal error; the minimal distance between parts exceeded.", 0);
+
+	}
+
+	delete[] centersParent;
+	delete[] centers;
+	return round2(currentDistance);
+}
+
+void Node::getState(State *_state, const Pt3D &parentSize)
+{
+	if (state != nullptr)
+		delete state;
+	if (parent == nullptr)
+		state = _state;
+	else
+		state = new State(_state);
+
+
+	// Update state by modifiers
+	for (auto it = modifiers.begin(); it != modifiers.end(); ++it)
+	{
+		char mod = it->first;
+		double multiplier = pow(MODIFIER_MULTIPLIER, it->second);
+		if (mod == MODIFIERS[0])
+			state->ing *= multiplier;
+		else if (mod == MODIFIERS[1])
+			state->fr *= multiplier;
+		else if (mod == MODIFIERS[2])
+			state->s *= multiplier;
+	}
+
+	Pt3D size = calculateSize();
+	if (parent != nullptr)
+	{
+		// Rotate
+		state->rotate(getVectorRotation());
+
+		double distance = getDistance(parentSize, size, state->v, getRotation(), getRotation());
+		state->addVector(distance);
+	}
+	for (int i = 0; i < int(children.size()); i++)
+		children[i]->getState(state, size);
+}
+
+void Node::getChildren(Substring &restOfGenotype)
+{
+	vector<Substring> branches = getBranches(restOfGenotype);
+	for (int i = 0; i < int(branches.size()); i++)
+	{
+		children.push_back(new Node(branches[i], modifierMode, paramMode, cycleMode, this));
+	}
+}
+
+vector<Substring> Node::getBranches(Substring &restOfGenotype)
+{
+	vector<Substring> children;
+	if (restOfGenotype.at(0) != BRANCH_START)
+	{
+		children.push_back(restOfGenotype);  // Only one child
+		return children;
+	}
+
+	int depth = 0;
+	int start = 1;
+	char c;
+	const char *str = restOfGenotype.c_str();
+	for (int i = 0; i < restOfGenotype.len; i++)
+	{
+		if (depth < 0)
+			throw fS_Exception("The number of branch start signs does not equal the number of branch end signs", restOfGenotype.start + i);
+		c = str[i];
+		if (c == BRANCH_START)
+			depth++;
+		else if ((c == BRANCH_SEPARATOR && depth == 1) || i + 1 == restOfGenotype.len)
+		{
+			Substring substring(restOfGenotype);
+			substring.startFrom(start);
+			substring.len = i - start;
+			children.push_back(substring);
+			start = i + 1;
+		} else if (c == BRANCH_END)
+			depth--;
+	}
+	if (depth != 1)    // T
+		throw fS_Exception("The number of branch start signs does not equal the number of branch end signs", restOfGenotype.start);
+	return children;
+}
+
+Pt3D Node::calculateSize()
+{
+	double sizeMultiplier = getParam(SIZE) * state->s;
+	double sx = getParam(SIZE_X) * sizeMultiplier;
+	double sy = getParam(SIZE_Y) * sizeMultiplier;
+	double sz = getParam(SIZE_Z) * sizeMultiplier;
+	return Pt3D(sx, sy, sz);
+}
+
+double Node::calculateVolume()
+{
+	double result;
+	Pt3D size = calculateSize();
+	double radiiProduct = size.x * size.y * size.z;
+	switch (partType)
+	{
+		case Part::Shape::SHAPE_CUBOID:
+			result = 8.0 * radiiProduct;
+			break;
+		case Part::Shape::SHAPE_CYLINDER:
+			result = 2.0 * M_PI * radiiProduct;
+			break;
+		case Part::Shape::SHAPE_ELLIPSOID:
+			result = (4.0 / 3.0) * M_PI * radiiProduct;
+			break;
+		default:
+			logMessage("fS", "calculateVolume", LOG_ERROR, "Invalid part type");
+	}
+	return result;
+}
+
+bool Node::isPartSizeValid()
+{
+	Pt3D size = calculateSize();
+	double volume = calculateVolume();
+	Part_MinMaxDef minP = Model::getMinPart();
+	Part_MinMaxDef maxP = Model::getMaxPart();
+
+	if (volume > maxP.volume || minP.volume > volume)
+		return false;
+	if (size.x < minP.scale.x || size.y < minP.scale.y || size.z < minP.scale.z)
+		return false;
+	if (size.x > maxP.scale.x || size.y > maxP.scale.y || size.z > maxP.scale.z)
+		return false;
+
+	if (partType == Part::Shape::SHAPE_ELLIPSOID && max3(size) != min3(size))
+		// When not all radii have different values
+		return false;
+	if (partType == Part::Shape::SHAPE_CYLINDER && size.x != size.y)
+		// If base radii have different values
+		return false;
+	return true;
+}
+
+bool Node::hasPartSizeParam()
+{
+	return params.count(SIZE_X) > 0 || params.count(SIZE_Y) > 0 || params.count(SIZE_Z) > 0;
+}
+
+Pt3D Node::getVectorRotation()
+{
+	double rx = getParam(ROT_X);
+	double ry = getParam(ROT_Y);
+	double rz = getParam(ROT_Z);
+	return Pt3D(rx, ry, rz);
+}
+
+Pt3D Node::getRotation()
+{
+	double rx = getParam(RX);
+	double ry = getParam(RY);
+	double rz = getParam(RZ);
+	return Pt3D(rx, ry, rz);
+}
+
+void Node::buildModel(Model &model, Node *parent)
+{
+	createPart();
+	model.addPart(part);
+	if (parent != nullptr)
+		addJointsToModel(model, parent);
+
+
+	for (int i = 0; i < int(neurons.size()); i++)
+	{
+		Neuro *neuro = new Neuro(*neurons[i]);
+		model.addNeuro(neuro);
+		if (neuro->getClass()->preflocation == 2 && parent != nullptr)
+		{
+			neuro->attachToJoint(model.getJoint(model.getJointCount() - 1));
+		} else
+			neuro->attachToPart(part);
+	}
+
+	model.checkpoint();
+	part->addMapping(partDescription->toMultiRange());
+
+	for (int i = 0; i < int(children.size()); i++)
+	{
+		Node *child = children[i];
+		child->buildModel(model, this);
+	}
+}
+
+void Node::createPart()
+{
+	part = new Part(partType);
+	part->p = Pt3D(round2(state->location.x),
+				   round2(state->location.y),
+				   round2(state->location.z));
+
+	part->friction = round2(getParam(FRICTION) * state->fr);
+	part->ingest = round2(getParam(INGESTION) * state->ing);
+	Pt3D size = calculateSize();
+	part->scale.x = round2(size.x);
+	part->scale.y = round2(size.y);
+	part->scale.z = round2(size.z);
+	part->setRot(getRotation());
+}
+
+void Node::addJointsToModel(Model &model, Node *parent)
+{
+	Joint *j = new Joint();
+	j->attachToParts(parent->part, part);
+	switch (joint)
+	{
+		case HINGE_X:
+			j->shape = Joint::Shape::SHAPE_HINGE_X;
+			break;
+		case HINGE_XY:
+			j->shape = Joint::Shape::SHAPE_HINGE_XY;
+			break;
+		default:
+			j->shape = Joint::Shape::SHAPE_FIXED;
+	}
+	model.addJoint(j);
+	j->addMapping(partDescription->toMultiRange());
+}
+
+
+void Node::getGeno(SString &result)
+{
+	if (joint != DEFAULT_JOINT)
+		result += joint;
+	for (auto it = modifiers.begin(); it != modifiers.end(); ++it)
+	{
+		char mod = it->first;
+		int count = it->second;
+		if(it->second < 0)
+		{
+			mod = tolower(mod);
+			count = fabs(count);
+		}
+		result += std::string(count, mod).c_str();
+	}
+	result += SHAPETYPE_TO_GENE.at(partType);
+
+	if (!neurons.empty())
+	{
+		// Add neurons to genotype string
+		result += NEURON_START;
+		for (int i = 0; i < int(neurons.size()); i++)
+		{
+			fS_Neuron *n = neurons[i];
+			if (i != 0)
+				result += NEURON_SEPARATOR;
+			if (n->getClassName() != "N")
+			{
+				result += n->getDetails();
+				if (!n->inputs.empty())
+					result += NEURON_INTERNAL_SEPARATOR;
+			}
+			for (auto it = n->inputs.begin(); it != n->inputs.end(); ++it)
+			{
+				if (it != n->inputs.begin())
+					result += NEURON_INTERNAL_SEPARATOR;
+				result += SString::valueOf(it->first);
+				if (it->second != DEFAULT_NEURO_CONNECTION_WEIGHT)
+				{
+					result += NEURON_I_W_SEPARATOR;
+					result += SString::valueOf(it->second);
+				}
+
+			}
+		}
+		result += NEURON_END;
+	}
+
+	if (!params.empty())
+	{
+		// Add parameters to genotype string
+		result += PARAM_START;
+		for (auto it = params.begin(); it != params.end(); ++it)
+		{
+			if (it != params.begin())
+				result += PARAM_SEPARATOR;
+
+			result += it->first.c_str();                    // Add parameter key to string
+			result += PARAM_KEY_VALUE_SEPARATOR;
+			string value_text = std::to_string(it->second);
+			// Round the value to two decimal places and add to string
+			result += value_text.substr(0, value_text.find(".") + fS_Genotype::precision).c_str();
+		}
+		result += PARAM_END;
+	}
+
+	if (children.size() == 1)
+		children[0]->getGeno(result);
+	else if (children.size() > 1)
+	{
+		result += BRANCH_START;
+		for (int i = 0; i < int(children.size()) - 1; i++)
+		{
+			children[i]->getGeno(result);
+			result += BRANCH_SEPARATOR;
+		}
+		children.back()->getGeno(result);
+		result += BRANCH_END;
+	}
+}
+
+
+bool Node::changeSizeParam(string paramKey, double multiplier, bool ensureCircleSection)
+{
+	double oldValue = getParam(paramKey);
+	params[paramKey] = oldValue * multiplier;
+	if (!ensureCircleSection || isPartSizeValid())
+		return true;
+	else
+	{
+		params[paramKey] = oldValue;
+		return false;
+	}
+}
+
+void Node::getAllNodes(vector<Node *> &allNodes)
+{
+	allNodes.push_back(this);
+	for (int i = 0; i < int(children.size()); i++)
+		children[i]->getAllNodes(allNodes);
+}
+
+int Node::getNodeCount()
+{
+	vector<Node*> allNodes;
+	getAllNodes(allNodes);
+	return allNodes.size();
+}
+
+fS_Genotype::fS_Genotype(const string &genotype)
+{
+	try
+	{
+		string geno = genotype.c_str();
+		// M - modifier mode, S - standard mode
+		size_t modeSeparatorIndex = geno.find(':');
+		if (modeSeparatorIndex == string::npos)
+			throw fS_Exception("No mode separator", 0);
+
+		string modeStr = geno.substr(0, modeSeparatorIndex).c_str();
+		bool modifierMode = modeStr.find(MODIFIER_MODE) != string::npos;
+		bool paramMode = modeStr.find(PARAM_MODE) != string::npos;
+		bool cycleMode = modeStr.find(CYCLE_MODE) != string::npos;
+
+		int actualGenoStart = modeSeparatorIndex + 1;
+		Substring substring(geno.c_str(), actualGenoStart, geno.length() - actualGenoStart);
+		startNode = new Node(substring, modifierMode, paramMode, cycleMode, nullptr);
+		validateNeuroInputs();
+	}
+	catch (fS_Exception &e)
+	{
+		delete startNode;
+		throw e;
+	}
+}
+
+fS_Genotype::~fS_Genotype()
+{
+	delete startNode;
+}
+
+void fS_Genotype::getState()
+{
+	State *initialState = new State(Pt3D(0), Pt3D(1, 0, 0));
+	startNode->getState(initialState, Pt3D(1.0));
+}
+
+double fS_Genotype::randomParamMultiplier()
+{
+	double multiplier = 1 + fabs(RndGen.GaussStd());
+	if (multiplier > PARAM_MAX_MULTIPLIER)
+		multiplier = PARAM_MAX_MULTIPLIER;
+	if (rndUint(2) == 0)
+		multiplier = 1.0 / multiplier;
+	return multiplier;
+}
+
+void fS_Genotype::buildModel(Model &model)
+{
+	getState();
+	startNode->buildModel(model, nullptr);
+
+	buildNeuroConnections(model);
+
+	// Additional joints
+	vector<Node*> allNodes = getAllNodes();
+	for (int i = 0; i < int(allNodes.size()); i++)
+	{
+		Node *node = allNodes[i];
+		if (node->params.find(JOINT_DISTANCE) != node->params.end())
+		{
+			Node *otherNode = getNearestNode(allNodes, node);
+			if (otherNode != nullptr)
+			{
+				// If other node is close enough, add a joint
+				double distance = node->state->location.distanceTo(otherNode->state->location);
+				if (distance < node->params[JOINT_DISTANCE])
+				{
+					Joint *joint = new Joint();
+					joint->attachToParts(node->part, otherNode->part);
+
+					joint->shape = Joint::Shape::SHAPE_FIXED;
+					model.addJoint(joint);
+				}
+			}
+		}
+	}
+}
+
+
+void fS_Genotype::buildNeuroConnections(Model &model)
+{
+	// All the neurons are already created in the model
+	vector<fS_Neuron*> allNeurons = getAllNeurons();
+	for (int i = 0; i < int(allNeurons.size()); i++)
+	{
+		fS_Neuron *neuron = allNeurons[i];
+		Neuro *modelNeuro = model.getNeuro(i);
+		for (auto it = neuron->inputs.begin(); it != neuron->inputs.end(); ++it)
+		{
+			Neuro *inputNeuro = model.getNeuro(it->first);
+			modelNeuro->addInput(inputNeuro, it->second);
+
+		}
+	}
+}
+
+Node *fS_Genotype::getNearestNode(vector<Node *> allNodes, Node *node)
+{
+	Node *result = nullptr;
+	double minDistance = DBL_MAX, distance = DBL_MAX;
+	for (int i = 0; i < int(allNodes.size()); i++)
+	{
+		Node *otherNode = allNodes[i];
+		auto v = node->children;
+		if (otherNode != node &&
+			find(v.begin(), v.end(), otherNode) == v.end())
+		{   // Not the same node and not a child
+			distance = node->state->location.distanceTo(otherNode->state->location);
+			if (distance < minDistance)
+			{
+				minDistance = distance;
+				result = otherNode;
+			}
+		}
+	}
+	return result;
+}
+
+SString fS_Genotype::getGeno()
+{
+	SString geno;
+	geno.memoryHint(100);     // Provide a small buffer from the start to improve performance
+
+	if (startNode->modifierMode)
+		geno += MODIFIER_MODE;
+	if (startNode->paramMode)
+		geno += PARAM_MODE;
+	if (startNode->cycleMode)
+		geno += CYCLE_MODE;
+
+	geno += ':';
+	startNode->getGeno(geno);
+	return geno;
+}
+
+vector<fS_Neuron *> fS_Genotype::extractNeurons(Node *node)
+{
+	vector<Node*> allNodes;
+	node->getAllNodes(allNodes);
+
+	vector<fS_Neuron*> allNeurons;
+	for (int i = 0; i < int(allNodes.size()); i++)
+	{
+		for (int j = 0; j < int(allNodes[i]->neurons.size()); j++)
+		{
+			allNeurons.push_back(allNodes[i]->neurons[j]);
+		}
+	}
+	return allNeurons;
+}
+
+int fS_Genotype::getNeuronIndex(vector<fS_Neuron *> neurons, fS_Neuron *changedNeuron)
+{
+	int neuronIndex = -1;
+	for (int i = 0; i < int(neurons.size()); i++)
+	{
+		if (changedNeuron == neurons[i])
+		{
+			neuronIndex = i;
+			break;
+		}
+	}
+	return neuronIndex;
+}
+
+void fS_Genotype::shiftNeuroConnections(vector<fS_Neuron *> &neurons, int start, int end, SHIFT shift)
+{
+	if (start == -1 || end == -1)
+		return;
+	int shiftValue = end - start + 1;
+	if (shift == SHIFT::LEFT)
+		shiftValue *= -1;
+
+	for (int i = 0; i < int(neurons.size()); i++)
+	{
+		fS_Neuron *n = neurons[i];
+		std::map<int, double> newInputs;
+		for (auto it = n->inputs.begin(); it != n->inputs.end(); ++it)
+		{
+			if (start > it->first)
+				newInputs[it->first] = it->second;
+			else if (it->first >= start)
+			{
+				if (end >= it->first)
+				{
+					if (shift == SHIFT::RIGHT)
+						newInputs[it->first + shiftValue] = it->second;
+					// If shift == -1, just delete the input
+				} else if (it->first > end)
+					newInputs[it->first + shiftValue] = it->second;
+			}
+		}
+		n->inputs = newInputs;
+	}
+}
+
+vector<Node *> fS_Genotype::getAllNodes()
+{
+	vector<Node*> allNodes;
+	startNode->getAllNodes(allNodes);
+	return allNodes;
+}
+
+vector<fS_Neuron *> fS_Genotype::getAllNeurons()
+{
+	return extractNeurons(startNode);
+}
+
+Node *fS_Genotype::chooseNode(int fromIndex)
+{
+	vector<Node*> allNodes = getAllNodes();
+	return allNodes[fromIndex + rndUint(allNodes.size() - fromIndex)];
+}
+
+int fS_Genotype::getNodeCount()
+{
+	return startNode->getNodeCount();
+}
+
+int fS_Genotype::checkValidityOfPartSizes()
+{
+	getState();
+	vector<Node*> nodes = getAllNodes();
+	for (int i = 0; i < int(nodes.size()); i++)
+	{
+		if (!nodes[i]->isPartSizeValid())
+		{
+			return nodes[i]->partDescription->start;
+		}
+	}
+	return 0;
+}
+
+
+void fS_Genotype::validateNeuroInputs()
+{
+
+	// Validate neuro input numbers
+	vector<fS_Neuron*> allNeurons = getAllNeurons();
+	int allNeuronsSize = allNeurons.size();
+	for(int i=0; i<allNeuronsSize; i++)
+	{
+		fS_Neuron *n = allNeurons[i];
+		for (auto it = n->inputs.begin(); it != n->inputs.end(); ++it)
+		{
+			if (it->first < 0 || it->first >= allNeuronsSize)
+				throw fS_Exception("Invalid neuron input", 0);
+		}
+	}
+}
+
+
+void fS_Genotype::rearrangeNeuronConnections(fS_Neuron *changedNeuron, SHIFT shift)
+{
+	vector<fS_Neuron*> neurons = getAllNeurons();
+	int changedNeuronIndex = getNeuronIndex(neurons, changedNeuron);
+	shiftNeuroConnections(neurons, changedNeuronIndex, changedNeuronIndex, shift);
+}
+
Index: cpp/frams/genetics/fS/fS_general.h
===================================================================
--- cpp/frams/genetics/fS/fS_general.h	(revision 958)
+++ cpp/frams/genetics/fS/fS_general.h	(revision 958)
@@ -0,0 +1,596 @@
+// This file is a part of Framsticks SDK.  http://www.framsticks.com/
+// Copyright (C) 2019-2020  Maciej Komosinski and Szymon Ulatowski.
+// See LICENSE.txt for details.
+
+#ifndef _FS_GENERAL_H_
+#define _FS_GENERAL_H_
+
+#include <iostream>
+#include <vector>
+#include <map>
+#include <unordered_map>
+#include <exception>
+#include "frams/model/model.h"
+#include "frams/util/multirange.h"
+
+/** @name Names of genotype modes */
+//@{
+#define MODIFIER_MODE 'M'
+#define PARAM_MODE 'S'
+#define CYCLE_MODE 'J'
+//@}
+
+/** @name Values of constants used in encoding */
+//@{
+#define BRANCH_START '('
+#define BRANCH_END ')'
+#define BRANCH_SEPARATOR ','
+#define PARAM_START '{'
+#define PARAM_END '}'
+const char PARAM_SEPARATOR = ';';
+const char PARAM_KEY_VALUE_SEPARATOR = '=';
+#define NEURON_START '['
+const char NEURON_END = ']';
+const char NEURON_SEPARATOR = ';';
+const SString NEURON_INTERNAL_SEPARATOR("_");
+#define NEURON_I_W_SEPARATOR ':'
+//@}
+
+enum class SHIFT
+{
+	LEFT = -1,
+	RIGHT = 1
+};
+
+/** @name Every modifier changes the underlying value by this multiplier */
+const double MODIFIER_MULTIPLIER = 1.1;
+/** @name In mutation parameters will be multiplied by at most this value */
+const double PARAM_MAX_MULTIPLIER = 1.5;
+
+/**
+ * Used in finding the proper distance between the parts
+ * distance between spheres / sphere radius
+ * That default value can be changed in certain cases
+ * */
+const float SPHERE_RELATIVE_DISTANCE = 0.25;
+/**
+ * Used in finding the proper distance between the parts
+ * The maximal allowed value for
+ * maximal radius of the node / sphere radius
+ */
+const int MAX_DIAMETER_QUOTIENT = 30;
+/**
+ * The tolerance of the value of distance between parts
+ */
+const double SPHERE_DISTANCE_TOLERANCE = 0.99;
+
+
+/** @name Names of node parameters and modifiers*/
+//@{
+#define INGESTION "i"
+#define FRICTION "f"
+#define SIZE "s"
+#define SIZE_X "x"
+#define SIZE_Y "y"
+#define SIZE_Z "z"
+#define ROT_X "tx"
+#define ROT_Y "ty"
+#define ROT_Z "tz"
+#define RX "rx"
+#define RY "ry"
+#define RZ "rz"
+#define JOINT_DISTANCE "j"
+//@}
+/** @name Macros and values used in collision detection */
+//@{
+#define DISJOINT 0
+#define COLLISION 1
+#define ADJACENT 2
+//@}
+
+#define HINGE_X 'b'
+#define HINGE_XY 'c'
+
+const double DEFAULT_NEURO_CONNECTION_WEIGHT = 1.0;
+
+const char ELLIPSOID = 'E';
+const char CUBOID = 'C';
+const char CYLINDER = 'R';
+const std::unordered_map<Part::Shape, char> SHAPETYPE_TO_GENE = {
+		{Part::Shape::SHAPE_ELLIPSOID, ELLIPSOID},
+		{Part::Shape::SHAPE_CUBOID,    CUBOID},
+		{Part::Shape::SHAPE_CYLINDER,  CYLINDER},
+};
+
+// This map is inverse to SHAPE_TO_SYMBOL. Those two should be compatible
+const std::unordered_map<char, Part::Shape> GENE_TO_SHAPETYPE = {
+		{ELLIPSOID, Part::Shape::SHAPE_ELLIPSOID},
+		{CUBOID,    Part::Shape::SHAPE_CUBOID},
+		{CYLINDER,  Part::Shape::SHAPE_CYLINDER},
+};
+const int SHAPE_COUNT = 3;    // This should be the count of SHAPETYPE_TO_GENE and GENE_TO_SHAPETYPE
+
+const char DEFAULT_JOINT = 'a';
+const string JOINTS = "bc";
+const int JOINT_COUNT = JOINTS.length();
+const string MODIFIERS = "IFS";
+const char SIZE_MODIFIER = 's';
+const vector<string> PARAMS {INGESTION, FRICTION, ROT_X, ROT_Y, ROT_Z, RX, RY, RZ, SIZE, SIZE_X, SIZE_Y, SIZE_Z,
+							  JOINT_DISTANCE};
+
+/** @name Default values of node parameters*/
+static const Part defPart = Model::getDefPart();
+const std::map<string, double> defaultParamValues = {
+		{INGESTION,      defPart.ingest},
+		{FRICTION,       defPart.friction},
+		{ROT_X,          0.0},
+		{ROT_Y,          0.0},
+		{ROT_Z,          0.0},
+		{RX,             0.0},
+		{RY,             0.0},
+		{RZ,             0.0},
+		{SIZE,           1.0},
+		{SIZE_X,         1.0},
+		{SIZE_Y,         1.0},
+		{SIZE_Z,         1.0},
+		{JOINT_DISTANCE, 1.0}
+};
+
+/** @name Number of tries of performing a mutation before GENOPER_FAIL is returned */
+#define mutationTries  20
+
+class fS_Exception : public std::exception
+{
+	string msg;
+public:
+
+	int errorPosition;
+	virtual const char *what() const throw()
+	{
+		return msg.c_str();
+	}
+
+	fS_Exception(string _msg, int _errorPosition)
+	{
+		msg = _msg;
+		errorPosition = _errorPosition;
+	}
+};
+
+/**
+ * Draws an integer value from given range
+ * @param to maximal value
+ * @param from minimal value
+ * @return Drawn value
+ */
+int randomFromRange(int to, int from);
+
+/**
+ * Represents a substring of a larger string.
+ * The reference to the original string is stored along with indexes of beginning end length of the substring.
+ */
+class Substring
+{
+public:
+	char *str;        // Pointer to the beginning of the substring
+	int start;        // The beginning index of substring
+	int len;        // The length of substring
+
+	Substring(const char *_str, int _start, int _len)
+	{
+		str = (char *) _str + _start;
+		start = _start;
+		len = _len;
+	}
+
+	Substring(const Substring &other)
+	{
+		str = other.str;
+		start = other.start;
+		len = other.len;
+	}
+
+	const char *c_str()
+	{
+		return str;
+	}
+
+	SString substr(int relativeStart, int len)
+	{
+		const char *substrStart = str + relativeStart;
+		return SString(substrStart, len);
+	}
+
+	int indexOf(char ch)
+	{
+		for (int i = 0; i < len; i++)
+			if (str[i] == ch)
+				return i;
+		return -1;
+	}
+
+	void startFrom(int index)
+	{
+		str += index;
+		start += index;
+		len -= index;
+	}
+
+	void shortenBy(int charCount)
+	{
+		len = std::max(len - charCount, 0);
+	}
+
+	char at(int index)
+	{
+		return str[index];
+	}
+
+	/**
+	 * Create a new instance of multirange, corresponding to the substring
+	 * @return a created multirange
+	 */
+	MultiRange toMultiRange()
+	{
+		MultiRange range;
+		range.add(start, start + len - 1);
+		return range;
+	}
+};
+
+/**
+ * Stores the state of the node.
+ * The state consists od current location, the direction in which the branch develops
+ * and the current default values of the parameters (default values can be changed by modifiers).
+ */
+class State
+{
+public:
+	Pt3D location;  /// Location of the node
+	Pt3D v;         /// The normalised vector in which current branch develops
+	double fr = 1.0;      /// Friction multiplier
+	double ing = 1.0;      /// Ingestion multiplier
+	double s = 1.0;      /// Size multipliers
+
+	State(State *_state); /// Derive the state from parent
+
+	State(Pt3D _location, Pt3D _v); /// Create the state from parameters
+
+	/**
+	 * Add the vector of specified length to location
+	 * @param length the length of the vector
+	 */
+	void addVector(const double length);
+
+	/**
+	 * Rotate the vector by specified values
+	 * @param rx rotation by x axis
+	 * @param ry rotation by y axis
+	 * @param rz rotation by z axis
+	 */
+	void rotate(const Pt3D &rotation);
+};
+
+/**
+ * Represent a neuron and its inputs
+ */
+class fS_Neuron: public Neuro
+{
+public:
+	std::map<int, double> inputs;
+
+	fS_Neuron(const char *str, int start, int length);
+
+	bool acceptsInputs()
+	{
+		return getClass()->prefinputs < int(inputs.size());
+	}
+};
+
+/**
+ * Represents a node in the graph that represents a genotype.
+ * A node corresponds to a single part.
+ * However, it also stores attributes that are specific to fS encoding, such as modifiers and joint types.
+ */
+class Node
+{
+	friend class fS_Genotype;
+
+	friend class GenoOper_fS;
+
+private:
+	Substring *partDescription = nullptr;
+	bool cycleMode, modifierMode, paramMode; /// Possible modes
+	Node *parent;
+	Part *part;     /// A part object built from node. Used in building the Model
+	int partCodeLen; /// The length of substring that directly describes the corresponding part
+
+	std::map<string, double> params; /// The map of all the node params
+	vector<Node *> children;    /// Vector of all direct children
+	std::map<char, int> modifiers;     /// Vector of all modifiers
+	char joint = DEFAULT_JOINT;           /// Set of all joints
+	vector<fS_Neuron *> neurons;    /// Vector of all the neurons
+
+	static double calculateRadiusFromVolume(Part::Shape partType, double volume)
+	{
+		double result;
+		switch (partType)
+		{
+			case Part::Shape::SHAPE_CUBOID:
+				result = std::cbrt(volume / 8.0);
+				break;
+			case Part::Shape::SHAPE_CYLINDER:
+				result = std::cbrt(volume / (2.0 * M_PI));
+				break;
+			case Part::Shape::SHAPE_ELLIPSOID:
+				result = std::cbrt(volume / ((4.0 / 3.0) * M_PI));
+				break;
+			default:
+				logMessage("fS", "calculateVolume", LOG_ERROR, "Invalid part type");
+		}
+		return result;
+	}
+
+	void cleanUp();
+
+	Pt3D getRotation();
+
+	Pt3D getVectorRotation();
+
+	bool isPartSizeValid();
+
+	bool hasPartSizeParam();
+
+	/**
+	 * Get the position of part type in genotype
+	 *
+	 * @return the position of part type
+	 */
+	int getPartPosition(Substring &restOfGenotype);
+
+	/**
+	 * Extract modifiers from the rest of genotype
+	 * @return the remainder of the genotype
+	 */
+	void extractModifiers(Substring &restOfGenotype);
+
+	/**
+	 * Extract part type from the rest of genotype
+	 * @return the remainder of the genotype
+	 */
+	void extractPartType(Substring &restOfGenotype);
+
+	/**
+	 * Extract neurons from the rest of genotype
+	 * @return the remainder of the genotype
+	 */
+	void extractNeurons(Substring &restOfGenotype);
+
+	/**
+	 * Extract params from the rest of genotype
+	 * @return the length og the remainder of the genotype
+	 */
+	void extractParams(Substring &restOfGenotype);
+
+	/**
+	 * Extract child branches from the rest of genotype
+	 * @return vector of child branches
+	 */
+	vector<Substring> getBranches(Substring &restOfGenotype);
+
+	/**
+	 * Get phenotypic state that derives from ancestors.
+	 * Used when building model
+	 * @param _state state of the parent
+	 */
+	void getState(State *_state, const Pt3D &parentSize);
+
+	/**
+	 * Build children internal representations from fS genotype
+	 * @param restOfGenotype part of genotype that describes the subtree
+	 */
+	void getChildren(Substring &restOfGenotype);
+
+	/**
+	 * Create part object from internal representation
+	 */
+	void createPart();
+
+	/**
+	 * Add joints between current node and the specified child
+	 * Used in building model
+	 * @param mode pointer to build model
+	 * @param child pointer to the child
+	 */
+	void addJointsToModel(Model &model, Node *parent);
+
+	/**
+	 * Get all the nodes from the subtree that starts in this node
+	 * @param reference to vector which contains nodes
+	 */
+	void getAllNodes(vector<Node *> &allNodes);
+
+
+	/**
+	 * Build model from the subtree that starts in this node
+	 * @param pointer to model
+	 */
+	void buildModel(Model &model, Node *parent);
+
+public:
+	Part::Shape partType;  /// The type of the part
+	State *state = nullptr; /// The phenotypic state that inherits from ancestors
+
+	Node(Substring &genotype, bool _modifierMode, bool _paramMode, bool _cycleMode, Node *parent);
+
+	~Node();
+
+	/**
+	 * Get fS representation of the subtree that starts from this node
+	 * @param result the reference to an object which is used to contain fS genotype
+	 */
+	void getGeno(SString &result);
+
+	/**
+	 * Calculate the effective size of the part (after applying all multipliers and params)
+	 * @return The effective size
+	 */
+	Pt3D calculateSize();
+
+	/**
+	 * Calculate the effective volume of the part
+	 * @return The effective volume
+	 */
+	double calculateVolume();
+
+	/**
+	 * Change the value of the size parameter by given multiplier
+	 * Do not change the value if any of the size restrictions is not satisfied
+	 * @param paramKey
+	 * @param multiplier
+	 * @param ensureCircleSection
+	 * @return True if the parameter value was change, false otherwise
+	 */
+	bool changeSizeParam(string paramKey, double multiplier, bool ensureCircleSection);
+
+	/**
+	 * Counts all the nodes in subtree
+	 * @return node count
+	 */
+	int getNodeCount();
+
+	/**
+	 * Extract the value of parameter or return default if parameter not exists
+	 * @return the param value
+	 */
+	double getParam(string key);
+};
+
+/**
+ * Represents an fS genotype.
+ */
+class fS_Genotype
+{
+	friend class Node;
+
+	friend class GenoOper_fS;
+
+private:
+	/**
+	 * Draws a node that has an index greater that specified
+	 * @param fromIndex minimal index of the node
+	 * @return pointer to drawn node
+	 */
+	Node *chooseNode(int fromIndex=0);
+
+	/**
+	 * Draws a value from defined distribution
+	 * @return Drawn value
+	 */
+	void randomFromDistribution();
+
+	/**
+	 * Find a node that is nearest (euclidean distance to specified node) and is not a child of specified node
+	 * @return Nearest node
+	 */
+	Node *getNearestNode(vector<Node *> allNodes, Node *node);
+
+public:
+	Node *startNode = nullptr;    /// The start (root) node. All other nodes are its descendants
+
+
+	static int precision;
+
+	/**
+	 * Build internal representation from fS format
+	 * @param genotype in fS format
+	 */
+	fS_Genotype(const string &genotype);
+
+	~fS_Genotype();
+
+	void getState();
+
+	/**
+	 * Get a random multiplier for parameter mutation
+	 * @return Random parameter multiplier
+	 */
+	static double randomParamMultiplier();
+
+	/**
+	 * Get all existing nodes
+	 * @return vector of all nodes
+	 */
+	vector<Node *> getAllNodes();
+
+	/**
+	 * Get all the neurons from the subtree that starts in given node
+	 * @param node The beginning of subtree
+	 * @return The vector of neurons
+	 */
+	static vector<fS_Neuron *> extractNeurons(Node *node);
+
+	/**
+	 * Get the index of the neuron in vector of neurons
+	 * @param neurons
+	 * @param changedNeuron
+	 * @return
+	 */
+	static int getNeuronIndex(vector<fS_Neuron *> neurons, fS_Neuron *changedNeuron);
+
+	/**
+	 * Left- or right- shift the indexes of neuro connections by the given range
+	 * @param neurons
+	 * @param start The beginning of the range
+	 * @param end The end of the range
+	 * @param shift
+	 */
+	static void shiftNeuroConnections(vector<fS_Neuron *> &neurons, int start, int end, SHIFT shift);
+
+	/**
+	 * Get all existing neurons
+	 * @return vector of all neurons
+	 */
+	vector<fS_Neuron *> getAllNeurons();
+
+	/**
+	 * Counts all the nodes in genotype
+	 * @return node count
+	 */
+	int getNodeCount();
+
+	/**
+	 * Check if sizes of all parts in genotype are valid
+	\retval error_position 1-based
+	\retval 0 when all part sizes are valid
+	 */
+	int checkValidityOfPartSizes();
+
+	void validateNeuroInputs();
+
+	/**
+	 * Builds Model object from internal representation
+	 * @param a reference to a model that will contain a built model
+	 */
+	void buildModel(Model &model);
+
+	/**
+	 * Adds neuro connections to model
+	 * @param a reference to a model where the connections will be added
+	 */
+	void buildNeuroConnections(Model &model);
+
+	/**
+	 * @return genotype in fS format
+	 */
+	SString getGeno();
+
+	/**
+	 * After creating or deleting a new neuron, rearrange other neurons so that the inputs match
+	 */
+	void rearrangeNeuronConnections(fS_Neuron *newNeuron, SHIFT shift);
+
+};
+
+
+#endif
Index: cpp/frams/genetics/fS/fS_oper.cpp
===================================================================
--- cpp/frams/genetics/fS/fS_oper.cpp	(revision 958)
+++ cpp/frams/genetics/fS/fS_oper.cpp	(revision 958)
@@ -0,0 +1,699 @@
+// This file is a part of Framsticks SDK.  http://www.framsticks.com/
+// Copyright (C) 2019-2020  Maciej Komosinski and Szymon Ulatowski.
+// See LICENSE.txt for details.
+
+#include <float.h>
+#include <assert.h>
+#include "fS_oper.h"
+#include "frams/util/rndutil.h"
+
+#define FIELDSTRUCT GenoOper_fS
+static ParamEntry GENOfSparam_tab[] =
+		{
+				{"Genetics: fS",            1, FS_OPCOUNT + 5,},
+				{"fS_mut_add_part",         0, 0, "Add part",                 "f 0 100 10", FIELD(prob[FS_ADD_PART]),             "mutation: probability of adding a part",},
+				{"fS_mut_rem_part",         0, 0, "Remove part",              "f 0 100 10", FIELD(prob[FS_REM_PART]),             "mutation: probability of deleting a part",},
+				{"fS_mut_mod_part",         0, 0, "Modify part",              "f 0 100 10", FIELD(prob[FS_MOD_PART]),             "mutation: probability of changing the part type",},
+				{"fS_mut_add_joint",        0, 0, "Add joint",                "f 0 100 10", FIELD(prob[FS_ADD_JOINT]),            "mutation: probability of adding a joint",},
+				{"fS_mut_rem_joint",        0, 0, "Remove joint",             "f 0 100 10", FIELD(prob[FS_REM_JOINT]),            "mutation: probability of removing a joint",},
+				{"fS_mut_add_param",        0, 0, "Add param",                "f 0 100 10", FIELD(prob[FS_ADD_PARAM]),            "mutation: probability of adding a parameter",},
+				{"fS_mut_rem_param",        0, 0, "Remove param",             "f 0 100 10", FIELD(prob[FS_REM_PARAM]),            "mutation: probability of removing a parameter",},
+				{"fS_mut_mod_param",        0, 0, "Modify param",             "f 0 100 10", FIELD(prob[FS_MOD_PARAM]),            "mutation: probability of modifying a parameter",},
+				{"fS_mut_mod_mod",          0, 0, "Modify modifier",           "f 0 100 10", FIELD(prob[FS_MOD_MOD]),              "mutation: probability of modifying a modifier",},
+				{"fS_mut_add_neuro",        0, 0, "Add neuron",               "f 0 100 10", FIELD(prob[FS_ADD_NEURO]),            "mutation: probability of adding a neuron",},
+				{"fS_mut_rem_neuro",        0, 0, "Remove neuron",            "f 0 100 10", FIELD(prob[FS_REM_NEURO]),            "mutation: probability of removing a neuron",},
+				{"fS_mut_mod_neuro_conn",        0, 0, "Modify neuron connection",            "f 0 100 10", FIELD(prob[FS_MOD_NEURO_CONNECTION]), "mutation: probability of changing a neuron connection",},
+				{"fS_mut_add_neuro_conn",   0, 0, "Add neuron connection",    "f 0 100 10", FIELD(prob[FS_ADD_NEURO_CONNECTION]), "mutation: probability of adding a neuron connection",},
+				{"fS_mut_rem neuro_conn",   0, 0, "Remove neuron connection", "f 0 100 10", FIELD(prob[FS_REM_NEURO_CONNECTION]), "mutation: probability of removing a neuron connection",},
+				{"fS_mut_mod_neuro_params", 0, 0, "Modify neuron params",     "f 0 100 10", FIELD(prob[FS_MOD_NEURO_PARAMS]),     "mutation: probability of changing a neuron param",},
+				{"fS_circle_section",       0, 0, "Ensure circle section",    "d 0 1 1",    FIELD(ensureCircleSection),           "Ensure that ellipsoids and cylinders have circle cross-section"},
+				{"fS_use_elli",       0, 0, "Use ellipsoids in mutations",    "d 0 1 1",    FIELD(useElli),           "Use ellipsoids in mutations"},
+				{"fS_use_cub",       0, 0, "Use cuboids in mutations",    "d 0 1 1",    FIELD(useCub),           "Use cuboids in mutations"},
+				{"fS_use_cyl",       0, 0, "Use cylinders in mutations",    "d 0 1 1",    FIELD(useCyl),           "Use cylinders in mutations"},
+				{"fS_mut_add_part_strong",       0, 0, "Strong add part mutation",    "d 0 1 1",    FIELD(strongAddPart),           "Add part mutation will produce more parametrized parts"},
+		};
+
+#undef FIELDSTRUCT
+
+GenoOper_fS::GenoOper_fS()
+{
+	par.setParamTab(GENOfSparam_tab);
+	par.select(this);
+	par.setDefault();
+	supported_format = "S";
+}
+
+int GenoOper_fS::checkValidity(const char *geno, const char *genoname)
+{
+	try
+	{
+		fS_Genotype genotype(geno);
+		int errorPosition = genotype.checkValidityOfPartSizes();
+		if(errorPosition != 0)
+		{
+			logPrintf("GenoOper_fS", "checkValidity", LOG_ERROR, "Invalid part size");
+			return 1 + errorPosition;
+		}
+	}
+	catch (fS_Exception &e)
+	{
+		logPrintf("GenoOper_fS", "checkValidity", LOG_ERROR, e.what());
+		return 1 + e.errorPosition;
+	}
+	return 0;
+}
+
+
+int GenoOper_fS::mutate(char *&geno, float &chg, int &method)
+{
+	fS_Genotype genotype(geno);
+
+	// Calculate available part types
+	string availableTypes;
+	if(useElli)
+		availableTypes += ELLIPSOID;
+	if(useCub)
+		availableTypes += CUBOID;
+	if(useCyl)
+		availableTypes += CYLINDER;
+
+	// Select a mutation
+	bool result = false;
+	method = GenoOperators::roulette(prob, FS_OPCOUNT);
+	switch (method)
+	{
+		case FS_ADD_PART:
+			result = addPart(genotype, availableTypes);
+			break;
+		case FS_REM_PART:
+			result = removePart(genotype);
+			break;
+		case FS_MOD_PART:
+			result = changePartType(genotype, availableTypes);
+			break;
+		case FS_ADD_JOINT:
+			result = addJoint(genotype);
+			break;
+		case FS_REM_JOINT:
+			result = removeJoint(genotype);
+			break;
+		case FS_ADD_PARAM:
+			result = addParam(genotype);
+			break;
+		case FS_REM_PARAM:
+			result = removeParam(genotype);
+			break;
+		case FS_MOD_PARAM:
+			result = changeParam(genotype);
+			break;
+		case FS_MOD_MOD:
+			result = changeModifier(genotype);
+			break;
+		case FS_ADD_NEURO:
+			result = addNeuro(genotype);
+			break;
+		case FS_REM_NEURO:
+			result = removeNeuro(genotype);
+			break;
+		case FS_MOD_NEURO_CONNECTION:
+			result = changeNeuroConnection(genotype);
+			break;
+		case FS_ADD_NEURO_CONNECTION:
+			result = addNeuroConnection(genotype);
+			break;
+		case FS_REM_NEURO_CONNECTION:
+			result = removeNeuroConnection(genotype);
+			break;
+		case FS_MOD_NEURO_PARAMS:
+			result = changeNeuroParam(genotype);
+			break;
+	}
+
+	if (result)
+	{
+		free(geno);
+		geno = strdup(genotype.getGeno().c_str());
+		return GENOPER_OK;
+	}
+	return GENOPER_OPFAIL;
+}
+
+int GenoOper_fS::crossOver(char *&g0, char *&g1, float &chg0, float &chg1)
+{
+	assert(PARENT_COUNT == 2); // Cross over works only for 2 parents
+	fS_Genotype *parents[PARENT_COUNT] = {new fS_Genotype(g0), new fS_Genotype(g1)};
+
+	// Choose random subtrees that have similar size
+	Node *selected[PARENT_COUNT];
+	vector<Node*> allNodes0 = parents[0]->getAllNodes();
+	vector<Node*> allNodes1 = parents[1]->getAllNodes();
+
+	double bestQuotient = DBL_MAX;
+	for (int i = 0; i < crossOverTries; i++)
+	{
+		Node *tmp0 = allNodes0[rndUint(allNodes0.size())];
+		Node *tmp1 = allNodes1[rndUint(allNodes1.size())];
+		// Choose this pair if it is the most similar
+		double quotient = double(tmp0->getNodeCount()) / double(tmp1->getNodeCount());
+		if(quotient < 1.0)
+			quotient = 1.0 / quotient;
+		if (quotient < bestQuotient)
+		{
+			bestQuotient = quotient;
+			selected[0] = tmp0;
+			selected[1] = tmp1;
+		}
+		if (bestQuotient == 1.0)
+			break;
+	}
+
+	// Compute gene percentages in children
+	double subtreeSizes[PARENT_COUNT], restSizes[PARENT_COUNT];
+	for (int i = 0; i < PARENT_COUNT; i++)
+	{
+
+		subtreeSizes[i] = selected[i]->getNodeCount();
+		restSizes[i] = parents[i]->getNodeCount() - subtreeSizes[i];
+	}
+	chg0 = restSizes[0] / (restSizes[0] + subtreeSizes[1]);
+	chg1 = restSizes[1] / (restSizes[1] + subtreeSizes[0]);
+
+	// Rearrange neurons before crossover
+	int subOldStart[PARENT_COUNT] {-1, -1};
+	rearrangeConnectionsBeforeCrossover(parents[0], selected[0], subOldStart[0]);
+	rearrangeConnectionsBeforeCrossover(parents[1], selected[1], subOldStart[1]);
+
+	// Swap the subtress
+	for(int i=0; i<PARENT_COUNT; i++)
+	{
+		Node *other = selected[1 - i];
+		Node *p = selected[i]->parent;
+		if (p != nullptr)
+		{
+			size_t index = std::distance(p->children.begin(), std::find(p->children.begin(), p->children.end(), selected[i]));
+			p->children[index] = other;
+		} else
+			parents[i]->startNode = other;
+	}
+
+	// Rearrange neurons after crossover
+	rearrangeConnectionsAfterCrossover(parents[0], selected[1], subOldStart[0]);
+	rearrangeConnectionsAfterCrossover(parents[1], selected[0], subOldStart[1]);
+
+	// Clenup, assign children to result strings
+	free(g0);
+	free(g1);
+	g0 = strdup(parents[0]->getGeno().c_str());
+	g1 = strdup(parents[1]->getGeno().c_str());
+
+	delete parents[0];
+	delete parents[1];
+	return GENOPER_OK;
+}
+
+const char* GenoOper_fS::getSimplest()
+{
+	return "S:C{x=0.80599;y=0.80599;z=0.80599}";
+}
+
+uint32_t GenoOper_fS::style(const char *geno, int pos)
+{
+	char ch = geno[pos];
+	uint32_t style = GENSTYLE_CS(0, GENSTYLE_NONE);
+	if (ch == ELLIPSOID || ch == CUBOID || ch == CYLINDER) // part type
+	{
+		style = GENSTYLE_RGBS(0, 0, 200, GENSTYLE_BOLD);
+	}
+	else if(JOINTS.find(ch) != string::npos)	// Joint type
+	{
+		style = GENSTYLE_RGBS(0, 200, 200, GENSTYLE_BOLD);
+	}
+	else if(MODIFIERS.find(ch) != string::npos) // Modifier
+	{
+		style = GENSTYLE_RGBS(0, 200, 0, GENSTYLE_NONE);
+	}
+	else if (isdigit(ch) || strchr(".=", ch)) // Numerical value
+	{
+		style = GENSTYLE_RGBS(200, 0, 0, GENSTYLE_NONE);
+	}
+	else if(strchr("()_;[],", ch))
+	{
+		style = GENSTYLE_CS(0, GENSTYLE_BOLD); // Important char
+	}
+
+	return style;
+}
+
+void GenoOper_fS::rearrangeConnectionsBeforeCrossover(fS_Genotype *geno, Node *sub, int &subStart)
+{
+	vector<fS_Neuron*> genoNeurons = geno->getAllNeurons();
+	vector<fS_Neuron*> subNeurons = fS_Genotype::extractNeurons(sub);
+
+	if (!subNeurons.empty())
+	{
+		subStart = fS_Genotype::getNeuronIndex(genoNeurons, subNeurons[0]);
+		fS_Genotype::shiftNeuroConnections(genoNeurons, subStart, subStart + subNeurons.size() - 1, SHIFT::LEFT);
+	}
+}
+
+void GenoOper_fS::rearrangeConnectionsAfterCrossover(fS_Genotype *geno, Node *sub, int subOldStart)
+{
+	vector<fS_Neuron*> genoNeurons = geno->getAllNeurons();
+	vector<fS_Neuron*> subNeurons = fS_Genotype::extractNeurons(sub);
+
+	// Shift the inputs right
+	if (!subNeurons.empty())
+	{
+		int subStart = fS_Genotype::getNeuronIndex(genoNeurons, subNeurons[0]);
+		int subCount = subNeurons.size();
+		int subEnd = subStart + subCount - 1;
+		for (int i = 0; i < subCount; i++)
+		{
+			auto inputs = subNeurons[i]->inputs;
+			std::map<int, double> newInputs;
+			// TODO figure out how to keep internal connections in subtree
+//			for (auto it = inputs.begin(); it != inputs.end(); ++it)
+//			{
+//				int newIndex = it->first + subStart;
+//				if(subEnd > newIndex && newIndex > subStart)
+//					newInputs[newIndex] = it->second;
+//			}
+			subNeurons[i]->inputs = newInputs;
+		}
+		fS_Genotype::shiftNeuroConnections(genoNeurons, subStart, subEnd, SHIFT::RIGHT);
+	}
+}
+
+bool GenoOper_fS::addPart(fS_Genotype &geno, string availableTypes, bool mutateSize)
+{
+	geno.getState();
+	Node *node = geno.chooseNode();
+	char partType = availableTypes[rndUint(availableTypes.length())];
+
+	Substring substring(&partType, 0, 1);
+	Node *newNode = new Node(substring, node->modifierMode, node->paramMode, node->cycleMode, node);
+	// Add random rotation
+	string rotationParams[]{ROT_X, ROT_Y, ROT_Z};
+	if(strongAddPart)
+	{
+		for(int i=0; i < 3; i++)
+			newNode->params[rotationParams[i]] = RndGen.Uni(-90, 90);
+	}
+	else
+	{
+		string selectedParam = rotationParams[rndUint(3)];
+		newNode->params[selectedParam] = RndGen.Uni(-90, 90);
+	}
+	string rParams[]{RX, RY, RZ};
+	if(strongAddPart)
+	{
+		for(int i=0; i < 3; i++)
+			newNode->params[rParams[i]] = RndGen.Uni(-90, 90);
+	}
+	else
+	{
+		string selectedParam = rParams[rndUint(3)];
+		newNode->params[selectedParam] = RndGen.Uni(-90, 90);
+	}
+	// Assign part size to default value
+	double volumeMultiplier = pow(node->getParam(SIZE) * node->state->s, 3);
+	double minVolume = Model::getMinPart().volume;
+	double defVolume = Model::getDefPart().volume * volumeMultiplier;    // Default value after applying modifiers
+	double maxVolume = Model::getMaxPart().volume;
+	double volume = std::min(maxVolume, std::max(minVolume, defVolume));
+	double relativeVolume = volume / volumeMultiplier;    // Volume without applying modifiers
+
+	double newRadius = Node::calculateRadiusFromVolume(newNode->partType, relativeVolume);
+	newNode->params[SIZE_X] = newRadius;
+	newNode->params[SIZE_Y] = newRadius;
+	newNode->params[SIZE_Z] = newRadius;
+	node->children.push_back(newNode);
+
+	if (mutateSize)
+	{
+		geno.getState();
+		newNode->changeSizeParam(SIZE_X, fS_Genotype::randomParamMultiplier(), true);
+		newNode->changeSizeParam(SIZE_Y, fS_Genotype::randomParamMultiplier(), true);
+		newNode->changeSizeParam(SIZE_Z, fS_Genotype::randomParamMultiplier(), true);
+	}
+	return true;
+}
+
+bool GenoOper_fS::removePart(fS_Genotype &geno)
+{
+	Node *randomNode, *selectedChild;
+	// Choose a parent with children
+	for (int i = 0; i < mutationTries; i++)
+	{
+		randomNode = geno.chooseNode();
+		int childCount = randomNode->children.size();
+		if (childCount > 0)
+		{
+			int selectedIndex = rndUint(childCount);
+			selectedChild = randomNode->children[selectedIndex];
+			if (selectedChild->children.empty() && selectedChild->neurons.empty())
+			{
+				// Remove the selected child
+				swap(randomNode->children[selectedIndex], randomNode->children[childCount - 1]);
+				randomNode->children.pop_back();
+				randomNode->children.shrink_to_fit();
+				delete selectedChild;
+				return true;
+			}
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::changePartType(fS_Genotype &geno, string availTypes)
+{
+	int availTypesLength = availTypes.length();
+	for (int i = 0; i < mutationTries; i++)
+	{
+		Node *randomNode = geno.chooseNode();
+		int index = rndUint(availTypesLength);
+		if (availTypes[index] == SHAPETYPE_TO_GENE.at(randomNode->partType))
+			index = (index + 1 + rndUint(availTypesLength)) % availTypesLength;
+		char newTypeChr = availTypes[index];
+
+		auto itr = GENE_TO_SHAPETYPE.find(newTypeChr);
+		Part::Shape newType = itr->second;
+
+#ifdef _DEBUG
+		if(newType == randomNode->partType)
+			throw fS_Exception("Internal error: invalid part type chosen in mutation.", 1);
+#endif
+
+		if (ensureCircleSection)
+		{
+			geno.getState();
+			if (randomNode->partType == Part::Shape::SHAPE_CUBOID
+				|| (randomNode->partType == Part::Shape::SHAPE_CYLINDER && newType == Part::Shape::SHAPE_ELLIPSOID))
+			{
+				double sizeMultiplier = randomNode->getParam(SIZE) * randomNode->state->s;
+				double relativeVolume = randomNode->calculateVolume() / pow(sizeMultiplier, 3.0);
+				double newRelativeRadius = Node::calculateRadiusFromVolume(newType, relativeVolume);
+				randomNode->params[SIZE_X] = newRelativeRadius;
+				randomNode->params[SIZE_Y] = newRelativeRadius;
+				randomNode->params[SIZE_Z] = newRelativeRadius;
+			}
+		}
+		randomNode->partType = newType;
+		return true;
+	}
+	return false;
+}
+
+bool GenoOper_fS::addJoint(fS_Genotype &geno)
+{
+	if (geno.startNode->children.empty())
+		return false;
+
+	Node *randomNode;
+	for (int i = 0; i < mutationTries; i++)
+	{
+		char randomJoint = JOINTS[rndUint(JOINT_COUNT)];
+		randomNode = geno.chooseNode(1);        // First part does not have joints
+		if (randomNode->joint == DEFAULT_JOINT)
+		{
+			randomNode->joint = randomJoint;
+			return true;
+		}
+	}
+	return false;
+}
+
+
+bool GenoOper_fS::removeJoint(fS_Genotype &geno)
+{
+	// This operator may can lower success rate that others, as it does not work when there is only one node
+	if (geno.startNode->children.size() < 1) // Only one node; there are no joints
+		return false;
+
+	// Choose a node with joints
+	for (int i = 0; i < mutationTries; i++)
+	{
+		Node *randomNode = geno.chooseNode(1);    // First part does not have joints
+		if (randomNode->joint != DEFAULT_JOINT)
+		{
+			randomNode->joint = DEFAULT_JOINT;
+			return true;
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::addParam(fS_Genotype &geno)
+{
+	Node *randomNode = geno.chooseNode();
+	int paramCount = randomNode->params.size();
+	if (paramCount == int(PARAMS.size()))
+		return false;
+	string selectedParam = PARAMS[rndUint(PARAMS.size())];
+	// Not allow 'j' parameter when the cycle mode is not on
+	if (selectedParam == JOINT_DISTANCE && !geno.startNode->cycleMode)
+		return false;
+	if (randomNode->params.count(selectedParam) > 0)
+		return false;
+	// Do not allow invalid changes in part size
+	bool isRadiusOfBase = selectedParam == SIZE_X || selectedParam == SIZE_Y;
+	bool isRadius = isRadiusOfBase || selectedParam == SIZE_Z;
+	if (ensureCircleSection && isRadius)
+	{
+		if (randomNode->partType == Part::Shape::SHAPE_ELLIPSOID)
+			return false;
+		if (randomNode->partType == Part::Shape::SHAPE_CYLINDER && isRadiusOfBase)
+			return false;
+	}
+	// Add modified default value for param
+	randomNode->params[selectedParam] = defaultParamValues.at(selectedParam);
+	return true;
+}
+
+bool GenoOper_fS::removeParam(fS_Genotype &geno)
+{
+	// Choose a node with params
+	for (int i = 0; i < mutationTries; i++)
+	{
+		Node *randomNode = geno.chooseNode();
+		int paramCount = randomNode->params.size();
+		if (paramCount >= 1)
+		{
+			auto it = randomNode->params.begin();
+			advance(it, rndUint(paramCount));
+			randomNode->params.erase(it->first);
+			return true;
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::changeParam(fS_Genotype &geno)
+{
+	geno.getState();
+	for (int i = 0; i < mutationTries; i++)
+	{
+		Node *randomNode = geno.chooseNode();
+		int paramCount = randomNode->params.size();
+		if (paramCount >= 1)
+		{
+			auto it = randomNode->params.begin();
+			advance(it, rndUint(paramCount));
+
+			double multiplier = fS_Genotype::randomParamMultiplier();
+
+
+			// Do not allow invalid changes in part size
+			if (it->first != SIZE_X && it->first != SIZE_Y && it->first != SIZE_Z)
+			{
+				it->second *= multiplier;
+				return true;
+			} else
+				return randomNode->changeSizeParam(it->first, multiplier, ensureCircleSection);
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::changeModifier(fS_Genotype &geno)
+{
+	Node *randomNode = geno.chooseNode();
+	char randomModifier = MODIFIERS[rndUint(MODIFIERS.length())];
+	randomNode->modifiers[randomModifier] += rndUint(2) == 0 ? 1 : -1;
+
+	bool isSizeMod = tolower(randomModifier) == SIZE_MODIFIER;
+	if (isSizeMod && geno.checkValidityOfPartSizes() != 0)
+	{
+		randomNode->modifiers[randomModifier]++;
+		return false;
+	}
+	return true;
+}
+
+bool GenoOper_fS::addNeuro(fS_Genotype &geno)
+{
+	Node *randomNode = geno.chooseNode();
+	fS_Neuron *newNeuron;
+	NeuroClass *rndclass = GenoOperators::getRandomNeuroClass(Model::SHAPE_SOLIDS);
+	if(rndclass->preflocation == 2 && randomNode == geno.startNode)
+		return false;
+
+	const char *name = rndclass->getName().c_str();
+	newNeuron = new fS_Neuron(name, randomNode->partDescription->start, strlen(name));
+	int effectiveInputCount = rndclass->prefinputs > -1 ? rndclass->prefinputs : 1;
+	if (effectiveInputCount > 0)
+	{
+		// Create as many connections for the neuron as possible (at most prefinputs)
+		vector<fS_Neuron*> allNeurons = geno.getAllNeurons();
+		vector<int> neuronsWithOutput;
+		for (int i = 0; i < int(allNeurons.size()); i++)
+		{
+			if (allNeurons[i]->getClass()->prefoutput > 0)
+				neuronsWithOutput.push_back(i);
+		}
+		int size = neuronsWithOutput.size();
+		if (size > 0)
+		{
+			for (int i = 0; i < effectiveInputCount; i++)
+			{
+				int selectedNeuron = neuronsWithOutput[rndUint(size)];
+				newNeuron->inputs[selectedNeuron] = DEFAULT_NEURO_CONNECTION_WEIGHT;
+			}
+		}
+	}
+
+	randomNode->neurons.push_back(newNeuron);
+
+	geno.rearrangeNeuronConnections(newNeuron, SHIFT::RIGHT);
+	return true;
+}
+
+bool GenoOper_fS::removeNeuro(fS_Genotype &geno)
+{
+	Node *randomNode = geno.chooseNode();
+	for (int i = 0; i < mutationTries; i++)
+	{
+		randomNode = geno.chooseNode();
+		if (!randomNode->neurons.empty())
+		{
+			// Remove the selected neuron
+			int size = randomNode->neurons.size();
+			fS_Neuron *it = randomNode->neurons[rndUint(size)];
+			geno.rearrangeNeuronConnections(it, SHIFT::LEFT);        // Important to rearrange the neurons before deleting
+			swap(it, randomNode->neurons.back());
+			randomNode->neurons.pop_back();
+			randomNode->neurons.shrink_to_fit();
+			delete it;
+			return true;
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::changeNeuroConnection(fS_Genotype &geno)
+{
+	vector<fS_Neuron*> neurons = geno.getAllNeurons();
+	if (neurons.empty())
+		return false;
+
+	int size = neurons.size();
+	for (int i = 0; i < mutationTries; i++)
+	{
+		fS_Neuron *selectedNeuron = neurons[rndUint(size)];
+		if (!selectedNeuron->inputs.empty())
+		{
+			int inputCount = selectedNeuron->inputs.size();
+			auto it = selectedNeuron->inputs.begin();
+			advance(it, rndUint(inputCount));
+
+			it->second = GenoOperators::mutateNeuronProperty(it->second, selectedNeuron, -1);
+			return true;
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::addNeuroConnection(fS_Genotype &geno)
+{
+	vector<fS_Neuron*> neurons = geno.getAllNeurons();
+	if (neurons.empty())
+		return false;
+
+	int size = neurons.size();
+	fS_Neuron *selectedNeuron;
+	for (int i = 0; i < mutationTries; i++)
+	{
+		selectedNeuron = neurons[rndUint(size)];
+		if (selectedNeuron->acceptsInputs())
+			break;
+	}
+	if (!selectedNeuron->acceptsInputs())
+		return false;
+
+	for (int i = 0; i < mutationTries; i++)
+	{
+		int index = rndUint(size);
+		if (selectedNeuron->inputs.count(index) == 0 && neurons[index]->getClass()->getPreferredOutput() > 0)
+		{
+
+			selectedNeuron->inputs[index] = DEFAULT_NEURO_CONNECTION_WEIGHT;
+			return true;
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::removeNeuroConnection(fS_Genotype &geno)
+{
+	vector<fS_Neuron*> neurons = geno.getAllNeurons();
+	if (neurons.empty())
+		return false;
+
+	int size = neurons.size();
+	for (int i = 0; i < mutationTries; i++)
+	{
+		fS_Neuron *selectedNeuron = neurons[rndUint(size)];
+		if (!selectedNeuron->inputs.empty())
+		{
+			int inputCount = selectedNeuron->inputs.size();
+			auto it = selectedNeuron->inputs.begin();
+			advance(it, rndUint(inputCount));
+			selectedNeuron->inputs.erase(it->first);
+			return true;
+		}
+	}
+	return false;
+}
+
+bool GenoOper_fS::changeNeuroParam(fS_Genotype &geno)
+{
+	vector<fS_Neuron*> neurons = geno.getAllNeurons();
+	if (neurons.empty())
+		return false;
+
+	fS_Neuron *neu = neurons[rndUint(neurons.size())];
+	SyntParam par = neu->classProperties();
+
+	if (par.getPropCount() > 0)
+	{
+		int i = rndUint(par.getPropCount());
+		if (*par.type(i) == 'f')
+		{
+			double change = GenoOperators::mutateNeuronProperty(par.getDouble(i), neu, GenoOperators::NEUROCLASS_PROP_OFFSET + i);
+			par.setDouble(i, change);
+		}
+		SString line;
+		int tmp = 0;
+		par.update(&line);
+		SString props;
+		line.getNextToken(tmp, props, '\n'); // removal of newline character
+		if (props != "")
+		{
+			SString det = neu->getClass()->name + ": " + props;
+			neu->setDetails(det);
+			return true;
+		}
+	}
+
+	return false;
+}
Index: cpp/frams/genetics/fS/fS_oper.h
===================================================================
--- cpp/frams/genetics/fS/fS_oper.h	(revision 958)
+++ cpp/frams/genetics/fS/fS_oper.h	(revision 958)
@@ -0,0 +1,140 @@
+// This file is a part of Framsticks SDK.  http://www.framsticks.com/
+// Copyright (C) 2019-2020  Maciej Komosinski and Szymon Ulatowski.
+// See LICENSE.txt for details.
+
+#ifndef _FS_OPER_H_
+#define _FS_OPER_H_
+
+#include "fS_general.h"
+#include "../genooperators.h"
+
+
+/** @name Codes for general mutation types */
+//@{
+#define FS_ADD_PART 0
+#define FS_REM_PART 1
+#define FS_MOD_PART 2
+#define FS_ADD_JOINT 3
+#define FS_REM_JOINT 4
+#define FS_ADD_PARAM 5
+#define FS_REM_PARAM 6
+#define FS_MOD_PARAM 7
+#define FS_MOD_MOD 8
+#define FS_ADD_NEURO 9
+#define FS_REM_NEURO 10
+#define FS_MOD_NEURO_CONNECTION 11
+#define FS_ADD_NEURO_CONNECTION 12
+#define FS_REM_NEURO_CONNECTION 13
+#define FS_MOD_NEURO_PARAMS 14
+#define FS_OPCOUNT 15
+//@}
+
+
+const int PARENT_COUNT = 2;
+
+class GenoOper_fS : public GenoOperators
+{
+public:
+	static const int crossOverTries = 100;
+	double prob[FS_OPCOUNT];
+	paInt ensureCircleSection;
+	paInt useElli, useCub,  useCyl;
+	paInt strongAddPart;
+
+	GenoOper_fS();
+
+	int crossOver(char *&g1, char *&g2, float &chg1, float &chg2);
+
+	int checkValidity(const char *geno, const char *genoname);
+
+	int mutate(char *&geno, float &chg, int &method);
+
+	uint32_t style(const char *g, int pos);
+
+	const char* getSimplest();
+
+	/**
+	 * Remove connections to the subtree that will be removed from genotype
+	 * @param geno An fS_Genotype
+	 * @param sub A subtree that will be removed from genotype
+	 * @param subStart An index of the first neuron in the removed genotype
+	 */
+	void rearrangeConnectionsBeforeCrossover(fS_Genotype *geno, Node *sub, int &subStart);
+
+	/**
+	 *
+	 * @param geno An fS_Genotype
+	 * @param sub A subtree that was added to genotype
+	 * @param subOldStart An index of the first neuron in the subtree when it was in old genotype
+	 */
+	void rearrangeConnectionsAfterCrossover(fS_Genotype *geno, Node *sub, int subOldStart);
+
+
+	/**
+	 * Performs add part mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool addPart(fS_Genotype &geno, string availableTypes = "ECR", bool mutateSize = true);
+
+	/**
+	 * Performs remove part type mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool removePart(fS_Genotype &geno);
+
+	/**
+	 * Performs change part type mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool changePartType(fS_Genotype &geno, string availableTypes = "ECR");
+
+	/**
+	 * Performs add joint mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool addJoint(fS_Genotype &geno);
+
+	/**
+	 * Performs remove mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool removeJoint(fS_Genotype &geno);
+
+	/**
+	 * Performs add param mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool addParam(fS_Genotype &geno);
+
+	/**
+	 * Performs remove param mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool removeParam(fS_Genotype &geno);
+
+	/**
+	 * Performs change param mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool changeParam(fS_Genotype &geno);
+
+	/**
+	 * Performs change modifier mutation on genotype
+	 * @return true if mutation succeeded, false otherwise
+	 */
+	bool changeModifier(fS_Genotype &geno);
+
+	bool addNeuro(fS_Genotype &geno);
+
+	bool removeNeuro(fS_Genotype &geno);
+
+	bool changeNeuroConnection(fS_Genotype &geno);
+
+	bool addNeuroConnection(fS_Genotype &geno);
+
+	bool removeNeuroConnection(fS_Genotype &geno);
+
+	bool changeNeuroParam(fS_Genotype &geno);
+};
+
+#endif
Index: cpp/frams/genetics/gen-config-SDK.h
===================================================================
--- cpp/frams/genetics/gen-config-SDK.h	(revision 957)
+++ cpp/frams/genetics/gen-config-SDK.h	(revision 958)
@@ -1,4 +1,4 @@
 // This file is a part of Framsticks SDK.  http://www.framsticks.com/
-// Copyright (C) 1999-2015  Maciej Komosinski and Szymon Ulatowski.
+// Copyright (C) 1999-2020  Maciej Komosinski and Szymon Ulatowski.
 // See LICENSE.txt for details.
 
@@ -24,4 +24,5 @@
 #define USE_GENCONV_fH0
 #define USE_GENCONV_fL0
+#define USE_GENCONV_fS0 //solids
 
 //#define USE_GENMAN_f0
@@ -38,2 +39,3 @@
 #define USE_GENMAN_fH
 #define USE_GENMAN_fL
+#define USE_GENMAN_fS //solids
