Index: cpp/frams/genetics/f4/f4_general.cpp
===================================================================
--- cpp/frams/genetics/f4/f4_general.cpp	(revision 1233)
+++ cpp/frams/genetics/f4/f4_general.cpp	(revision 1234)
@@ -7,5 +7,5 @@
 
 #include "f4_general.h"
-#include "../genooperators.h" //for GENOPER_ constants
+#include "../genooperators.h" // for GENOPER_ constants
 #include <common/nonstd_stl.h>
 #include <common/log.h>
@@ -306,5 +306,6 @@
 				break;
 			}
-			case 'r':  case 'R':
+			case 'r':
+			case 'R':
 			{
 				// error: if neuron
@@ -340,5 +341,5 @@
 				// error: if neuron
 				if (type == CELL_NEURON) //some neurons have the same single-letter names as modifiers (for example G,S,D), but they are supposed to have is_neuroclass==true so they should indeed not be handled here
-				{//however, what we see here is actually modifiers such as IdqEbWL (so not valid neuroclasses) that occurred within an already differentiated cell type==CELL_NEURON.
+				{//however, what we see here is actually modifiers such as IdqEbWL (so not valid neuroclasses) that occurred within an already differentiated cell of type==CELL_NEURON.
 					//printf("Handled as a modifier, but type==CELL_NEURON: '%c'\n", name);
 					// fix: delete it
@@ -691,20 +692,20 @@
 int f4_Cells::simulate()
 {
-	constexpr bool print_debugging = false; //print the state of cells during development
+	const bool PRINT_CELLS_DEVELOPMENT = false; //print the state of cells
 	errorcode = GENOPER_OK;
 
 	for (int i = 0; i < cell_count; i++)  C[i]->active = true;
 
-	if (print_debugging) f4_Node::print_tree(C[0]->genot, 0);
-	if (print_debugging) print_cells("Initialization");
+	if (PRINT_CELLS_DEVELOPMENT) f4_Node::print_tree(C[0]->genot, 0);
+	if (PRINT_CELLS_DEVELOPMENT) print_cells("Initialization");
 
 	// execute oneStep() in a cycle
-	while (oneStep()) if (print_debugging) print_cells("Development step");
-	if (print_debugging) print_cells("After last development step");
+	while (oneStep()) if (PRINT_CELLS_DEVELOPMENT) print_cells("Development step");
+	if (PRINT_CELLS_DEVELOPMENT) print_cells("After last development step");
 
 #ifdef EXTRA_STEP_CELL_DEVELOPMENT
 	if (errorcode == GENOPER_OK)
 	{
-		oneStep(); if (print_debugging) print_cells("After extra step"); //for these "halted" (yielding) cells (they have active==false) that wait for other cells to develop. Without this step, the last, recently halted one(s) may miss the "retrying" step if all active==true cells became active==false in the last step.
+		oneStep(); if (PRINT_CELLS_DEVELOPMENT) print_cells("After extra step"); //for these "halted" (yielding) cells (they have active==false) that wait for other cells to develop. Without this step, the last, recently halted one(s) may miss the "retrying" step if all active==true cells became active==false in the last step.
 	}
 #endif
@@ -745,5 +746,5 @@
 	//DB( printf("Cell simulation done, %d cells. \n", nc); )
 
-	if (print_debugging) print_cells("Final");
+	if (PRINT_CELLS_DEVELOPMENT) print_cells("Final");
 
 	return errorcode;
@@ -1058,5 +1059,5 @@
 {
 	for (int i = 0; i < indent; i++) printf(" ");
-	printf("%s (%d)", root->name.c_str(), root->count());
+	printf("%s%s%s (%d)", root->neuclass != NULL ? "N:" : "", root->name.c_str(), root->name == "#" ? std::to_string(root->reps).c_str() : "", root->count() - 1);
 	if (root->name == "[")
 		printf("     from=%-3d  weight=%g", root->conn_from, root->conn_weight);
@@ -1253,21 +1254,26 @@
 }
 
-// scan genotype string and build tree
+// scan genotype string and build a tree
 // return >1 for error (errorpos)
-int f4_processRecur(const char* genot, int &pos_inout, f4_Node *parent)
-{
+int f4_processRecur(const char* genot, const int genot_len, int &pos_inout, f4_Node *parent)
+{
+	static const char *all_modifiers_no_comma = F14_MODIFIERS; //I did experiments with added comma (see all_modifiers_for_simplify below) which had the advantage of commas not breaking sequences of modifiers, thus longer sequences of modifiers (including commas) could be simplified and genetic bloat was further reduced. But since we impose a limit on the number of modifier chars in GenoOperators::simplifiedModifiers(), it would also influence commas (e.g. no more than 8 commas per sequence), so in order to leave commas entirely unlimited let's exclude them from simplification. Note that currently 'X' or any other non-F14_MODIFIERS char also separates the sequence to be simplified, so if we wanted a really intensive simplification, it should occur during development, when we know precisely which genes influence each f4_Cell.
+	//const char *Geno_f4::all_modifiers_for_simplify = F14_MODIFIERS ",\1"; //'\1' added to keep the number of chars even, avoid exceptions in logic and save the simple rule that the sequence is made of pairs (gene,contradictory gene), where a comma has no contradictory gene and \1 is unlikely to occur in the f4 genotype (and not allowed), so no risk it will cancel out a comma during simplification.
+
+
 	f4_Node *par = parent;
 
-	if (pos_inout >= (int)strlen(genot))
-		return (int)strlen(genot) + 1;
-
-	while (pos_inout < (int)strlen(genot))
-	{
-//#define PRINT_PARSING_LOCATION
-#ifdef PRINT_PARSING_LOCATION
-		printf("%s\n", genot);
-		for (int i = 0; i < pos_inout; i++) printf(" ");
-		printf("^\n");
-#endif
+	if (pos_inout >= genot_len)
+		return genot_len + 1;
+
+	while (pos_inout < genot_len)
+	{
+		const bool PRINT_PARSING_LOCATION = false;
+		if (PRINT_PARSING_LOCATION)
+		{
+			printf("%s\n", genot);
+			for (int i = 0; i < pos_inout; i++) printf(" ");
+			printf("^\n");
+		}
 		switch (genot[pos_inout])
 		{
@@ -1277,9 +1283,9 @@
 			par = node;
 			pos_inout++; //move after '<'
-			int res = f4_processRecur(genot, pos_inout, par);
+			int res = f4_processRecur(genot, genot_len, pos_inout, par);
 			if (res) return res;
-			if (pos_inout < (int)strlen(genot))
-			{
-				res = f4_processRecur(genot, pos_inout, par);
+			if (pos_inout < genot_len)
+			{
+				res = f4_processRecur(genot, genot_len, pos_inout, par);
 				if (res) return res;
 			}
@@ -1287,7 +1293,7 @@
 			{
 				//MacKo 2023-04, more strict behavior: instead of silent repair (no visible effect to the user, genotype stays invalid but is interpreted and reported as valid), we now point out where the error is. For example <X> or <X><X or <X><N:N>
-				return (int)strlen(genot) + 1;
+				return genot_len + 1;
 				//old silent repair:
-				//node = new f4_Node(">", par, int(strlen(genot)) - 1);
+				//node = new f4_Node(">", par, genot_len - 1);
 			}
 			return 0;  // OK
@@ -1310,16 +1316,16 @@
 			// skip number
 			pos_inout += end - (genot + pos_inout);
-			int res = f4_processRecur(genot, pos_inout, node);
+			int res = f4_processRecur(genot, genot_len, pos_inout, node);
 			if (res) return res;
-			if (pos_inout < (int)strlen(genot))
-			{
-				res = f4_processRecur(genot, pos_inout, node);
+			if (pos_inout < genot_len)
+			{
+				res = f4_processRecur(genot, genot_len, pos_inout, node);
 				if (res) return res;
 			}
 			else // ran out
 			{
-				return (int)strlen(genot) + 1; //MacKo 2023-04: report an error, better to be more strict instead of a silent repair (genotype stays invalid but is interpreted and reported as valid) with non-obvious consequences?
+				return genot_len + 1; //MacKo 2023-04: report an error, better to be more strict instead of a silent repair (genotype stays invalid but is interpreted and reported as valid) with non-obvious consequences?
 				//earlier apporach - silently treating this problem (we don't ever see where the error is because it gets corrected in some way here, while parsing the genotype, and error location in the genotype is never reported):
-				//node = new f4_Node(">", par, int(strlen(genot)) - 1); // check if needed and if this is really the best repair operation; seemed to happen too many times in succession for some genotypes even though they were only a result of f4 operators, not manually created... and the operators should not generate invalid genotypes, right? Or maybe crossover does? Seems like too many #N's for closing >'s; removing #N or adding > helped. Operators somehow don't do it properly sometimes? But F4_ADD_REP adds '>'... (TODO)
+				//node = new f4_Node(">", par, genot_len - 1); // check if needed and if this is really the best repair operation; seemed to happen too many times in succession for some genotypes even though they were only a result of f4 operators, not manually created... and the operators should not generate invalid genotypes, right? Or maybe crossover does? Seems like too many #N's for closing >'s; removing #N or adding > helped. Operators somehow don't do it properly sometimes? But F4_ADD_REP adds '>'... (TODO)
 			}
 			return 0;  // OK
@@ -1391,11 +1397,48 @@
 			break;
 		}
-		default: // 'X' and ',' and all modifiers and also invalid symbols - add a node, for invalid symbols build will give the error or repair
+		default: // 'X' and ',' and all modifiers and also invalid symbols - add a node. For symbols that are not valid in f4, the cell development process will give the error or repair
 		{
 			//printf("any regular character '%c'\n", genot[pos_inout]);
-			//TODO here: read a continuous sequence of modifiers, sort and optimize ("collapse") it like in f1, then add to tree
+#define F4_SIMPLIFY_MODIFIERS //avoid long sequences like ...<X>llmlIilImmimiimmimifmfl<fifmmimilimmmiimiliffmfliIfififlliflimfliffififmiffmflllfflimlififfiiffifIr<r<... - another option, instead of simplifying while parsing here, would be mutations: when they add/modify/remove a modifier node, they could "clean" the tree by removing nodes when they encounter contradictory modifiers on the same subpath, and also limit the number of modifiers just as GenoOperators::simplifiedModifiers() does.
+#ifdef F4_SIMPLIFY_MODIFIERS
+			char *ptr = (char*)(genot + pos_inout);
+
+#ifdef __BORLANDC__ // "[bcc32c Error] cannot compile this non-trivial TLS destruction yet" (C++B 10.4u2)
+			static
+#else
+			thread_local 
+#endif
+				vector<int> modifs_counts(strlen(all_modifiers_no_comma)); ///<an array with a known constant size storing counters of each modifier symbol from all_modifiers_no_comma, created once to avoid reallocation every time when modifier genes are simplified during parsing. Initialization of required size; it will never be resized.
+			std::fill(modifs_counts.begin(), modifs_counts.end(), 0); //zeroing only needed if we encountered a char from all_modifiers_no_comma and enter the 'while' loop below
+
+			while (char *m = GenoOperators::strchrn0(all_modifiers_no_comma, *ptr)) //only processes a section of chars known in all_modifiers_no_comma, other characters will exit the loop
+			{
+				modifs_counts[m - all_modifiers_no_comma]++;
+				GenoOperators::skipWS(++ptr); //advance and ignore whitespace
+			}
+			int advanced = ptr - (genot + pos_inout);
+			if (advanced > 0) //found modifiers
+			{
+				string simplified = GenoOperators::simplifiedModifiers(all_modifiers_no_comma, modifs_counts);
+				// add a node for each char in "simplified"
+				for (size_t i = 0; i < simplified.length(); i++)
+				{
+					int pos = GenoOperators::strchrn0(genot + pos_inout, simplified[i]) - genot; //unnecessarily finding the same char, if it occurrs multiple times in simplified
+					f4_Node *node = new f4_Node(simplified[i], par, pos); //location is approximate. In the simplification process we don't trace where the origin(s) of the simplified[i] gene were. We provide 'pos' as the first occurrence of simplified[i] (for example, all 'L' will have the same location assigned, but at least this is where 'L' occurred in the genotype, so in case of any modification of a node (repair, removal, whatever... even mapping of genes) the indicated gene will be one of the responsible ones)
+					par = node;
+				}
+				pos_inout += advanced;
+			}
+			else // genot[pos_inout] is a character not present in all_modifiers_no_comma, so treat it as a regular individual char just as it would be without simplification
+			{
+				f4_Node *node = new f4_Node(genot[pos_inout], par, pos_inout);
+				par = node;
+				pos_inout++;
+			}
+#else
 			f4_Node *node = new f4_Node(genot[pos_inout], par, pos_inout);
 			par = node;
 			pos_inout++;
+#endif // F4_SIMPLIFY_MODIFIERS
 			break;
 		}
@@ -1406,7 +1449,7 @@
 	if (par && par->name != ">")
 	{
-		//happens when pos_inout == strlen(genot)
+		//happens when pos_inout == genot_len
 		//return pos_inout; //MacKo 2023-04: could report an error instead of silent repair, but repair operators only work in Cells (i.e., after the f4_Node tree has been parsed without errors and Cells can start developing) so we don't want to make a fatal error because of missing '>' here. Also after conversions from Cells to text, trailing '>' is deliberately removed... and also the simplest genotype is officially X, not X>.
-		new f4_Node('>', par, int(strlen(genot)) - 1);
+		new f4_Node('>', par, genot_len - 1);
 	}
 
@@ -1417,5 +1460,5 @@
 {
 	int pos = 0;
-	int res = f4_processRecur(genot, pos, root);
+	int res = f4_processRecur(genot, (int)strlen(genot), pos, root);
 	if (res > 0)
 		return res; //parsing error
Index: cpp/frams/genetics/f4/f4_general.h
===================================================================
--- cpp/frams/genetics/f4/f4_general.h	(revision 1233)
+++ cpp/frams/genetics/f4/f4_general.h	(revision 1234)
@@ -471,9 +471,10 @@
  * This is an internal function; for regular cases, use f4_process().
  * @param genot the string with the entire genotype
+ * @param genot_len length of genot (precomputed for efficiency)
  * @param pos_inout the current position of processing in string (advanced by the function)
  * @param parent current parent of the analysed branch of the genotype
  * @return 0 if processing was successful, otherwise returns the position of an error in the genotype
  */
-int f4_processRecur(const char *genot, int &pos_inout, f4_Node *parent);
+int f4_processRecur(const char *genot, const int genot_len, int &pos_inout, f4_Node *parent);
 
 /**
Index: cpp/frams/genetics/f4/f4_oper.cpp
===================================================================
--- cpp/frams/genetics/f4/f4_oper.cpp	(revision 1233)
+++ cpp/frams/genetics/f4/f4_oper.cpp	(revision 1234)
@@ -11,13 +11,14 @@
 // may help, but it would be better to improve the source code to make genetic operators neutral in terms of genotype length. Adding such a penalty
 // removes "work in progress" changes in genotypes thus promoting immediate, straightforward improvements while hindering slower, multifaceted progress.
-// TODO getting rid of redundancy (valid genotypes with a lot of "junk code") in this representation looks like a good idea; many improvements to this end have already been done in April & May 2023.
+// TODO getting rid of redundancy (valid genotypes with a lot of "junk code") in this representation looks like a good idea; many improvements to this end have already been done in April & May 2023, so maybe it is not a big problem now?
 // 
 //
 // TODO the behavior of neuron input indexes during mutation seems badly implemented (see also TREAT_BAD_CONNECTIONS_AS_INVALID_GENO). Are they kept properly maintained when nodes are added and removed? This could be done well because during mutation we operate on the tree structure with cross-references between nodes (so they should not be affected by local changes in the tree), and then convert the tree back to string. Yet, the f4_Node.conn_from is an integer and these fields in nodes do not seem to be maintained on tree node adding/removal... change these integer offsets to references to node objects? But actually, do the offsets that constitute relative connection references concern the f4_Node tree structure (and all these sophisticated calculations of offsets during mutation are useful) or rather they concern the f4_Cells development? verify all situations in f4_Cell::oneStep(), case '['.
-// TODO add simplifying sequences of modifiers (so capital and small letter cancel out, like in f1) - but seems like each single modifier is a separate f4_Node? and perhaps we don't want to use the repair mechanism for this... maybe mutations, when they add/modify/remove a modifier node, should be "cleaning" the tree by removing nodes when they encounter contradictory modifiers on the same subpath, and also limit the number of modifiers of each type just like in f1? To avoid sequences like ...<X>llmlIilImmimiimmimifmfl<fifmmimilimmmiimiliffmfliIfififlliflimfliffififmiffmfliflifmIlimimiflimfiffmllliflmimifllifliliflifmIlimimiflimfiffmllliflmimifllfmIlimimiflimfiffmllliflmimiflliflimimmiflimfliffmiflifmfiffllIlififliffififmiffmfliflifIliflimimflimflfflimimifllfflifllfflimlififfiiffifIr<r<...
 // TODO in mutation, adding the '#' gene does not seem to be effective. The gene is added and genotypes are valid, but hardly ever #n is effective, i.e., it hardly ever multiplicates body or brain parts... investigate!
 // TODO add support for properties of (any class of) neurons - not just sigmoid/force/intertia (':' syntax) for N
 // TODO add mapping genotype character ranges for neural [connections]
 // TODO change the default branching plane (to match f1) so they do not grow perfectly vertical (cheating vertpos) so easily? (so they require Rr or other modifiers)
+// TODO for some genotypes, #defining/undefining F4_SIMPLIFY_MODIFIERS produces significantly different phenotypes (e.g. length of some Joint changes from 1.25 to 1.499, coordinates of Parts change, friction of some part changes from 1.28 to 0.32). Comparing f4_Node trees, the simplification works as intended, there are no huge changes apart from removing contradicting modifiers like 'R' and 'r' or 'L' and 'l', and dispersing the modifiers (changed order). There is no reason for such a significant influence of this. A hypothesis is that something may be wrong with calculating the influence of individual modifiers, e.g. some strong nonlinearity is introduced where it should not be, or some compensation between modifiers that should not influence each other (like L and R), or some modifier f4_Nodes are skipped/ignored when applying? Investigate. Example genotype: /*4*/,i<qlM,C<X>N:*#1>>,r<MRF<Xcm>N:Gpart>#5#1#2MLL#1>#1>>>>#5ML#2L#1>>>Lf,r<#1>rM<CqmLlCfqiFLqXFfl><F,<<XI>iN:|[-1:4.346]><XF><<XrRQ>N:G#3>>QiXFMR>fXM#2MfcR>R#3>>X
+
 
 
@@ -32,5 +33,5 @@
 
 
-const char *Geno_f4::all_modifiers = F14_MODIFIERS ","; //comma in f4 is handled the same way (simple node, F4_ADD_SIMP) as modifiers
+const char *Geno_f4::all_modifiers = F14_MODIFIERS ","; //comma in f4 is handled the same way (simple node, F4_ADD_SIMP) as modifiers. See also all_modifiers_no_comma in f4_general.cpp.
 
 // codes that can be changed (apart from being added/deleted)
@@ -294,9 +295,11 @@
 		{
 			// add simple node
-			// choose a simple node from ADD_SIMPLE_CODES
+			int modifier_index = GenoOperators::getRandomChar(all_modifiers, excluded_modifiers.c_str());
+			if (modifier_index < 0)
+				return GENOPER_OPFAIL;
 			node_mutated->parent->removeChild(node_mutated);
-			//f4_Node *n2 = new f4_Node(ADD_SIMPLE_CODES[rndUint(strlen(ADD_SIMPLE_CODES))], n1->parent, n1->parent->pos);
-			int modifierid = GenoOperators::getRandomChar(all_modifiers, excluded_modifiers.c_str());
-			f4_Node *n2 = new f4_Node(all_modifiers[modifierid], node_mutated->parent, node_mutated->parent->pos);
+			// old source: choose a simple node from ADD_SIMPLE_CODES
+			//f4_Node *n2 = new f4_Node(ADD_SIMPLE_CODES[rndUint(strlen(ADD_SIMPLE_CODES))], node_mutated->parent, node_mutated->parent->pos);
+			f4_Node *n2 = new f4_Node(all_modifiers[modifier_index], node_mutated->parent, node_mutated->parent->pos);
 			n2->addChild(node_mutated);
 			node_mutated->parent = n2;
Index: cpp/frams/genetics/f4/f4_oper.h
===================================================================
--- cpp/frams/genetics/f4/f4_oper.h	(revision 1233)
+++ cpp/frams/genetics/f4/f4_oper.h	(revision 1234)
@@ -62,5 +62,5 @@
 	static const char *all_modifiers;
 
-protected:
+private:
 
 	/**
