From 596220ab58befb5c6de7e7927b615e4049893ccd Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:22:46 +0200 Subject: [PATCH 01/47] Create l_system_decoding.py Just created a basic structure with Copilot ... now we can start the real stuf :) --- .../decoders/l_system_decoding.py | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py new file mode 100644 index 00000000..65f47985 --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -0,0 +1,152 @@ +"""Example of L-system-based decoding for modular robot graphs. + +Author: omn (with help from GitHub Copilot) +Date: 2025-09-25 +Py Ver: 3.12 +OS: macOS Sequoia 15.3.1 +Status: Prototype + +Notes +----- + * This decoder uses an L-system string as the genotype to generate a directed graph (DiGraph) using NetworkX. + * The L-system rules and axiom define the growth of the modular robot structure. + +References +---------- + [1] https://en.wikipedia.org/wiki/L-system + [2] https://networkx.org/documentation/stable/reference/readwrite/generated/networkx.readwrite.json_graph.tree_data.html + +""" + +# Standard library +import json +from pathlib import Path +from typing import Any, Callable, Dict, Optional + +# Third-party libraries +import matplotlib.pyplot as plt +import networkx as nx +from networkx import DiGraph +from networkx.readwrite import json_graph + +# Local libraries (reuse draw/save from hi_prob_decoding if available) +# from .hi_prob_decoding import draw_graph, save_graph_as_json + +SEED = 42 +DPI = 300 + +class LSystemDecoder: + """Implements an L-system-based decoder for modular robot graphs.""" + + def __init__( + self, + axiom: str, + rules: Dict[str, str], + iterations: int = 2, + module_type_map: Optional[Dict[str, str]] = None, + ) -> None: + """ + Initialize the L-system decoder. + + Parameters + ---------- + axiom : str + The initial string (genotype) of the L-system. + rules : dict + Production rules for the L-system. + iterations : int + Number of iterations to apply the rules. + module_type_map : dict, optional + Maps L-system symbols to module types (for node attributes). + """ + self.axiom = axiom + self.rules = rules + self.iterations = iterations + self.module_type_map = module_type_map or {} + self.lsystem_string = self._generate_lsystem() + self.graph = nx.DiGraph() + self._decode_lsystem_to_graph() + + def _generate_lsystem(self) -> str: + """Generate the L-system string after the given number of iterations.""" + current = self.axiom + for _ in range(self.iterations): + next_str = "".join(self.rules.get(c, c) for c in current) + current = next_str + return current + + def _decode_lsystem_to_graph(self) -> None: + """ + Decode the L-system string into a directed graph. + Supports branches using '[' (push) and ']' (pop) as in classic L-systems. + Each symbol is a module; edges are created from the current parent node. + """ + stack = [] # Stack for branching + prev_node = None + idx = 0 + for symbol in self.lsystem_string: + if symbol == '[': + # Push the current node onto the stack + stack.append(prev_node) + elif symbol == ']': + # Pop the node from the stack + if stack: + prev_node = stack.pop() + else: + node_label = f"{symbol}{idx}" + self.graph.add_node( + node_label, + type=self.module_type_map.get(symbol, symbol), + ) + if prev_node is not None: + self.graph.add_edge(prev_node, node_label) + prev_node = node_label + idx += 1 + + def get_graph(self) -> DiGraph: + """Return the generated NetworkX DiGraph.""" + return self.graph + + def save_graph_as_json(self, save_file: Path | str | None = None) -> None: + """Save the graph as a JSON file (node-link format).""" + if save_file is None: + return + data = json_graph.node_link_data(self.graph, edges="edges") + json_string = json.dumps(data, indent=4) + with Path(save_file).open("w", encoding="utf-8") as f: + f.write(json_string) + + def draw_graph( + self, + title: str = "L-System Decoded Graph", + save_file: Path | str | None = None, + ) -> None: + """Draw the decoded graph using matplotlib and networkx.""" + plt.figure() + pos = nx.spring_layout(self.graph, seed=SEED) + options = { + "with_labels": True, + "node_size": 150, + "node_color": "#FFFFFF00", + "edgecolors": "blue", + "font_size": 8, + "width": 0.5, + } + nx.draw(self.graph, pos, **options) + plt.title(title) + if save_file: + plt.savefig(save_file, dpi=DPI) + else: + plt.show() + +# Example usage (can be removed or moved to a test file) +if __name__ == "__main__": + # Example: F->F+F, axiom F, 3 iterations + axiom = "F" + rules = {"F": "C[H][B][H][N]", + "H" : "HB", + "B" : "BH"} + module_type_map = {"C": "CORE", "H": "HINGE", "B": "BRICK"} + decoder = LSystemDecoder(axiom, rules, iterations=3, module_type_map=module_type_map) + decoder.draw_graph() + decoder.save_graph_as_json("lsystem_graph.json") \ No newline at end of file From 729e441bf2d95076cdadd1fbbe69ecda6dc1a6c6 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Thu, 25 Sep 2025 22:18:22 +0200 Subject: [PATCH 02/47] Update l_system_decoding.py added the rotation in it --- .../decoders/l_system_decoding.py | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index 65f47985..79ea7948 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -58,7 +58,7 @@ def __init__( Number of iterations to apply the rules. module_type_map : dict, optional Maps L-system symbols to module types (for node attributes). - """ + """ self.axiom = axiom self.rules = rules self.iterations = iterations @@ -79,24 +79,49 @@ def _decode_lsystem_to_graph(self) -> None: """ Decode the L-system string into a directed graph. Supports branches using '[' (push) and ']' (pop) as in classic L-systems. - Each symbol is a module; edges are created from the current parent node. + Each gene can encode orientation as SYMBOL(orientation), e.g., B(90). + Orientation is stored as a node attribute (default 0 if not specified). """ + import re stack = [] # Stack for branching prev_node = None idx = 0 - for symbol in self.lsystem_string: - if symbol == '[': - # Push the current node onto the stack + # Regex to match symbol with optional orientation, e.g., B(90) + token_pattern = re.compile(r"([A-Za-z])(?:\((\d{1,3})\))?") + # Tokenize the string, preserving brackets + tokens = [] + i = 0 + s = self.lsystem_string + while i < len(s): + if s[i] in '[]': + tokens.append(s[i]) + i += 1 + elif s[i].isalpha(): + m = token_pattern.match(s, i) + if m: + symbol = m.group(1) + orientation = int(m.group(2)) if m.group(2) is not None else 0 + tokens.append((symbol, orientation)) + i = m.end() + else: + tokens.append((s[i], 0)) + i += 1 + else: + i += 1 # skip any other character + + for token in tokens: + if token == '[': stack.append(prev_node) - elif symbol == ']': - # Pop the node from the stack + elif token == ']': if stack: prev_node = stack.pop() else: + symbol, orientation = token node_label = f"{symbol}{idx}" self.graph.add_node( node_label, type=self.module_type_map.get(symbol, symbol), + orientation=orientation, ) if prev_node is not None: self.graph.add_edge(prev_node, node_label) @@ -143,9 +168,9 @@ def draw_graph( if __name__ == "__main__": # Example: F->F+F, axiom F, 3 iterations axiom = "F" - rules = {"F": "C[H][B][H][N]", - "H" : "HB", - "B" : "BH"} + rules = {"F": "C[H(180)][B(45)][H(90)][N]", + "H" : "H(45)B", + "B" : "BH(45)"} module_type_map = {"C": "CORE", "H": "HINGE", "B": "BRICK"} decoder = LSystemDecoder(axiom, rules, iterations=3, module_type_map=module_type_map) decoder.draw_graph() From b57d60dc139858f890475afd9be8929414ffdda2 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Fri, 26 Sep 2025 08:24:13 +0200 Subject: [PATCH 03/47] Update l_system_decoding.py updated to add parameter for rotation and face as well as to check that we stay in the values defined in the config file enums. --- .../decoders/l_system_decoding.py | 76 +++++++++++++------ 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index 79ea7948..30207624 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -1,3 +1,4 @@ + """Example of L-system-based decoding for modular robot graphs. Author: omn (with help from GitHub Copilot) @@ -19,9 +20,12 @@ """ # Standard library + import json +import re from pathlib import Path from typing import Any, Callable, Dict, Optional +from enum import Enum # Third-party libraries import matplotlib.pyplot as plt @@ -29,12 +33,21 @@ from networkx import DiGraph from networkx.readwrite import json_graph -# Local libraries (reuse draw/save from hi_prob_decoding if available) -# from .hi_prob_decoding import draw_graph, save_graph_as_json + +# Local libraries +from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType SEED = 42 DPI = 300 +class SymbolToModuleType(Enum): + """Enum for module types.""" + + C = 'CORE' + B = 'BRICK' + H = 'HINGE' + N = 'NONE' + class LSystemDecoder: """Implements an L-system-based decoder for modular robot graphs.""" @@ -79,19 +92,19 @@ def _decode_lsystem_to_graph(self) -> None: """ Decode the L-system string into a directed graph. Supports branches using '[' (push) and ']' (pop) as in classic L-systems. - Each gene can encode orientation as SYMBOL(orientation), e.g., B(90). - Orientation is stored as a node attribute (default 0 if not specified). + Each gene can encode orientation and face as SYMBOL(rotation,face), e.g., B(90,FRONT). + Orientation and face are stored as node attributes (defaults: 0, FRONT). """ - import re stack = [] # Stack for branching prev_node = None idx = 0 - # Regex to match symbol with optional orientation, e.g., B(90) - token_pattern = re.compile(r"([A-Za-z])(?:\((\d{1,3})\))?") + # Regex to match symbol with optional rotation and face, e.g., B(90,FRONT) + token_pattern = re.compile(r"([A-Za-z])(?:\((\d{1,3})(?:,([A-Za-z]+))?\))?") # Tokenize the string, preserving brackets tokens = [] i = 0 s = self.lsystem_string + core_count = 0 while i < len(s): if s[i] in '[]': tokens.append(s[i]) @@ -100,11 +113,35 @@ def _decode_lsystem_to_graph(self) -> None: m = token_pattern.match(s, i) if m: symbol = m.group(1) - orientation = int(m.group(2)) if m.group(2) is not None else 0 - tokens.append((symbol, orientation)) + # Enforce node type from ModuleType enum + try: + symbol_to_look = SymbolToModuleType[symbol] + node_type = ModuleType[symbol_to_look.value] + except KeyError: + raise ValueError(f"Symbol '{symbol}' is not a valid ModuleType enum name.") + if node_type == ModuleType.CORE: + core_count += 1 + if core_count > 1: + raise ValueError("L-system string contains more than one CORE module.") + # Parse and validate orientation + if m.group(2) is not None: + try: + rotation_val = int(m.group(2)) + rotation_enum = next((r for r in ModuleRotationsTheta if r.value == rotation_val), ModuleRotationsTheta.DEG_0) + except Exception: + rotation_enum = ModuleRotationsTheta.DEG_0 + else: + rotation_enum = ModuleRotationsTheta.DEG_0 + face_str = m.group(3) if m.group(3) is not None else "FRONT" + try: + face = ModuleFaces[face_str] + except KeyError: + face = ModuleFaces.FRONT + tokens.append((symbol, node_type, rotation_enum, face)) i = m.end() else: - tokens.append((s[i], 0)) + # fallback: treat as NONE + tokens.append((s[i], ModuleType.NONE, ModuleRotationsTheta.DEG_0, ModuleFaces.FRONT)) i += 1 else: i += 1 # skip any other character @@ -116,12 +153,13 @@ def _decode_lsystem_to_graph(self) -> None: if stack: prev_node = stack.pop() else: - symbol, orientation = token + symbol, node_type, rotation_enum, face = token node_label = f"{symbol}{idx}" self.graph.add_node( node_label, - type=self.module_type_map.get(symbol, symbol), - orientation=orientation, + type=node_type, + rotation=rotation_enum, + face=face, ) if prev_node is not None: self.graph.add_edge(prev_node, node_label) @@ -163,15 +201,3 @@ def draw_graph( plt.savefig(save_file, dpi=DPI) else: plt.show() - -# Example usage (can be removed or moved to a test file) -if __name__ == "__main__": - # Example: F->F+F, axiom F, 3 iterations - axiom = "F" - rules = {"F": "C[H(180)][B(45)][H(90)][N]", - "H" : "H(45)B", - "B" : "BH(45)"} - module_type_map = {"C": "CORE", "H": "HINGE", "B": "BRICK"} - decoder = LSystemDecoder(axiom, rules, iterations=3, module_type_map=module_type_map) - decoder.draw_graph() - decoder.save_graph_as_json("lsystem_graph.json") \ No newline at end of file From 1452dab6ebb46495b7f942a546bf55c9fd13e191 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:09:53 +0200 Subject: [PATCH 04/47] Update l_system_decoding.py major update with refact of the code... should be good now --- .../decoders/l_system_decoding.py | 227 +++++++++++------- 1 file changed, 134 insertions(+), 93 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index 30207624..ff3caedc 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -56,115 +56,154 @@ def __init__( axiom: str, rules: Dict[str, str], iterations: int = 2, - module_type_map: Optional[Dict[str, str]] = None, ) -> None: """ Initialize the L-system decoder. - - Parameters - ---------- - axiom : str - The initial string (genotype) of the L-system. - rules : dict - Production rules for the L-system. - iterations : int - Number of iterations to apply the rules. - module_type_map : dict, optional - Maps L-system symbols to module types (for node attributes). - """ + Automatically expands the L-system and builds the graph. + """ self.axiom = axiom self.rules = rules self.iterations = iterations - self.module_type_map = module_type_map or {} - self.lsystem_string = self._generate_lsystem() self.graph = nx.DiGraph() - self._decode_lsystem_to_graph() + self.lsystem_string = self.expand_lsystem() + self.build_graph_from_string(self.lsystem_string) + + def expand_lsystem(self, axiom: str = None, rules: Dict[str, str] = None, iterations: int = None) -> str: + """ + Generate the L-system string after the given number of iterations, recursively expanding inside brackets as well, but stopping at the required depth. + Each token is replaced in place by its rule expansion (not appended after). + """ + gene_pattern = re.compile(r"([A-Za-z](?:\(\d{1,3}(?:,[A-Za-z]+)?\))?)|\[|\]") + axiom = axiom if axiom is not None else self.axiom + rules = rules if rules is not None else self.rules + iterations = iterations if iterations is not None else self.iterations + + def expand_all(s, depth): + if depth == 0: + return s + tokens = [m.group(0) for m in gene_pattern.finditer(s)] + result = [] + i = 0 + while i < len(tokens): + token = tokens[i] + if token == '[': + # Find the matching closing bracket + bracket_level = 1 + j = i + 1 + while j < len(tokens) and bracket_level > 0: + if tokens[j] == '[': + bracket_level += 1 + elif tokens[j] == ']': + bracket_level -= 1 + j += 1 + # Recursively expand the inside of the brackets + inside = expand_all(''.join(tokens[i+1:j-1]), depth) + result.append('[' + inside + ']') + i = j + elif token == ']': + i += 1 + elif token in rules: + # Replace the token in place with its expansion + replacement = rules[token] + expanded = expand_all(replacement, depth-1) + result.append(expanded) # This replaces the token at this position + i += 1 + else: + result.append(token) + i += 1 + return ''.join(result) - def _generate_lsystem(self) -> str: - """Generate the L-system string after the given number of iterations.""" - current = self.axiom - for _ in range(self.iterations): - next_str = "".join(self.rules.get(c, c) for c in current) - current = next_str - return current + return expand_all(axiom, iterations) - def _decode_lsystem_to_graph(self) -> None: + def build_graph_from_string(self, lsystem_string: str) -> None: """ - Decode the L-system string into a directed graph. - Supports branches using '[' (push) and ']' (pop) as in classic L-systems. - Each gene can encode orientation and face as SYMBOL(rotation,face), e.g., B(90,FRONT). - Orientation and face are stored as node attributes (defaults: 0, FRONT). + Build the graph from a fully expanded L-system string. """ - stack = [] # Stack for branching - prev_node = None - idx = 0 - # Regex to match symbol with optional rotation and face, e.g., B(90,FRONT) + self.graph = nx.DiGraph() token_pattern = re.compile(r"([A-Za-z])(?:\((\d{1,3})(?:,([A-Za-z]+))?\))?") - # Tokenize the string, preserving brackets - tokens = [] - i = 0 - s = self.lsystem_string + s = lsystem_string core_count = 0 - while i < len(s): - if s[i] in '[]': - tokens.append(s[i]) - i += 1 - elif s[i].isalpha(): - m = token_pattern.match(s, i) - if m: - symbol = m.group(1) - # Enforce node type from ModuleType enum - try: - symbol_to_look = SymbolToModuleType[symbol] - node_type = ModuleType[symbol_to_look.value] - except KeyError: - raise ValueError(f"Symbol '{symbol}' is not a valid ModuleType enum name.") - if node_type == ModuleType.CORE: - core_count += 1 - if core_count > 1: - raise ValueError("L-system string contains more than one CORE module.") - # Parse and validate orientation - if m.group(2) is not None: - try: - rotation_val = int(m.group(2)) - rotation_enum = next((r for r in ModuleRotationsTheta if r.value == rotation_val), ModuleRotationsTheta.DEG_0) - except Exception: - rotation_enum = ModuleRotationsTheta.DEG_0 + idx_counter = [0] # mutable counter for unique node labels + + def parse_tokens(s): + # Parse the string into a tree of (gene, [children]) + tokens = [] + i = 0 + while i < len(s): + if s[i] == '[': + # Find matching bracket + bracket_level = 1 + j = i + 1 + while j < len(s) and bracket_level > 0: + if s[j] == '[': + bracket_level += 1 + elif s[j] == ']': + bracket_level -= 1 + j += 1 + subtree = parse_tokens(s[i+1:j-1]) + tokens.append(subtree) + i = j + elif s[i] == ']': + i += 1 + elif s[i].isalpha(): + m = token_pattern.match(s, i) + if m: + tokens.append(s[i:m.end()]) + i = m.end() else: - rotation_enum = ModuleRotationsTheta.DEG_0 - face_str = m.group(3) if m.group(3) is not None else "FRONT" - try: - face = ModuleFaces[face_str] - except KeyError: - face = ModuleFaces.FRONT - tokens.append((symbol, node_type, rotation_enum, face)) - i = m.end() + tokens.append(s[i]) + i += 1 else: - # fallback: treat as NONE - tokens.append((s[i], ModuleType.NONE, ModuleRotationsTheta.DEG_0, ModuleFaces.FRONT)) i += 1 - else: - i += 1 # skip any other character - - for token in tokens: - if token == '[': - stack.append(prev_node) - elif token == ']': - if stack: - prev_node = stack.pop() - else: - symbol, node_type, rotation_enum, face = token - node_label = f"{symbol}{idx}" - self.graph.add_node( - node_label, - type=node_type, - rotation=rotation_enum, - face=face, - ) - if prev_node is not None: - self.graph.add_edge(prev_node, node_label) - prev_node = node_label - idx += 1 + return tokens + + def build_graph(tree, parent=None): + nonlocal core_count + for node in tree: + if isinstance(node, list): + # This is a branch, attach to the same parent + build_graph(node, parent) + else: + m = token_pattern.match(node) + if m: + symbol = m.group(1) + try: + symbol_to_look = SymbolToModuleType[symbol] + node_type = ModuleType[symbol_to_look.value] + except KeyError: + raise ValueError(f"Symbol '{symbol}' is not a valid ModuleType enum name.") + if node_type == ModuleType.CORE: + core_count += 1 + if core_count > 1: + raise ValueError("L-system string contains more than one CORE module.") + if m.group(2) is not None: + try: + rotation_val = int(m.group(2)) + rotation_enum = next((r for r in ModuleRotationsTheta if r.value == rotation_val), ModuleRotationsTheta.DEG_0) + except Exception: + rotation_enum = ModuleRotationsTheta.DEG_0 + else: + rotation_enum = ModuleRotationsTheta.DEG_0 + face_str = m.group(3) if m.group(3) is not None else "FRONT" + try: + face = ModuleFaces[face_str] + except KeyError: + face = ModuleFaces.FRONT + node_label = f"{symbol}{idx_counter[0]}" + self.graph.add_node( + node_label, + type=node_type, + rotation=rotation_enum, + face=face, + ) + if parent is not None: + self.graph.add_edge(parent, node_label) + idx_counter[0] += 1 + parent = node_label + return parent + + tree = parse_tokens(s) + build_graph(tree) def get_graph(self) -> DiGraph: """Return the generated NetworkX DiGraph.""" @@ -201,3 +240,5 @@ def draw_graph( plt.savefig(save_file, dpi=DPI) else: plt.show() + + From dc85b69bfe232d9276f8b0fc6cf3c34eb1018250 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:12:29 +0200 Subject: [PATCH 05/47] Update l_system_decoding.py added an example main() for testing --- .../robogen_lite/decoders/l_system_decoding.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index ff3caedc..724e3f71 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -241,4 +241,21 @@ def draw_graph( else: plt.show() +# in case we want to test on an example +def main(): + # Example: axiom with orientation and face + axiom = "C[H(0,FRONT)][H(0,LEFT)][H(0,RIGHT)]" + rules = {"H(0,FRONT)" : "H(0,FRONT)B(0,FRONT)", + "H(0,LEFT)" : "H(0,LEFT)B(0,FRONT)", + "H(0,RIGHT)" : "H(0,RIGHT)B(0,FRONT)", + } + decoder = LSystemDecoder(axiom, rules, iterations=4) + print("Nodes and attributes:") + for n, d in decoder.graph.nodes(data=True): + print(n, d) + print(decoder.lsystem_string) + decoder.draw_graph() + +if __name__ == "__main__": + main() \ No newline at end of file From a88310dbf743b631ba4564a7d9d60c2126f6af71 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:01:57 +0200 Subject: [PATCH 06/47] Update l_system_decoding.py added some comments to explain the code. --- .../decoders/l_system_decoding.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index 724e3f71..ea04f878 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -40,7 +40,7 @@ SEED = 42 DPI = 300 -class SymbolToModuleType(Enum): +class SymbolToModuleType(Enum): # for auto-transcoding between L-system string characters and ModuleType elements """Enum for module types.""" C = 'CORE' @@ -65,8 +65,9 @@ def __init__( self.rules = rules self.iterations = iterations self.graph = nx.DiGraph() - self.lsystem_string = self.expand_lsystem() - self.build_graph_from_string(self.lsystem_string) + self.lsystem_string = self.expand_lsystem() # first we expand the string applying recursively all the rules + # and return the fully expanded L-system string intermediate genotype + self.build_graph_from_string(self.lsystem_string) # we create the graph with networkx from a fully expanded L-system string def expand_lsystem(self, axiom: str = None, rules: Dict[str, str] = None, iterations: int = None) -> str: """ @@ -74,19 +75,19 @@ def expand_lsystem(self, axiom: str = None, rules: Dict[str, str] = None, iterat Each token is replaced in place by its rule expansion (not appended after). """ gene_pattern = re.compile(r"([A-Za-z](?:\(\d{1,3}(?:,[A-Za-z]+)?\))?)|\[|\]") - axiom = axiom if axiom is not None else self.axiom - rules = rules if rules is not None else self.rules - iterations = iterations if iterations is not None else self.iterations + axiom = axiom if axiom is not None else self.axiom # if we call it without axiom defined then we pick the one already assigned + rules = rules if rules is not None else self.rules # same for rules + iterations = iterations if iterations is not None else self.iterations # same for iterations def expand_all(s, depth): - if depth == 0: + if depth == 0: # end of recursion ... just return the string. return s tokens = [m.group(0) for m in gene_pattern.finditer(s)] result = [] i = 0 - while i < len(tokens): + while i < len(tokens): #go through all the token identified token = tokens[i] - if token == '[': + if token == '[': # Find the matching closing bracket bracket_level = 1 j = i + 1 @@ -167,7 +168,7 @@ def build_graph(tree, parent=None): m = token_pattern.match(node) if m: symbol = m.group(1) - try: + try: # checl if the type of elements is authorized (part of ModuleType enum) symbol_to_look = SymbolToModuleType[symbol] node_type = ModuleType[symbol_to_look.value] except KeyError: @@ -177,26 +178,26 @@ def build_graph(tree, parent=None): if core_count > 1: raise ValueError("L-system string contains more than one CORE module.") if m.group(2) is not None: - try: + try: # check if the rotation is part of the allowed rotations (Module RotationsTheta enum) rotation_val = int(m.group(2)) rotation_enum = next((r for r in ModuleRotationsTheta if r.value == rotation_val), ModuleRotationsTheta.DEG_0) - except Exception: + except Exception: # if error then default to 0 rotation_enum = ModuleRotationsTheta.DEG_0 - else: + else: # if no rotation is provided then is is defaulted to 0 rotation_enum = ModuleRotationsTheta.DEG_0 face_str = m.group(3) if m.group(3) is not None else "FRONT" - try: + try: # check if the face is in the allowed faces (Module ModuleFaces enum) face = ModuleFaces[face_str] - except KeyError: + except KeyError: # if error then default to FRONT face = ModuleFaces.FRONT - node_label = f"{symbol}{idx_counter[0]}" + node_label = f"{symbol}{idx_counter[0]}" # generate a unique ID for the node self.graph.add_node( node_label, type=node_type, rotation=rotation_enum, face=face, - ) - if parent is not None: + ) #create and add the node to the graph + if parent is not None: # if there is a parent, create a link in the graph self.graph.add_edge(parent, node_label) idx_counter[0] += 1 parent = node_label From 1931e803f522f93aa160eafc5681a8aadae638d6 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Fri, 26 Sep 2025 13:04:12 +0200 Subject: [PATCH 07/47] Update l_system_decoding.py --- .../robogen_lite/decoders/l_system_decoding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index ea04f878..f02e4451 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -2,9 +2,9 @@ """Example of L-system-based decoding for modular robot graphs. Author: omn (with help from GitHub Copilot) -Date: 2025-09-25 +Date: 2025-09-26 Py Ver: 3.12 -OS: macOS Sequoia 15.3.1 +OS: macOS Tahoe 26 Status: Prototype Notes From 1f4685d9a1afadbe8a618ef6f570830b85050013 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Sat, 27 Sep 2025 15:55:30 +0200 Subject: [PATCH 08/47] Create l_system_genetic_utils.py --- .../decoders/l_system_genetic_utils.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py new file mode 100644 index 00000000..275a7a31 --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py @@ -0,0 +1,192 @@ +""" +L-system genotype mutation and crossover utilities for modular robots. + +Author: omn (with help from GitHub Copilot) +Date: 2025-09-27 +""" + +import random +from typing import Tuple, Dict +import re + +def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1) -> Tuple[str, Dict[str, str]]: + """ + Mutate the axiom and/or rules of an L-system genotype. + - Randomly changes, adds, or removes symbols in the axiom. + - Randomly mutates rule replacements. + Returns mutated (axiom, rules). + """ + # Tokenize axiom using regex to preserve gene and bracket structure + gene_pattern = re.compile(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[|\]") + axiom_tokens = [m.group(0) for m in gene_pattern.finditer(axiom)] + faces = ['FRONT', 'LEFT', 'RIGHT', 'BACK', 'TOP', 'BOTTOM'] + letters = ['C', 'B', 'H', 'N'] + allowed_numbers = [0, 90, 180, 270] + def random_gene(): + # Only generate valid gene tokens (never bare letters) + letter = random.choice(['B', 'H']) + number = random.choice(allowed_numbers) + face = random.choice(faces) + return f"{letter}({number},{face})" + + def random_branch(): + # Always at least one gene inside brackets + num_genes = random.randint(1, 3) + genes = [random_gene() for _ in range(num_genes)] + return '[' + ''.join(genes) + ']' + for i in range(len(axiom_tokens)): + token = axiom_tokens[i] + # Only mutate if token is a gene (not a bracket) + if re.fullmatch(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", token): + if random.random() < mutation_rate: + op = random.choice(['replace', 'delete', 'insert']) + if op == 'replace': + axiom_tokens[i] = random_gene() + elif op == 'delete': + axiom_tokens[i] = '' + elif op == 'insert': + axiom_tokens[i] += random_gene() + # Remove any C genes from axiom tokens + axiom_tokens = [t for t in axiom_tokens if not re.fullmatch(r"C\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", t)] + mutated_axiom = ''.join(axiom_tokens) + # Ensure axiom contains exactly one C (no parameters) at the start + if not mutated_axiom.startswith('C'): + mutated_axiom = 'C' + mutated_axiom + # Remove any additional C occurrences + mutated_axiom = 'C' + mutated_axiom.replace('C', '', 1) + + # Mutate rules: operate on rule replacement strings as token sequences + mutated_rules = rules.copy() + def random_rule_key(): + # Only B or H allowed in rule keys + letter = random.choice(['B', 'H']) + number = random.choice(allowed_numbers) + face = random.choice(faces) + return f"{letter}({number},{face})" + for k in list(mutated_rules.keys()): + # If key is not a valid gene, replace it + if not re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", k): + new_k = random_rule_key() if k != 'N' else 'N' + mutated_rules[new_k] = mutated_rules.pop(k) + k = new_k + if random.random() < mutation_rate: + op = random.choice(['replace', 'delete', 'add']) + if op == 'replace': + # Replace with random sequence of gene tokens and branches + new_tokens = [] + for _ in range(random.randint(1, 5)): + if random.random() < 0.4: + new_tokens.append(random_branch()) + else: + new_tokens.append(random_gene()) + rule_val = ''.join(new_tokens) + # Post-process to ensure only valid gene tokens and branches + valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") + valid_tokens = [m.group(0) for m in valid_pattern.finditer(rule_val)] + # Filter out any gene tokens with invalid numbers or faces + filtered_tokens = [] + for token in valid_tokens: + m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) + if m: + if int(m.group(2)) in allowed_numbers: + filtered_tokens.append(token) + elif token == 'N' or (token.startswith('[') and token.endswith(']')): + filtered_tokens.append(token) + mutated_rules[k] = ''.join(filtered_tokens) + elif op == 'delete': + del mutated_rules[k] + elif op == 'add': + # Only add a branch or a gene, never concatenate gene tokens directly + if random.random() < 0.4: + add_val = random_branch() + else: + add_val = random_gene() + # Post-process addition + valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") + valid_tokens = [m.group(0) for m in valid_pattern.finditer(add_val)] + filtered_tokens = [] + for token in valid_tokens: + m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) + if m: + if int(m.group(2)) in allowed_numbers: + filtered_tokens.append(token) + elif token == 'N' or (token.startswith('[') and token.endswith(']')): + filtered_tokens.append(token) + mutated_rules[k] += ''.join(filtered_tokens) + # Possibly add a new rule + if random.random() < mutation_rate: + new_key = random_rule_key() if random.random() < 0.75 else 'N' + new_val = '' + for _ in range(random.randint(1, 4)): + if random.random() < 0.4: + new_val += random_branch() + else: + new_val += random_gene() + # Post-process to ensure only valid gene tokens and branches + valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") + valid_tokens = [m.group(0) for m in valid_pattern.finditer(new_val)] + filtered_tokens = [] + for token in valid_tokens: + m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) + if m: + if int(m.group(2)) in allowed_numbers: + filtered_tokens.append(token) + elif token == 'N' or (token.startswith('[') and token.endswith(']')): + filtered_tokens.append(token) + mutated_rules[new_key] = ''.join(filtered_tokens) + return mutated_axiom, mutated_rules + +def crossover_lsystem( + axiom1: str, rules1: Dict[str, str], + axiom2: str, rules2: Dict[str, str] +) -> Tuple[Tuple[str, Dict[str, str]], Tuple[str, Dict[str, str]]]: + """ + Perform crossover between two L-system genotypes (axiom and rules). + - Single-point crossover for axiom strings. + - Rule crossover: randomly swap rule replacements between parents. + Returns two offspring (axiom, rules) tuples. + """ + # Tokenize axiom strings + gene_pattern = re.compile(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[|\]") + tokens1 = [m.group(0) for m in gene_pattern.finditer(axiom1)] + tokens2 = [m.group(0) for m in gene_pattern.finditer(axiom2)] + min_len = min(len(tokens1), len(tokens2)) + if min_len > 1: + point = random.randint(1, min_len - 1) + else: + point = 1 + def ensure_single_c(axiom_tokens): + # Remove any C genes with parameters + axiom_tokens = [t for t in axiom_tokens if not re.fullmatch(r"C\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", t)] + axiom_str = ''.join(axiom_tokens) + # Ensure axiom contains exactly one C (no parameters) at the start + if not axiom_str.startswith('C'): + axiom_str = 'C' + axiom_str + # Remove any additional C occurrences + axiom_str = 'C' + axiom_str.replace('C', '', 1) + return axiom_str + + child1_tokens = tokens1[:point] + tokens2[point:] + child2_tokens = tokens2[:point] + tokens1[point:] + child1_axiom = ensure_single_c(child1_tokens) + child2_axiom = ensure_single_c(child2_tokens) + + # Rule crossover (unchanged) + keys1 = set(rules1.keys()) + keys2 = set(rules2.keys()) + all_keys = list(keys1 | keys2) + child1_rules = {} + child2_rules = {} + for k in all_keys: + if k in rules1 and k in rules2: + if random.random() < 0.5: + child1_rules[k] = rules1[k] + child2_rules[k] = rules2[k] + else: + child1_rules[k] = rules2[k] + child2_rules[k] = rules1[k] + elif k in rules1: + child1_rules[k] = rules1[k] + elif k in rules2: + child2_rules[k] = rules2[k] + return (child1_axiom, child1_rules), (child2_axiom, child2_rules) From 07a2fe5224ef6802c95c8d1b80b8e029d00af2cf Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:37:56 +0200 Subject: [PATCH 09/47] updated l-system decoder and mutation --- .../decoders/l_system_decoding.py | 75 ++++--- .../decoders/l_system_genetic_utils.py | 192 ---------------- .../decoders/l_system_mutation.py | 205 ++++++++++++++++++ 3 files changed, 246 insertions(+), 226 deletions(-) delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index f02e4451..a544d0da 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -1,4 +1,3 @@ - """Example of L-system-based decoding for modular robot graphs. Author: omn (with help from GitHub Copilot) @@ -66,7 +65,6 @@ def __init__( self.iterations = iterations self.graph = nx.DiGraph() self.lsystem_string = self.expand_lsystem() # first we expand the string applying recursively all the rules - # and return the fully expanded L-system string intermediate genotype self.build_graph_from_string(self.lsystem_string) # we create the graph with networkx from a fully expanded L-system string def expand_lsystem(self, axiom: str = None, rules: Dict[str, str] = None, iterations: int = None) -> str: @@ -74,7 +72,8 @@ def expand_lsystem(self, axiom: str = None, rules: Dict[str, str] = None, iterat Generate the L-system string after the given number of iterations, recursively expanding inside brackets as well, but stopping at the required depth. Each token is replaced in place by its rule expansion (not appended after). """ - gene_pattern = re.compile(r"([A-Za-z](?:\(\d{1,3}(?:,[A-Za-z]+)?\))?)|\[|\]") + # Match C as a single character, and other genes as X(num,FACE) + gene_pattern = re.compile(r"(C|[A-Za-z]\(\d{1,3},[A-Za-z]+\))|\[|\]") axiom = axiom if axiom is not None else self.axiom # if we call it without axiom defined then we pick the one already assigned rules = rules if rules is not None else self.rules # same for rules iterations = iterations if iterations is not None else self.iterations # same for iterations @@ -87,7 +86,7 @@ def expand_all(s, depth): i = 0 while i < len(tokens): #go through all the token identified token = tokens[i] - if token == '[': + if token == '[': # Find the matching closing bracket bracket_level = 1 j = i + 1 @@ -97,8 +96,8 @@ def expand_all(s, depth): elif tokens[j] == ']': bracket_level -= 1 j += 1 - # Recursively expand the inside of the brackets - inside = expand_all(''.join(tokens[i+1:j-1]), depth) + # Recursively expand the inside of the brackets with depth-1 + inside = expand_all(''.join(tokens[i+1:j-1]), depth-1) result.append('[' + inside + ']') i = j elif token == ']': @@ -121,7 +120,8 @@ def build_graph_from_string(self, lsystem_string: str) -> None: Build the graph from a fully expanded L-system string. """ self.graph = nx.DiGraph() - token_pattern = re.compile(r"([A-Za-z])(?:\((\d{1,3})(?:,([A-Za-z]+))?\))?") + # Match C as a single character, and other genes as X(num,FACE) + token_pattern = re.compile(r"C|([A-Za-z])\((\d{1,3}),(\w+)\)") s = lsystem_string core_count = 0 idx_counter = [0] # mutable counter for unique node labels @@ -167,29 +167,35 @@ def build_graph(tree, parent=None): else: m = token_pattern.match(node) if m: - symbol = m.group(1) - try: # checl if the type of elements is authorized (part of ModuleType enum) - symbol_to_look = SymbolToModuleType[symbol] - node_type = ModuleType[symbol_to_look.value] - except KeyError: - raise ValueError(f"Symbol '{symbol}' is not a valid ModuleType enum name.") - if node_type == ModuleType.CORE: - core_count += 1 - if core_count > 1: - raise ValueError("L-system string contains more than one CORE module.") - if m.group(2) is not None: - try: # check if the rotation is part of the allowed rotations (Module RotationsTheta enum) - rotation_val = int(m.group(2)) - rotation_enum = next((r for r in ModuleRotationsTheta if r.value == rotation_val), ModuleRotationsTheta.DEG_0) - except Exception: # if error then default to 0 - rotation_enum = ModuleRotationsTheta.DEG_0 - else: # if no rotation is provided then is is defaulted to 0 + if m.group(0) == "C": + symbol = "C" + node_type = ModuleType.CORE rotation_enum = ModuleRotationsTheta.DEG_0 - face_str = m.group(3) if m.group(3) is not None else "FRONT" - try: # check if the face is in the allowed faces (Module ModuleFaces enum) - face = ModuleFaces[face_str] - except KeyError: # if error then default to FRONT face = ModuleFaces.FRONT + else: + symbol = m.group(1) + try: # check if the type of elements is authorized (part of ModuleType enum) + symbol_to_look = SymbolToModuleType[symbol] + node_type = ModuleType[symbol_to_look.value] + except KeyError: + raise ValueError(f"Symbol '{symbol}' is not a valid ModuleType enum name.") + if node_type == ModuleType.CORE: + core_count += 1 + if core_count > 1: + raise ValueError("L-system string contains more than one CORE module.") + if m.group(2) is not None: + try: # check if the rotation is part of the allowed rotations (Module RotationsTheta enum) + rotation_val = int(m.group(2)) + rotation_enum = next((r for r in ModuleRotationsTheta if r.value == rotation_val), ModuleRotationsTheta.DEG_0) + except Exception: # if error then default to 0 + rotation_enum = ModuleRotationsTheta.DEG_0 + else: # if no rotation is provided then is is defaulted to 0 + rotation_enum = ModuleRotationsTheta.DEG_0 + face_str = m.group(3) if m.group(3) is not None else "FRONT" + try: # check if the face is in the allowed faces (Module ModuleFaces enum) + face = ModuleFaces[face_str] + except KeyError: # if error then default to FRONT + face = ModuleFaces.FRONT node_label = f"{symbol}{idx_counter[0]}" # generate a unique ID for the node self.graph.add_node( node_label, @@ -245,13 +251,14 @@ def draw_graph( # in case we want to test on an example def main(): - # Example: axiom with orientation and face + # Example: axiom with orientation and face, and C expands into branches axiom = "C[H(0,FRONT)][H(0,LEFT)][H(0,RIGHT)]" - rules = {"H(0,FRONT)" : "H(0,FRONT)B(0,FRONT)", - "H(0,LEFT)" : "H(0,LEFT)B(0,FRONT)", - "H(0,RIGHT)" : "H(0,RIGHT)B(0,FRONT)", - } - decoder = LSystemDecoder(axiom, rules, iterations=4) + rules = { + "H(0,FRONT)": "H(0,FRONT)B(0,FRONT)", + "H(0,LEFT)": "H(0,LEFT)B(0,FRONT)", + "H(0,RIGHT)": "H(0,RIGHT)B(0,FRONT)" # Example for N, can be expanded as needed + } + decoder = LSystemDecoder(axiom, rules, iterations=2) print("Nodes and attributes:") for n, d in decoder.graph.nodes(data=True): print(n, d) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py deleted file mode 100644 index 275a7a31..00000000 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genetic_utils.py +++ /dev/null @@ -1,192 +0,0 @@ -""" -L-system genotype mutation and crossover utilities for modular robots. - -Author: omn (with help from GitHub Copilot) -Date: 2025-09-27 -""" - -import random -from typing import Tuple, Dict -import re - -def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1) -> Tuple[str, Dict[str, str]]: - """ - Mutate the axiom and/or rules of an L-system genotype. - - Randomly changes, adds, or removes symbols in the axiom. - - Randomly mutates rule replacements. - Returns mutated (axiom, rules). - """ - # Tokenize axiom using regex to preserve gene and bracket structure - gene_pattern = re.compile(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[|\]") - axiom_tokens = [m.group(0) for m in gene_pattern.finditer(axiom)] - faces = ['FRONT', 'LEFT', 'RIGHT', 'BACK', 'TOP', 'BOTTOM'] - letters = ['C', 'B', 'H', 'N'] - allowed_numbers = [0, 90, 180, 270] - def random_gene(): - # Only generate valid gene tokens (never bare letters) - letter = random.choice(['B', 'H']) - number = random.choice(allowed_numbers) - face = random.choice(faces) - return f"{letter}({number},{face})" - - def random_branch(): - # Always at least one gene inside brackets - num_genes = random.randint(1, 3) - genes = [random_gene() for _ in range(num_genes)] - return '[' + ''.join(genes) + ']' - for i in range(len(axiom_tokens)): - token = axiom_tokens[i] - # Only mutate if token is a gene (not a bracket) - if re.fullmatch(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", token): - if random.random() < mutation_rate: - op = random.choice(['replace', 'delete', 'insert']) - if op == 'replace': - axiom_tokens[i] = random_gene() - elif op == 'delete': - axiom_tokens[i] = '' - elif op == 'insert': - axiom_tokens[i] += random_gene() - # Remove any C genes from axiom tokens - axiom_tokens = [t for t in axiom_tokens if not re.fullmatch(r"C\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", t)] - mutated_axiom = ''.join(axiom_tokens) - # Ensure axiom contains exactly one C (no parameters) at the start - if not mutated_axiom.startswith('C'): - mutated_axiom = 'C' + mutated_axiom - # Remove any additional C occurrences - mutated_axiom = 'C' + mutated_axiom.replace('C', '', 1) - - # Mutate rules: operate on rule replacement strings as token sequences - mutated_rules = rules.copy() - def random_rule_key(): - # Only B or H allowed in rule keys - letter = random.choice(['B', 'H']) - number = random.choice(allowed_numbers) - face = random.choice(faces) - return f"{letter}({number},{face})" - for k in list(mutated_rules.keys()): - # If key is not a valid gene, replace it - if not re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", k): - new_k = random_rule_key() if k != 'N' else 'N' - mutated_rules[new_k] = mutated_rules.pop(k) - k = new_k - if random.random() < mutation_rate: - op = random.choice(['replace', 'delete', 'add']) - if op == 'replace': - # Replace with random sequence of gene tokens and branches - new_tokens = [] - for _ in range(random.randint(1, 5)): - if random.random() < 0.4: - new_tokens.append(random_branch()) - else: - new_tokens.append(random_gene()) - rule_val = ''.join(new_tokens) - # Post-process to ensure only valid gene tokens and branches - valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") - valid_tokens = [m.group(0) for m in valid_pattern.finditer(rule_val)] - # Filter out any gene tokens with invalid numbers or faces - filtered_tokens = [] - for token in valid_tokens: - m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) - if m: - if int(m.group(2)) in allowed_numbers: - filtered_tokens.append(token) - elif token == 'N' or (token.startswith('[') and token.endswith(']')): - filtered_tokens.append(token) - mutated_rules[k] = ''.join(filtered_tokens) - elif op == 'delete': - del mutated_rules[k] - elif op == 'add': - # Only add a branch or a gene, never concatenate gene tokens directly - if random.random() < 0.4: - add_val = random_branch() - else: - add_val = random_gene() - # Post-process addition - valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") - valid_tokens = [m.group(0) for m in valid_pattern.finditer(add_val)] - filtered_tokens = [] - for token in valid_tokens: - m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) - if m: - if int(m.group(2)) in allowed_numbers: - filtered_tokens.append(token) - elif token == 'N' or (token.startswith('[') and token.endswith(']')): - filtered_tokens.append(token) - mutated_rules[k] += ''.join(filtered_tokens) - # Possibly add a new rule - if random.random() < mutation_rate: - new_key = random_rule_key() if random.random() < 0.75 else 'N' - new_val = '' - for _ in range(random.randint(1, 4)): - if random.random() < 0.4: - new_val += random_branch() - else: - new_val += random_gene() - # Post-process to ensure only valid gene tokens and branches - valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") - valid_tokens = [m.group(0) for m in valid_pattern.finditer(new_val)] - filtered_tokens = [] - for token in valid_tokens: - m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) - if m: - if int(m.group(2)) in allowed_numbers: - filtered_tokens.append(token) - elif token == 'N' or (token.startswith('[') and token.endswith(']')): - filtered_tokens.append(token) - mutated_rules[new_key] = ''.join(filtered_tokens) - return mutated_axiom, mutated_rules - -def crossover_lsystem( - axiom1: str, rules1: Dict[str, str], - axiom2: str, rules2: Dict[str, str] -) -> Tuple[Tuple[str, Dict[str, str]], Tuple[str, Dict[str, str]]]: - """ - Perform crossover between two L-system genotypes (axiom and rules). - - Single-point crossover for axiom strings. - - Rule crossover: randomly swap rule replacements between parents. - Returns two offspring (axiom, rules) tuples. - """ - # Tokenize axiom strings - gene_pattern = re.compile(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[|\]") - tokens1 = [m.group(0) for m in gene_pattern.finditer(axiom1)] - tokens2 = [m.group(0) for m in gene_pattern.finditer(axiom2)] - min_len = min(len(tokens1), len(tokens2)) - if min_len > 1: - point = random.randint(1, min_len - 1) - else: - point = 1 - def ensure_single_c(axiom_tokens): - # Remove any C genes with parameters - axiom_tokens = [t for t in axiom_tokens if not re.fullmatch(r"C\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", t)] - axiom_str = ''.join(axiom_tokens) - # Ensure axiom contains exactly one C (no parameters) at the start - if not axiom_str.startswith('C'): - axiom_str = 'C' + axiom_str - # Remove any additional C occurrences - axiom_str = 'C' + axiom_str.replace('C', '', 1) - return axiom_str - - child1_tokens = tokens1[:point] + tokens2[point:] - child2_tokens = tokens2[:point] + tokens1[point:] - child1_axiom = ensure_single_c(child1_tokens) - child2_axiom = ensure_single_c(child2_tokens) - - # Rule crossover (unchanged) - keys1 = set(rules1.keys()) - keys2 = set(rules2.keys()) - all_keys = list(keys1 | keys2) - child1_rules = {} - child2_rules = {} - for k in all_keys: - if k in rules1 and k in rules2: - if random.random() < 0.5: - child1_rules[k] = rules1[k] - child2_rules[k] = rules2[k] - else: - child1_rules[k] = rules2[k] - child2_rules[k] = rules1[k] - elif k in rules1: - child1_rules[k] = rules1[k] - elif k in rules2: - child2_rules[k] = rules2[k] - return (child1_axiom, child1_rules), (child2_axiom, child2_rules) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py new file mode 100644 index 00000000..8c4cc5a0 --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py @@ -0,0 +1,205 @@ +""" +L-system genotype mutation and crossover utilities for modular robots. + +Author: omn (with help from GitHub Copilot) +Date: 2025-09-27 +""" + +import random +from typing import Tuple, Dict +import re + +def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1) -> Tuple[str, Dict[str, str]]: + """ + Mutate the axiom and/or rules of an L-system genotype. + - Randomly changes, adds, or removes symbols in the axiom. + - Randomly mutates rule replacements. + Returns mutated (axiom, rules). + """ + # Tokenize axiom using regex to preserve gene and bracket structure + gene_pattern = re.compile(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[|\]|C") + axiom_tokens = [m.group(0) for m in gene_pattern.finditer(axiom)] + faces = ['FRONT', 'LEFT', 'RIGHT', 'BACK', 'TOP', 'BOTTOM'] + letters = ['C', 'B', 'H', 'N'] + allowed_numbers = [0, 90, 180, 270] + def random_gene(): + letter = random.choice(['B', 'H']) + number = random.choice(allowed_numbers) + face = random.choice(faces) + return f"{letter}({number},{face})" + def random_branch(): + num_genes = random.randint(1, 3) + genes = [random_gene() for _ in range(num_genes)] + return '[' + ''.join(genes) + ']' + # Only mutate one gene/branch in the axiom per call, and only if random < mutation_rate + gene_indices = [i for i, token in enumerate(axiom_tokens) if re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", token)] + modified_gene = None + new_gene = None + if gene_indices and random.random() < mutation_rate: + i = random.choice(gene_indices) + token = axiom_tokens[i] + op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene']) + if op == 'add_gene': + # Only add one gene + axiom_tokens.insert(i+1, random_gene()) + elif op == 'remove_gene': + # Only remove one gene + axiom_tokens[i] = '' + elif op == 'create_branch': + axiom_tokens[i] = '[' + axiom_tokens[i] + ']' + elif op == 'remove_branch': + if i > 0 and axiom_tokens[i-1] == '[' and i+1 < len(axiom_tokens) and axiom_tokens[i+1] == ']': + axiom_tokens[i-1] = '' + axiom_tokens[i+1] = '' + elif op == 'modify_gene': + letter = token[0] + number = random.choice(allowed_numbers) + face = random.choice(faces) + new_gene = f"{letter}({number},{face})" + modified_gene = token + axiom_tokens[i] = new_gene + # Mutate rules: operate on rule replacement strings as token sequences + mutated_rules = rules.copy() + # If a gene was modified in the axiom, update all rules and rule values + if modified_gene and new_gene: + # Update rule keys + new_rules = {} + for k, v in mutated_rules.items(): + new_k = new_gene if k == modified_gene else k + # Update all occurrences in rule values + new_v = v.replace(modified_gene, new_gene) + new_rules[new_k] = new_v + mutated_rules = new_rules + # Remove any C genes with parameters from axiom tokens + axiom_tokens = [t for t in axiom_tokens if not re.fullmatch(r"C\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", t)] + + # Remove empty branches: if a gene is removed and is the only one inside [ ], remove the brackets too + i = 0 + while i < len(axiom_tokens): + if axiom_tokens[i] == '[': + # Find the matching closing bracket + j = i + 1 + while j < len(axiom_tokens) and axiom_tokens[j] != ']': + j += 1 + # If only one non-empty token inside, and it's empty, remove the brackets + inside = [t for t in axiom_tokens[i+1:j] if t.strip() != ''] + if len(inside) == 0 and j < len(axiom_tokens): + axiom_tokens[i] = '' + axiom_tokens[j] = '' + # Optionally, remove all empty tokens between i and j + for k in range(i+1, j): + axiom_tokens[k] = '' + i = j + i += 1 + + # Ensure axiom contains exactly one C (no parameters) at the start + mutated_axiom = ''.join(axiom_tokens) + if not mutated_axiom.startswith('C'): + mutated_axiom = 'C' + mutated_axiom + mutated_axiom = 'C' + mutated_axiom.replace('C', '', 1) + + # Mutate rules: operate on rule replacement strings as token sequences + mutated_rules = rules.copy() + def random_rule_key(): + # Only B or H allowed in rule keys + letter = random.choice(['B', 'H']) + number = random.choice(allowed_numbers) + face = random.choice(faces) + return f"{letter}({number},{face})" + # Only mutate one rule per call, and only if random < mutation_rate + rule_keys = list(mutated_rules.keys()) + modified_rule_gene = None + new_rule_gene = None + if rule_keys and random.random() < mutation_rate: + k = random.choice(rule_keys) + # If key is not a valid gene, replace it + if not re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", k): + new_k = random_rule_key() if k != 'N' else 'N' + mutated_rules[new_k] = mutated_rules.pop(k) + k = new_k + op = random.choice(['replace', 'delete', 'add', 'modify_gene']) + if op == 'replace': + # Only replace with one gene or one branch + if random.random() < 0.4: + rule_val = random_branch() + else: + rule_val = random_gene() + valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") + valid_tokens = [m.group(0) for m in valid_pattern.finditer(rule_val)] + filtered_tokens = [] + for token in valid_tokens: + m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) + if m: + if int(m.group(2)) in allowed_numbers: + filtered_tokens.append(token) + elif token == 'N' or (token.startswith('[') and token.endswith(']')): + filtered_tokens.append(token) + mutated_rules[k] = ''.join(filtered_tokens) + elif op == 'delete': + del mutated_rules[k] + elif op == 'add': + # Only add one gene or one branch + if random.random() < 0.4: + add_val = random_branch() + else: + add_val = random_gene() + valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") + valid_tokens = [m.group(0) for m in valid_pattern.finditer(add_val)] + filtered_tokens = [] + for token in valid_tokens: + m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) + if m: + if int(m.group(2)) in allowed_numbers: + filtered_tokens.append(token) + elif token == 'N' or (token.startswith('[') and token.endswith(']')): + filtered_tokens.append(token) + mutated_rules[k] += ''.join(filtered_tokens) + elif op == 'modify_gene': + # Only allow modify_gene if k is a gene + if re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))", k): + letter = k[0] + number = random.choice(allowed_numbers) + face = random.choice(faces) + new_rule_gene = f"{letter}({number},{face})" + modified_rule_gene = k + # Update rule key + mutated_rules[new_rule_gene] = mutated_rules.pop(k) + # If a gene was modified in the rules, update all occurrences in axiom and rule values + if modified_rule_gene and new_rule_gene: + # Update axiom + axiom_tokens = [new_rule_gene if t == modified_rule_gene else t for t in axiom_tokens] + # Update all rule values + for rk in mutated_rules: + mutated_rules[rk] = mutated_rules[rk].replace(modified_rule_gene, new_rule_gene) + # Possibly add a new rule + if random.random() < mutation_rate: + new_key = random_rule_key() if random.random() < 0.75 else 'N' + new_val = '' + for _ in range(random.randint(1, 4)): + if random.random() < 0.4: + new_val += random_branch() + else: + new_val += random_gene() + # Post-process to ensure only valid gene tokens and branches + valid_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[[^\[\]]+\]") + valid_tokens = [m.group(0) for m in valid_pattern.finditer(new_val)] + filtered_tokens = [] + for token in valid_tokens: + m = re.fullmatch(r"([BH])\((\d+),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", token) + if m: + if int(m.group(2)) in allowed_numbers: + filtered_tokens.append(token) + elif token == 'N' or (token.startswith('[') and token.endswith(']')): + filtered_tokens.append(token) + mutated_rules[new_key] = ''.join(filtered_tokens) + # Remove rules whose key is not present in the mutated axiom or in any rule value + gene_token_pattern = re.compile(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N") + # Find all gene tokens in axiom + present_genes = set(m.group(0) for m in gene_token_pattern.finditer(mutated_axiom)) + # Find all gene tokens in rule values + for v in mutated_rules.values(): + present_genes.update(m.group(0) for m in gene_token_pattern.finditer(v)) + # Remove rules whose key is not present + mutated_rules = {k: v for k, v in mutated_rules.items() if k in present_genes} + return mutated_axiom, mutated_rules + From 840d381a468b9524531fddbb249f4f877c6468da Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 1 Oct 2025 12:48:50 +0200 Subject: [PATCH 10/47] Added basic tree genome + conversion to digraph --- .../decoders/tree_genome/tree_genome.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py new file mode 100644 index 00000000..0f8ebdcc --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py @@ -0,0 +1,130 @@ +from __future__ import annotations +from ast import Dict +from typing import Any +from zipfile import Path +import matplotlib.pyplot as plt +import ariel.src.ariel.body_phenotypes.robogen_lite.config as config + +import networkx as nx +from networkx import DiGraph +from networkx.readwrite import json_graph + +class Tree: + def __init__(self): + self.graph = nx.DiGraph() + self.root = TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={}), depth=0) + + def tree_to_digraph(self) -> nx.DiGraph: + """ + Convert Tree (rooted at self.root) to a NetworkX DiGraph. + Nodes are given integer ids (0..N-1) in DFS order. + + Node attrs: type=, rotation=, depth= + Edge attrs: face= + """ + # Stable ids for each TreeNode instance + node_id: Dict[TreeNode, int] = {} + next_id = 0 + + def get_id(n: TreeNode) -> int: + nonlocal next_id + if n not in node_id: + node_id[n] = next_id + next_id += 1 + return node_id[n] + + def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None): + id = get_id(child) + # add/update the node with attributes (use .name to make JSON-friendly) + self.graph.add_node( + id, + type=child.module_type.name, + rotation=child.rotation.name, + # depth=child._depth, + ) + + if parent is not None: + parent_id = get_id(parent) + # face stored as string (Enum.name) for readability / JSON + self.graph.add_edge(parent_id, id, face=via_face.name if via_face else None) + + # descend + for face, sub in child.children.items(): + # Expect sub to be a TreeNode + dfs(child, sub, face) + + dfs(None, self.root, None) + +class TreeNode: + def __init__(self, module: config.ModuleInstance, depth: int = 0): + self.module_type = module.type + self.rotation = module.rotation + # type: dict[ModuleFaces, TreeNode] + self.children = module.links + self._depth = depth + + def add_child(self, face: config.ModuleFaces, child_module: config.ModuleInstance): + if face in self.children: + raise ValueError(f"Face {face} already has a child.") + if face not in config.ALLOWED_FACES[self.module_type]: + raise ValueError(f"Face {face} is not allowed for module type {self.module_type}.") + self.children[face] = TreeNode(child_module, depth=self._depth + 1) + +# from matplotlib.figure import Figure +# from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas +# from pathlib import Path + +# def draw_graph( +# graph: DiGraph[Any], +# title: str = "NetworkX Directed Graph", +# save_file: Path | str = "graph.png", +# ) -> None: +# # --- NO pyplot here; use Figure + Agg canvas --- +# fig = Figure() +# canvas = FigureCanvas(fig) +# ax = fig.add_subplot(111) + +# # Layouts (deterministic seed) +# pos = nx.spectral_layout(graph) +# pos = nx.spring_layout(graph, pos=pos, k=1, iterations=20, seed=42) + +# # Draw on explicit axes +# nx.draw( +# graph, +# pos, +# with_labels=True, +# node_size=150, +# node_color="#FFFFFF00", +# edgecolors="blue", +# font_size=8, +# width=0.5, +# ax=ax, +# ) + +# edge_labels = nx.get_edge_attributes(graph, "face") +# nx.draw_networkx_edge_labels( +# graph, +# pos, +# edge_labels=edge_labels, +# font_color="red", +# font_size=8, +# ax=ax, +# ) + +# ax.set_title(title) +# fig.tight_layout() + +# # Save via Agg canvas (no GUI backend involved) +# fig.savefig(save_file, dpi=300, bbox_inches="tight") + + +# Generate a simple tree for demonstration +tree = Tree() +tree.root.add_child(config.ModuleFaces.FRONT, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) +tree.root.add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) +tree.root.children[config.ModuleFaces.FRONT].add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) +tree.tree_to_digraph() +graph = tree.graph +print(graph.nodes(data=True)) +# draw_graph(graph, title="Tree Structure", save_file="tree_structure.png") + From b55ffdd62d220a3e6c7aec5d417196a0203a4268 Mon Sep 17 00:00:00 2001 From: Lukas Bierling Date: Wed, 1 Oct 2025 16:07:35 +0200 Subject: [PATCH 11/47] tree node refinement --- src/ariel/.idea/.gitignore | 8 + src/ariel/.idea/AugmentWebviewStateStore.xml | 10 + src/ariel/.idea/ariel.iml | 12 + .../inspectionProfiles/Project_Default.xml | 7 + .../inspectionProfiles/profiles_settings.xml | 6 + src/ariel/.idea/misc.xml | 7 + src/ariel/.idea/modules.xml | 8 + src/ariel/.idea/vcs.xml | 6 + .../robogen_lite/decoders/tree_genome.py | 524 ++++++++++++++++++ .../decoders/tree_genome/tree_genome.py | 130 ----- src/ariel/ec/a000.py | 15 +- 11 files changed, 602 insertions(+), 131 deletions(-) create mode 100644 src/ariel/.idea/.gitignore create mode 100644 src/ariel/.idea/AugmentWebviewStateStore.xml create mode 100644 src/ariel/.idea/ariel.iml create mode 100644 src/ariel/.idea/inspectionProfiles/Project_Default.xml create mode 100644 src/ariel/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 src/ariel/.idea/misc.xml create mode 100644 src/ariel/.idea/modules.xml create mode 100644 src/ariel/.idea/vcs.xml create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py diff --git a/src/ariel/.idea/.gitignore b/src/ariel/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/src/ariel/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/ariel/.idea/AugmentWebviewStateStore.xml b/src/ariel/.idea/AugmentWebviewStateStore.xml new file mode 100644 index 00000000..8ae4cf5c --- /dev/null +++ b/src/ariel/.idea/AugmentWebviewStateStore.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/ariel/.idea/ariel.iml b/src/ariel/.idea/ariel.iml new file mode 100644 index 00000000..1a2fd2af --- /dev/null +++ b/src/ariel/.idea/ariel.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/ariel/.idea/inspectionProfiles/Project_Default.xml b/src/ariel/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..9d943cdb --- /dev/null +++ b/src/ariel/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/ariel/.idea/inspectionProfiles/profiles_settings.xml b/src/ariel/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/src/ariel/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/ariel/.idea/misc.xml b/src/ariel/.idea/misc.xml new file mode 100644 index 00000000..bcd4c767 --- /dev/null +++ b/src/ariel/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/ariel/.idea/modules.xml b/src/ariel/.idea/modules.xml new file mode 100644 index 00000000..9606e907 --- /dev/null +++ b/src/ariel/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/ariel/.idea/vcs.xml b/src/ariel/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/src/ariel/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py new file mode 100644 index 00000000..96f8f7cb --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py @@ -0,0 +1,524 @@ +from __future__ import annotations +from ast import Dict +from typing import Any +from zipfile import Path +import matplotlib.pyplot as plt +import ariel.body_phenotypes.robogen_lite.config as config +import contextlib +from collections import deque + +import networkx as nx +from jedi.inference.gradual.typing import Callable +from networkx import DiGraph +from networkx.readwrite import json_graph + + +''' +class Tree: + def __init__(self): + self.graph = nx.DiGraph() + self.root = TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={}), depth=0) + + def __repr__(self) -> str: + """Return a nice string representation of the tree structure.""" + if not self.root: + return "Tree(empty)" + + node_count = len(list(self._iter_nodes())) + lines = [f"Tree({node_count} nodes):"] + lines.extend(self._format_node(self.root, "", True)) + return "\n".join(lines) + + def _iter_nodes(self): + """Iterator over all nodes in the tree.""" + if self.root: + yield from self._iter_nodes_recursive(self.root) + + def _iter_nodes_recursive(self, node: 'TreeNode'): + """Recursively iterate over nodes.""" + yield node + for child in node.children.values(): + yield from self._iter_nodes_recursive(child) + + def _format_node(self, node: 'TreeNode', prefix: str, is_last: bool) -> list[str]: + """Helper method to format a node and its children recursively.""" + # Current node line + connector = "└── " if is_last else "├── " + node_info = f"{node.module_type.name}({node.rotation.name}, depth={node._depth})" + lines = [f"{prefix}{connector}{node_info}"] + + # Prepare prefix for children + child_prefix = prefix + (" " if is_last else "│ ") + + # Add children + if node.children: + child_items = list(node.children.items()) + for i, (face, child) in enumerate(child_items): + is_last_child = (i == len(child_items) - 1) + face_connector = "└── " if is_last_child else "├── " + lines.append(f"{child_prefix}{face_connector}[{face.name}]") + + # Add the child node with additional indentation + grandchild_prefix = child_prefix + (" " if is_last_child else "│ ") + lines.extend(self._format_node(child, grandchild_prefix, True)) + + return lines + + def tree_to_digraph(self) -> nx.DiGraph: + """ + Convert Tree (rooted at self.root) to a NetworkX DiGraph. + Nodes are given integer ids (0..N-1) in DFS order. + + Node attrs: type=, rotation=, depth= + Edge attrs: face= + """ + # Stable ids for each TreeNode instance + node_id: Dict[TreeNode, int] = {} + next_id = 0 + + def get_id(n: TreeNode) -> int: + nonlocal next_id + if n not in node_id: + node_id[n] = next_id + next_id += 1 + return node_id[n] + + def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None): + id = get_id(child) + # add/update the node with attributes (use .name to make JSON-friendly) + self.graph.add_node( + id, + type=child.module_type.name, + rotation=child.rotation.name, + # depth=child._depth, + ) + + if parent is not None: + parent_id = get_id(parent) + # face stored as string (Enum.name) for readability / JSON + self.graph.add_edge(parent_id, id, face=via_face.name if via_face else None) + + # descend + for face, sub in child.children.items(): + # Expect sub to be a TreeNode + dfs(child, sub, face) + + dfs(None, self.root, None) +''' +# from matplotlib.figure import Figure +# from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas +# from pathlib import Path + +# def draw_graph( +# graph: DiGraph[Any], +# title: str = "NetworkX Directed Graph", +# save_file: Path | str = "graph.png", +# ) -> None: +# # --- NO pyplot here; use Figure + Agg canvas --- +# fig = Figure() +# canvas = FigureCanvas(fig) +# ax = fig.add_subplot(111) + +# # Layouts (deterministic seed) +# pos = nx.spectral_layout(graph) +# pos = nx.spring_layout(graph, pos=pos, k=1, iterations=20, seed=42) + +# # Draw on explicit axes +# nx.draw( +# graph, +# pos, +# with_labels=True, +# node_size=150, +# node_color="#FFFFFF00", +# edgecolors="blue", +# font_size=8, +# width=0.5, +# ax=ax, +# ) + +# edge_labels = nx.get_edge_attributes(graph, "face") +# nx.draw_networkx_edge_labels( +# graph, +# pos, +# edge_labels=edge_labels, +# font_color="red", +# font_size=8, +# ax=ax, +# ) + +# ax.set_title(title) +# fig.tight_layout() + +# # Save via Agg canvas (no GUI backend involved) +# fig.savefig(save_file, dpi=300, bbox_inches="tight") + + +class TreeGenome: + def __init__(self, root: TreeNodeLukas | None = None): + self._root = root + + @classmethod + def default_init(cls, *args, **kwargs): + """Default instantiation with a core root.""" + return cls(root=TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, + rotation=config.ModuleRotationsIdx.DEG_90, + links={}))) + @property + def root(self) -> TreeNodeLukas | None: + return self._root + + @root.setter + def root(self, value: TreeNodeLukas | None): + if self._root is not None: + raise ValueError("Root node cannot be changed once set.") + self._root = value + + def __repr__(self) -> str: + """Return a nice string representation of the tree genome.""" + if not self._root: + return "TreeGenome(empty)" + + node_count = len(list(self._iter_nodes())) + lines = [f"TreeGenome({node_count} nodes):"] + lines.extend(self._format_node(self._root, "", True)) + return "\n".join(lines) + + def _iter_nodes(self): + """Iterator over all nodes in the genome.""" + if self._root: + yield from self._iter_nodes_recursive(self._root) + + def _iter_nodes_recursive(self, node: TreeNodeLukas): + """Recursively iterate over nodes.""" + yield node + for child in node.children.values(): + yield from self._iter_nodes_recursive(child) + + def _format_node(self, node: TreeNodeLukas, prefix: str, is_last: bool) -> list[str]: + """Helper method to format a node and its children recursively.""" + connector = "└── " if is_last else "├── " + node_info = f"{node.module_type.name}({node.rotation.name}, depth={node._depth})" + lines = [f"{prefix}{connector}{node_info}"] + + child_prefix = prefix + (" " if is_last else "│ ") + + if node.children: + child_items = list(node.children.items()) + for i, (face, child) in enumerate(child_items): + is_last_child = (i == len(child_items) - 1) + face_connector = "└── " if is_last_child else "├── " + lines.append(f"{child_prefix}{face_connector}[{face.name}]") + + grandchild_prefix = child_prefix + (" " if is_last_child else "│ ") + lines.extend(self._format_node(child, grandchild_prefix, True)) + + return lines + + def add_child_to_node(self, node: TreeNodeLukas, face: config.ModuleFaces, child_module: config.ModuleInstance): + """Helper method to add a child to a specific node. However, not recommended to use. Rather use """ + if face not in node.available_faces(): + raise ValueError(f"Face {face} is not available on this node.") + + child_node = TreeNodeLukas(child_module, depth=node._depth + 1) + setattr(node, face.name.lower(), child_node) + + def find_node(self, target_id: int, method: str = "dfs") -> TreeNodeLukas | None: + """Find a node by ID in the entire genome.""" + if not self._root: + return None + + if method.lower() == "bfs": + return self._root.find_node_bfs(target_id) + else: + return self._root.find_node_dfs(target_id) + + def find_nodes_by_type(self, module_type: config.ModuleType, method: str = "dfs") -> list[TreeNodeLukas]: + """Find all nodes of a specific module type.""" + if not self._root: + return [] + + predicate = lambda node: node.module_type == module_type + + if method.lower() == "bfs": + return self._root.find_all_nodes_bfs(predicate) + else: + return self._root.find_all_nodes_dfs(predicate) + + +class TreeNode: + def __init__(self, module: config.ModuleInstance, depth: int = 0, node_id: int = None): + self.module_type = module.type + self.rotation = module.rotation + # Keep reference to the original module. Why? Because then the links get automatically filled and we can just read them out when decoding + self.module = module + self._depth = depth + self._front: TreeNode | None = None + self._back: TreeNode | None = None + self._right: TreeNode | None = None + self._left: TreeNode | None = None + self._top: TreeNode | None = None + self._bottom: TreeNode | None = None + + self._enable_replacement: bool = False + + self._id = id(self) if node_id is None else node_id + + @property + def id(self) -> int: + return self._id + + @id.setter + def id(self, value: int | None): + raise ValueError("ID cannot be changed once set.") + + @contextlib.contextmanager + def enable_replacement(self): + """Context manager to temporarily allow replacement of existing children.""" + try: + self._enable_replacement = True + yield + finally: + self._enable_replacement = False + + def _can_attach_to_face(self, face: config.ModuleFaces, node: TreeNode | None) -> bool: + """Check if a node can be attached to the given face.""" + if node is None: + return True # Can always detach (set to None) + if face not in config.ALLOWED_FACES[self.module_type]: + return False + # Check if face is already occupied (unless replacement is enabled) + if not self._enable_replacement: + face_attr = face.name.lower() + if getattr(self, f"_{face_attr}") is not None: + return False # Face already occupied + return True + + def _set_face(self, face: config.ModuleFaces, value: 'TreeNodeLukas | TreeGenome | None'): + """Common method to validate and set a face attribute.""" + # Handle TreeGenome by extracting its root + if isinstance(value, TreeGenome): + if value.root is None: + raise ValueError("Cannot attach empty TreeGenome (root is None)") + actual_value = value.root + else: + actual_value = value + + if not self._can_attach_to_face(face, actual_value): + if actual_value is not None and getattr(self, f"_{face.name.lower()}") is not None: + raise ValueError(f"{face.name} face already occupied on {self.module_type}") + raise ValueError(f"Cannot attach to {face.name} face of {self.module_type}") + + # Update the internal attribute with the actual node + setattr(self, f"_{face.name.lower()}", actual_value) + + # Update the module's links dictionary + if actual_value is not None: + self.module.links[face] = self._id + else: + self.module.links.pop(face, None) + + @property + def front(self) -> TreeNode | None: + return self._front + + @front.setter + def front(self, value: 'TreeNodeLukas | TreeGenome | None'): + self._set_face(config.ModuleFaces.FRONT, value) + + @property + def back(self) -> TreeNode | None: + return self._back + + @back.setter + def back(self, value: 'TreeNodeLukas | TreeGenome | None'): + self._set_face(config.ModuleFaces.BACK, value) + + @property + def right(self) -> TreeNode | None: + return self._right + + @right.setter + def right(self, value: 'TreeNodeLukas | TreeGenome | None'): + self._set_face(config.ModuleFaces.RIGHT, value) + + @property + def left(self) -> TreeNode | None: + return self._left + + @left.setter + def left(self, value: 'TreeNodeLukas | TreeGenome | None'): + self._set_face(config.ModuleFaces.LEFT, value) + + @property + def top(self) -> TreeNode | None: + return self._top + + @top.setter + def top(self, value: 'TreeNodeLukas | TreeGenome | None'): + self._set_face(config.ModuleFaces.TOP, value) + + @property + def bottom(self) -> TreeNode | None: + return self._bottom + + @bottom.setter + def bottom(self, value: 'TreeNodeLukas | TreeGenome | None'): + self._set_face(config.ModuleFaces.BOTTOM, value) + + @property + def children(self) -> dict[config.ModuleFaces, TreeNode]: + result = {} + face_mapping = { + config.ModuleFaces.FRONT: self._front, + config.ModuleFaces.BACK: self._back, + config.ModuleFaces.RIGHT: self._right, + config.ModuleFaces.LEFT: self._left, + config.ModuleFaces.TOP: self._top, + config.ModuleFaces.BOTTOM: self._bottom, + } + + for face in config.ALLOWED_FACES[self.module_type]: + child = face_mapping[face] + if child is not None: + result[face] = child + return result + + def available_faces(self) -> list[config.ModuleFaces]: + """Return list of faces that can still accept children.""" + available = [] + face_mapping = { + config.ModuleFaces.FRONT: self._front, + config.ModuleFaces.BACK: self._back, + config.ModuleFaces.RIGHT: self._right, + config.ModuleFaces.LEFT: self._left, + config.ModuleFaces.TOP: self._top, + config.ModuleFaces.BOTTOM: self._bottom, + } + + for face in config.ALLOWED_FACES[self.module_type]: + if face_mapping[face] is None: + available.append(face) + return available + + def __repr__(self) -> str: + """Return a nice string representation of the tree node.""" + child_count = len(self.children) + available_count = len(self.available_faces()) + child_info = f", {child_count} children" if child_count > 0 else "" + available_info = f", {available_count} available faces" if available_count > 0 else "" + return f"TreeNodeLukas({self.module_type.name}, {self.rotation.name}, depth={self._depth}{child_info}{available_info})" + + def add_child(self, face: config.ModuleFaces, child_module: config.ModuleInstance): + """Add a child to the specified face.""" + if face not in self.available_faces(): + raise ValueError(f"Face {face} is not available for attachment.") + + child_node = TreeNodeLukas(child_module, depth=self._depth + 1) + setattr(self, face.name.lower(), child_node) + + def remove_child(self, face: config.ModuleFaces): + """Remove a child from the specified face.""" + if face not in config.ALLOWED_FACES[self.module_type]: + raise ValueError(f"Face {face} is not valid for module type {self.module_type}.") + + setattr(self, face.name.lower(), None) + + def get_child(self, face: config.ModuleFaces) -> 'TreeNodeLukas | None': + """Get the child at the specified face.""" + if face not in config.ALLOWED_FACES[self.module_type]: + return None + return getattr(self, face.name.lower(), None) + + def find_node_dfs(self, target_id: int) -> 'TreeNodeLukas | None': + """Find a node by ID using Depth-First Search.""" + if self._id == target_id: + return self + + # Search children recursively + for child in self.children.values(): + result = child.find_node_dfs(target_id) + if result is not None: + return result + + return None + + def find_node_bfs(self, target_id: int) -> 'TreeNodeLukas | None': + """Find a node by ID using Breadth-First Search.""" + queue = deque([self]) + + while queue: + current = queue.popleft() + + if current._id == target_id: + return current + + # Add all children to queue + queue.extend(current.children.values()) + + return None + + def find_all_nodes_dfs(self, predicate: Callable[TreeNode, bool] = None) -> list['TreeNodeLukas']: + """Find all nodes matching a predicate using DFS.""" + result = [] + + def dfs_helper(node: 'TreeNodeLukas'): + if predicate is None or predicate(node): + result.append(node) + + for child in node.children.values(): + dfs_helper(child) + + dfs_helper(self) + return result + + def find_all_nodes_bfs(self, predicate: Callable[TreeNode, bool] = None) -> list['TreeNodeLukas']: + """Find all nodes matching a predicate using BFS.""" + result = [] + queue = deque([self]) + + while queue: + current = queue.popleft() + + if predicate is None or predicate(current): + result.append(current) + + queue.extend(current.children.values()) + + return result + +# Generate a simple tree for demonstration +def davide(): + tree = Tree() + root = tree.root + root.add_child(config.ModuleFaces.FRONT, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + root.add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) + root.children[config.ModuleFaces.FRONT].add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + print(tree) + print(root) + tree.tree_to_digraph() + graph = tree.graph + print(graph.nodes(data=True)) + # draw_graph(graph, title="Tree Structure", save_file="tree_structure.png") + + +def lukas(): + genome = TreeGenome() + root = TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + genome.root = root + subtree = TreeGenome() + subtree.root = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + subtree.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + subtree.root.left = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + with root.enable_replacement(): + root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + subtree.root.find_node_bfs(subtree.root.id) + root.left = subtree + #root.front.back = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + #root.back = root.front + print(root.front.available_faces()) + print(genome) # Shows full tree structure + print(root) # Shows node details with available faces + + +lukas() diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py deleted file mode 100644 index 0f8ebdcc..00000000 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome/tree_genome.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import annotations -from ast import Dict -from typing import Any -from zipfile import Path -import matplotlib.pyplot as plt -import ariel.src.ariel.body_phenotypes.robogen_lite.config as config - -import networkx as nx -from networkx import DiGraph -from networkx.readwrite import json_graph - -class Tree: - def __init__(self): - self.graph = nx.DiGraph() - self.root = TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={}), depth=0) - - def tree_to_digraph(self) -> nx.DiGraph: - """ - Convert Tree (rooted at self.root) to a NetworkX DiGraph. - Nodes are given integer ids (0..N-1) in DFS order. - - Node attrs: type=, rotation=, depth= - Edge attrs: face= - """ - # Stable ids for each TreeNode instance - node_id: Dict[TreeNode, int] = {} - next_id = 0 - - def get_id(n: TreeNode) -> int: - nonlocal next_id - if n not in node_id: - node_id[n] = next_id - next_id += 1 - return node_id[n] - - def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None): - id = get_id(child) - # add/update the node with attributes (use .name to make JSON-friendly) - self.graph.add_node( - id, - type=child.module_type.name, - rotation=child.rotation.name, - # depth=child._depth, - ) - - if parent is not None: - parent_id = get_id(parent) - # face stored as string (Enum.name) for readability / JSON - self.graph.add_edge(parent_id, id, face=via_face.name if via_face else None) - - # descend - for face, sub in child.children.items(): - # Expect sub to be a TreeNode - dfs(child, sub, face) - - dfs(None, self.root, None) - -class TreeNode: - def __init__(self, module: config.ModuleInstance, depth: int = 0): - self.module_type = module.type - self.rotation = module.rotation - # type: dict[ModuleFaces, TreeNode] - self.children = module.links - self._depth = depth - - def add_child(self, face: config.ModuleFaces, child_module: config.ModuleInstance): - if face in self.children: - raise ValueError(f"Face {face} already has a child.") - if face not in config.ALLOWED_FACES[self.module_type]: - raise ValueError(f"Face {face} is not allowed for module type {self.module_type}.") - self.children[face] = TreeNode(child_module, depth=self._depth + 1) - -# from matplotlib.figure import Figure -# from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas -# from pathlib import Path - -# def draw_graph( -# graph: DiGraph[Any], -# title: str = "NetworkX Directed Graph", -# save_file: Path | str = "graph.png", -# ) -> None: -# # --- NO pyplot here; use Figure + Agg canvas --- -# fig = Figure() -# canvas = FigureCanvas(fig) -# ax = fig.add_subplot(111) - -# # Layouts (deterministic seed) -# pos = nx.spectral_layout(graph) -# pos = nx.spring_layout(graph, pos=pos, k=1, iterations=20, seed=42) - -# # Draw on explicit axes -# nx.draw( -# graph, -# pos, -# with_labels=True, -# node_size=150, -# node_color="#FFFFFF00", -# edgecolors="blue", -# font_size=8, -# width=0.5, -# ax=ax, -# ) - -# edge_labels = nx.get_edge_attributes(graph, "face") -# nx.draw_networkx_edge_labels( -# graph, -# pos, -# edge_labels=edge_labels, -# font_color="red", -# font_size=8, -# ax=ax, -# ) - -# ax.set_title(title) -# fig.tight_layout() - -# # Save via Agg canvas (no GUI backend involved) -# fig.savefig(save_file, dpi=300, bbox_inches="tight") - - -# Generate a simple tree for demonstration -tree = Tree() -tree.root.add_child(config.ModuleFaces.FRONT, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) -tree.root.add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) -tree.root.children[config.ModuleFaces.FRONT].add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) -tree.tree_to_digraph() -graph = tree.graph -print(graph.nodes(data=True)) -# draw_graph(graph, title="Tree Structure", save_file="tree_structure.png") - diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 42484398..72ace288 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -11,6 +11,8 @@ from rich.console import Console from rich.traceback import install +from ariel.body_phenotypes.robogen_lite.decoders.tree_genome import TreeNode, TreeGenome + # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] CWD = Path.cwd() @@ -138,7 +140,18 @@ def integer_creep( mutation_mask = mutator * sub_mask * do_mask new_genotype = ind_arr + mutation_mask return cast("Integers", new_genotype.astype(int).tolist()) - + + +class TreeGenerator: + @staticmethod + def __call__(*args, **kwargs) -> TreeGenome: + return TreeGenome.default_init() + + @staticmethod + def default(): + return TreeGenome.default_init() + + def main() -> None: """Entry point.""" console.log(IntegersGenerator.integers(-5, 5, 5)) From 8367cf020811c74b45ad70f66d33ef75ff35777b Mon Sep 17 00:00:00 2001 From: Lukas Bierling Date: Wed, 1 Oct 2025 16:08:48 +0200 Subject: [PATCH 12/47] tree node refinement --- src/ariel/.idea/.gitignore | 8 -------- src/ariel/.idea/AugmentWebviewStateStore.xml | 10 ---------- src/ariel/.idea/ariel.iml | 12 ------------ .../.idea/inspectionProfiles/Project_Default.xml | 7 ------- .../.idea/inspectionProfiles/profiles_settings.xml | 6 ------ src/ariel/.idea/misc.xml | 7 ------- src/ariel/.idea/modules.xml | 8 -------- src/ariel/.idea/vcs.xml | 6 ------ 8 files changed, 64 deletions(-) delete mode 100644 src/ariel/.idea/.gitignore delete mode 100644 src/ariel/.idea/AugmentWebviewStateStore.xml delete mode 100644 src/ariel/.idea/ariel.iml delete mode 100644 src/ariel/.idea/inspectionProfiles/Project_Default.xml delete mode 100644 src/ariel/.idea/inspectionProfiles/profiles_settings.xml delete mode 100644 src/ariel/.idea/misc.xml delete mode 100644 src/ariel/.idea/modules.xml delete mode 100644 src/ariel/.idea/vcs.xml diff --git a/src/ariel/.idea/.gitignore b/src/ariel/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/src/ariel/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/src/ariel/.idea/AugmentWebviewStateStore.xml b/src/ariel/.idea/AugmentWebviewStateStore.xml deleted file mode 100644 index 8ae4cf5c..00000000 --- a/src/ariel/.idea/AugmentWebviewStateStore.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/ariel/.idea/ariel.iml b/src/ariel/.idea/ariel.iml deleted file mode 100644 index 1a2fd2af..00000000 --- a/src/ariel/.idea/ariel.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/ariel/.idea/inspectionProfiles/Project_Default.xml b/src/ariel/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 9d943cdb..00000000 --- a/src/ariel/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/ariel/.idea/inspectionProfiles/profiles_settings.xml b/src/ariel/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2da..00000000 --- a/src/ariel/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/ariel/.idea/misc.xml b/src/ariel/.idea/misc.xml deleted file mode 100644 index bcd4c767..00000000 --- a/src/ariel/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/ariel/.idea/modules.xml b/src/ariel/.idea/modules.xml deleted file mode 100644 index 9606e907..00000000 --- a/src/ariel/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/ariel/.idea/vcs.xml b/src/ariel/.idea/vcs.xml deleted file mode 100644 index b2bdec2d..00000000 --- a/src/ariel/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 632464fc4fae30066bbea247af27266c22f162c5 Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 1 Oct 2025 18:55:15 +0200 Subject: [PATCH 13/47] Added crossover for trees and helper methods --- .../robogen_lite/decoders/tree_genome.py | 173 ++++++++++-------- src/ariel/ec/a005.py | 39 ++++ 2 files changed, 138 insertions(+), 74 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py index 96f8f7cb..08c6bdbc 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py @@ -3,7 +3,7 @@ from typing import Any from zipfile import Path import matplotlib.pyplot as plt -import ariel.body_phenotypes.robogen_lite.config as config +import src.ariel.body_phenotypes.robogen_lite.config as config import contextlib from collections import deque @@ -11,7 +11,7 @@ from jedi.inference.gradual.typing import Callable from networkx import DiGraph from networkx.readwrite import json_graph - +from functools import reduce ''' class Tree: @@ -154,7 +154,7 @@ def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | class TreeGenome: - def __init__(self, root: TreeNodeLukas | None = None): + def __init__(self, root: TreeNode | None = None): self._root = root @classmethod @@ -164,11 +164,11 @@ def default_init(cls, *args, **kwargs): rotation=config.ModuleRotationsIdx.DEG_90, links={}))) @property - def root(self) -> TreeNodeLukas | None: + def root(self) -> TreeNode | None: return self._root @root.setter - def root(self, value: TreeNodeLukas | None): + def root(self, value: TreeNode | None): if self._root is not None: raise ValueError("Root node cannot be changed once set.") self._root = value @@ -188,13 +188,13 @@ def _iter_nodes(self): if self._root: yield from self._iter_nodes_recursive(self._root) - def _iter_nodes_recursive(self, node: TreeNodeLukas): + def _iter_nodes_recursive(self, node: TreeNode): """Recursively iterate over nodes.""" yield node for child in node.children.values(): yield from self._iter_nodes_recursive(child) - def _format_node(self, node: TreeNodeLukas, prefix: str, is_last: bool) -> list[str]: + def _format_node(self, node: TreeNode, prefix: str, is_last: bool) -> list[str]: """Helper method to format a node and its children recursively.""" connector = "└── " if is_last else "├── " node_info = f"{node.module_type.name}({node.rotation.name}, depth={node._depth})" @@ -214,15 +214,15 @@ def _format_node(self, node: TreeNodeLukas, prefix: str, is_last: bool) -> list[ return lines - def add_child_to_node(self, node: TreeNodeLukas, face: config.ModuleFaces, child_module: config.ModuleInstance): + def add_child_to_node(self, node: TreeNode, face: config.ModuleFaces, child_module: config.ModuleInstance): """Helper method to add a child to a specific node. However, not recommended to use. Rather use """ if face not in node.available_faces(): raise ValueError(f"Face {face} is not available on this node.") - child_node = TreeNodeLukas(child_module, depth=node._depth + 1) + child_node = TreeNode(child_module, depth=node._depth + 1) setattr(node, face.name.lower(), child_node) - def find_node(self, target_id: int, method: str = "dfs") -> TreeNodeLukas | None: + def find_node(self, target_id: int, method: str = "dfs") -> TreeNode | None: """Find a node by ID in the entire genome.""" if not self._root: return None @@ -232,7 +232,7 @@ def find_node(self, target_id: int, method: str = "dfs") -> TreeNodeLukas | None else: return self._root.find_node_dfs(target_id) - def find_nodes_by_type(self, module_type: config.ModuleType, method: str = "dfs") -> list[TreeNodeLukas]: + def find_nodes_by_type(self, module_type: config.ModuleType, method: str = "dfs") -> list[TreeNode]: """Find all nodes of a specific module type.""" if not self._root: return [] @@ -246,6 +246,7 @@ def find_nodes_by_type(self, module_type: config.ModuleType, method: str = "dfs" class TreeNode: + def __init__(self, module: config.ModuleInstance, depth: int = 0, node_id: int = None): self.module_type = module.type self.rotation = module.rotation @@ -293,7 +294,7 @@ def _can_attach_to_face(self, face: config.ModuleFaces, node: TreeNode | None) - return False # Face already occupied return True - def _set_face(self, face: config.ModuleFaces, value: 'TreeNodeLukas | TreeGenome | None'): + def _set_face(self, face: config.ModuleFaces, value: 'TreeNode | TreeGenome | None'): """Common method to validate and set a face attribute.""" # Handle TreeGenome by extracting its root if isinstance(value, TreeGenome): @@ -317,12 +318,28 @@ def _set_face(self, face: config.ModuleFaces, value: 'TreeNodeLukas | TreeGenome else: self.module.links.pop(face, None) + def _get_face_given_child(self, child_id: int) -> config.ModuleFaces | None: + # Weird flex + # ids_to_faces = reduce(lambda acc, x: {**acc, **x}, map(lambda face_node: {face_node[1].id: face_node[0]}, self.children.items()), {}) + return {face_node[1].id: face_node[0] for face_node in self.children.items()}[child_id] + + def face_mapping(self, face: config.ModuleFaces): + mapping = { + config.ModuleFaces.FRONT: self._front, + config.ModuleFaces.BACK: self._back, + config.ModuleFaces.RIGHT: self._right, + config.ModuleFaces.LEFT: self._left, + config.ModuleFaces.TOP: self._top, + config.ModuleFaces.BOTTOM: self._bottom, + } + return mapping[face] + @property def front(self) -> TreeNode | None: return self._front @front.setter - def front(self, value: 'TreeNodeLukas | TreeGenome | None'): + def front(self, value: 'TreeNode | TreeGenome | None'): self._set_face(config.ModuleFaces.FRONT, value) @property @@ -330,7 +347,7 @@ def back(self) -> TreeNode | None: return self._back @back.setter - def back(self, value: 'TreeNodeLukas | TreeGenome | None'): + def back(self, value: 'TreeNode | TreeGenome | None'): self._set_face(config.ModuleFaces.BACK, value) @property @@ -338,7 +355,7 @@ def right(self) -> TreeNode | None: return self._right @right.setter - def right(self, value: 'TreeNodeLukas | TreeGenome | None'): + def right(self, value: 'TreeNode | TreeGenome | None'): self._set_face(config.ModuleFaces.RIGHT, value) @property @@ -346,7 +363,7 @@ def left(self) -> TreeNode | None: return self._left @left.setter - def left(self, value: 'TreeNodeLukas | TreeGenome | None'): + def left(self, value: 'TreeNode | TreeGenome | None'): self._set_face(config.ModuleFaces.LEFT, value) @property @@ -354,7 +371,7 @@ def top(self) -> TreeNode | None: return self._top @top.setter - def top(self, value: 'TreeNodeLukas | TreeGenome | None'): + def top(self, value: 'TreeNode | TreeGenome | None'): self._set_face(config.ModuleFaces.TOP, value) @property @@ -362,23 +379,14 @@ def bottom(self) -> TreeNode | None: return self._bottom @bottom.setter - def bottom(self, value: 'TreeNodeLukas | TreeGenome | None'): + def bottom(self, value: 'TreeNode | TreeGenome | None'): self._set_face(config.ModuleFaces.BOTTOM, value) @property def children(self) -> dict[config.ModuleFaces, TreeNode]: result = {} - face_mapping = { - config.ModuleFaces.FRONT: self._front, - config.ModuleFaces.BACK: self._back, - config.ModuleFaces.RIGHT: self._right, - config.ModuleFaces.LEFT: self._left, - config.ModuleFaces.TOP: self._top, - config.ModuleFaces.BOTTOM: self._bottom, - } - for face in config.ALLOWED_FACES[self.module_type]: - child = face_mapping[face] + child = self.face_mapping(face) if child is not None: result[face] = child return result @@ -386,17 +394,8 @@ def children(self) -> dict[config.ModuleFaces, TreeNode]: def available_faces(self) -> list[config.ModuleFaces]: """Return list of faces that can still accept children.""" available = [] - face_mapping = { - config.ModuleFaces.FRONT: self._front, - config.ModuleFaces.BACK: self._back, - config.ModuleFaces.RIGHT: self._right, - config.ModuleFaces.LEFT: self._left, - config.ModuleFaces.TOP: self._top, - config.ModuleFaces.BOTTOM: self._bottom, - } - for face in config.ALLOWED_FACES[self.module_type]: - if face_mapping[face] is None: + if self.face_mapping(face) is None: available.append(face) return available @@ -406,14 +405,14 @@ def __repr__(self) -> str: available_count = len(self.available_faces()) child_info = f", {child_count} children" if child_count > 0 else "" available_info = f", {available_count} available faces" if available_count > 0 else "" - return f"TreeNodeLukas({self.module_type.name}, {self.rotation.name}, depth={self._depth}{child_info}{available_info})" + return f"TreeNode({self.module_type.name}, {self.rotation.name}, depth={self._depth}{child_info}{available_info})" def add_child(self, face: config.ModuleFaces, child_module: config.ModuleInstance): """Add a child to the specified face.""" if face not in self.available_faces(): raise ValueError(f"Face {face} is not available for attachment.") - child_node = TreeNodeLukas(child_module, depth=self._depth + 1) + child_node = TreeNode(child_module, depth=self._depth + 1) setattr(self, face.name.lower(), child_node) def remove_child(self, face: config.ModuleFaces): @@ -423,13 +422,13 @@ def remove_child(self, face: config.ModuleFaces): setattr(self, face.name.lower(), None) - def get_child(self, face: config.ModuleFaces) -> 'TreeNodeLukas | None': + def get_child(self, face: config.ModuleFaces) -> 'TreeNode | None': """Get the child at the specified face.""" if face not in config.ALLOWED_FACES[self.module_type]: return None return getattr(self, face.name.lower(), None) - def find_node_dfs(self, target_id: int) -> 'TreeNodeLukas | None': + def find_node_dfs(self, target_id: int) -> 'TreeNode | None': """Find a node by ID using Depth-First Search.""" if self._id == target_id: return self @@ -442,7 +441,7 @@ def find_node_dfs(self, target_id: int) -> 'TreeNodeLukas | None': return None - def find_node_bfs(self, target_id: int) -> 'TreeNodeLukas | None': + def find_node_bfs(self, target_id: int) -> 'TreeNode | None': """Find a node by ID using Breadth-First Search.""" queue = deque([self]) @@ -457,11 +456,11 @@ def find_node_bfs(self, target_id: int) -> 'TreeNodeLukas | None': return None - def find_all_nodes_dfs(self, predicate: Callable[TreeNode, bool] = None) -> list['TreeNodeLukas']: + def find_all_nodes_dfs(self, predicate: Callable[TreeNode, bool] | None = None) -> list['TreeNode']: """Find all nodes matching a predicate using DFS.""" result = [] - def dfs_helper(node: 'TreeNodeLukas'): + def dfs_helper(node: 'TreeNode'): if predicate is None or predicate(node): result.append(node) @@ -471,7 +470,7 @@ def dfs_helper(node: 'TreeNodeLukas'): dfs_helper(self) return result - def find_all_nodes_bfs(self, predicate: Callable[TreeNode, bool] = None) -> list['TreeNodeLukas']: + def find_all_nodes_bfs(self, predicate: Callable[TreeNode, bool] = None) -> list['TreeNode']: """Find all nodes matching a predicate using BFS.""" result = [] queue = deque([self]) @@ -485,40 +484,66 @@ def find_all_nodes_bfs(self, predicate: Callable[TreeNode, bool] = None) -> list queue.extend(current.children.values()) return result + + def get_all_nodes(self, mode: str = "dfs", exclude_root: bool = True): + """ + Returns all the nodes in the subtree that has self as root node + """ + predicate_root: Callable[TreeNode, bool] = (lambda x: x.id != self.id) if exclude_root else (lambda _: True) + if mode == "dfs": + return self.find_all_nodes_dfs(predicate=predicate_root) + elif mode == "bfs": + return self.find_all_nodes_bfs(predicate=predicate_root) + else: + raise ValueError("Invalid mode. Valid modes: dfs, bfs") -# Generate a simple tree for demonstration -def davide(): - tree = Tree() - root = tree.root - root.add_child(config.ModuleFaces.FRONT, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - root.add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) - root.children[config.ModuleFaces.FRONT].add_child(config.ModuleFaces.TOP, config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - print(tree) - print(root) - tree.tree_to_digraph() - graph = tree.graph - print(graph.nodes(data=True)) - # draw_graph(graph, title="Tree Structure", save_file="tree_structure.png") + def get_internal_nodes(self, mode: str = "dfs", exclude_root: bool = True): + """ + Returns all the non-leaf nodes in the subtree that has self as root node + """ + predicate_root: Callable[TreeNode, bool] = (lambda x: x.id != self.id) if exclude_root else (lambda _: True) + predicate_internal_nodes: Callable[TreeNode, bool] = (lambda x: len(x.children) > 0) + predicate_list = [predicate_root, predicate_internal_nodes] + predicate: Callable[TreeNode, bool] = lambda x: reduce(lambda p, q: p(x) and q(x), predicate_list) + if mode == "dfs": + return self.find_all_nodes_dfs(predicate=predicate) + elif mode == "bfs": + return self.find_all_nodes_bfs(predicate=predicate) + else: + raise ValueError("Invalid mode. Valid modes: dfs, bfs") + def replace_node(self, node_to_remove: TreeNode, node_to_add: TreeNode): + """ + 1) Finds the father of node_to_remove in self subtree + 2) Replaces node_to_remove with node_to_add in the father's children + 3) + """ + predicate_is_father = lambda x: node_to_remove in x.children.values() + father = self.find_all_nodes_dfs(predicate=predicate_is_father) + if not father or len(father) > 1: + raise RuntimeError("Father not found, are you sure node_to_remove is in subtree?") + # We expect a list of len 1 in which there is the father + father = father[0] + if self._enable_replacement: + with father.enable_replacement(): + father._set_face(father._get_face_given_child(node_to_remove.id), node_to_add) + else: + raise RuntimeError("Replacement not enabled, use 'with node.enable_replacement()' context manager") def lukas(): genome = TreeGenome() - root = TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - genome.root = root - subtree = TreeGenome() - subtree.root = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - subtree.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - subtree.root.left = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - with root.enable_replacement(): - root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - subtree.root.find_node_bfs(subtree.root.id) - root.left = subtree - #root.front.back = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - #root.back = root.front - print(root.front.available_faces()) + genome.root = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + genome.root.left = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + #genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + with genome.root.enable_replacement(): + genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + print(genome.root.front.available_faces()) print(genome) # Shows full tree structure - print(root) # Shows node details with available faces + print(genome.root) # Shows node details with available faces + with genome.root.enable_replacement(): + genome.root.replace_node(genome.root.front, genome.root.left) + print(genome.root.get_all_nodes("dfs", True)) lukas() diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 1d8e32d6..33991f0e 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -4,6 +4,7 @@ from pathlib import Path # Third-party libraries +from ariel.src.ariel.body_phenotypes.robogen_lite.decoders.tree_genome.tree_genome import Tree import numpy as np from rich.console import Console from rich.traceback import install @@ -59,6 +60,44 @@ def one_point( child1 = child1.reshape(parent_i_arr_shape).astype(int).tolist() child2 = child2.reshape(parent_j_arr_shape).astype(int).tolist() return child1, child2 + +class TreeCrossover: + @staticmethod + def koza_default( + parent_i: Tree, + parent_j: Tree, + koza_internal_node_prob: float = 0.9, + ) -> tuple[Tree, Tree]: + """ + Koza default: + - In Parent A: choose an internal node with high probability (e.g., 90%) excluding root. + Falls back to any node if A has no internal nodes. + - In Parent B: choose any node uniformly (internal or terminal). + + Forcing at least one internal node increases the chance you actually change structure + (not just swapping a leaf for a leaf), while letting the other parent be unrestricted adds variety. + """ + parent_i_root, parent_j_root = parent_i.root, parent_j.root + parent_i_internal_nodes = parent_i_root.get_internal_nodes(mode="dfs", exclude_root=True) + + if RNG.random() > koza_internal_node_prob and parent_i_internal_nodes: + node_a = RNG.choice(parent_i_internal_nodes) + else: + node_a = RNG.choice(parent_i_root.get_all_nodes(mode="dfs", exclude_root=True)) + + parent_j_all_nodes = parent_j_root.get_all_nodes() + node_b = RNG.choice(parent_j_all_nodes) + + child1 = parent_i.copy() + child2 = parent_j.copy() + + with child1.enable_replacement(): + child1.replace_node(node_a, node_b) + with child2.enable_replacement(): + child2.replace_node(node_b, node_a) + + return child1, child2 + def main() -> None: From 81414f4ffca03c11caadcda7a1f8c69ad625b449 Mon Sep 17 00:00:00 2001 From: Lukas Bierling Date: Wed, 1 Oct 2025 20:40:16 +0200 Subject: [PATCH 14/47] fix the IDs copy pls --- .../robogen_lite/decoders/tree_genome.py | 130 +++++++++++++++--- src/ariel/ec/a005.py | 60 ++++++-- 2 files changed, 159 insertions(+), 31 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py index 08c6bdbc..08cf75dc 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py @@ -3,9 +3,10 @@ from typing import Any from zipfile import Path import matplotlib.pyplot as plt -import src.ariel.body_phenotypes.robogen_lite.config as config +import body_phenotypes.robogen_lite.config as config import contextlib from collections import deque +import copy import networkx as nx from jedi.inference.gradual.typing import Callable @@ -244,10 +245,33 @@ def find_nodes_by_type(self, module_type: config.ModuleType, method: str = "dfs" else: return self._root.find_all_nodes_dfs(predicate) + def copy(self) -> 'TreeGenome': + """Create a shallow copy of the TreeGenome.""" + new_genome = TreeGenome() + if self._root: + new_genome._root = self._root.copy() + return new_genome + + def __copy__(self) -> 'TreeGenome': + """Support for copy.copy().""" + return self.copy() + + def __deepcopy__(self, memo) -> 'TreeGenome': + """Support for copy.deepcopy().""" + new_genome = TreeGenome() + if self._root: + new_genome._root = copy.deepcopy(self._root, memo) + return new_genome + class TreeNode: - def __init__(self, module: config.ModuleInstance, depth: int = 0, node_id: int = None): + def __init__(self, module: config.ModuleInstance = None, depth: int = 0, node_id: int = None, + module_type: config.ModuleType = None, module_rotation: config.ModuleRotationsIdx = None,): + if module is None: + assert module_type is not None, "Module type cannot be None if module is not specified" + assert module_rotation is not None, "Module rotation cannot be None if module is not specified" + module = config.ModuleInstance(type=module_type, rotation=module_rotation, links={}) self.module_type = module.type self.rotation = module.rotation # Keep reference to the original module. Why? Because then the links get automatically filled and we can just read them out when decoding @@ -322,7 +346,7 @@ def _get_face_given_child(self, child_id: int) -> config.ModuleFaces | None: # Weird flex # ids_to_faces = reduce(lambda acc, x: {**acc, **x}, map(lambda face_node: {face_node[1].id: face_node[0]}, self.children.items()), {}) return {face_node[1].id: face_node[0] for face_node in self.children.items()}[child_id] - + def face_mapping(self, face: config.ModuleFaces): mapping = { config.ModuleFaces.FRONT: self._front, @@ -391,6 +415,21 @@ def children(self) -> dict[config.ModuleFaces, TreeNode]: result[face] = child return result + def __eq__(self, other) -> bool: + """Two nodes are equal if they have the same ID.""" + # This is my approach now (Lukas), we could also check for other equalities. + if not isinstance(other, TreeNode): + return False + return self._id == other._id + + def __hash__(self) -> int: + """Make TreeNode hashable using its ID.""" + return hash(self._id) + + def __ne__(self, other) -> bool: + """Not equal is the opposite of equal.""" + return not self.__eq__(other) + def available_faces(self) -> list[config.ModuleFaces]: """Return list of faces that can still accept children.""" available = [] @@ -484,7 +523,7 @@ def find_all_nodes_bfs(self, predicate: Callable[TreeNode, bool] = None) -> list queue.extend(current.children.values()) return result - + def get_all_nodes(self, mode: str = "dfs", exclude_root: bool = True): """ Returns all the nodes in the subtree that has self as root node @@ -514,21 +553,76 @@ def get_internal_nodes(self, mode: str = "dfs", exclude_root: bool = True): def replace_node(self, node_to_remove: TreeNode, node_to_add: TreeNode): """ - 1) Finds the father of node_to_remove in self subtree - 2) Replaces node_to_remove with node_to_add in the father's children - 3) + 1) Finds the parent of node_to_remove in self subtree + 2) Replaces node_to_remove with node_to_add in the parent's children + 3) """ - predicate_is_father = lambda x: node_to_remove in x.children.values() - father = self.find_all_nodes_dfs(predicate=predicate_is_father) - if not father or len(father) > 1: + predicate_is_parent = lambda x: node_to_remove in set(x.children.values()) + parent = self.find_all_nodes_dfs(predicate=predicate_is_parent) + if not parent or len(parent) > 1: raise RuntimeError("Father not found, are you sure node_to_remove is in subtree?") - # We expect a list of len 1 in which there is the father - father = father[0] - if self._enable_replacement: - with father.enable_replacement(): - father._set_face(father._get_face_given_child(node_to_remove.id), node_to_add) - else: - raise RuntimeError("Replacement not enabled, use 'with node.enable_replacement()' context manager") + # We expect a list of len 1 in which there is the parent + parent = parent[0] + parent._set_face(parent._get_face_given_child(node_to_remove.id), node_to_add) + + def copy(self) -> 'TreeNode': + """Create a deep copy of this node and all its children.""" + # Create new module instance + new_module = config.ModuleInstance( + type=self.module_type, + rotation=self.rotation, + links={} # Will be rebuilt + ) + + # Create new node + new_node = TreeNode(new_module, depth=self._depth) + + # Recursively copy children + for face, child in self.children.items(): + child_copy = child.copy() + new_node._set_face(face, child_copy) + + return new_node + + def __copy__(self) -> 'TreeNode': + """Support for copy.copy() - creates deep copy for tree structures.""" + return self.copy() + + def __deepcopy__(self, memo) -> 'TreeNode': + """Support for copy.deepcopy().""" + # Check if already copied + if id(self) in memo: + return memo[id(self)] + + # Create new module instance + new_module = config.ModuleInstance( + type=self.module_type, + rotation=self.rotation, + links={} + ) + + # Create new node + new_node = TreeNode(new_module, depth=self._depth) + memo[id(self)] = new_node + + # Recursively deepcopy children + for face, child in self.children.items(): + child_copy = copy.deepcopy(child, memo) + new_node._set_face(face, child_copy) + + return new_node + +@contextlib.contextmanager +def enable_replacement(*nodes: TreeNode): + try: + for node in nodes: + node._enable_replacement = True + yield + finally: + for node in nodes: + node._enable_replacement = False + + def lukas(): genome = TreeGenome() @@ -546,4 +640,4 @@ def lukas(): print(genome.root.get_all_nodes("dfs", True)) -lukas() +#lukas() diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 33991f0e..795ace5c 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -4,7 +4,7 @@ from pathlib import Path # Third-party libraries -from ariel.src.ariel.body_phenotypes.robogen_lite.decoders.tree_genome.tree_genome import Tree +from ariel.body_phenotypes.robogen_lite.decoders.tree_genome import TreeGenome, enable_replacement import numpy as np from rich.console import Console from rich.traceback import install @@ -60,14 +60,14 @@ def one_point( child1 = child1.reshape(parent_i_arr_shape).astype(int).tolist() child2 = child2.reshape(parent_j_arr_shape).astype(int).tolist() return child1, child2 - + class TreeCrossover: @staticmethod def koza_default( - parent_i: Tree, - parent_j: Tree, + parent_i: TreeGenome, + parent_j: TreeGenome, koza_internal_node_prob: float = 0.9, - ) -> tuple[Tree, Tree]: + ) -> tuple[TreeGenome, TreeGenome]: """ Koza default: - In Parent A: choose an internal node with high probability (e.g., 90%) excluding root. @@ -88,16 +88,50 @@ def koza_default( parent_j_all_nodes = parent_j_root.get_all_nodes() node_b = RNG.choice(parent_j_all_nodes) - child1 = parent_i.copy() - child2 = parent_j.copy() + parent_i_old = parent_i.copy() + parent_j_old = parent_j.copy() + child1 = parent_i + child2 = parent_j - with child1.enable_replacement(): - child1.replace_node(node_a, node_b) - with child2.enable_replacement(): - child2.replace_node(node_b, node_a) + with enable_replacement(child1, child2, node_a, node_b): + child1.root.replace_node(node_a, node_b) + child2.root.replace_node(node_b, node_a) + parent_i = parent_i_old + parent_j = parent_j_old return child1, child2 - + + +def tree_main(): + import body_phenotypes.robogen_lite.config as config + from ariel.body_phenotypes.robogen_lite.decoders.tree_genome import TreeNode, TreeGenome + + # Create first tree + genome1 = TreeGenome() + genome1.root = TreeNode( + config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) + genome1.root.front = TreeNode( + config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + genome1.root.back = TreeNode( + config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + + # Create second tree + genome2 = TreeGenome() + genome2.root = TreeNode( + config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) + genome2.root.right = TreeNode( + config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_180, links={})) + genome2.root.back = TreeNode( + config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_270, links={})) + + console.log("Parent 1:", genome1) + console.log("Parent 2:", genome2) + + # Perform crossover + child1, child2 = TreeCrossover.koza_default(genome1, genome2) + + console.log("Child 1:", child1) + console.log("Child 2:", child2) def main() -> None: @@ -111,4 +145,4 @@ def main() -> None: if __name__ == "__main__": - main() + tree_main() From 3b3cd9dc42f1b3c9ebd3bb4b387aedb3aa4f5909 Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 1 Oct 2025 22:26:36 +0200 Subject: [PATCH 15/47] Added tree decoder to NetworkX graph, fixed replacement of node bug, fixed context manager for enabling replacement --- .../robogen_lite/decoders/tree_decoder.py | 84 ++++++++ src/ariel/ec/a000.py | 2 +- src/ariel/ec/a005.py | 9 +- .../genotypes/tree}/tree_genome.py | 192 +----------------- 4 files changed, 101 insertions(+), 186 deletions(-) create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py rename src/ariel/{body_phenotypes/robogen_lite/decoders => ec/genotypes/tree}/tree_genome.py (74%) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py new file mode 100644 index 00000000..d44dc796 --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from typing import Optional, dict, Callable +import networkx as nx +from ariel.ec.genotypes.tree.tree_genome import TreeNode, TreeGenome +from ariel.body_phenotypes.robogen_lite import config + +def to_digraph(genome: TreeGenome, use_node_ids: bool = True) -> nx.DiGraph: + """ + Convert this genome (rooted at `genome.root`) to a NetworkX directed graph. + + The graph has a parent→child edge for each attachment in the tree. By default, + nodes in the graph are keyed by the `TreeNode.id` values maintained in your + structure. Optionally, you can request contiguous integer IDs (0..N-1) in DFS + order via `use_node_ids=False`. + + Node attributes + ---------------- + type : str + The module type (enum `.name`). + rotation : str + The rotation (enum `.name`). + depth : int + The tree depth stored on the node (`node._depth`). + raw_id : int + The original `TreeNode.id` (always present, even when `use_node_ids=False`). + + Edge attributes + ---------------- + face : str + The face label (enum `.name`) used to attach the child to its parent. + + Parameters + ---------- + use_node_ids : bool, optional (default: True) + If True, graph nodes are keyed by `TreeNode.id`. If False, assigns + contiguous integer IDs in DFS order starting at 0. + + Returns + ------- + nx.DiGraph + A directed graph representation of the tree. If the genome is empty + (`self.root is None`), returns an empty graph. + """ + g = nx.DiGraph() + root = genome.root + if root is None: + return g + + # Stable mapping: either identity (node.id) or contiguous DFS ids. + node_key: Callable[[TreeNode], int] + if use_node_ids: + node_key = lambda n: n.id + else: + # Assign 0..N-1 in first-seen (DFS) order + seen: dict[int, int] = {} + next_id = 0 + def node_key(n: TreeNode) -> int: + nonlocal next_id + if n.id not in seen: + seen[n.id] = next_id + next_id += 1 + return seen[n.id] + + def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None) -> None: + cid = node_key(child) + # Add/update child node with attributes (use enum names for JSON-friendliness) + g.add_node( + cid, + type=child.module_type.name, + rotation=child.rotation.name, + raw_id=child.id, + ) + + if parent is not None: + pid = node_key(parent) + g.add_edge(pid, cid, face=via_face.name if via_face is not None else None) + + # Recurse over children (face -> subnode) + for face, sub in child.children.items(): + dfs(child, sub, face) + + dfs(None, root, None) + return g \ No newline at end of file diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 72ace288..527759cb 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -11,7 +11,7 @@ from rich.console import Console from rich.traceback import install -from ariel.body_phenotypes.robogen_lite.decoders.tree_genome import TreeNode, TreeGenome +from ariel.ec.genotypes.tree.tree_genome import TreeGenome # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 795ace5c..1c2e6759 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -4,12 +4,12 @@ from pathlib import Path # Third-party libraries -from ariel.body_phenotypes.robogen_lite.decoders.tree_genome import TreeGenome, enable_replacement import numpy as np from rich.console import Console from rich.traceback import install # Local libraries +from ariel.ec.genotypes.tree.tree_genome import TreeGenome from ariel.ec.a000 import IntegersGenerator from ariel.ec.a001 import JSONIterable @@ -93,8 +93,9 @@ def koza_default( child1 = parent_i child2 = parent_j - with enable_replacement(child1, child2, node_a, node_b): + with child1.root.enable_replacement(): child1.root.replace_node(node_a, node_b) + with child2.root.enable_replacement(): child2.root.replace_node(node_b, node_a) parent_i = parent_i_old @@ -103,8 +104,8 @@ def koza_default( def tree_main(): - import body_phenotypes.robogen_lite.config as config - from ariel.body_phenotypes.robogen_lite.decoders.tree_genome import TreeNode, TreeGenome + import ariel.body_phenotypes.robogen_lite.config as config + from ariel.ec.genotypes.tree.tree_genome import TreeNode, TreeGenome # Create first tree genome1 = TreeGenome() diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py similarity index 74% rename from src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py rename to src/ariel/ec/genotypes/tree/tree_genome.py index 08cf75dc..ed033f96 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -3,7 +3,7 @@ from typing import Any from zipfile import Path import matplotlib.pyplot as plt -import body_phenotypes.robogen_lite.config as config +import ariel.body_phenotypes.robogen_lite.config as config import contextlib from collections import deque import copy @@ -14,145 +14,6 @@ from networkx.readwrite import json_graph from functools import reduce -''' -class Tree: - def __init__(self): - self.graph = nx.DiGraph() - self.root = TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={}), depth=0) - - def __repr__(self) -> str: - """Return a nice string representation of the tree structure.""" - if not self.root: - return "Tree(empty)" - - node_count = len(list(self._iter_nodes())) - lines = [f"Tree({node_count} nodes):"] - lines.extend(self._format_node(self.root, "", True)) - return "\n".join(lines) - - def _iter_nodes(self): - """Iterator over all nodes in the tree.""" - if self.root: - yield from self._iter_nodes_recursive(self.root) - - def _iter_nodes_recursive(self, node: 'TreeNode'): - """Recursively iterate over nodes.""" - yield node - for child in node.children.values(): - yield from self._iter_nodes_recursive(child) - - def _format_node(self, node: 'TreeNode', prefix: str, is_last: bool) -> list[str]: - """Helper method to format a node and its children recursively.""" - # Current node line - connector = "└── " if is_last else "├── " - node_info = f"{node.module_type.name}({node.rotation.name}, depth={node._depth})" - lines = [f"{prefix}{connector}{node_info}"] - - # Prepare prefix for children - child_prefix = prefix + (" " if is_last else "│ ") - - # Add children - if node.children: - child_items = list(node.children.items()) - for i, (face, child) in enumerate(child_items): - is_last_child = (i == len(child_items) - 1) - face_connector = "└── " if is_last_child else "├── " - lines.append(f"{child_prefix}{face_connector}[{face.name}]") - - # Add the child node with additional indentation - grandchild_prefix = child_prefix + (" " if is_last_child else "│ ") - lines.extend(self._format_node(child, grandchild_prefix, True)) - - return lines - - def tree_to_digraph(self) -> nx.DiGraph: - """ - Convert Tree (rooted at self.root) to a NetworkX DiGraph. - Nodes are given integer ids (0..N-1) in DFS order. - - Node attrs: type=, rotation=, depth= - Edge attrs: face= - """ - # Stable ids for each TreeNode instance - node_id: Dict[TreeNode, int] = {} - next_id = 0 - - def get_id(n: TreeNode) -> int: - nonlocal next_id - if n not in node_id: - node_id[n] = next_id - next_id += 1 - return node_id[n] - - def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None): - id = get_id(child) - # add/update the node with attributes (use .name to make JSON-friendly) - self.graph.add_node( - id, - type=child.module_type.name, - rotation=child.rotation.name, - # depth=child._depth, - ) - - if parent is not None: - parent_id = get_id(parent) - # face stored as string (Enum.name) for readability / JSON - self.graph.add_edge(parent_id, id, face=via_face.name if via_face else None) - - # descend - for face, sub in child.children.items(): - # Expect sub to be a TreeNode - dfs(child, sub, face) - - dfs(None, self.root, None) -''' -# from matplotlib.figure import Figure -# from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas -# from pathlib import Path - -# def draw_graph( -# graph: DiGraph[Any], -# title: str = "NetworkX Directed Graph", -# save_file: Path | str = "graph.png", -# ) -> None: -# # --- NO pyplot here; use Figure + Agg canvas --- -# fig = Figure() -# canvas = FigureCanvas(fig) -# ax = fig.add_subplot(111) - -# # Layouts (deterministic seed) -# pos = nx.spectral_layout(graph) -# pos = nx.spring_layout(graph, pos=pos, k=1, iterations=20, seed=42) - -# # Draw on explicit axes -# nx.draw( -# graph, -# pos, -# with_labels=True, -# node_size=150, -# node_color="#FFFFFF00", -# edgecolors="blue", -# font_size=8, -# width=0.5, -# ax=ax, -# ) - -# edge_labels = nx.get_edge_attributes(graph, "face") -# nx.draw_networkx_edge_labels( -# graph, -# pos, -# edge_labels=edge_labels, -# font_color="red", -# font_size=8, -# ax=ax, -# ) - -# ax.set_title(title) -# fig.tight_layout() - -# # Save via Agg canvas (no GUI backend involved) -# fig.savefig(save_file, dpi=300, bbox_inches="tight") - class TreeGenome: def __init__(self, root: TreeNode | None = None): @@ -266,8 +127,8 @@ def __deepcopy__(self, memo) -> 'TreeGenome': class TreeNode: - def __init__(self, module: config.ModuleInstance = None, depth: int = 0, node_id: int = None, - module_type: config.ModuleType = None, module_rotation: config.ModuleRotationsIdx = None,): + def __init__(self, module: config.ModuleInstance | None = None, depth: int = 0, node_id: int | None = None, + module_type: config.ModuleType | None = None, module_rotation: config.ModuleRotationsIdx | None = None): if module is None: assert module_type is not None, "Module type cannot be None if module is not specified" assert module_rotation is not None, "Module rotation cannot be None if module is not specified" @@ -299,11 +160,14 @@ def id(self, value: int | None): @contextlib.contextmanager def enable_replacement(self): """Context manager to temporarily allow replacement of existing children.""" + all_nodes_to_enable = list(self.get_all_nodes(mode="dfs", exclude_root=False)) try: - self._enable_replacement = True + for n in all_nodes_to_enable: + n._enable_replacement = True yield finally: - self._enable_replacement = False + for n in all_nodes_to_enable: + n._enable_replacement = False def _can_attach_to_face(self, face: config.ModuleFaces, node: TreeNode | None) -> bool: """Check if a node can be attached to the given face.""" @@ -415,7 +279,7 @@ def children(self) -> dict[config.ModuleFaces, TreeNode]: result[face] = child return result - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: """Two nodes are equal if they have the same ID.""" # This is my approach now (Lukas), we could also check for other equalities. if not isinstance(other, TreeNode): @@ -426,7 +290,7 @@ def __hash__(self) -> int: """Make TreeNode hashable using its ID.""" return hash(self._id) - def __ne__(self, other) -> bool: + def __ne__(self, other: object) -> bool: """Not equal is the opposite of equal.""" return not self.__eq__(other) @@ -575,7 +439,7 @@ def copy(self) -> 'TreeNode': ) # Create new node - new_node = TreeNode(new_module, depth=self._depth) + new_node = TreeNode(new_module, depth=self._depth, node_id=self._id) # Recursively copy children for face, child in self.children.items(): @@ -588,40 +452,6 @@ def __copy__(self) -> 'TreeNode': """Support for copy.copy() - creates deep copy for tree structures.""" return self.copy() - def __deepcopy__(self, memo) -> 'TreeNode': - """Support for copy.deepcopy().""" - # Check if already copied - if id(self) in memo: - return memo[id(self)] - - # Create new module instance - new_module = config.ModuleInstance( - type=self.module_type, - rotation=self.rotation, - links={} - ) - - # Create new node - new_node = TreeNode(new_module, depth=self._depth) - memo[id(self)] = new_node - - # Recursively deepcopy children - for face, child in self.children.items(): - child_copy = copy.deepcopy(child, memo) - new_node._set_face(face, child_copy) - - return new_node - -@contextlib.contextmanager -def enable_replacement(*nodes: TreeNode): - try: - for node in nodes: - node._enable_replacement = True - yield - finally: - for node in nodes: - node._enable_replacement = False - def lukas(): From 1d78642f5cfd20249f1e92d86c25cde460ef178e Mon Sep 17 00:00:00 2001 From: Lukas Bierling <98786106+Coluding@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:09:33 +0200 Subject: [PATCH 16/47] Revert "Added basic tree genome + conversion to digraph" --- .../robogen_lite/decoders/tree_decoder.py | 84 ---- src/ariel/ec/a000.py | 15 +- src/ariel/ec/a005.py | 76 +-- src/ariel/ec/genotypes/tree/tree_genome.py | 473 ------------------ 4 files changed, 2 insertions(+), 646 deletions(-) delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py delete mode 100644 src/ariel/ec/genotypes/tree/tree_genome.py diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py deleted file mode 100644 index d44dc796..00000000 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -from typing import Optional, dict, Callable -import networkx as nx -from ariel.ec.genotypes.tree.tree_genome import TreeNode, TreeGenome -from ariel.body_phenotypes.robogen_lite import config - -def to_digraph(genome: TreeGenome, use_node_ids: bool = True) -> nx.DiGraph: - """ - Convert this genome (rooted at `genome.root`) to a NetworkX directed graph. - - The graph has a parent→child edge for each attachment in the tree. By default, - nodes in the graph are keyed by the `TreeNode.id` values maintained in your - structure. Optionally, you can request contiguous integer IDs (0..N-1) in DFS - order via `use_node_ids=False`. - - Node attributes - ---------------- - type : str - The module type (enum `.name`). - rotation : str - The rotation (enum `.name`). - depth : int - The tree depth stored on the node (`node._depth`). - raw_id : int - The original `TreeNode.id` (always present, even when `use_node_ids=False`). - - Edge attributes - ---------------- - face : str - The face label (enum `.name`) used to attach the child to its parent. - - Parameters - ---------- - use_node_ids : bool, optional (default: True) - If True, graph nodes are keyed by `TreeNode.id`. If False, assigns - contiguous integer IDs in DFS order starting at 0. - - Returns - ------- - nx.DiGraph - A directed graph representation of the tree. If the genome is empty - (`self.root is None`), returns an empty graph. - """ - g = nx.DiGraph() - root = genome.root - if root is None: - return g - - # Stable mapping: either identity (node.id) or contiguous DFS ids. - node_key: Callable[[TreeNode], int] - if use_node_ids: - node_key = lambda n: n.id - else: - # Assign 0..N-1 in first-seen (DFS) order - seen: dict[int, int] = {} - next_id = 0 - def node_key(n: TreeNode) -> int: - nonlocal next_id - if n.id not in seen: - seen[n.id] = next_id - next_id += 1 - return seen[n.id] - - def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None) -> None: - cid = node_key(child) - # Add/update child node with attributes (use enum names for JSON-friendliness) - g.add_node( - cid, - type=child.module_type.name, - rotation=child.rotation.name, - raw_id=child.id, - ) - - if parent is not None: - pid = node_key(parent) - g.add_edge(pid, cid, face=via_face.name if via_face is not None else None) - - # Recurse over children (face -> subnode) - for face, sub in child.children.items(): - dfs(child, sub, face) - - dfs(None, root, None) - return g \ No newline at end of file diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 527759cb..42484398 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -11,8 +11,6 @@ from rich.console import Console from rich.traceback import install -from ariel.ec.genotypes.tree.tree_genome import TreeGenome - # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] CWD = Path.cwd() @@ -140,18 +138,7 @@ def integer_creep( mutation_mask = mutator * sub_mask * do_mask new_genotype = ind_arr + mutation_mask return cast("Integers", new_genotype.astype(int).tolist()) - - -class TreeGenerator: - @staticmethod - def __call__(*args, **kwargs) -> TreeGenome: - return TreeGenome.default_init() - - @staticmethod - def default(): - return TreeGenome.default_init() - - + def main() -> None: """Entry point.""" console.log(IntegersGenerator.integers(-5, 5, 5)) diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 1c2e6759..1d8e32d6 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -9,7 +9,6 @@ from rich.traceback import install # Local libraries -from ariel.ec.genotypes.tree.tree_genome import TreeGenome from ariel.ec.a000 import IntegersGenerator from ariel.ec.a001 import JSONIterable @@ -61,79 +60,6 @@ def one_point( child2 = child2.reshape(parent_j_arr_shape).astype(int).tolist() return child1, child2 -class TreeCrossover: - @staticmethod - def koza_default( - parent_i: TreeGenome, - parent_j: TreeGenome, - koza_internal_node_prob: float = 0.9, - ) -> tuple[TreeGenome, TreeGenome]: - """ - Koza default: - - In Parent A: choose an internal node with high probability (e.g., 90%) excluding root. - Falls back to any node if A has no internal nodes. - - In Parent B: choose any node uniformly (internal or terminal). - - Forcing at least one internal node increases the chance you actually change structure - (not just swapping a leaf for a leaf), while letting the other parent be unrestricted adds variety. - """ - parent_i_root, parent_j_root = parent_i.root, parent_j.root - parent_i_internal_nodes = parent_i_root.get_internal_nodes(mode="dfs", exclude_root=True) - - if RNG.random() > koza_internal_node_prob and parent_i_internal_nodes: - node_a = RNG.choice(parent_i_internal_nodes) - else: - node_a = RNG.choice(parent_i_root.get_all_nodes(mode="dfs", exclude_root=True)) - - parent_j_all_nodes = parent_j_root.get_all_nodes() - node_b = RNG.choice(parent_j_all_nodes) - - parent_i_old = parent_i.copy() - parent_j_old = parent_j.copy() - child1 = parent_i - child2 = parent_j - - with child1.root.enable_replacement(): - child1.root.replace_node(node_a, node_b) - with child2.root.enable_replacement(): - child2.root.replace_node(node_b, node_a) - - parent_i = parent_i_old - parent_j = parent_j_old - return child1, child2 - - -def tree_main(): - import ariel.body_phenotypes.robogen_lite.config as config - from ariel.ec.genotypes.tree.tree_genome import TreeNode, TreeGenome - - # Create first tree - genome1 = TreeGenome() - genome1.root = TreeNode( - config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) - genome1.root.front = TreeNode( - config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - genome1.root.back = TreeNode( - config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - - # Create second tree - genome2 = TreeGenome() - genome2.root = TreeNode( - config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_0, links={})) - genome2.root.right = TreeNode( - config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_180, links={})) - genome2.root.back = TreeNode( - config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_270, links={})) - - console.log("Parent 1:", genome1) - console.log("Parent 2:", genome2) - - # Perform crossover - child1, child2 = TreeCrossover.koza_default(genome1, genome2) - - console.log("Child 1:", child1) - console.log("Child 2:", child2) - def main() -> None: """Entry point.""" @@ -146,4 +72,4 @@ def main() -> None: if __name__ == "__main__": - tree_main() + main() diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py deleted file mode 100644 index ed033f96..00000000 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ /dev/null @@ -1,473 +0,0 @@ -from __future__ import annotations -from ast import Dict -from typing import Any -from zipfile import Path -import matplotlib.pyplot as plt -import ariel.body_phenotypes.robogen_lite.config as config -import contextlib -from collections import deque -import copy - -import networkx as nx -from jedi.inference.gradual.typing import Callable -from networkx import DiGraph -from networkx.readwrite import json_graph -from functools import reduce - - -class TreeGenome: - def __init__(self, root: TreeNode | None = None): - self._root = root - - @classmethod - def default_init(cls, *args, **kwargs): - """Default instantiation with a core root.""" - return cls(root=TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, - rotation=config.ModuleRotationsIdx.DEG_90, - links={}))) - @property - def root(self) -> TreeNode | None: - return self._root - - @root.setter - def root(self, value: TreeNode | None): - if self._root is not None: - raise ValueError("Root node cannot be changed once set.") - self._root = value - - def __repr__(self) -> str: - """Return a nice string representation of the tree genome.""" - if not self._root: - return "TreeGenome(empty)" - - node_count = len(list(self._iter_nodes())) - lines = [f"TreeGenome({node_count} nodes):"] - lines.extend(self._format_node(self._root, "", True)) - return "\n".join(lines) - - def _iter_nodes(self): - """Iterator over all nodes in the genome.""" - if self._root: - yield from self._iter_nodes_recursive(self._root) - - def _iter_nodes_recursive(self, node: TreeNode): - """Recursively iterate over nodes.""" - yield node - for child in node.children.values(): - yield from self._iter_nodes_recursive(child) - - def _format_node(self, node: TreeNode, prefix: str, is_last: bool) -> list[str]: - """Helper method to format a node and its children recursively.""" - connector = "└── " if is_last else "├── " - node_info = f"{node.module_type.name}({node.rotation.name}, depth={node._depth})" - lines = [f"{prefix}{connector}{node_info}"] - - child_prefix = prefix + (" " if is_last else "│ ") - - if node.children: - child_items = list(node.children.items()) - for i, (face, child) in enumerate(child_items): - is_last_child = (i == len(child_items) - 1) - face_connector = "└── " if is_last_child else "├── " - lines.append(f"{child_prefix}{face_connector}[{face.name}]") - - grandchild_prefix = child_prefix + (" " if is_last_child else "│ ") - lines.extend(self._format_node(child, grandchild_prefix, True)) - - return lines - - def add_child_to_node(self, node: TreeNode, face: config.ModuleFaces, child_module: config.ModuleInstance): - """Helper method to add a child to a specific node. However, not recommended to use. Rather use """ - if face not in node.available_faces(): - raise ValueError(f"Face {face} is not available on this node.") - - child_node = TreeNode(child_module, depth=node._depth + 1) - setattr(node, face.name.lower(), child_node) - - def find_node(self, target_id: int, method: str = "dfs") -> TreeNode | None: - """Find a node by ID in the entire genome.""" - if not self._root: - return None - - if method.lower() == "bfs": - return self._root.find_node_bfs(target_id) - else: - return self._root.find_node_dfs(target_id) - - def find_nodes_by_type(self, module_type: config.ModuleType, method: str = "dfs") -> list[TreeNode]: - """Find all nodes of a specific module type.""" - if not self._root: - return [] - - predicate = lambda node: node.module_type == module_type - - if method.lower() == "bfs": - return self._root.find_all_nodes_bfs(predicate) - else: - return self._root.find_all_nodes_dfs(predicate) - - def copy(self) -> 'TreeGenome': - """Create a shallow copy of the TreeGenome.""" - new_genome = TreeGenome() - if self._root: - new_genome._root = self._root.copy() - return new_genome - - def __copy__(self) -> 'TreeGenome': - """Support for copy.copy().""" - return self.copy() - - def __deepcopy__(self, memo) -> 'TreeGenome': - """Support for copy.deepcopy().""" - new_genome = TreeGenome() - if self._root: - new_genome._root = copy.deepcopy(self._root, memo) - return new_genome - - -class TreeNode: - - def __init__(self, module: config.ModuleInstance | None = None, depth: int = 0, node_id: int | None = None, - module_type: config.ModuleType | None = None, module_rotation: config.ModuleRotationsIdx | None = None): - if module is None: - assert module_type is not None, "Module type cannot be None if module is not specified" - assert module_rotation is not None, "Module rotation cannot be None if module is not specified" - module = config.ModuleInstance(type=module_type, rotation=module_rotation, links={}) - self.module_type = module.type - self.rotation = module.rotation - # Keep reference to the original module. Why? Because then the links get automatically filled and we can just read them out when decoding - self.module = module - self._depth = depth - self._front: TreeNode | None = None - self._back: TreeNode | None = None - self._right: TreeNode | None = None - self._left: TreeNode | None = None - self._top: TreeNode | None = None - self._bottom: TreeNode | None = None - - self._enable_replacement: bool = False - - self._id = id(self) if node_id is None else node_id - - @property - def id(self) -> int: - return self._id - - @id.setter - def id(self, value: int | None): - raise ValueError("ID cannot be changed once set.") - - @contextlib.contextmanager - def enable_replacement(self): - """Context manager to temporarily allow replacement of existing children.""" - all_nodes_to_enable = list(self.get_all_nodes(mode="dfs", exclude_root=False)) - try: - for n in all_nodes_to_enable: - n._enable_replacement = True - yield - finally: - for n in all_nodes_to_enable: - n._enable_replacement = False - - def _can_attach_to_face(self, face: config.ModuleFaces, node: TreeNode | None) -> bool: - """Check if a node can be attached to the given face.""" - if node is None: - return True # Can always detach (set to None) - if face not in config.ALLOWED_FACES[self.module_type]: - return False - # Check if face is already occupied (unless replacement is enabled) - if not self._enable_replacement: - face_attr = face.name.lower() - if getattr(self, f"_{face_attr}") is not None: - return False # Face already occupied - return True - - def _set_face(self, face: config.ModuleFaces, value: 'TreeNode | TreeGenome | None'): - """Common method to validate and set a face attribute.""" - # Handle TreeGenome by extracting its root - if isinstance(value, TreeGenome): - if value.root is None: - raise ValueError("Cannot attach empty TreeGenome (root is None)") - actual_value = value.root - else: - actual_value = value - - if not self._can_attach_to_face(face, actual_value): - if actual_value is not None and getattr(self, f"_{face.name.lower()}") is not None: - raise ValueError(f"{face.name} face already occupied on {self.module_type}") - raise ValueError(f"Cannot attach to {face.name} face of {self.module_type}") - - # Update the internal attribute with the actual node - setattr(self, f"_{face.name.lower()}", actual_value) - - # Update the module's links dictionary - if actual_value is not None: - self.module.links[face] = self._id - else: - self.module.links.pop(face, None) - - def _get_face_given_child(self, child_id: int) -> config.ModuleFaces | None: - # Weird flex - # ids_to_faces = reduce(lambda acc, x: {**acc, **x}, map(lambda face_node: {face_node[1].id: face_node[0]}, self.children.items()), {}) - return {face_node[1].id: face_node[0] for face_node in self.children.items()}[child_id] - - def face_mapping(self, face: config.ModuleFaces): - mapping = { - config.ModuleFaces.FRONT: self._front, - config.ModuleFaces.BACK: self._back, - config.ModuleFaces.RIGHT: self._right, - config.ModuleFaces.LEFT: self._left, - config.ModuleFaces.TOP: self._top, - config.ModuleFaces.BOTTOM: self._bottom, - } - return mapping[face] - - @property - def front(self) -> TreeNode | None: - return self._front - - @front.setter - def front(self, value: 'TreeNode | TreeGenome | None'): - self._set_face(config.ModuleFaces.FRONT, value) - - @property - def back(self) -> TreeNode | None: - return self._back - - @back.setter - def back(self, value: 'TreeNode | TreeGenome | None'): - self._set_face(config.ModuleFaces.BACK, value) - - @property - def right(self) -> TreeNode | None: - return self._right - - @right.setter - def right(self, value: 'TreeNode | TreeGenome | None'): - self._set_face(config.ModuleFaces.RIGHT, value) - - @property - def left(self) -> TreeNode | None: - return self._left - - @left.setter - def left(self, value: 'TreeNode | TreeGenome | None'): - self._set_face(config.ModuleFaces.LEFT, value) - - @property - def top(self) -> TreeNode | None: - return self._top - - @top.setter - def top(self, value: 'TreeNode | TreeGenome | None'): - self._set_face(config.ModuleFaces.TOP, value) - - @property - def bottom(self) -> TreeNode | None: - return self._bottom - - @bottom.setter - def bottom(self, value: 'TreeNode | TreeGenome | None'): - self._set_face(config.ModuleFaces.BOTTOM, value) - - @property - def children(self) -> dict[config.ModuleFaces, TreeNode]: - result = {} - for face in config.ALLOWED_FACES[self.module_type]: - child = self.face_mapping(face) - if child is not None: - result[face] = child - return result - - def __eq__(self, other: object) -> bool: - """Two nodes are equal if they have the same ID.""" - # This is my approach now (Lukas), we could also check for other equalities. - if not isinstance(other, TreeNode): - return False - return self._id == other._id - - def __hash__(self) -> int: - """Make TreeNode hashable using its ID.""" - return hash(self._id) - - def __ne__(self, other: object) -> bool: - """Not equal is the opposite of equal.""" - return not self.__eq__(other) - - def available_faces(self) -> list[config.ModuleFaces]: - """Return list of faces that can still accept children.""" - available = [] - for face in config.ALLOWED_FACES[self.module_type]: - if self.face_mapping(face) is None: - available.append(face) - return available - - def __repr__(self) -> str: - """Return a nice string representation of the tree node.""" - child_count = len(self.children) - available_count = len(self.available_faces()) - child_info = f", {child_count} children" if child_count > 0 else "" - available_info = f", {available_count} available faces" if available_count > 0 else "" - return f"TreeNode({self.module_type.name}, {self.rotation.name}, depth={self._depth}{child_info}{available_info})" - - def add_child(self, face: config.ModuleFaces, child_module: config.ModuleInstance): - """Add a child to the specified face.""" - if face not in self.available_faces(): - raise ValueError(f"Face {face} is not available for attachment.") - - child_node = TreeNode(child_module, depth=self._depth + 1) - setattr(self, face.name.lower(), child_node) - - def remove_child(self, face: config.ModuleFaces): - """Remove a child from the specified face.""" - if face not in config.ALLOWED_FACES[self.module_type]: - raise ValueError(f"Face {face} is not valid for module type {self.module_type}.") - - setattr(self, face.name.lower(), None) - - def get_child(self, face: config.ModuleFaces) -> 'TreeNode | None': - """Get the child at the specified face.""" - if face not in config.ALLOWED_FACES[self.module_type]: - return None - return getattr(self, face.name.lower(), None) - - def find_node_dfs(self, target_id: int) -> 'TreeNode | None': - """Find a node by ID using Depth-First Search.""" - if self._id == target_id: - return self - - # Search children recursively - for child in self.children.values(): - result = child.find_node_dfs(target_id) - if result is not None: - return result - - return None - - def find_node_bfs(self, target_id: int) -> 'TreeNode | None': - """Find a node by ID using Breadth-First Search.""" - queue = deque([self]) - - while queue: - current = queue.popleft() - - if current._id == target_id: - return current - - # Add all children to queue - queue.extend(current.children.values()) - - return None - - def find_all_nodes_dfs(self, predicate: Callable[TreeNode, bool] | None = None) -> list['TreeNode']: - """Find all nodes matching a predicate using DFS.""" - result = [] - - def dfs_helper(node: 'TreeNode'): - if predicate is None or predicate(node): - result.append(node) - - for child in node.children.values(): - dfs_helper(child) - - dfs_helper(self) - return result - - def find_all_nodes_bfs(self, predicate: Callable[TreeNode, bool] = None) -> list['TreeNode']: - """Find all nodes matching a predicate using BFS.""" - result = [] - queue = deque([self]) - - while queue: - current = queue.popleft() - - if predicate is None or predicate(current): - result.append(current) - - queue.extend(current.children.values()) - - return result - - def get_all_nodes(self, mode: str = "dfs", exclude_root: bool = True): - """ - Returns all the nodes in the subtree that has self as root node - """ - predicate_root: Callable[TreeNode, bool] = (lambda x: x.id != self.id) if exclude_root else (lambda _: True) - if mode == "dfs": - return self.find_all_nodes_dfs(predicate=predicate_root) - elif mode == "bfs": - return self.find_all_nodes_bfs(predicate=predicate_root) - else: - raise ValueError("Invalid mode. Valid modes: dfs, bfs") - - def get_internal_nodes(self, mode: str = "dfs", exclude_root: bool = True): - """ - Returns all the non-leaf nodes in the subtree that has self as root node - """ - predicate_root: Callable[TreeNode, bool] = (lambda x: x.id != self.id) if exclude_root else (lambda _: True) - predicate_internal_nodes: Callable[TreeNode, bool] = (lambda x: len(x.children) > 0) - predicate_list = [predicate_root, predicate_internal_nodes] - predicate: Callable[TreeNode, bool] = lambda x: reduce(lambda p, q: p(x) and q(x), predicate_list) - if mode == "dfs": - return self.find_all_nodes_dfs(predicate=predicate) - elif mode == "bfs": - return self.find_all_nodes_bfs(predicate=predicate) - else: - raise ValueError("Invalid mode. Valid modes: dfs, bfs") - - def replace_node(self, node_to_remove: TreeNode, node_to_add: TreeNode): - """ - 1) Finds the parent of node_to_remove in self subtree - 2) Replaces node_to_remove with node_to_add in the parent's children - 3) - """ - predicate_is_parent = lambda x: node_to_remove in set(x.children.values()) - parent = self.find_all_nodes_dfs(predicate=predicate_is_parent) - if not parent or len(parent) > 1: - raise RuntimeError("Father not found, are you sure node_to_remove is in subtree?") - # We expect a list of len 1 in which there is the parent - parent = parent[0] - parent._set_face(parent._get_face_given_child(node_to_remove.id), node_to_add) - - def copy(self) -> 'TreeNode': - """Create a deep copy of this node and all its children.""" - # Create new module instance - new_module = config.ModuleInstance( - type=self.module_type, - rotation=self.rotation, - links={} # Will be rebuilt - ) - - # Create new node - new_node = TreeNode(new_module, depth=self._depth, node_id=self._id) - - # Recursively copy children - for face, child in self.children.items(): - child_copy = child.copy() - new_node._set_face(face, child_copy) - - return new_node - - def __copy__(self) -> 'TreeNode': - """Support for copy.copy() - creates deep copy for tree structures.""" - return self.copy() - - - -def lukas(): - genome = TreeGenome() - genome.root = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - genome.root.left = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - #genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - with genome.root.enable_replacement(): - genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.HINGE, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - print(genome.root.front.available_faces()) - print(genome) # Shows full tree structure - print(genome.root) # Shows node details with available faces - with genome.root.enable_replacement(): - genome.root.replace_node(genome.root.front, genome.root.left) - print(genome.root.get_all_nodes("dfs", True)) - - -#lukas() From da2f606a2a5e96b09cf74ba5df58eff4bbbc5713 Mon Sep 17 00:00:00 2001 From: Lukas Bierling Date: Thu, 2 Oct 2025 09:11:48 +0200 Subject: [PATCH 17/47] new tree generators added, mutator WIP --- src/ariel/ec/a000.py | 128 ++++++++++++++++++++++++++++++++++++++++++- src/ariel/ec/a005.py | 2 + 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 527759cb..83987784 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -11,7 +11,8 @@ from rich.console import Console from rich.traceback import install -from ariel.ec.genotypes.tree.tree_genome import TreeGenome +from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode +import ariel.body_phenotypes.robogen_lite.config as pheno_config # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] @@ -151,6 +152,121 @@ def __call__(*args, **kwargs) -> TreeGenome: def default(): return TreeGenome.default_init() + @staticmethod + def linear_chain(length: int = 3) -> TreeGenome: + """Generate a linear chain of modules (snake-like).""" + genome = TreeGenome.default_init() # Start with CORE + current_node = genome.root + + for i in range(length): + module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) + rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) + module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) + + # Always attach to FRONT face for linear chain + if pheno_config.ModuleFaces.FRONT in current_node.available_faces(): + child = TreeNode(module, depth=current_node._depth + 1) + current_node._set_face(pheno_config.ModuleFaces.FRONT, child) + current_node = child + + return genome + + @staticmethod + def star_shape(num_arms: int = 3) -> TreeGenome: + """Generate a star-shaped tree with arms radiating from center.""" + genome = TreeGenome.default_init() # Start with CORE + available_faces = genome.root.available_faces() + + # Limit arms to available faces + actual_arms = min(num_arms, len(available_faces)) + selected_faces = RNG.choice(available_faces, size=actual_arms, replace=False) + + for face in selected_faces: + module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) + rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) + module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) + + child = TreeNode(module, depth=1) + genome.root._set_face(face, child) + + return genome + + @staticmethod + def binary_tree(depth: int = 2) -> TreeGenome: + """Generate a binary-like tree structure.""" + def build_subtree(current_depth: int, max_depth: int) -> TreeNode | None: + if current_depth >= max_depth: + return None + + module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) + rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) + module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) + + node = TreeNode(module, depth=current_depth) + available_faces = node.available_faces() + + # Add 1-2 children randomly + if available_faces and current_depth < max_depth - 1: + num_children = RNG.integers(1, min(3, len(available_faces) + 1)) + selected_faces = RNG.choice(available_faces, size=num_children, replace=False) + + for face in selected_faces: + child = build_subtree(current_depth + 1, max_depth) + if child: + node._set_face(face, child) + + return node + + genome = TreeGenome.default_init() + + # Add children to root + available_faces = genome.root.available_faces() + if available_faces: + num_children = RNG.integers(1, min(3, len(available_faces) + 1)) + selected_faces = RNG.choice(available_faces, size=num_children, replace=False) + + for face in selected_faces: + child = build_subtree(1, depth) + if child: + genome.root._set_face(face, child) + + return genome + + @staticmethod + def random_tree(max_depth: int = 4, branching_prob: float = 0.7) -> TreeGenome: + """Generate a random tree with pheno_configurable branching probability.""" + def build_random_subtree(current_depth: int) -> TreeNode | None: + if current_depth >= max_depth: + return None + + module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) + rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) + module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) + + node = TreeNode(module, depth=current_depth) + available_faces = node.available_faces() + + # Randomly decide to add children + for face in available_faces: + if RNG.random() < branching_prob: + child = build_random_subtree(current_depth + 1) + if child: + node._set_face(face, child) + + return node + + genome = TreeGenome.default_init() + + # Add children to root + available_faces = genome.root.available_faces() + for face in available_faces: + if RNG.random() < branching_prob: + child = build_random_subtree(1) + if child: + genome.root._set_face(face, child) + + return genome + def main() -> None: """Entry point.""" @@ -164,6 +280,16 @@ def main() -> None: ) console.log(example2) + console.rule("[bold blue]Tree Generator Examples") + + # Show different tree types + console.log("Linear chain:", TreeGenerator.linear_chain(4)) + console.log("Star shape:", TreeGenerator.star_shape(3)) + console.log("Binary tree:", TreeGenerator.binary_tree(3)) + console.log("Random tree:", TreeGenerator.random_tree(3, 0.6)) + + + if __name__ == "__main__": main() diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 1c2e6759..19f039b6 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -128,6 +128,8 @@ def tree_main(): console.log("Parent 1:", genome1) console.log("Parent 2:", genome2) + genome2.root.replace_node(genome1, genome2) + # Perform crossover child1, child2 = TreeCrossover.koza_default(genome1, genome2) From 7f2c5895efeb28acec115baf3cb2f5ae289e45bd Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 2 Oct 2025 23:22:20 +0200 Subject: [PATCH 18/47] Added integer mutator, added random treenode creation, fixed some typings. NB: THIS CODE IS NOT YET BEEN TESTEDgit add .git add .! --- src/ariel/ec/a000.py | 26 ++++++++++ src/ariel/ec/a001.py | 2 +- src/ariel/ec/genotypes/tree/tree_genome.py | 55 +++++++++++++++++----- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 83987784..87d4b17f 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -8,6 +8,7 @@ # Third-party libraries import numpy as np from pydantic_settings import BaseSettings +from collections import deque from rich.console import Console from rich.traceback import install @@ -267,6 +268,31 @@ def build_random_subtree(current_depth: int) -> TreeNode | None: return genome +class TreeMutator: + @staticmethod + def random_subtree_replacement( + individual: TreeGenome, + max_subtree_depth: int = 2, + ) -> TreeGenome: + """Replace a random subtree with a new random subtree.""" + if individual.root is None: + return individual + + # Collect all nodes in the tree + all_nodes = individual.root.get_all_nodes(exclude_root=True) + + # Select a random node to replace (excluding root) + if len(all_nodes) <= 1: + return individual # No replacement possible + + node_to_replace = RNG.choice(all_nodes[1:]) # Avoid replacing root + + # Generate a new random subtree + new_subtree = TreeNode.random_tree_node(max_depth=max_subtree_depth) + + individual.root.replace_node(node_to_replace, new_subtree) + + return individual def main() -> None: """Entry point.""" diff --git a/src/ariel/ec/a001.py b/src/ariel/ec/a001.py index edeedf69..31c363c0 100644 --- a/src/ariel/ec/a001.py +++ b/src/ariel/ec/a001.py @@ -81,7 +81,7 @@ def fitness(self) -> float: return self.fitness_ @fitness.setter - def fitness(self, fitness_value: float) -> None: + def fitness(self, fitness_value: float | None) -> None: if fitness_value is None: msg = "Trying to assign `None` to fitness!\n" msg += f"--> {self.fitness_value=}" diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index ed033f96..7192e268 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -1,19 +1,15 @@ from __future__ import annotations -from ast import Dict -from typing import Any -from zipfile import Path -import matplotlib.pyplot as plt import ariel.body_phenotypes.robogen_lite.config as config import contextlib from collections import deque import copy -import networkx as nx from jedi.inference.gradual.typing import Callable -from networkx import DiGraph -from networkx.readwrite import json_graph from functools import reduce +import numpy as np +SEED = 42 +RNG = np.random.default_rng(SEED) class TreeGenome: def __init__(self, root: TreeNode | None = None): @@ -25,6 +21,7 @@ def default_init(cls, *args, **kwargs): return cls(root=TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, rotation=config.ModuleRotationsIdx.DEG_90, links={}))) + @property def root(self) -> TreeNode | None: return self._root @@ -35,6 +32,10 @@ def root(self, value: TreeNode | None): raise ValueError("Root node cannot be changed once set.") self._root = value + @root.getter + def root(self) -> TreeNode | None: + return self._root + def __repr__(self) -> str: """Return a nice string representation of the tree genome.""" if not self._root: @@ -117,12 +118,13 @@ def __copy__(self) -> 'TreeGenome': """Support for copy.copy().""" return self.copy() - def __deepcopy__(self, memo) -> 'TreeGenome': - """Support for copy.deepcopy().""" - new_genome = TreeGenome() - if self._root: - new_genome._root = copy.deepcopy(self._root, memo) - return new_genome + # TODO: Implement this + # def __deepcopy__(self, memo) -> 'TreeGenome': + # """Support for copy.deepcopy().""" + # new_genome = TreeGenome() + # if self._root: + # new_genome._root = copy.deepcopy(self._root, memo) + # return new_genome class TreeNode: @@ -156,6 +158,33 @@ def id(self) -> int: @id.setter def id(self, value: int | None): raise ValueError("ID cannot be changed once set.") + + @classmethod + def random_tree_node(cls, max_depth: int = 2, branch_prob: float = 0.5) -> 'TreeNode': + """Create a random tree node with random children up to max_depth.""" + if max_depth < 0: + raise ValueError("max_depth must be non-negative") + + # Exclude CORE and NONE from random selection + module_type = RNG.choice([mt for mt in config.ModuleType if mt not in {config.ModuleType.CORE, config.ModuleType.NONE}]) + module_rotation = RNG.choice(list(config.ModuleRotationsIdx)) + node = cls(module_type=module_type, module_rotation=module_rotation) + + if max_depth == 0: + return node + + available_faces = config.ALLOWED_FACES[module_type] + num_children = RNG.integers(0, len(available_faces) + 1) + chosen_faces = RNG.choice(available_faces, size=num_children, replace=False) + + for face in chosen_faces: + if RNG.random() > branch_prob: + continue # Skip adding a child based on branch probability + # Recursively create child nodes with reduced depth + child_node = cls.random_tree_node(max_depth - 1) + node._set_face(face, child_node) + + return node @contextlib.contextmanager def enable_replacement(self): From 97f5531ca962d5899e231e503910b74a547f8fcc Mon Sep 17 00:00:00 2001 From: Olivier Moulin Date: Sat, 4 Oct 2025 17:34:45 +0200 Subject: [PATCH 19/47] updated mutation, not finished --- docs/source/ea_intro/ea_example.ipynb | 615 ------------------ .../decoders/l_system_mutation.py | 359 +++++----- 2 files changed, 193 insertions(+), 781 deletions(-) delete mode 100644 docs/source/ea_intro/ea_example.ipynb diff --git a/docs/source/ea_intro/ea_example.ipynb b/docs/source/ea_intro/ea_example.ipynb deleted file mode 100644 index 2b2e43d2..00000000 --- a/docs/source/ea_intro/ea_example.ipynb +++ /dev/null @@ -1,615 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "7f975200", - "metadata": {}, - "source": [ - "# This file shows the process of creating an EA using ARIEL " - ] - }, - { - "cell_type": "markdown", - "id": "c1a738e9", - "metadata": {}, - "source": [ - "### How does ARIEL EC module work?\n", - "\n", - "The ARIEL EC (Evolutionary Computing) module works a bit different than other EAs (Evolutionary Algorithms). While other EAs represent the population as a simple list of individuals, here they the population is made as its own class, with a population being type of `list[Individuals]`. \n", - "\n", - "Similarly, in traditional EA architecture an individual is chosen for certain operators (for example parent selection) according to some criteria, put into a separate list and then given to the operator function. ARIEL works by giving individuals what we call `tags`. An individual has `tags` that can be toggled, which qualify it for any and all operations. The tag can be whether an individual can crossover or mutate in the future, but it can also show if it can enter the learning cycle.\n", - "\n", - "The tags can be changed at all times, and default values for each tag can be given to more closely represent a traditional EA structure.\n", - "\n", - "Additionally, ARIEL utilizes an SQL database to handle the variables and outputs of the code. This makes the code run faster, but it adds an extra step to the process.\n", - "\n", - "This file demonstrates the process of initializing an EA class and running it for a simple problem, in our case, the Sphere function." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e608ecf", - "metadata": {}, - "outputs": [], - "source": [ - "# Standard library\n", - "import random\n", - "from typing import Literal, cast\n", - "\n", - "# Pretty little errors and progress bars\n", - "from rich.console import Console\n", - "from rich.traceback import install\n", - "\n", - "# Third-party libraries\n", - "import numpy as np\n", - "\n", - "# Local libraries\n", - "from ariel.ec.a000 import IntegerMutator\n", - "from ariel.ec.a001 import Individual\n", - "from ariel.ec.a005 import Crossover\n", - "from ariel.ec.a004 import EASettings, EAStep, EA, Population\n", - "\n", - "# Library to show fitness landscape\n", - "from fitness_plot import fitness_landscape_plot\n" - ] - }, - { - "cell_type": "markdown", - "id": "f90615ed", - "metadata": {}, - "source": [ - "#### Define fitness function" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0bdef6b6", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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Po6wblH96JVXXglWLwl2IdKDRLsQGYAILtUqSI6Mwp/V0jseEJ8IdfIJiHiOGzcueN7xwePXPvLII0P+AX74rAp47CAu2Lt16ybBXgt42xHt3/jGN+yQQw4JFaLwwo8YMSJUpSGqjgUGD/tPf/rTINZ5IfqFEOlB9hghMpSIuu+++9of//jHqogbi/YVV1yhRNQEwZQ8derU4H/nRUQUMYag50VZySyIVxfsRILZ3GThPZcicZdNINWMEPA889R///Of/2xnnnlm0SvRCCFKi0S7EBkq+UjiKfYLxDslHx999NHgsVYSWjJheuY0BNGF+CL/gLreiHd88JUafZZgL/44GjBggD3wwAOhDClRd3ztjKOTTjopWLSaNm0a92UKITaBRLsQGQK7xW233WYzZsywr3/96/aHP/whROBF8mGqpoOmC3hqauPxpgpNv379gle+EsQttg78/tg9qNRTCe8p7nFz0003hf4ClHXs2bNnEOz43v00h3FFV99f/OIX4d+FEMlEol0IIVLagIfOq4iu559/PlidiJwi4PExY4NIGxLsxR8n//M//2N33HFHEOzVG4L517z99tthHJ111lmb7EEghIgPiXYhhEg5tJ8fPHhwEF7Uk6fsn1to9tprr1QIeBfs2223ne24444S7AXC0s5JGidrQ4YMCVVmhBDpRqJdCCEqzA/+3HPPBQsNQp4GPFhoEPFYoagilGTBnmvzKrExLOt33323XXfddWEs7L///nFfkhCiCEi0CyFEBVcPGTp0aBDwTz31VKjtTRMnLDQHHnhgIqqHUEscwd65c2cJ9iLAkv7Xv/41+NPZtB188MFxX5IQokhItAshRAZYuXJlSF5FwD/55JPBfnL88ccHAU9zns022yw2wY4fH0uMKAyWcxp1UYudTVqfPn3iviQhRBGRaBdCiIzx1VdfhfKRAwcODAIeQU/1EDzwhx56aFnK/7lg79KlS6g9LwqDpZyyrt///vft8ccftyOOOCLuSxJCFBmJdiGEyDCU/xs1alSIwCP2ENPHHHNMEPDU727evHnRf6YEe/Hh87v00ktD7wU2YEKIykOiXQghRICOma+99lqVgJ81a1ZoxkMSK7+2atWqKJVu3nrrLQn2IkLpz+985zv2j3/8I2y2hBCViUS7EEKIGgU80XAEPKUkp0yZEiwXCHgi8W3atMm5LCOCne+JWO/atWvJrj1LPPPMM6HTMd1OTz311LgvRwhRQpJfvFeIjIN9gUoftBuPQnMdKm5QJQLwslKTGz8y3U6FKARqu++zzz5288032yeffBI6aO65556hWQ+iG4FI0iPdNOsT+2G8SrAXlxdeeCEI9vvuu0+CXYgMoEi7EClg7NixQYizONO1EM4991x799137fXXX7cmTZoE0d6jRw/797//be+995698847cV+2qEBYMj7++GN77LHHgoXmww8/tEMOOSTYMqhG0759+40i8CS9Mh75GmwxonC4pwj1O++8Mwh3NaMSovKRaBciJdDd8Nprrw0iacyYMWHBRrBXb03O1zzxxBMS7aLksHyMHz++SsC//fbb1qtXryDOaejUoUOHIC5PO+00+9GPfmTXXHNN3JdcEbzyyivh5I1Tj4svvliCXYiMINEuRErgUT3ssMNCR8v333/f+vfvbwMGDNjo6yTaRVzj8/PPP6/ywHPis+uuu4ZToksuucRuuukmicsiwH1lU3TDDTfY5ZdfrnsqRIaQaBciReAtRgjtscceoQJHTR0tJdpF3LCsUNHkjDPOsG233da++OIL++Y3vxmSWHnhaZfYzB1yAjjB+NWvfmU//OEPdQ+FyBhKRBUiRdCevEWLFjZp0qRQzUMUB6LAJF22bt3att566xDJ/PTTT+O+rNRC0io5F9g3sM8g2s8//3x76aWXQm7GwQcfbLfddluIwituVD/IX2HDc/XVV0uwC5FRJNqFSAmjR4+23//+9/b000/bvvvuG+oyS/AUB3zXWA2oUT506FBbtWqVHXnkkbZkyZK4Ly2Vfuujjz7abrnlFvve974XxGXHjh3tsssuC/d2+vTpVfd6v/32s/33399uvPFG++ijjzSea4E8lhNOOCGI9auuukqCXYiMInuMEClg6dKlIUKJGCIh9bPPPgsWmVtvvTWIoSiyxxQOTYWIuCPme/fuHfflpAaEOJsdouj42OuCpWf+/PnBRoMHfsiQIaGyDNFkTjoY35SdzDpY4qiLT8LpddddJ8EuRIaRaBciBfzgBz8ITVQ4IsceA/fcc4/95Cc/CUmpeISxISxevNjuvvtuGzZsmD3yyCPh63bbbbdQElLUH+5lt27dwr3t2bNn3JeTGmbOnBnGHl72XKHxEqdICPjnnnsuVJ5xAY8fPosCfty4cUGwU+aVk4ss3gMhxHok2oVIOER7Dz/8cBs+fLgddNBBG/wbreW/+uqr0GTl0EMPDV9bHfzvamaTWydQkv2IAo8aNSruy8kk2JKeffbZUIlm8ODB1q5du/CZIOCxhlFBqdLhueVkrV+/fnb77bcnXrCvWLEi2J0ILFD6Uw3ehCg+Eu1CCBEBuxGCEcG+3XbbxX05mWfZsmXBOoOAJxLfvHnz4O9GwNMpuKYKSmmHxF0EO6+77ror8YLdTwM5GeDZkWgXojRItAshxH+44oor7Mknn7QRI0bYDjvsEPfliGqsXLkynCoh4PmciLjThZVoNBVpNttsM0s706ZNCydoffr0sXvvvTcVpwoI9SuvvDJ8LrvvvrtEuxAlQqJdCJF5mAZpVkVXT2xI+NlFsqHCD3YwurGSeM2fEfD44LGKNW3a1NLGjBkzgocdm8n999+fCsFOHsNee+0VPoP27duHza5EuxClQaJdCJF5KE340EMPhehtjx49qv5+8803D3YMkWxWr15tI0eODJFeNl4kZB977LFBwPft2zcVnyEVi7hmqub8/e9/T4XtB/nANffq1St0Z6aqlUS7EKVDol0IkXlqK6NHtJOmQCJdAp7Sky7gZ8+eHbzhCHhsJy1btrSkMWfOHDvuuONs5513DlWf4rb50MCJajV18fHHH4dcg0cffTSceHAqINEuRGmRaBdCCFGxlYDefPPNYKFBwE+dOtWOOOKIIOCxobRp0ybuSwxVirD1dOrUKWw0klCelag/G4m62HHHHe20006zp556aoNNL5smBDxlKh988MEyXK0Q2UGiXQghRCYE/HvvvRcEPLXgJ06cGEqpIuCJcrdt27bsjYuoTU8pyy222CJ4wps1a2Zpq3LDe6ieRMs9xpev6ktCFBeJdiGEEJmCZe+jjz6qisDze6q1UEaSqPeWW25ZcgGP756qNwh1L2WZdmSPEaK0SLQLIYTILCyB1Bd3Af/OO++EJmZE4ImC05m12AJ+6dKldvLJJ4ff0zyqVatWVglItAtRWiTahRBCiP8IeIQn3nIsNGPGjLEDDjggiHdEPL7zQgU8zaLwgvPrc889lwhfvRAiHUi0CyGEENVgaZwyZUoQ77xGjx4d6pEj3nl16dIlZwG/YsUKO/PMM23u3Lmh8go+eiGEqC8S7UIIIUQdsEzS+Aj7DFF4OubuueeeVQKeUo2bEvB0cz3nnHPCRuDFF18MyadCCJELEu1CCCFEPWHJpPY7jbgQ8C+99FJoyOUCftddd91IwNOt9YILLgje+WHDhoXOoUIIkSsS7UIIIUQesHzOmzfPBg0aFCw0Q4cOta5duwbxTiWanj17hlKT3/3ud0O5SQQ7ia1CCJEPEu1CCCFEEViwYEEo34iAJ8l0m222CaUcqRYzatQo23bbbeO+RCFEipFoF0IIIUpQhx0LzS9/+csQiSfqLoQQhSDRLoQQQgghRMJpGPcFCCGEEEIIIepGol0IIYQQQoiEI9EuhBAiZ26++eZQ2vCHP/xh3JcihBCZQKJdCCFETrz++ut2zz33hAZDQgghyoNEuxBCiJyqopx11ll23333Wbt27eK+HCGEyAwS7UIIIerN5Zdfbscdd5z17ds37ksRQohM0TjuCxBCCJEOHn74YXvrrbeCPUYIIUR5kWgXQgixSSZPnmw/+MEPbOjQodasWbO4L0cIITKHmisJIYTYJE888YT169fPGjVqVPV3q1evDhVkGjZsaCtWrNjg34QQQhQXiXYhhBCbZNGiRfb5559v8HcXXHCB7bLLLvazn/3MevbsGdu1CSFEFpA9RgghxCZp3br1RsK8ZcuWtuWWW0qwCyFEGVD1GCGEEEIIIRKO7DFCCCGEEEIkHEXahRBCCCGESDgS7UIIIYQQQiQciXYhhBBCCCESjkS7EEIIIYQQCUeiXQghhBBCiIQj0S6EEEIIIUTCkWgXQgghhBAi4Ui0CyGEEEIIkXAk2oUQQgghhEg4Eu1CCCGEEEIkHIl2IYQQQgghEo5EuxBCCCGEEAlHol0IIYQQQoiEI9EuhBBCCCFEwpFoF0IIIYQQIuFItAshhBBCCJFwJNqFEEIIIYRIOBLtQgghhBBCJByJdiGEEEIIIRKORLsQQgghhBAJR6JdCCGEEEKIhCPRLoQQQgghRMKRaBdCCCGEECLhSLQLIYQQQgiRcCTahRBCCCGESDgS7UIIIYQQQiQciXYhhBBCCCESjkS7EEIIIYQQCUeiXQghhBBCiIQj0S6EEEIIIUTCkWgXQgghhBAi4Ui0CyGEEEIIkXAk2oUQQgghhEg4Eu1CCCGEEEIkHIl2IYQQQgghEo5EuxBCCCGEEAlHol0IIYQQQoiEI9EuhBBCCCFEwpFoF0IIIYQQIuFItAshhBBCCJFwJNqFEEIIIYSwZPP/AcdpNqHaolhfAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#### Define the fitness function\n", - "def Ackley(x):\n", - " \"\"\"source: https://www.sfu.ca/~ssurjano/ackley.html\"\"\"\n", - "\n", - " x = np.array(x)\n", - " # Ackley function parameters\n", - " a = 20\n", - " b = 0.2\n", - " c = 2 * np.pi\n", - " dimension = len(x)\n", - "\n", - " # Individual terms\n", - " term1 = -a * np.exp(-b * np.sqrt(sum(x**2) / dimension))\n", - " term2 = -np.exp(sum(np.cos(c * xi) for xi in x) / dimension)\n", - " return term1 + term2 + a + np.exp(1)\n", - "\n", - "def evaluate_ind(ind: Individual) -> float:\n", - " \"\"\"Evaluate an individual by calculating its fitness using the Ackley function.\"\"\"\n", - "\n", - " return Ackley(cast(\"list[float]\", ind.genotype))\n", - "\n", - "def evaluate_pop(population: Population) -> Population:\n", - " \"\"\"Evaluate a population by calculating the fitness of each individual.\"\"\"\n", - " for ind in population:\n", - " if ind.requires_eval:\n", - " ind.fitness = evaluate_ind(ind)\n", - " return population\n", - "\n", - "fitness_landscape_plot()\n" - ] - }, - { - "cell_type": "markdown", - "id": "46cc2ac5", - "metadata": {}, - "source": [ - "#### Initialize the global constants" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "72bb9020", - "metadata": {}, - "outputs": [], - "source": [ - "# A seed is optional, but it helps with reproducibility\n", - "SEED = None # e.g., 42\n", - "\n", - "# The database has a few handling modes\n", - " # \"delete\" will delete the existing database\n", - " # \"halt\" will stop the execution if a database already exists\n", - "DB_HANDLING_MODES = Literal[\"delete\", \"halt\"]\n", - "\n", - "# Initialize RNG\n", - "RNG = np.random.default_rng(SEED)\n", - "\n", - "# Initialize rich console and traceback handler\n", - "install()\n", - "console = Console()" - ] - }, - { - "cell_type": "markdown", - "id": "96c38813", - "metadata": {}, - "source": [ - "### Initialize the EASettings class. \n", - "\n", - "The EASettings class acts as the handles the database and other parameters " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "2c598983", - "metadata": {}, - "outputs": [], - "source": [ - "# Set config\n", - "config = EASettings()\n", - "config.is_maximisation = False\n", - "config.db_handling = \"delete\"\n" - ] - }, - { - "cell_type": "markdown", - "id": "985a5321", - "metadata": {}, - "source": [ - "#### And just like that we have everything we need to get started. Now all we need to do define our evolutionary operators\n", - "\n", - "Keep in mind that all operators in ARIEL have to work with the `Individual` and `Population` classes. You could define your own operators from scratch, but using the built in ones is easier." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "70b5cd55", - "metadata": {}, - "outputs": [], - "source": [ - "def create_individual(num_dims) -> Individual:\n", - " ind = Individual()\n", - " ind.genotype = cast(\"list[float]\", np.random.normal(loc=0, \n", - " scale=50, \n", - " size=num_dims).tolist())\n", - " return ind\n", - "\n", - "def parent_selection(population: Population) -> Population:\n", - " \"\"\"Tournament selection\"\"\"\n", - " \n", - " # Shuffle population to avoid bias\n", - " random.shuffle(population)\n", - "\n", - " # Tournament selection\n", - " for idx in range(0, len(population) - 1, 2):\n", - " ind_i = population[idx]\n", - " ind_j = population[idx + 1]\n", - "\n", - " # Compare fitness values and update tags\n", - " if ind_i.fitness > ind_j.fitness and config.is_maximisation:\n", - " ind_i.tags = {\"ps\": True}\n", - " ind_j.tags = {\"ps\": False}\n", - " else:\n", - " ind_i.tags = {\"ps\": False}\n", - " ind_j.tags = {\"ps\": True}\n", - " return population\n", - " \n", - "def crossover(population: Population) -> Population:\n", - " \"\"\"One point crossover\"\"\"\n", - "\n", - " parents = [ind for ind in population if ind.tags.get(\"ps\", False)]\n", - " for idx in range(0, len(parents), 2):\n", - " parent_i = parents[idx]\n", - " parent_j = parents[idx]\n", - " genotype_i, genotype_j = Crossover.one_point(\n", - " cast(\"list[float]\", parent_i.genotype),\n", - " cast(\"list[float]\", parent_j.genotype),\n", - " )\n", - "\n", - " # First child\n", - " child_i = Individual()\n", - " child_i.genotype = genotype_i\n", - " child_i.tags = {\"mut\": True}\n", - " child_i.requires_eval = True\n", - "\n", - " # Second child\n", - " child_j = Individual()\n", - " child_j.genotype = genotype_j\n", - " child_j.tags = {\"mut\": True}\n", - " child_j.requires_eval = True\n", - "\n", - " population.extend([child_i, child_j])\n", - " return population\n", - "\n", - "def mutation(population: Population) -> Population:\n", - " for ind in population:\n", - " if ind.tags.get(\"mut\", False):\n", - " genes = cast(\"list[int]\", ind.genotype)\n", - " mutated = IntegerMutator.float_creep(\n", - " individual=genes,\n", - " span=5,\n", - " mutation_probability=0.5,\n", - " )\n", - " ind.genotype = mutated\n", - " return population\n", - "\n", - "def survivor_selection(population: Population) -> Population:\n", - "\n", - " # Shuffle population to avoid bias\n", - " random.shuffle(population)\n", - " current_pop_size = len(population)\n", - "\n", - " for idx in range(len(population)):\n", - " ind_i = population[idx]\n", - " ind_j = population[idx + 1]\n", - "\n", - " # Kill worse individual\n", - " if ind_i.fitness > ind_j.fitness and config.is_maximisation:\n", - " ind_j.alive = False\n", - " else:\n", - " ind_i.alive = False\n", - "\n", - " # Termination condition\n", - " current_pop_size -= 1\n", - " if current_pop_size <= config.target_population_size:\n", - " break\n", - " return population" - ] - }, - { - "cell_type": "markdown", - "id": "f02e88e3", - "metadata": {}, - "source": [ - "### Define evolutionary loop\n", - "\n", - "Now that all our operators are done, we can define the evolutionary loop and run the algorithm\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "8be92b97", - "metadata": {}, - "outputs": [], - "source": [ - "def main() -> EA:\n", - " \"\"\"Entry point.\"\"\"\n", - " # Create initial population\n", - " population_list = [create_individual(num_dims=10) for _ in range(10)]\n", - " print(population_list)\n", - " population_list = evaluate_pop(population_list)\n", - "\n", - " # Create EA steps\n", - " ops = [\n", - " EAStep(\"evaluation\", evaluate_pop),\n", - " EAStep(\"parent_selection\", parent_selection),\n", - " EAStep(\"crossover\", crossover),\n", - " EAStep(\"mutation\", mutation),\n", - " EAStep(\"evaluation\", evaluate_pop),\n", - " EAStep(\"survivor_selection\", survivor_selection),\n", - " ]\n", - "\n", - " # Initialize EA\n", - " ea = EA(\n", - " population_list,\n", - " operations=ops,\n", - " num_of_generations=100,\n", - " )\n", - "\n", - " ea.run()\n", - "\n", - " best = ea.get_solution(\"best\", only_alive=False)\n", - " console.log(best)\n", - "\n", - " median = ea.get_solution(\"median\", only_alive=False)\n", - " console.log(median)\n", - "\n", - " worst = ea.get_solution(\"worst\", only_alive=False)\n", - " console.log(worst)\n", - "\n", - " return ea" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "7428e0cf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[2.408246382881381, 5.79605190806094, -79.54098188412165, 15.722240082230273, 40.908873629938554, 23.655791886798323, -29.0401778456501, -44.0910910860608, 46.6994979834531, 44.51539693959488], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[38.220368939127326, 60.658833461353524, 55.66507395396577, 71.31449041051489, 90.68420709242794, -41.79576370072132, -48.79004377864702, 31.46344668265647, -48.003370589026225, 14.02324800551763], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[-32.90740512919547, 25.036141835548538, 0.36711466327349074, 53.76353799300189, -38.53125411144, 5.35745175330088, -23.019156012124697, -66.337062441846, -50.75845026113798, 48.871897267390615], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[8.480801108291654, 37.324015811202074, 13.77265902980842, -1.674551608353941, 33.60714493740689, -68.04372634235611, -21.07910761451974, 90.43188951031527, 40.810541611687874, -41.991014937738484], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[22.598011128899692, 81.21670228397613, 107.72432244657797, 62.451789603804855, 4.115812806901929, -22.829906859778426, -95.05978577286338, -21.393570976959076, 54.62456102918504, 56.26303001018417], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[-74.90248116978553, -11.608668709580524, -6.543964231789149, -13.846612344646648, 51.83963355511509, 14.54237506248581, -81.0733495761288, 77.61737393510838, -7.5836843180492775, -0.4067953373474761], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[-67.80960209698705, 33.39780118847392, 32.55670675817498, -16.403100152228347, -9.433215961739206, 22.568662989164785, -30.05464476440501, -41.711418138880255, 55.80315469671937, -13.910639063186434], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[-33.06434449432155, 52.54747489946278, -44.17130149134966, 115.0643332294387, 32.58912362478761, 64.33393127521214, 16.71974489005757, 36.90015504392898, -54.005293408302826, -9.982137689745969], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[-24.170818363594336, -33.78692216610353, -30.895768744016184, 41.402978496168906, -15.979658595357071, 48.43945857621487, 15.745167596368304, -73.02747531014909, 66.7679632217159, -69.10790924577002], tags_={}), Individual(id=None, alive=True, time_of_birth=-1, time_of_death=-1, requires_eval=True, fitness_=None, requires_init=False, genotype_=[-79.3089113486005, 20.924907957331843, 76.49876452092195, -3.2991870397473755, 110.76415118349598, 83.50069159571638, -43.16998478652831, 4.037131419505667, 12.567409545787934, -31.844919779373075], tags_={})]\n" - ] - }, - { - "data": { - "text/html": [ - "
[15:04:58] Database file exists at d:\\University\\EC TA\\ariel\\docs\\source\\EA_intro\\__data__\\database.db!  a004.py:98\n",
-       "           Behaviour is set to: 'delete' --> ⚠️  Deleting file!                                                     \n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m[15:04:58]\u001b[0m\u001b[2;36m \u001b[0m\u001b[33mDatabase file exists at d:\\University\\EC TA\\ariel\\docs\\source\\EA_intro\\__data__\\database.db! \u001b[0m \u001b[2ma004.py\u001b[0m\u001b[2m:\u001b[0m\u001b[2m98\u001b[0m\n", - "\u001b[2;36m \u001b[0m\u001b[33mBehaviour is set to: \u001b[0m\u001b[32m'delete'\u001b[0m\u001b[33m --> ⚠️ Deleting file! \u001b[0m \u001b[2m \u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
───────────────────────────────────────────────── EA Initialised ──────────────────────────────────────────────────\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[92m───────────────────────────────────────────────── \u001b[0m\u001b[34mEA Initialised\u001b[0m\u001b[92m ──────────────────────────────────────────────────\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3b8040c383ae4036aee4d229d3a5ac8c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Output()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n"
-      ],
-      "text/plain": []
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "data": {
-      "text/html": [
-       "
─────────────────────────────────────────────── EA Finished Running ───────────────────────────────────────────────\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[92m─────────────────────────────────────────────── \u001b[0m\u001b[32mEA Finished Running\u001b[0m\u001b[92m ───────────────────────────────────────────────\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
[15:05:25] Individual(                                                                             2235138345.py:28\n",
-       "               time_of_birth=53,                                                                                   \n",
-       "               alive=False,                                                                                        \n",
-       "               id=2441,                                                                                            \n",
-       "               requires_eval=False,                                                                                \n",
-       "               requires_init=False,                                                                                \n",
-       "               tags_={'mut': True, 'ps': False},                                                                   \n",
-       "               time_of_death=55,                                                                                   \n",
-       "               fitness_=19.943074283612287,                                                                        \n",
-       "               genotype_=[                                                                                         \n",
-       "                   -44.349598792593326,                                                                            \n",
-       "                   22.71370375509321,                                                                              \n",
-       "                   41.98038192155346,                                                                              \n",
-       "                   -23.57200595364493,                                                                             \n",
-       "                   -4.0,                                                                                           \n",
-       "                   9.710941958036557,                                                                              \n",
-       "                   -15.0,                                                                                          \n",
-       "                   -29.219839593502666,                                                                            \n",
-       "                   45.0,                                                                                           \n",
-       "                   -3.596580788360625                                                                              \n",
-       "               ]                                                                                                   \n",
-       "           )                                                                                                       \n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m[15:05:25]\u001b[0m\u001b[2;36m \u001b[0m\u001b[1;35mIndividual\u001b[0m\u001b[1m(\u001b[0m \u001b]8;id=907311;file://C:\\Users\\johng\\AppData\\Local\\Temp\\ipykernel_13864\\2235138345.py\u001b\\\u001b[2m2235138345.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=763723;file://C:\\Users\\johng\\AppData\\Local\\Temp\\ipykernel_13864\\2235138345.py#28\u001b\\\u001b[2m28\u001b[0m\u001b]8;;\u001b\\\n", - "\u001b[2;36m \u001b[0m \u001b[33mtime_of_birth\u001b[0m=\u001b[1;36m53\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33malive\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mid\u001b[0m=\u001b[1;36m2441\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mrequires_eval\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mrequires_init\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mtags_\u001b[0m=\u001b[1m{\u001b[0m\u001b[32m'mut'\u001b[0m: \u001b[3;92mTrue\u001b[0m, \u001b[32m'ps'\u001b[0m: \u001b[3;91mFalse\u001b[0m\u001b[1m}\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mtime_of_death\u001b[0m=\u001b[1;36m55\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mfitness_\u001b[0m=\u001b[1;36m19\u001b[0m\u001b[1;36m.943074283612287\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mgenotype_\u001b[0m=\u001b[1m[\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-44.349598792593326\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m22.71370375509321\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m41.98038192155346\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-23.57200595364493\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-4.0\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m9.710941958036557\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-15.0\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-29.219839593502666\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m45.0\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-3.596580788360625\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1m]\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
           Individual(                                                                             2235138345.py:31\n",
-       "               time_of_birth=36,                                                                                   \n",
-       "               alive=False,                                                                                        \n",
-       "               id=1594,                                                                                            \n",
-       "               requires_eval=False,                                                                                \n",
-       "               requires_init=False,                                                                                \n",
-       "               tags_={'mut': True, 'ps': False},                                                                   \n",
-       "               time_of_death=38,                                                                                   \n",
-       "               fitness_=21.039838308157428,                                                                        \n",
-       "               genotype_=[                                                                                         \n",
-       "                   -22.123023231520712,                                                                            \n",
-       "                   -51.0,                                                                                          \n",
-       "                   -29.0,                                                                                          \n",
-       "                   19.334789764916277,                                                                             \n",
-       "                   -11.003167733933545,                                                                            \n",
-       "                   54.33341825346689,                                                                              \n",
-       "                   6.893693921909005,                                                                              \n",
-       "                   -59.29463815270952,                                                                             \n",
-       "                   61.643872996796034,                                                                             \n",
-       "                   -71.15502894628382                                                                              \n",
-       "               ]                                                                                                   \n",
-       "           )                                                                                                       \n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m \u001b[0m\u001b[2;36m \u001b[0m\u001b[1;35mIndividual\u001b[0m\u001b[1m(\u001b[0m \u001b]8;id=400151;file://C:\\Users\\johng\\AppData\\Local\\Temp\\ipykernel_13864\\2235138345.py\u001b\\\u001b[2m2235138345.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=598167;file://C:\\Users\\johng\\AppData\\Local\\Temp\\ipykernel_13864\\2235138345.py#31\u001b\\\u001b[2m31\u001b[0m\u001b]8;;\u001b\\\n", - "\u001b[2;36m \u001b[0m \u001b[33mtime_of_birth\u001b[0m=\u001b[1;36m36\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33malive\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mid\u001b[0m=\u001b[1;36m1594\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mrequires_eval\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mrequires_init\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mtags_\u001b[0m=\u001b[1m{\u001b[0m\u001b[32m'mut'\u001b[0m: \u001b[3;92mTrue\u001b[0m, \u001b[32m'ps'\u001b[0m: \u001b[3;91mFalse\u001b[0m\u001b[1m}\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mtime_of_death\u001b[0m=\u001b[1;36m38\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mfitness_\u001b[0m=\u001b[1;36m21\u001b[0m\u001b[1;36m.039838308157428\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mgenotype_\u001b[0m=\u001b[1m[\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-22.123023231520712\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-51.0\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-29.0\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m19.334789764916277\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-11.003167733933545\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m54.33341825346689\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m6.893693921909005\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-59.29463815270952\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m61.643872996796034\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-71.15502894628382\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1m]\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
           Individual(                                                                             2235138345.py:34\n",
-       "               time_of_birth=60,                                                                                   \n",
-       "               alive=False,                                                                                        \n",
-       "               id=2817,                                                                                            \n",
-       "               requires_eval=False,                                                                                \n",
-       "               requires_init=False,                                                                                \n",
-       "               tags_={'mut': True, 'ps': False},                                                                   \n",
-       "               time_of_death=61,                                                                                   \n",
-       "               fitness_=22.007885345384366,                                                                        \n",
-       "               genotype_=[                                                                                         \n",
-       "                   -38.43861042164279,                                                                             \n",
-       "                   -25.580955959432835,                                                                            \n",
-       "                   -11.373770419240499,                                                                            \n",
-       "                   50.55614494636733,                                                                              \n",
-       "                   1.7959731101461838,                                                                             \n",
-       "                   60.154819324843714,                                                                             \n",
-       "                   19.764137429216152,                                                                             \n",
-       "                   -43.37437022761751,                                                                             \n",
-       "                   74.18702515362334,                                                                              \n",
-       "                   -64.28798233024189                                                                              \n",
-       "               ]                                                                                                   \n",
-       "           )                                                                                                       \n",
-       "
\n" - ], - "text/plain": [ - "\u001b[2;36m \u001b[0m\u001b[2;36m \u001b[0m\u001b[1;35mIndividual\u001b[0m\u001b[1m(\u001b[0m \u001b]8;id=605709;file://C:\\Users\\johng\\AppData\\Local\\Temp\\ipykernel_13864\\2235138345.py\u001b\\\u001b[2m2235138345.py\u001b[0m\u001b]8;;\u001b\\\u001b[2m:\u001b[0m\u001b]8;id=622651;file://C:\\Users\\johng\\AppData\\Local\\Temp\\ipykernel_13864\\2235138345.py#34\u001b\\\u001b[2m34\u001b[0m\u001b]8;;\u001b\\\n", - "\u001b[2;36m \u001b[0m \u001b[33mtime_of_birth\u001b[0m=\u001b[1;36m60\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33malive\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mid\u001b[0m=\u001b[1;36m2817\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mrequires_eval\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mrequires_init\u001b[0m=\u001b[3;91mFalse\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mtags_\u001b[0m=\u001b[1m{\u001b[0m\u001b[32m'mut'\u001b[0m: \u001b[3;92mTrue\u001b[0m, \u001b[32m'ps'\u001b[0m: \u001b[3;91mFalse\u001b[0m\u001b[1m}\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mtime_of_death\u001b[0m=\u001b[1;36m61\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mfitness_\u001b[0m=\u001b[1;36m22\u001b[0m\u001b[1;36m.007885345384366\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[33mgenotype_\u001b[0m=\u001b[1m[\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-38.43861042164279\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-25.580955959432835\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-11.373770419240499\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m50.55614494636733\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m1.7959731101461838\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m60.154819324843714\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m19.764137429216152\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-43.37437022761751\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m74.18702515362334\u001b[0m, \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1;36m-64.28798233024189\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m \u001b[1m]\u001b[0m \u001b[2m \u001b[0m\n", - "\u001b[2;36m \u001b[0m\u001b[1m)\u001b[0m \u001b[2m \u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ea = main()" - ] - }, - { - "cell_type": "markdown", - "id": "ea9c19d0", - "metadata": {}, - "source": [ - "## Work in progress \n", - "\n", - "### Will update with the plot showing the mean performance and standard deviation of the algorithm" - ] - }, - { - "cell_type": "markdown", - "id": "4d698940", - "metadata": {}, - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "ariel", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py index 8c4cc5a0..af659a03 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py @@ -22,8 +22,11 @@ def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1 faces = ['FRONT', 'LEFT', 'RIGHT', 'BACK', 'TOP', 'BOTTOM'] letters = ['C', 'B', 'H', 'N'] allowed_numbers = [0, 90, 180, 270] + mutated_rules = rules.copy() + mutated_axiom = '' + mutation = '' def random_gene(): - letter = random.choice(['B', 'H']) + letter = random.choice(['B', 'H','N']) number = random.choice(allowed_numbers) face = random.choice(faces) return f"{letter}({number},{face})" @@ -31,175 +34,199 @@ def random_branch(): num_genes = random.randint(1, 3) genes = [random_gene() for _ in range(num_genes)] return '[' + ''.join(genes) + ']' - # Only mutate one gene/branch in the axiom per call, and only if random < mutation_rate - gene_indices = [i for i, token in enumerate(axiom_tokens) if re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", token)] - modified_gene = None - new_gene = None - if gene_indices and random.random() < mutation_rate: - i = random.choice(gene_indices) - token = axiom_tokens[i] - op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene']) - if op == 'add_gene': - # Only add one gene - axiom_tokens.insert(i+1, random_gene()) - elif op == 'remove_gene': - # Only remove one gene - axiom_tokens[i] = '' - elif op == 'create_branch': - axiom_tokens[i] = '[' + axiom_tokens[i] + ']' - elif op == 'remove_branch': - if i > 0 and axiom_tokens[i-1] == '[' and i+1 < len(axiom_tokens) and axiom_tokens[i+1] == ']': - axiom_tokens[i-1] = '' - axiom_tokens[i+1] = '' - elif op == 'modify_gene': - letter = token[0] - number = random.choice(allowed_numbers) - face = random.choice(faces) - new_gene = f"{letter}({number},{face})" - modified_gene = token - axiom_tokens[i] = new_gene - # Mutate rules: operate on rule replacement strings as token sequences - mutated_rules = rules.copy() - # If a gene was modified in the axiom, update all rules and rule values - if modified_gene and new_gene: - # Update rule keys - new_rules = {} - for k, v in mutated_rules.items(): - new_k = new_gene if k == modified_gene else k - # Update all occurrences in rule values - new_v = v.replace(modified_gene, new_gene) - new_rules[new_k] = new_v - mutated_rules = new_rules - # Remove any C genes with parameters from axiom tokens - axiom_tokens = [t for t in axiom_tokens if not re.fullmatch(r"C\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\)", t)] - - # Remove empty branches: if a gene is removed and is the only one inside [ ], remove the brackets too - i = 0 - while i < len(axiom_tokens): - if axiom_tokens[i] == '[': - # Find the matching closing bracket - j = i + 1 - while j < len(axiom_tokens) and axiom_tokens[j] != ']': - j += 1 - # If only one non-empty token inside, and it's empty, remove the brackets - inside = [t for t in axiom_tokens[i+1:j] if t.strip() != ''] - if len(inside) == 0 and j < len(axiom_tokens): - axiom_tokens[i] = '' - axiom_tokens[j] = '' - # Optionally, remove all empty tokens between i and j - for k in range(i+1, j): - axiom_tokens[k] = '' - i = j - i += 1 + + mod_gene=random.choice(['axiom','rules']) + if mod_gene=='axiom': + mutation+='Mutate axiom' + # Only mutate one gene/branch in the axiom per call, and only if random < mutation_rate + gene_indices = [i for i, token in enumerate(axiom_tokens) if re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", token)] + modified_gene = None + new_gene = None + deleted_gene = None + if gene_indices and random.random() < mutation_rate: + i = random.choice(gene_indices) + is_branch = False + nb_open = 0 + for j in range(0,i+1): + if axiom_tokens[j] =='[': + nb_open+=1 + elif axiom_tokens[j] ==']': + nb_open-=1 + if nb_open>0: + is_branch = True + token = axiom_tokens[i] + if is_branch==True: + op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene']) + else: + op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'modify_gene']) + if op == 'add_gene': + mutation+=' - add_gene' + # Only add one gene + axiom_tokens.insert(i+1, random_gene()) + elif op == 'remove_gene': + mutation+=' - remove_gene' + # Only remove one gene + while i 0: + j = i + while j>0: + if axiom_tokens[j] !='[': + break + j-=1 + axiom_tokens[j]='' + j = i + while axiom_tokens[j] !=']': + if j0: + is_branch = True + token = rule_tokens[pos] + if is_branch==True: + op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene']) + else: + op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'modify_gene']) + if op == 'add_gene': + mutation+=' - add_gene' + # Only add one gene + rule_tokens.insert(pos+1, random_gene()) + elif op == 'remove_gene': + mutation+=' - remove_gene' + # Only remove one gene + while i 0: + j = pos + while j>0: + if rule_tokens[j] !='[': + break + j-=1 + rule_tokens[j]='' + j = pos + while j Date: Sat, 4 Oct 2025 21:32:15 +0200 Subject: [PATCH 20/47] mutation completed ... far more complex than before :) --- .../decoders/l_system_mutation.py | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py index af659a03..ec75ca2e 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py @@ -67,7 +67,7 @@ def random_branch(): mutation+=' - remove_gene' # Only remove one gene while i Date: Tue, 7 Oct 2025 10:00:41 +0200 Subject: [PATCH 21/47] fixed some bugs --- pyproject.toml | 6 +++++- .../decoders/l_system_decoding.py | 15 ++++++------- .../decoders/l_system_initializing.py | 21 +++++++++++++++++++ ...ystem_mutation.py => l_system_mutating.py} | 15 ++++++------- uv.lock | 2 ++ 5 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py rename src/ariel/body_phenotypes/robogen_lite/decoders/{l_system_mutation.py => l_system_mutating.py} (96%) diff --git a/pyproject.toml b/pyproject.toml index c349bd4e..dcf46bf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,11 @@ Documentation = "https://ariel.readthedocs.io" Changelog = "https://github.com/ci-group/ariel/releases" [dependency-groups] -dev = ["opencv-stubs>=0.1.0", "types-networkx>=3.5.0.20250918"] +dev = [ + "ipykernel>=6.30.1", + "opencv-stubs>=0.1.0", + "types-networkx>=3.5.0.20250918", +] docs = [ "jupyter-sphinx>=0.5.3", "linkify-it-py>=2.0.3", diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index a544d0da..653a1af1 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -1,6 +1,6 @@ """Example of L-system-based decoding for modular robot graphs. -Author: omn (with help from GitHub Copilot) +Author: omn Date: 2025-09-26 Py Ver: 3.12 OS: macOS Tahoe 26 @@ -160,10 +160,11 @@ def parse_tokens(s): def build_graph(tree, parent=None): nonlocal core_count + current_parent = parent for node in tree: if isinstance(node, list): - # This is a branch, attach to the same parent - build_graph(node, parent) + # This is a branch, attach to the same parent (do not update current_parent) + build_graph(node, current_parent) else: m = token_pattern.match(node) if m: @@ -203,11 +204,11 @@ def build_graph(tree, parent=None): rotation=rotation_enum, face=face, ) #create and add the node to the graph - if parent is not None: # if there is a parent, create a link in the graph - self.graph.add_edge(parent, node_label) + if current_parent is not None: # if there is a parent, create a link in the graph + self.graph.add_edge(current_parent, node_label) idx_counter[0] += 1 - parent = node_label - return parent + current_parent = node_label # Only update parent after a single node, not after a branch + return current_parent tree = parse_tokens(s) build_graph(tree) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py new file mode 100644 index 00000000..52320fca --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py @@ -0,0 +1,21 @@ +""" +L-system genotype random initialization utility for modular robots. + +Author: omn +Date: 2025-10-07 +""" + +import random +from typing import Tuple, Dict +import re + +def random_lsystem(): + rules = {} + axiom = '' + allowed_numbers = [0, 90, 180, 270] + for face in ['FRONT', 'LEFT', 'RIGHT', 'BACK', 'TOP', 'BOTTOM']: + letter = random.choice(['B', 'H','N']) + number = random.choice(allowed_numbers) + axiom+=f"{letter}({number},{face})" + return axiom, rules + diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py similarity index 96% rename from src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py rename to src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py index ec75ca2e..7565da46 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutation.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py @@ -1,8 +1,8 @@ """ -L-system genotype mutation and crossover utilities for modular robots. +L-system genotype mutation utilities for modular robots. -Author: omn (with help from GitHub Copilot) -Date: 2025-09-27 +Author: omn +Date: 2025-10-07 """ import random @@ -82,13 +82,13 @@ def random_branch(): if i > 0: j = i while j>0: - if axiom_tokens[j] !='[': + if axiom_tokens[j] =='[': break j-=1 axiom_tokens[j]='' j = i - while axiom_tokens[j] !=']': - if j Date: Thu, 9 Oct 2025 22:45:30 +0200 Subject: [PATCH 22/47] Finalized tree genotype implementation + test cases: tree_genome.py, tree_decoder, a000.py (mutator), a005.py (crossover) --- .../robogen_lite/decoders/tree_decoder.py | 29 ++++++++++++++-- src/ariel/ec/a000.py | 33 +++++++++++-------- src/ariel/ec/genotypes/tree/tree_genome.py | 12 ++++--- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py index d44dc796..a4ea4fc9 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, dict, Callable +from typing import Optional, Dict, Callable import networkx as nx from ariel.ec.genotypes.tree.tree_genome import TreeNode, TreeGenome from ariel.body_phenotypes.robogen_lite import config @@ -53,7 +53,7 @@ def to_digraph(genome: TreeGenome, use_node_ids: bool = True) -> nx.DiGraph: node_key = lambda n: n.id else: # Assign 0..N-1 in first-seen (DFS) order - seen: dict[int, int] = {} + seen: Dict[int, int] = {} next_id = 0 def node_key(n: TreeNode) -> int: nonlocal next_id @@ -81,4 +81,27 @@ def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | dfs(child, sub, face) dfs(None, root, None) - return g \ No newline at end of file + return g + +def test(): + # Create a simple tree genome for testing + genome = TreeGenome() + genome.root = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) + genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + genome.root.left = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) + + # Convert to directed graph + digraph = to_digraph(genome, use_node_ids=False) + + # Print the graph nodes and edges with attributes + print("Nodes:") + for node, attrs in digraph.nodes(data=True): + print(f" {node}: {attrs}") + + print("\nEdges:") + for u, v, attrs in digraph.edges(data=True): + print(f" {u} -> {v}: {attrs}") + +# Test code +if __name__ == "__main__": + test() \ No newline at end of file diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 87d4b17f..d5be51a9 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -8,10 +8,9 @@ # Third-party libraries import numpy as np from pydantic_settings import BaseSettings -from collections import deque from rich.console import Console from rich.traceback import install - +import copy from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode import ariel.body_phenotypes.robogen_lite.config as pheno_config @@ -272,29 +271,32 @@ class TreeMutator: @staticmethod def random_subtree_replacement( individual: TreeGenome, - max_subtree_depth: int = 2, + max_subtree_depth: int = 1, ) -> TreeGenome: """Replace a random subtree with a new random subtree.""" if individual.root is None: return individual + new_individual = copy.copy(individual) + # Collect all nodes in the tree - all_nodes = individual.root.get_all_nodes(exclude_root=True) + all_nodes = new_individual.root.get_all_nodes(exclude_root=True) # Select a random node to replace (excluding root) if len(all_nodes) <= 1: - return individual # No replacement possible + return new_individual node_to_replace = RNG.choice(all_nodes[1:]) # Avoid replacing root # Generate a new random subtree new_subtree = TreeNode.random_tree_node(max_depth=max_subtree_depth) - individual.root.replace_node(node_to_replace, new_subtree) + with new_individual.root.enable_replacement(): + new_individual.root.replace_node(node_to_replace, new_subtree) - return individual + return new_individual -def main() -> None: +def test() -> None: """Entry point.""" console.log(IntegersGenerator.integers(-5, 5, 5)) example = IntegersGenerator.choice([1, 3, 4], (2, 5)) @@ -308,14 +310,17 @@ def main() -> None: console.rule("[bold blue]Tree Generator Examples") - # Show different tree types - console.log("Linear chain:", TreeGenerator.linear_chain(4)) - console.log("Star shape:", TreeGenerator.star_shape(3)) - console.log("Binary tree:", TreeGenerator.binary_tree(3)) - console.log("Random tree:", TreeGenerator.random_tree(3, 0.6)) + genome = TreeGenome() + genome.root = TreeNode(pheno_config.ModuleInstance(type=pheno_config.ModuleType.BRICK, rotation=pheno_config.ModuleRotationsIdx.DEG_90, links={})) + genome.root.front = TreeNode(pheno_config.ModuleInstance(type=pheno_config.ModuleType.BRICK, rotation=pheno_config.ModuleRotationsIdx.DEG_45, links={})) + genome.root.left = TreeNode(pheno_config.ModuleInstance(type=pheno_config.ModuleType.BRICK, rotation=pheno_config.ModuleRotationsIdx.DEG_45, links={})) + tree_mutator = TreeMutator() + mutated_genome = tree_mutator.random_subtree_replacement(genome, max_subtree_depth=1) + console.log("Original Genome:", genome) + console.log("Mutated Genome:", mutated_genome) if __name__ == "__main__": - main() + test() diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index 7192e268..5ab916f9 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -160,10 +160,10 @@ def id(self, value: int | None): raise ValueError("ID cannot be changed once set.") @classmethod - def random_tree_node(cls, max_depth: int = 2, branch_prob: float = 0.5) -> 'TreeNode': + def random_tree_node(cls, max_depth: int = 1, branch_prob: float = 0.5) -> 'TreeNode' | None: """Create a random tree node with random children up to max_depth.""" if max_depth < 0: - raise ValueError("max_depth must be non-negative") + return None # Exclude CORE and NONE from random selection module_type = RNG.choice([mt for mt in config.ModuleType if mt not in {config.ModuleType.CORE, config.ModuleType.NONE}]) @@ -182,7 +182,8 @@ def random_tree_node(cls, max_depth: int = 2, branch_prob: float = 0.5) -> 'Tree continue # Skip adding a child based on branch probability # Recursively create child nodes with reduced depth child_node = cls.random_tree_node(max_depth - 1) - node._set_face(face, child_node) + if child_node is not None: + node._set_face(face, child_node) return node @@ -483,7 +484,7 @@ def __copy__(self) -> 'TreeNode': -def lukas(): +def test(): genome = TreeGenome() genome.root = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) @@ -499,4 +500,5 @@ def lukas(): print(genome.root.get_all_nodes("dfs", True)) -#lukas() +if __name__ == "__main__": + test() \ No newline at end of file From a9250c39bc6b95ca989a063d19f320a5e445b75b Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 9 Oct 2025 22:52:40 +0200 Subject: [PATCH 23/47] Modified TreeGenerator --- src/ariel/ec/a000.py | 39 +++++++++------------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index d5be51a9..48d40be2 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -235,36 +235,11 @@ def build_subtree(current_depth: int, max_depth: int) -> TreeNode | None: @staticmethod def random_tree(max_depth: int = 4, branching_prob: float = 0.7) -> TreeGenome: """Generate a random tree with pheno_configurable branching probability.""" - def build_random_subtree(current_depth: int) -> TreeNode | None: - if current_depth >= max_depth: - return None - - module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) - rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) - module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) - - node = TreeNode(module, depth=current_depth) - available_faces = node.available_faces() - - # Randomly decide to add children - for face in available_faces: - if RNG.random() < branching_prob: - child = build_random_subtree(current_depth + 1) - if child: - node._set_face(face, child) - - return node - - genome = TreeGenome.default_init() - - # Add children to root - available_faces = genome.root.available_faces() - for face in available_faces: - if RNG.random() < branching_prob: - child = build_random_subtree(1) - if child: - genome.root._set_face(face, child) - + genome = TreeGenome.default_init() # Start with CORE + face = RNG.choice(genome.root.available_faces()) + subtree = TreeNode.random_tree_node(max_depth=max_depth - 1, branch_prob=branching_prob) + if subtree: + genome.root._set_face(face, subtree) return genome class TreeMutator: @@ -310,6 +285,10 @@ def test() -> None: console.rule("[bold blue]Tree Generator Examples") + treeGenerator = TreeGenerator() + random_tree = treeGenerator.random_tree(max_depth=3, branching_prob=0.7) + console.log("Random Tree:", random_tree) + genome = TreeGenome() genome.root = TreeNode(pheno_config.ModuleInstance(type=pheno_config.ModuleType.BRICK, rotation=pheno_config.ModuleRotationsIdx.DEG_90, links={})) genome.root.front = TreeNode(pheno_config.ModuleInstance(type=pheno_config.ModuleType.BRICK, rotation=pheno_config.ModuleRotationsIdx.DEG_45, links={})) From 105796db2d4ee3939511cca545b4082271ed38e2 Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 9 Oct 2025 22:58:17 +0200 Subject: [PATCH 24/47] Modified TreeMutator --- src/ariel/ec/a000.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 48d40be2..11404c45 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -247,6 +247,7 @@ class TreeMutator: def random_subtree_replacement( individual: TreeGenome, max_subtree_depth: int = 1, + branching_prob: float = 0.5, ) -> TreeGenome: """Replace a random subtree with a new random subtree.""" if individual.root is None: @@ -264,7 +265,7 @@ def random_subtree_replacement( node_to_replace = RNG.choice(all_nodes[1:]) # Avoid replacing root # Generate a new random subtree - new_subtree = TreeNode.random_tree_node(max_depth=max_subtree_depth) + new_subtree = TreeNode.random_tree_node(max_depth=max_subtree_depth, branch_prob=branching_prob) with new_individual.root.enable_replacement(): new_individual.root.replace_node(node_to_replace, new_subtree) From 20367bf0b90f12aaab0736d50b8dbedc0154b75b Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 9 Oct 2025 23:31:59 +0200 Subject: [PATCH 25/47] Modified TreeCrossover --- src/ariel/ec/a005.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 19f039b6..b9da75ab 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -87,6 +87,9 @@ def koza_default( parent_j_all_nodes = parent_j_root.get_all_nodes() node_b = RNG.choice(parent_j_all_nodes) + if node_a is None or node_b is None: + # If either tree is just a root, return copies of parents + return parent_i.copy(), parent_j.copy() parent_i_old = parent_i.copy() parent_j_old = parent_j.copy() @@ -101,6 +104,46 @@ def koza_default( parent_i = parent_i_old parent_j = parent_j_old return child1, child2 + + @staticmethod + def normal( + parent_i: TreeGenome, + parent_j: TreeGenome, + ) -> tuple[TreeGenome, TreeGenome]: + """ + Normal tree crossover: + - Pick a random node from Parent A (uniform over all nodes). + - Pick a random node from Parent B (uniform over all nodes). + - Swap the selected subtrees. + + Returns two children produced by swapping the chosen subtrees. + """ + parent_i_root, parent_j_root = parent_i.root, parent_j.root + + # Uniformly choose any node (root, internal, or leaf) + node_a = RNG.choice(parent_i_root.get_all_nodes(exclude_root=True)) + node_b = RNG.choice(parent_j_root.get_all_nodes(exclude_root=True)) + + if not node_a or not node_b: + # If either tree is just a root, return copies of parents + return parent_i.copy(), parent_j.copy() + + # Preserve originals (same pattern as in koza_default) + parent_i_old = parent_i.copy() + parent_j_old = parent_j.copy() + child1 = parent_i + child2 = parent_j + + # Perform the swap + with child1.root.enable_replacement(): + child1.root.replace_node(node_a, node_b) + with child2.root.enable_replacement(): + child2.root.replace_node(node_b, node_a) + + # Restore parent handles for caller (as in your koza_default) + parent_i = parent_i_old + parent_j = parent_j_old + return child1, child2 def tree_main(): From a1cfdb7097059b7c910effef7aef179be668ead6 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:19:55 +0200 Subject: [PATCH 26/47] Create l_system_crossover.py --- .../decoders/l_system_crossover.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py new file mode 100644 index 00000000..d1b5cecf --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py @@ -0,0 +1,18 @@ +""" +L-system genotype mutation utilities for modular robots. + +Author: omn +Date: 2025-10-07 +""" + +import random +from typing import Tuple, Dict +import re + +def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: Dict[str, str], crossover_rate: float = 0.1): + offspring1_axiom = axiom1 + offspring2_axiom = axiom2 + offspring1_rules = rules1.copy() + offspring2_rules = rules2.copy() + return offspring1_axiom, offspring1_rules, offspring2_axiom, offspring2_rules + \ No newline at end of file From 0a53c1372d82bb668e1c0ef774a3f4478663a58a Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:16:49 +0200 Subject: [PATCH 27/47] update --- .../decoders/l_system_crossover.py | 53 ++++++++- .../decoders/l_system_initializing.py | 4 +- .../decoders/l_system_mutating.py | 110 +++++------------- 3 files changed, 78 insertions(+), 89 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py index d1b5cecf..e744fa52 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py @@ -9,10 +9,51 @@ from typing import Tuple, Dict import re -def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: Dict[str, str], crossover_rate: float = 0.1): - offspring1_axiom = axiom1 - offspring2_axiom = axiom2 - offspring1_rules = rules1.copy() - offspring2_rules = rules2.copy() +def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: Dict[str, str], crossover_rate: float = 0.3): + crossover_len = int(1+(6*crossover_rate/2)) + crossover_point = random.randint(0,6) + if crossover_len+crossover_point>=6: + crossover_point = 6-crossover_len-1 + gene_pattern = re.compile(r"([CBHN]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))||\[|\]|C") + axiom_tokens_tmp = [m.group(0) for m in gene_pattern.finditer(axiom1)] + axiom_tokens1 = [token for i, token in enumerate(axiom_tokens_tmp) if token not in ['[',']','C','']] + axiom_tokens_tmp = [m.group(0) for m in gene_pattern.finditer(axiom2)] + axiom_tokens2 = [token for i, token in enumerate(axiom_tokens_tmp) if token not in ['[',']','C','']] + print("crossover position:",crossover_point) + print("crossover length:",crossover_len) + offspring1_axiom = 'C' + offspring2_axiom = 'C' + rules1_keys = list(rules1.keys()) + rules2_keys = list(rules2.keys()) + rules1_values = list(rules1.values()) + rules2_values = list(rules2.values()) + offspring1_rules = {} + offspring2_rules = {} + for i in range(0,crossover_point): + offspring1_axiom+=f"[{axiom_tokens1[i]}]" + offspring2_axiom+=f"[{axiom_tokens2[i]}]" + for j in range(0,len(rules1_keys)): + if axiom_tokens1[i]==rules1_keys[j]: + offspring1_rules[rules1_keys[j]]=rules1_values[j] + for j in range(0,len(rules2_keys)): + if axiom_tokens2[i]==rules2_keys[j]: + offspring2_rules[rules2_keys[j]]=rules2_values[j] + for i in range(crossover_point,crossover_point+crossover_len): + offspring1_axiom+=f"[{axiom_tokens2[i]}]" + offspring2_axiom+=f"[{axiom_tokens1[i]}]" + for j in range(0,len(rules1_keys)): + if axiom_tokens1[i]==rules1_keys[j]: + offspring2_rules[rules1_keys[j]]=rules1_values[j] + for j in range(0,len(rules2_keys)): + if axiom_tokens2[i]==rules2_keys[j]: + offspring1_rules[rules2_keys[j]]=rules2_values[j] + for i in range(crossover_point+crossover_len,len(axiom_tokens1)): + offspring1_axiom+=f"[{axiom_tokens1[i]}]" + offspring2_axiom+=f"[{axiom_tokens2[i]}]" + for j in range(0,len(rules1_keys)): + if axiom_tokens1[i]==rules1_keys[j]: + offspring1_rules[rules1_keys[j]]=rules1_values[j] + for j in range(0,len(rules2_keys)): + if axiom_tokens1[i]==rules2_keys[j]: + offspring2_rules[rules2_keys[j]]=rules2_values[j] return offspring1_axiom, offspring1_rules, offspring2_axiom, offspring2_rules - \ No newline at end of file diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py index 52320fca..a45be702 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py @@ -11,11 +11,11 @@ def random_lsystem(): rules = {} - axiom = '' + axiom = 'C' allowed_numbers = [0, 90, 180, 270] for face in ['FRONT', 'LEFT', 'RIGHT', 'BACK', 'TOP', 'BOTTOM']: letter = random.choice(['B', 'H','N']) number = random.choice(allowed_numbers) - axiom+=f"{letter}({number},{face})" + axiom+=f"[{letter}({number},{face})]" return axiom, rules diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py index 7565da46..b8a89441 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py @@ -17,7 +17,7 @@ def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1 Returns mutated (axiom, rules). """ # Tokenize axiom using regex to preserve gene and bracket structure - gene_pattern = re.compile(r"([CBH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N|\[|\]|C") + gene_pattern = re.compile(r"([CBHN]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))||\[|\]|C") axiom_tokens = [m.group(0) for m in gene_pattern.finditer(axiom)] faces = ['FRONT', 'LEFT', 'RIGHT', 'BACK', 'TOP', 'BOTTOM'] letters = ['C', 'B', 'H', 'N'] @@ -39,67 +39,24 @@ def random_branch(): if mod_gene=='axiom': mutation+='Mutate axiom' # Only mutate one gene/branch in the axiom per call, and only if random < mutation_rate - gene_indices = [i for i, token in enumerate(axiom_tokens) if re.fullmatch(r"([BH]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))|N", token)] + gene_indices = [i for i, token in enumerate(axiom_tokens) if re.fullmatch(r"([BHN]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))", token)] modified_gene = None new_gene = None deleted_gene = None - if gene_indices and random.random() < mutation_rate: + if gene_indices and random.random() < mutation_rate: i = random.choice(gene_indices) - is_branch = False - nb_open = 0 - for j in range(0,i+1): - if axiom_tokens[j] =='[': - nb_open+=1 - elif axiom_tokens[j] ==']': - nb_open-=1 - if nb_open>0: - is_branch = True + while axiom_tokens[i] in ['[',']','C','']: + i = random.choice(gene_indices) token = axiom_tokens[i] - if is_branch==True: - op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene']) - else: - op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'modify_gene']) - if op == 'add_gene': - mutation+=' - add_gene' - # Only add one gene - axiom_tokens.insert(i+1, random_gene()) - elif op == 'remove_gene': - mutation+=' - remove_gene' - # Only remove one gene - while i 0: - j = i - while j>0: - if axiom_tokens[j] =='[': - break - j-=1 - axiom_tokens[j]='' - j = i - while j Date: Wed, 15 Oct 2025 16:47:49 +0200 Subject: [PATCH 28/47] updated --- .../decoders/l_system_crossover.py | 34 +++++++++++++++---- .../decoders/l_system_mutating.py | 1 + 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py index e744fa52..6f4bc513 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py @@ -24,14 +24,16 @@ def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: D offspring1_axiom = 'C' offspring2_axiom = 'C' rules1_keys = list(rules1.keys()) - rules2_keys = list(rules2.keys()) + rules2_keys = list(rules2.keys()) rules1_values = list(rules1.values()) rules2_values = list(rules2.values()) offspring1_rules = {} offspring2_rules = {} + offspring1_axiom_token = [] + offspring2_axiom_token = [] for i in range(0,crossover_point): - offspring1_axiom+=f"[{axiom_tokens1[i]}]" - offspring2_axiom+=f"[{axiom_tokens2[i]}]" + offspring1_axiom_token.append(axiom_tokens1[i]) + offspring2_axiom_token.append(axiom_tokens2[i]) for j in range(0,len(rules1_keys)): if axiom_tokens1[i]==rules1_keys[j]: offspring1_rules[rules1_keys[j]]=rules1_values[j] @@ -39,8 +41,8 @@ def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: D if axiom_tokens2[i]==rules2_keys[j]: offspring2_rules[rules2_keys[j]]=rules2_values[j] for i in range(crossover_point,crossover_point+crossover_len): - offspring1_axiom+=f"[{axiom_tokens2[i]}]" - offspring2_axiom+=f"[{axiom_tokens1[i]}]" + offspring1_axiom_token.append(axiom_tokens2[i]) + offspring2_axiom_token.append(axiom_tokens1[i]) for j in range(0,len(rules1_keys)): if axiom_tokens1[i]==rules1_keys[j]: offspring2_rules[rules1_keys[j]]=rules1_values[j] @@ -48,12 +50,30 @@ def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: D if axiom_tokens2[i]==rules2_keys[j]: offspring1_rules[rules2_keys[j]]=rules2_values[j] for i in range(crossover_point+crossover_len,len(axiom_tokens1)): - offspring1_axiom+=f"[{axiom_tokens1[i]}]" - offspring2_axiom+=f"[{axiom_tokens2[i]}]" + offspring1_axiom_token.append(axiom_tokens1[i]) + offspring2_axiom_token.append(axiom_tokens2[i]) for j in range(0,len(rules1_keys)): if axiom_tokens1[i]==rules1_keys[j]: offspring1_rules[rules1_keys[j]]=rules1_values[j] for j in range(0,len(rules2_keys)): if axiom_tokens1[i]==rules2_keys[j]: offspring2_rules[rules2_keys[j]]=rules2_values[j] + + for i in range(0,len(offspring1_axiom_token)): + offspring1_axiom+="["+offspring1_axiom_token[i]+"]" + for i in range(0,len(offspring2_axiom_token)): + offspring2_axiom+="["+offspring2_axiom_token[i]+"]" + + rules1_not_assigned = [rule for rule in rules1_keys if rule not in offspring1_rules.keys()] + rules2_not_assigned = [rule for rule in rules2_keys if rule not in offspring2_rules.keys()] + for i in range(0,len(rules1_not_assigned)): + if random.random()<0.5: + offspring1_rules[rules1_not_assigned[i]]=rules1[rules1_not_assigned[i]] + else: + offspring2_rules[rules1_not_assigned[i]]=rules1[rules1_not_assigned[i]] + for i in range(0,len(rules2_not_assigned)): + if random.random()<0.5: + offspring2_rules[rules2_not_assigned[i]]=rules2[rules2_not_assigned[i]] + else: + offspring1_rules[rules2_not_assigned[i]]=rules2[rules2_not_assigned[i]] return offspring1_axiom, offspring1_rules, offspring2_axiom, offspring2_rules diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py index b8a89441..2a111beb 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py @@ -15,6 +15,7 @@ def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1 - Randomly changes, adds, or removes symbols in the axiom. - Randomly mutates rule replacements. Returns mutated (axiom, rules). + """ # Tokenize axiom using regex to preserve gene and bracket structure gene_pattern = re.compile(r"([CBHN]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))||\[|\]|C") From b7739e6a7f4eaa5775caa64e1d31004647850f9d Mon Sep 17 00:00:00 2001 From: Olivier Moulin Date: Tue, 21 Oct 2025 22:26:12 +0200 Subject: [PATCH 29/47] updated --- .../decoders/l_system_crossover.py | 26 +-- .../decoders/l_system_decoding.py | 31 ++- .../decoders/l_system_initializing.py | 2 +- .../decoders/l_system_mutating.py | 202 ++++++++++-------- 4 files changed, 157 insertions(+), 104 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py index 6f4bc513..6211a8ef 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py @@ -9,7 +9,7 @@ from typing import Tuple, Dict import re -def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: Dict[str, str], crossover_rate: float = 0.3): +def crossover_one_point_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: Dict[str, str], crossover_rate: float = 0.3): crossover_len = int(1+(6*crossover_rate/2)) crossover_point = random.randint(0,6) if crossover_len+crossover_point>=6: @@ -24,7 +24,7 @@ def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: D offspring1_axiom = 'C' offspring2_axiom = 'C' rules1_keys = list(rules1.keys()) - rules2_keys = list(rules2.keys()) + rules2_keys = list(rules2.keys()) rules1_values = list(rules1.values()) rules2_values = list(rules2.values()) offspring1_rules = {} @@ -35,28 +35,28 @@ def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: D offspring1_axiom_token.append(axiom_tokens1[i]) offspring2_axiom_token.append(axiom_tokens2[i]) for j in range(0,len(rules1_keys)): - if axiom_tokens1[i]==rules1_keys[j]: + if axiom_tokens1[i]==rules1_keys[j]: offspring1_rules[rules1_keys[j]]=rules1_values[j] for j in range(0,len(rules2_keys)): - if axiom_tokens2[i]==rules2_keys[j]: + if axiom_tokens2[i]==rules2_keys[j]: offspring2_rules[rules2_keys[j]]=rules2_values[j] - for i in range(crossover_point,crossover_point+crossover_len): + for i in range(crossover_point,crossover_point+crossover_len): offspring1_axiom_token.append(axiom_tokens2[i]) offspring2_axiom_token.append(axiom_tokens1[i]) for j in range(0,len(rules1_keys)): - if axiom_tokens1[i]==rules1_keys[j]: + if axiom_tokens1[i]==rules1_keys[j]: offspring2_rules[rules1_keys[j]]=rules1_values[j] for j in range(0,len(rules2_keys)): - if axiom_tokens2[i]==rules2_keys[j]: + if axiom_tokens2[i]==rules2_keys[j]: offspring1_rules[rules2_keys[j]]=rules2_values[j] - for i in range(crossover_point+crossover_len,len(axiom_tokens1)): + for i in range(crossover_point+crossover_len,len(axiom_tokens1)): offspring1_axiom_token.append(axiom_tokens1[i]) offspring2_axiom_token.append(axiom_tokens2[i]) for j in range(0,len(rules1_keys)): - if axiom_tokens1[i]==rules1_keys[j]: + if axiom_tokens1[i]==rules1_keys[j]: offspring1_rules[rules1_keys[j]]=rules1_values[j] for j in range(0,len(rules2_keys)): - if axiom_tokens1[i]==rules2_keys[j]: + if axiom_tokens1[i]==rules2_keys[j]: offspring2_rules[rules2_keys[j]]=rules2_values[j] for i in range(0,len(offspring1_axiom_token)): @@ -64,16 +64,16 @@ def crossover_lsystem(axiom1: str, rules1: Dict[str, str],axiom2: str, rules2: D for i in range(0,len(offspring2_axiom_token)): offspring2_axiom+="["+offspring2_axiom_token[i]+"]" - rules1_not_assigned = [rule for rule in rules1_keys if rule not in offspring1_rules.keys()] + rules1_not_assigned = [rule for rule in rules1_keys if rule not in offspring1_rules.keys()] rules2_not_assigned = [rule for rule in rules2_keys if rule not in offspring2_rules.keys()] for i in range(0,len(rules1_not_assigned)): if random.random()<0.5: offspring1_rules[rules1_not_assigned[i]]=rules1[rules1_not_assigned[i]] - else: + else: offspring2_rules[rules1_not_assigned[i]]=rules1[rules1_not_assigned[i]] for i in range(0,len(rules2_not_assigned)): if random.random()<0.5: offspring2_rules[rules2_not_assigned[i]]=rules2[rules2_not_assigned[i]] - else: + else: offspring1_rules[rules2_not_assigned[i]]=rules2[rules2_not_assigned[i]] return offspring1_axiom, offspring1_rules, offspring2_axiom, offspring2_rules diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py index 653a1af1..0e386b20 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py @@ -64,8 +64,8 @@ def __init__( self.rules = rules self.iterations = iterations self.graph = nx.DiGraph() - self.lsystem_string = self.expand_lsystem() # first we expand the string applying recursively all the rules - self.build_graph_from_string(self.lsystem_string) # we create the graph with networkx from a fully expanded L-system string + self.lsystem_string = self.expand_lsystem() # first we expand the string applying recursively all the rules + self.build_graph_from_string(self.lsystem_string) # we create the graph with networkx from a fully expanded L-system string def expand_lsystem(self, axiom: str = None, rules: Dict[str, str] = None, iterations: int = None) -> str: """ @@ -202,10 +202,9 @@ def build_graph(tree, parent=None): node_label, type=node_type, rotation=rotation_enum, - face=face, ) #create and add the node to the graph if current_parent is not None: # if there is a parent, create a link in the graph - self.graph.add_edge(current_parent, node_label) + self.graph.add_edge(current_parent, node_label,face=face_str) idx_counter[0] += 1 current_parent = node_label # Only update parent after a single node, not after a branch return current_parent @@ -242,7 +241,27 @@ def draw_graph( "font_size": 8, "width": 0.5, } - nx.draw(self.graph, pos, **options) + nx.draw( + self.graph, + pos, + with_labels=True, + node_size=150, + node_color="#FFFFFF00", + edgecolors="blue", + font_size=8, + width=0.5, + ) + + edge_labels = nx.get_edge_attributes(self.graph, "face") + + nx.draw_networkx_edge_labels( + self.graph, + pos, + edge_labels=edge_labels, + font_color="red", + font_size=8, + ) + plt.title(title) if save_file: plt.savefig(save_file, dpi=DPI) @@ -267,4 +286,4 @@ def main(): decoder.draw_graph() if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py index a45be702..c7564522 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py @@ -1,7 +1,7 @@ """ L-system genotype random initialization utility for modular robots. -Author: omn +Author: omn Date: 2025-10-07 """ diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py index 2a111beb..cbf2240f 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py @@ -9,7 +9,7 @@ from typing import Tuple, Dict import re -def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1) -> Tuple[str, Dict[str, str]]: +def mutate_lsystem(axiom: str, rules: Dict[str, str], mutation_rate: float = 0.1,axiom_temperature=0.2,add_temperature=0.5): """ Mutate the axiom and/or rules of an L-system genotype. - Randomly changes, adds, or removes symbols in the axiom. @@ -35,8 +35,8 @@ def random_branch(): num_genes = random.randint(1, 3) genes = [random_gene() for _ in range(num_genes)] return '[' + ''.join(genes) + ']' - - mod_gene=random.choice(['axiom','rules']) + + mod_gene=random.choices(['axiom','rules'],weights=[axiom_temperature,1-axiom_temperature])[0] if mod_gene=='axiom': mutation+='Mutate axiom' # Only mutate one gene/branch in the axiom per call, and only if random < mutation_rate @@ -44,10 +44,10 @@ def random_branch(): modified_gene = None new_gene = None deleted_gene = None - if gene_indices and random.random() < mutation_rate: + if gene_indices and random.random() < mutation_rate: i = random.choice(gene_indices) while axiom_tokens[i] in ['[',']','C','']: - i = random.choice(gene_indices) + i = random.choice(gene_indices) token = axiom_tokens[i] #print ('token : ',token) @@ -88,14 +88,14 @@ def random_branch(): if random.random() < mutation_rate: if len(mutated_rules)==0: i=0 - mod_element='new' + mod_element='rule' else: i = random.choice(range(0,len(mutated_rules))) - mod_element=random.choice(['key','rule','new','remove']) + mod_element=random.choice(['key','rule']) if mod_element =='key': mutation+=' - modify key' old_gene=list(mutated_rules.keys())[i] - old_rules=list(mutated_rules.values())[i] + old_rules=list(mutated_rules.values())[i] letter = random.choice(['B', 'H','N']) number = random.choice(allowed_numbers) face = random.choice(faces) @@ -110,88 +110,122 @@ def random_branch(): #print('axiom token - ',axiom_tokens) elif mod_element == 'rule': mutation+=' - modify rule' - rule=list(mutated_rules.values())[i] - rule_tokens = [m.group(0) for m in gene_pattern.finditer(rule)] - gene_indices = [j for j, token in enumerate(rule_tokens) if re.fullmatch(r"([BHN]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))", token)] - if gene_indices: - pos = random.choice(gene_indices) - is_branch = False - nb_open = 0 - for j in range(0,pos+1): - if rule_tokens[j] =='[': - nb_open+=1 - elif rule_tokens[j] ==']': - nb_open-=1 - if nb_open>0: - is_branch = True - token = rule_tokens[pos] - if is_branch==True: - op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene']) - else: - op = random.choice(['add_gene', 'remove_gene', 'create_branch', 'modify_gene']) - if op == 'add_gene': - mutation+=' - add_gene' + op = "" + if i==0: + op = 'new_rule' + else: + rule=list(mutated_rules.values())[i] + rule_tokens = [m.group(0) for m in gene_pattern.finditer(rule)] + gene_indices = [j for j, token in enumerate(rule_tokens) if re.fullmatch(r"([BHN]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))", token)] + if gene_indices: + pos = random.choice(gene_indices) + is_branch = False + nb_open = 0 + for j in range(0,pos+1): + if rule_tokens[j] =='[': + nb_open+=1 + elif rule_tokens[j] ==']': + nb_open-=1 + if nb_open>0: + is_branch = True + token = rule_tokens[pos] + if is_branch==True: + w = [add_temperature,(1-add_temperature),add_temperature,(1-add_temperature),add_temperature,(1-add_temperature),add_temperature] + wd = (add_temperature*4+(1-add_temperature)*3) + for l in range(0,len(w)): + w[l]=w[l]/wd + op = random.choices(['new_rule','remove_rule','add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene'],weights=w)[0] + else: + w = [add_temperature,(1-add_temperature),add_temperature,(1-add_temperature),add_temperature,add_temperature] + wd= (add_temperature*4+(1-add_temperature)*2) + for l in range(0,len(w)): + w[l]=w[l]/wd + op = random.choices(['new_rule','remove_rule','add_gene', 'remove_gene', 'create_branch', 'modify_gene'],weights=w)[0] + if op == 'add_gene': + mutation+=' - add_gene' # Only add one gene + while pos>0: + if rule_tokens[pos][0]=="N": + pos-=1 + else: + break + if pos==0: + rule_tokens.insert(pos, random_gene()) + else: rule_tokens.insert(pos+1, random_gene()) - elif op == 'remove_gene': - mutation+=' - remove_gene' + mutated_rules[list(mutated_rules.keys())[i]] = ''.join(rule_tokens) + elif op == 'remove_gene': + mutation+=' - remove_gene' # Only remove one gene - while i 0: - j = pos - while j>0: - if rule_tokens[j] !='[': - break - j-=1 - rule_tokens[j]='' - j = pos - while j 0: + j = pos + while j>0: + if rule_tokens[j] !='[': + break + j-=1 + rule_tokens[j]='' + j = pos + while j Date: Thu, 23 Oct 2025 21:13:54 +0200 Subject: [PATCH 30/47] New l-system genotype --- src/ariel/body_phenotypes/.DS_Store | Bin 6148 -> 8196 bytes .../decoders/l_system_crossover.py | 79 ---- .../decoders/l_system_decoding.py | 289 ------------ .../decoders/l_system_genotype.py | 416 ++++++++++++++++++ .../decoders/l_system_initializing.py | 21 - .../decoders/l_system_mutating.py | 235 ---------- 6 files changed, 416 insertions(+), 624 deletions(-) delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_crossover.py delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py create mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_initializing.py delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/l_system_mutating.py diff --git a/src/ariel/body_phenotypes/.DS_Store b/src/ariel/body_phenotypes/.DS_Store index 68be814856bcffcdefbf1232c0295d7c3ca443e2..93015b9b678c86c7f56db37c5fe04b5eb4b787ed 100644 GIT binary patch literal 8196 zcmeHM&2Q936n_)a#!Eszf*nn6v~+g8l#zWKhN57&P`-n+d~!VZbn87%&VN z2L2BW;4@n^E9bedv}QC67zR3$0daqDunR0{Y$}wu4jg0&fY^=GvZ0K1fb?-RmNYgM zN>_}jst2N7i7qjSa!0)*%n?f(n+laX5akX;pPA?ig~+p`pCiqIloXoLFkl$i&w!}i zdu7cY0ozl*Z%)TN@MG?yh1gVO>|<|aL#gG}fK3R%!@dGk#CgZ-;laG(+JqbMy8deL z>C^o9bv`wvS7luu313wHel+<$>+nBVH&)tLQNplr1QOujW3OR;3^R-+OLT9ZWCU&7 z*~#_uH`0b)|4)7+j?!#={CCoIsQaUjd#oO-*Sh7d@C`TfvbD7AC5z%{j`P@W$(}zS z(T%D-c$Krvix^F$LljapS-KgqkZ+Xv8VghD>nep+unJZC&8CK?5#_PgTL<=~Z zO{j=eiBJ}~6tBEev=&R>w1m%;U_(&}_<}yzk~EUyFX3NOo@1QRLZMii)hY|l!X$hP z^RNnAa1ZXo1Na#p!yoVxUXvbjij0u6XO@*kz_Q5{{w7tWd=fAuJX6Qf${sAnsQ&0c^ delta 139 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGjUEV6q~50C<+o_1dAmzBr+s3WH6*M#BVHI z&dA6y`GSDd=6: - crossover_point = 6-crossover_len-1 - gene_pattern = re.compile(r"([CBHN]\((0|90|180|270),(FRONT|LEFT|RIGHT|BACK|TOP|BOTTOM)\))||\[|\]|C") - axiom_tokens_tmp = [m.group(0) for m in gene_pattern.finditer(axiom1)] - axiom_tokens1 = [token for i, token in enumerate(axiom_tokens_tmp) if token not in ['[',']','C','']] - axiom_tokens_tmp = [m.group(0) for m in gene_pattern.finditer(axiom2)] - axiom_tokens2 = [token for i, token in enumerate(axiom_tokens_tmp) if token not in ['[',']','C','']] - print("crossover position:",crossover_point) - print("crossover length:",crossover_len) - offspring1_axiom = 'C' - offspring2_axiom = 'C' - rules1_keys = list(rules1.keys()) - rules2_keys = list(rules2.keys()) - rules1_values = list(rules1.values()) - rules2_values = list(rules2.values()) - offspring1_rules = {} - offspring2_rules = {} - offspring1_axiom_token = [] - offspring2_axiom_token = [] - for i in range(0,crossover_point): - offspring1_axiom_token.append(axiom_tokens1[i]) - offspring2_axiom_token.append(axiom_tokens2[i]) - for j in range(0,len(rules1_keys)): - if axiom_tokens1[i]==rules1_keys[j]: - offspring1_rules[rules1_keys[j]]=rules1_values[j] - for j in range(0,len(rules2_keys)): - if axiom_tokens2[i]==rules2_keys[j]: - offspring2_rules[rules2_keys[j]]=rules2_values[j] - for i in range(crossover_point,crossover_point+crossover_len): - offspring1_axiom_token.append(axiom_tokens2[i]) - offspring2_axiom_token.append(axiom_tokens1[i]) - for j in range(0,len(rules1_keys)): - if axiom_tokens1[i]==rules1_keys[j]: - offspring2_rules[rules1_keys[j]]=rules1_values[j] - for j in range(0,len(rules2_keys)): - if axiom_tokens2[i]==rules2_keys[j]: - offspring1_rules[rules2_keys[j]]=rules2_values[j] - for i in range(crossover_point+crossover_len,len(axiom_tokens1)): - offspring1_axiom_token.append(axiom_tokens1[i]) - offspring2_axiom_token.append(axiom_tokens2[i]) - for j in range(0,len(rules1_keys)): - if axiom_tokens1[i]==rules1_keys[j]: - offspring1_rules[rules1_keys[j]]=rules1_values[j] - for j in range(0,len(rules2_keys)): - if axiom_tokens1[i]==rules2_keys[j]: - offspring2_rules[rules2_keys[j]]=rules2_values[j] - - for i in range(0,len(offspring1_axiom_token)): - offspring1_axiom+="["+offspring1_axiom_token[i]+"]" - for i in range(0,len(offspring2_axiom_token)): - offspring2_axiom+="["+offspring2_axiom_token[i]+"]" - - rules1_not_assigned = [rule for rule in rules1_keys if rule not in offspring1_rules.keys()] - rules2_not_assigned = [rule for rule in rules2_keys if rule not in offspring2_rules.keys()] - for i in range(0,len(rules1_not_assigned)): - if random.random()<0.5: - offspring1_rules[rules1_not_assigned[i]]=rules1[rules1_not_assigned[i]] - else: - offspring2_rules[rules1_not_assigned[i]]=rules1[rules1_not_assigned[i]] - for i in range(0,len(rules2_not_assigned)): - if random.random()<0.5: - offspring2_rules[rules2_not_assigned[i]]=rules2[rules2_not_assigned[i]] - else: - offspring1_rules[rules2_not_assigned[i]]=rules2[rules2_not_assigned[i]] - return offspring1_axiom, offspring1_rules, offspring2_axiom, offspring2_rules diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py deleted file mode 100644 index 0e386b20..00000000 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_decoding.py +++ /dev/null @@ -1,289 +0,0 @@ -"""Example of L-system-based decoding for modular robot graphs. - -Author: omn -Date: 2025-09-26 -Py Ver: 3.12 -OS: macOS Tahoe 26 -Status: Prototype - -Notes ------ - * This decoder uses an L-system string as the genotype to generate a directed graph (DiGraph) using NetworkX. - * The L-system rules and axiom define the growth of the modular robot structure. - -References ----------- - [1] https://en.wikipedia.org/wiki/L-system - [2] https://networkx.org/documentation/stable/reference/readwrite/generated/networkx.readwrite.json_graph.tree_data.html - -""" - -# Standard library - -import json -import re -from pathlib import Path -from typing import Any, Callable, Dict, Optional -from enum import Enum - -# Third-party libraries -import matplotlib.pyplot as plt -import networkx as nx -from networkx import DiGraph -from networkx.readwrite import json_graph - - -# Local libraries -from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType - -SEED = 42 -DPI = 300 - -class SymbolToModuleType(Enum): # for auto-transcoding between L-system string characters and ModuleType elements - """Enum for module types.""" - - C = 'CORE' - B = 'BRICK' - H = 'HINGE' - N = 'NONE' - -class LSystemDecoder: - """Implements an L-system-based decoder for modular robot graphs.""" - - def __init__( - self, - axiom: str, - rules: Dict[str, str], - iterations: int = 2, - ) -> None: - """ - Initialize the L-system decoder. - Automatically expands the L-system and builds the graph. - """ - self.axiom = axiom - self.rules = rules - self.iterations = iterations - self.graph = nx.DiGraph() - self.lsystem_string = self.expand_lsystem() # first we expand the string applying recursively all the rules - self.build_graph_from_string(self.lsystem_string) # we create the graph with networkx from a fully expanded L-system string - - def expand_lsystem(self, axiom: str = None, rules: Dict[str, str] = None, iterations: int = None) -> str: - """ - Generate the L-system string after the given number of iterations, recursively expanding inside brackets as well, but stopping at the required depth. - Each token is replaced in place by its rule expansion (not appended after). - """ - # Match C as a single character, and other genes as X(num,FACE) - gene_pattern = re.compile(r"(C|[A-Za-z]\(\d{1,3},[A-Za-z]+\))|\[|\]") - axiom = axiom if axiom is not None else self.axiom # if we call it without axiom defined then we pick the one already assigned - rules = rules if rules is not None else self.rules # same for rules - iterations = iterations if iterations is not None else self.iterations # same for iterations - - def expand_all(s, depth): - if depth == 0: # end of recursion ... just return the string. - return s - tokens = [m.group(0) for m in gene_pattern.finditer(s)] - result = [] - i = 0 - while i < len(tokens): #go through all the token identified - token = tokens[i] - if token == '[': - # Find the matching closing bracket - bracket_level = 1 - j = i + 1 - while j < len(tokens) and bracket_level > 0: - if tokens[j] == '[': - bracket_level += 1 - elif tokens[j] == ']': - bracket_level -= 1 - j += 1 - # Recursively expand the inside of the brackets with depth-1 - inside = expand_all(''.join(tokens[i+1:j-1]), depth-1) - result.append('[' + inside + ']') - i = j - elif token == ']': - i += 1 - elif token in rules: - # Replace the token in place with its expansion - replacement = rules[token] - expanded = expand_all(replacement, depth-1) - result.append(expanded) # This replaces the token at this position - i += 1 - else: - result.append(token) - i += 1 - return ''.join(result) - - return expand_all(axiom, iterations) - - def build_graph_from_string(self, lsystem_string: str) -> None: - """ - Build the graph from a fully expanded L-system string. - """ - self.graph = nx.DiGraph() - # Match C as a single character, and other genes as X(num,FACE) - token_pattern = re.compile(r"C|([A-Za-z])\((\d{1,3}),(\w+)\)") - s = lsystem_string - core_count = 0 - idx_counter = [0] # mutable counter for unique node labels - - def parse_tokens(s): - # Parse the string into a tree of (gene, [children]) - tokens = [] - i = 0 - while i < len(s): - if s[i] == '[': - # Find matching bracket - bracket_level = 1 - j = i + 1 - while j < len(s) and bracket_level > 0: - if s[j] == '[': - bracket_level += 1 - elif s[j] == ']': - bracket_level -= 1 - j += 1 - subtree = parse_tokens(s[i+1:j-1]) - tokens.append(subtree) - i = j - elif s[i] == ']': - i += 1 - elif s[i].isalpha(): - m = token_pattern.match(s, i) - if m: - tokens.append(s[i:m.end()]) - i = m.end() - else: - tokens.append(s[i]) - i += 1 - else: - i += 1 - return tokens - - def build_graph(tree, parent=None): - nonlocal core_count - current_parent = parent - for node in tree: - if isinstance(node, list): - # This is a branch, attach to the same parent (do not update current_parent) - build_graph(node, current_parent) - else: - m = token_pattern.match(node) - if m: - if m.group(0) == "C": - symbol = "C" - node_type = ModuleType.CORE - rotation_enum = ModuleRotationsTheta.DEG_0 - face = ModuleFaces.FRONT - else: - symbol = m.group(1) - try: # check if the type of elements is authorized (part of ModuleType enum) - symbol_to_look = SymbolToModuleType[symbol] - node_type = ModuleType[symbol_to_look.value] - except KeyError: - raise ValueError(f"Symbol '{symbol}' is not a valid ModuleType enum name.") - if node_type == ModuleType.CORE: - core_count += 1 - if core_count > 1: - raise ValueError("L-system string contains more than one CORE module.") - if m.group(2) is not None: - try: # check if the rotation is part of the allowed rotations (Module RotationsTheta enum) - rotation_val = int(m.group(2)) - rotation_enum = next((r for r in ModuleRotationsTheta if r.value == rotation_val), ModuleRotationsTheta.DEG_0) - except Exception: # if error then default to 0 - rotation_enum = ModuleRotationsTheta.DEG_0 - else: # if no rotation is provided then is is defaulted to 0 - rotation_enum = ModuleRotationsTheta.DEG_0 - face_str = m.group(3) if m.group(3) is not None else "FRONT" - try: # check if the face is in the allowed faces (Module ModuleFaces enum) - face = ModuleFaces[face_str] - except KeyError: # if error then default to FRONT - face = ModuleFaces.FRONT - node_label = f"{symbol}{idx_counter[0]}" # generate a unique ID for the node - self.graph.add_node( - node_label, - type=node_type, - rotation=rotation_enum, - ) #create and add the node to the graph - if current_parent is not None: # if there is a parent, create a link in the graph - self.graph.add_edge(current_parent, node_label,face=face_str) - idx_counter[0] += 1 - current_parent = node_label # Only update parent after a single node, not after a branch - return current_parent - - tree = parse_tokens(s) - build_graph(tree) - - def get_graph(self) -> DiGraph: - """Return the generated NetworkX DiGraph.""" - return self.graph - - def save_graph_as_json(self, save_file: Path | str | None = None) -> None: - """Save the graph as a JSON file (node-link format).""" - if save_file is None: - return - data = json_graph.node_link_data(self.graph, edges="edges") - json_string = json.dumps(data, indent=4) - with Path(save_file).open("w", encoding="utf-8") as f: - f.write(json_string) - - def draw_graph( - self, - title: str = "L-System Decoded Graph", - save_file: Path | str | None = None, - ) -> None: - """Draw the decoded graph using matplotlib and networkx.""" - plt.figure() - pos = nx.spring_layout(self.graph, seed=SEED) - options = { - "with_labels": True, - "node_size": 150, - "node_color": "#FFFFFF00", - "edgecolors": "blue", - "font_size": 8, - "width": 0.5, - } - nx.draw( - self.graph, - pos, - with_labels=True, - node_size=150, - node_color="#FFFFFF00", - edgecolors="blue", - font_size=8, - width=0.5, - ) - - edge_labels = nx.get_edge_attributes(self.graph, "face") - - nx.draw_networkx_edge_labels( - self.graph, - pos, - edge_labels=edge_labels, - font_color="red", - font_size=8, - ) - - plt.title(title) - if save_file: - plt.savefig(save_file, dpi=DPI) - else: - plt.show() - -# in case we want to test on an example - -def main(): - # Example: axiom with orientation and face, and C expands into branches - axiom = "C[H(0,FRONT)][H(0,LEFT)][H(0,RIGHT)]" - rules = { - "H(0,FRONT)": "H(0,FRONT)B(0,FRONT)", - "H(0,LEFT)": "H(0,LEFT)B(0,FRONT)", - "H(0,RIGHT)": "H(0,RIGHT)B(0,FRONT)" # Example for N, can be expanded as needed - } - decoder = LSystemDecoder(axiom, rules, iterations=2) - print("Nodes and attributes:") - for n, d in decoder.graph.nodes(data=True): - print(n, d) - print(decoder.lsystem_string) - decoder.draw_graph() - -if __name__ == "__main__": - main() diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py new file mode 100644 index 00000000..2fbce1f1 --- /dev/null +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py @@ -0,0 +1,416 @@ +"""Example of L-system-based decoding for modular robot graphs. + +Author: omn +Date: 2025-09-26 +Py Ver: 3.12 +OS: macOS Tahoe 26 +Status: Prototype + +Notes +----- + * This decoder uses an L-system string as the genotype to generate a directed graph (DiGraph) using NetworkX. + * The L-system rules and axiom define the growth of the modular robot structure. + +References +---------- + [1] https://en.wikipedia.org/wiki/L-system + [2] https://networkx.org/documentation/stable/reference/readwrite/generated/networkx.readwrite.json_graph.tree_data.html + +""" + +# Standard library + +import json +import re +from pathlib import Path +from typing import Any, Callable, Dict, Optional +from enum import Enum + +# Third-party libraries +import matplotlib.pyplot as plt +import networkx as nx +from networkx import DiGraph +from networkx.readwrite import json_graph + + +# Local libraries +from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType + +SEED = 42 +DPI = 300 + +class SymbolToModuleType(Enum): # for auto-transcoding between L-system string characters and ModuleType elements + """Enum for module types.""" + + C = 'CORE' + B = 'BRICK' + H = 'HINGE' + N = 'NONE' + +class lsystem_element: + def __init__(self): + self.rotation=0 + self.front = None + self.back = None + self.right = None + self.left = None + self.top = None + self.bottom=None + self.allowed_connection=['TOP','BOTTOM','LEFT','RIGHT','FRONT','BACK'] + self.name='' + + def connect_to(self,side,obj,rotation): + if side in obj.allowed_connection: + match side: + case 'TOP': + if obj.top == None: + self.back=obj + self.rotation=rotation + self.back.top=self + case 'BOTTOM': + if obj.bottom == None: + self.back=obj + self.rotation=rotation + self.back.bottom=self + case 'LEFT': + if obj.left == None: + self.back=obj + self.rotation=rotation + self.back.left=self + case 'RIGHT': + if obj.right == None: + self.back=obj + self.rotation=rotation + self.back.right=self + case 'FRONT': + if obj.front == None: + self.back=obj + self.rotation=rotation + self.back.front=self + case 'BACK': + if obj.back == None: + self.back=obj + self.rotation=rotation + self.back.back=self + + def has_element(self,side): + has_element=False + if side in self.allowed_connection: + match side: + case 'TOP': + if self.top!=None: + has_element=True + case 'BOTTOM': + if self.bottom!=None: + has_element=True + case 'LEFT': + if self.left!=None: + has_element=True + case 'RIGHT': + if self.right!=None: + has_element=True + case 'FRONT': + if self.front!=None: + has_element=True + case 'BACK': + if self.back!=None: + has_element=True + return has_element + + +class lsystem_block(lsystem_element): + + def __init__(self): + self.rotation=0 + self.front = None + self.back = None + self.left = None + self.right = None + self.top = None + self.bottom = None + self.allowed_connection=['TOP','BOTTOM','LEFT','RIGHT','FRONT','BACK'] + self.name='B' + +class lsystem_hinge(lsystem_element): + + def __init__(self): + self.rotation=0 + self.front = None + self.back = None + self.allowed_connection=['FRONT','BACK'] + self.name='H' + +class lsystem_none(lsystem_element): + + def __init__(self): + self.rotation=0 + self.back = None + self.allowed_connection=['BACK'] + self.anme='N' + +class lsystem_core(lsystem_element): + + def __init__(self): + self.rotation=0 + self.front = None + self.back = None + self.left = None + self.right = None + self.top = None + self.bottom = None + self.allowed_connection=['TOP','BOTTOM','LEFT','RIGHT','FRONT','BACK'] + self.name='C' + +class LSystemDecoder: + """Implements an L-system-based decoder for modular robot graphs.""" + + def __init__( + self, + axiom: str, + rules: Dict[str, str], + iterations: int = 2, + ) -> None: + """ + Initialize the L-system decoder. + Automatically expands the L-system and builds the graph. + """ + self.axiom = axiom + self.rules = rules + self.iterations = iterations + self.graph = nx.DiGraph() + self.expanded_token=None + self.structure = None + + def expand_lsystem(self): + expanded_token = [] + expanded_token.append(self.axiom) + it = 0 + while it0: - is_branch = True - token = rule_tokens[pos] - if is_branch==True: - w = [add_temperature,(1-add_temperature),add_temperature,(1-add_temperature),add_temperature,(1-add_temperature),add_temperature] - wd = (add_temperature*4+(1-add_temperature)*3) - for l in range(0,len(w)): - w[l]=w[l]/wd - op = random.choices(['new_rule','remove_rule','add_gene', 'remove_gene', 'create_branch', 'remove_branch', 'modify_gene'],weights=w)[0] - else: - w = [add_temperature,(1-add_temperature),add_temperature,(1-add_temperature),add_temperature,add_temperature] - wd= (add_temperature*4+(1-add_temperature)*2) - for l in range(0,len(w)): - w[l]=w[l]/wd - op = random.choices(['new_rule','remove_rule','add_gene', 'remove_gene', 'create_branch', 'modify_gene'],weights=w)[0] - if op == 'add_gene': - mutation+=' - add_gene' - # Only add one gene - while pos>0: - if rule_tokens[pos][0]=="N": - pos-=1 - else: - break - if pos==0: - rule_tokens.insert(pos, random_gene()) - else: - rule_tokens.insert(pos+1, random_gene()) - mutated_rules[list(mutated_rules.keys())[i]] = ''.join(rule_tokens) - elif op == 'remove_gene': - mutation+=' - remove_gene' - # Only remove one gene - while i 0: - j = pos - while j>0: - if rule_tokens[j] !='[': - break - j-=1 - rule_tokens[j]='' - j = pos - while j Date: Thu, 23 Oct 2025 23:36:34 +0200 Subject: [PATCH 31/47] new l-system --- .../decoders/l_system_genotype.py | 122 +++++++++++++++++- src/ariel/ec/ec_l_system.py | 97 ++++++++++++++ 2 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/ariel/ec/ec_l_system.py diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py index 2fbce1f1..c298ee59 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py @@ -146,7 +146,7 @@ def __init__(self): self.rotation=0 self.back = None self.allowed_connection=['BACK'] - self.anme='N' + self.name='N' class lsystem_core(lsystem_element): @@ -410,7 +410,127 @@ def print_lsystem_element(self,element,id): def print_lsystem_structure(self): self.print_lsystem_element(self.structure,0) + def generate_lsystem_graph_element(self,element,id): + if id==0: + self.graph.add_node( + element.name+"-"+str(id), + type=ModuleType.CORE.value, + rotation=0, + ) + id_tmp = id + if element.has_element('FRONT')==True: + eltype = ModuleType.NONE.value + match element.front.name: + case 'B': + eltype = ModuleType.BRICK.value + case 'H': + eltype = ModuleType.HINGE.value + self.graph.add_node(element.front.name+"-"+str(id_tmp+1),type=eltype,rotation=element.front.rotation) + self.graph.add_edge(element.name+"-"+str(id),element.front.name+"-"+str(id_tmp+1),face='FRONT') + id_tmp=self.generate_lsystem_graph_element(element.front,id_tmp+1) + if element.name=="C": + if element.has_element('BACK')==True: + eltype = ModuleType.NONE.value + match element.back.name: + case 'B': + eltype = ModuleType.BRICK.value + case 'H': + eltype = ModuleType.HINGE.value + self.graph.add_node(element.back.name+"-"+str(id_tmp+1),type=eltype,rotation=element.back.rotation) + self.graph.add_edge(element.name+"-"+str(id),element.back.name+"-"+str(id_tmp+1),face='BACK') + id_tmp=self.generate_lsystem_graph_element(element.back,id_tmp+1) + if element.has_element('RIGHT')==True: + eltype = ModuleType.NONE.value + match element.right.name: + case 'B': + eltype = ModuleType.BRICK.value + case 'H': + eltype = ModuleType.HINGE.value + self.graph.add_node(element.right.name+"-"+str(id_tmp+1),type=eltype,rotation=element.right.rotation) + self.graph.add_edge(element.name+"-"+str(id),element.right.name+"-"+str(id_tmp+1),face='RIGHT') + id_tmp=self.generate_lsystem_graph_element(element.right,id_tmp+1) + if element.has_element('LEFT')==True: + eltype = ModuleType.NONE.value + match element.left.name: + case 'B': + eltype = ModuleType.BRICK.value + case 'H': + eltype = ModuleType.HINGE.value + self.graph.add_node(element.left.name+"-"+str(id_tmp+1),type=eltype,rotation=element.left.rotation) + self.graph.add_edge(element.name+"-"+str(id),element.left.name+"-"+str(id_tmp+1),face='LEFT') + id_tmp=self.generate_lsystem_graph_element(element.left,id_tmp+1) + if element.has_element('TOP')==True: + eltype = ModuleType.NONE.value + match element.top.name: + case 'B': + eltype = ModuleType.BRICK.value + case 'H': + eltype = ModuleType.HINGE.value + self.graph.add_node(element.top.name+"-"+str(id_tmp+1),type=eltype,rotation=element.top.rotation) + self.graph.add_edge(element.name+"-"+str(id),element.top.name+"-"+str(id_tmp+1),face='TOP') + id_tmp=self.generate_lsystem_graph_element(element.top,id_tmp+1) + if element.has_element('BOTTOM')==True: + eltype = ModuleType.NONE.value + match element.bottom.name: + case 'B': + eltype = ModuleType.BRICK.value + case 'H': + eltype = ModuleType.HINGE.value + self.graph.add_node(element.bottom.name+"-"+str(id_tmp+1),type=eltype,rotation=element.bottom.rotation) + self.graph.add_edge(element.name+"-"+str(id),element.bottom.name+"-"+str(id_tmp+1),face='BOTTOM') + id_tmp=self.generate_lsystem_graph_element(element.bottom,id_tmp+1) + return id_tmp + + + def generate_lsystem_graph(self): + self.graph = nx.DiGraph() + self.generate_lsystem_graph_element(self.structure,0) + def print_lsystem_expanded(self): print(self.expanded_token) + def save_graph_as_json(self,save_file): + if save_file is None: + return + data = json_graph.node_link_data(self.graph, edges="edges") + json_string = json.dumps(data, indent=4) + with Path(save_file).open("w", encoding="utf-8") as f: + f.write(json_string) + + def draw_graph(self, title = "L-System Decoded Graph",save_file = None): + """Draw the decoded graph using matplotlib and networkx.""" + plt.figure() + pos = nx.spring_layout(self.graph, seed=SEED) + options = { + "with_labels": True, + "node_size": 200, + "node_color": "#FFFFFF00", + "edgecolors": "blue", + "font_size": 8, + "width": 0.5, + } + nx.draw( + self.graph, + pos, + with_labels=True, + node_size=150, + node_color="#FFFFFF00", + edgecolors="blue", + font_size=8, + width=0.5, + ) + edge_labels = nx.get_edge_attributes(self.graph, "face") + nx.draw_networkx_edge_labels( + self.graph, + pos, + edge_labels=edge_labels, + font_color="red", + font_size=8, + ) + plt.title(title) + if save_file!=None: + plt.savefig(save_file, dpi=DPI) + else: + plt.show() + diff --git a/src/ariel/ec/ec_l_system.py b/src/ariel/ec/ec_l_system.py new file mode 100644 index 00000000..4416ab21 --- /dev/null +++ b/src/ariel/ec/ec_l_system.py @@ -0,0 +1,97 @@ +"""Example of L-system-based evolutionary computing algorithm for modular robot graphs. + +Author: omn +Date: 2025-09-26 +Py Ver: 3.12 +OS: macOS Tahoe 26 +Status: Prototype + +Notes +----- + +References +---------- + +""" + +# Standard library + +import json +import re +from pathlib import Path +from typing import Any, Callable, Dict, Optional +from enum import Enum +import random + + +# Local libraries +from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType +from ariel.body_phenotypes.robogen_lite.decoders.l_system_genotype import LSystemDecoder + +SEED = 42 +DPI = 300 + +def mutate_lsystem(lsystem,mut_rate,add_temperature=0.5): + op_completed = "" + if random.random() Date: Fri, 24 Oct 2025 13:25:36 +0200 Subject: [PATCH 32/47] New l-system with EC functions --- .../decoders/l_system_genotype.py | 191 +++++++++++------- src/ariel/ec/ec_l_system.py | 129 +++++++++++- 2 files changed, 246 insertions(+), 74 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py index c298ee59..78fd282b 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py @@ -205,212 +205,263 @@ def get_rotation(self,cmd): return int(rotation) - def build_lsystem_structure(self): + def build_lsystem_structure(self,verbose=0): if self.expanded_token!=None and self.expanded_token[0]=='C': self.structure=lsystem_core() turtle = self.structure tk=1 while tk < len(self.expanded_token): - print("token : ",tk) + if verbose==1: + print("token : ",tk) match self.expanded_token[tk][:4]: case 'addf': - print("add FRONT") + if verbose==1: + print("add FRONT") rotation = self.get_rotation(self.expanded_token[tk]) if tk+1=0: + splitted_rules.pop(gene_to_change-1) + splitted_rules.pop(gene_to_change-1) elif splitted_rules[gene_to_change][:4] in ['movf','movk','movl','movr','movt','movb']: op_completed="REMOVED : "+splitted_rules[gene_to_change] splitted_rules.pop(gene_to_change) @@ -93,5 +94,125 @@ def mutate_lsystem(lsystem,mut_rate,add_temperature=0.5): if new_rule!="": lsystem.rules[list(rules.keys())[rule_to_change]]=new_rule else: - del lsystem.rules[list(rules.keys())[rule_to_change]] + lsystem.rules[list(rules.keys())[rule_to_change]]=lsystem.rules[list(rules.keys())[rule_to_change]] return op_completed + +def crossover_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): + axiom_offspring1="C" + axiom_offspring2="C" + rules_offspring1={} + rules_offspring2={} + iter_offspring1=0 + iter_offspring2=0 + if random.random()>mutation_rate: + rules_offspring1['C']=lsystem_parent2.rules['C'] + rules_offspring2['C']=lsystem_parent1.rules['C'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['C']=lsystem_parent1.rules['C'] + rules_offspring2['C']=lsystem_parent2.rules['C'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + if random.random()>mutation_rate: + rules_offspring1['B']=lsystem_parent2.rules['B'] + rules_offspring2['B']=lsystem_parent1.rules['B'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['B']=lsystem_parent1.rules['B'] + rules_offspring2['B']=lsystem_parent2.rules['B'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + if random.random()>mutation_rate: + rules_offspring1['H']=lsystem_parent2.rules['H'] + rules_offspring2['H']=lsystem_parent1.rules['H'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['H']=lsystem_parent1.rules['H'] + rules_offspring2['H']=lsystem_parent2.rules['H'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + if random.random()>mutation_rate: + rules_offspring1['N']=lsystem_parent2.rules['N'] + rules_offspring2['N']=lsystem_parent1.rules['N'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['N']=lsystem_parent1.rules['N'] + rules_offspring2['N']=lsystem_parent2.rules['N'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + iteration_offspring1=int(iter_offspring1/4) + iteration_offspring2=int(iter_offspring2/4) + offspring1=LSystemDecoder(axiom_offspring1,rules_offspring1,iter_offspring1) + offspring2=LSystemDecoder(axiom_offspring2,rules_offspring2,iter_offspring2) + return offspring1,offspring2 + +def initialization_lsystem(): + axiom = "C" + rules = {} + nb_item_C=random.choice(range(6,10)) + nb_item_H=random.choice(range(2,10)) + nb_item_B=random.choice(range(2,10)) + nb_item_N=random.choice(range(2,10)) + rule_string_C = "C" + for i in range(0,nb_item_C): + what_to_add = random.choice(['add','mov']) + match what_to_add: + case 'add': + operator = random.choice(['addf','addk','addl','addr','addb','addt']) + rotation = random.choice([0,45,90,135,180,225,270]) + op_to_add=operator+"("+str(rotation)+")" + item_to_add=random.choice(['B','H','N']) + rule_string_C+=" "+op_to_add+" "+item_to_add + case 'mov': + operator = random.choice(['movf','movk','movl','movr','movb','movt']) + rule_string_C+=" "+operator + rule_string_B="B" + for i in range(0,nb_item_B): + what_to_add = random.choice(['add','mov']) + match what_to_add: + case 'add': + operator = random.choice(['addf','addk','addl','addr','addb','addt']) + rotation = random.choice([0,45,90,135,180,225,270]) + op_to_add=operator+"("+str(rotation)+")" + item_to_add=random.choice(['B','H','N']) + rule_string_B+=" "+op_to_add+" "+item_to_add + case 'mov': + operator = random.choice(['movf','movk','movl','movr','movb','movt']) + rule_string_B+=" "+operator + rule_string_H="H" + for i in range(0,nb_item_H): + what_to_add = random.choice(['add','mov']) + match what_to_add: + case 'add': + operator = random.choice(['addf','addk','addl','addr','addb','addt']) + rotation = random.choice([0,45,90,135,180,225,270]) + op_to_add=operator+"("+str(rotation)+")" + item_to_add=random.choice(['B','H','N']) + rule_string_H+=" "+op_to_add+" "+item_to_add + case 'mov': + operator = random.choice(['movf','movk','movl','movr','movb','movt']) + rule_string_H+=" "+operator + rule_string_N="N" + for i in range(0,nb_item_H): + what_to_add = random.choice(['add','mov']) + match what_to_add: + case 'add': + operator = random.choice(['addf','addk','addl','addr','addb','addt']) + rotation = random.choice([0,45,90,135,180,225,270]) + op_to_add=operator+"("+str(rotation)+")" + item_to_add=random.choice(['B','H','N']) + rule_string_N+=" "+op_to_add+" "+item_to_add + case 'mov': + operator = random.choice(['movf','movk','movl','movr','movb','movt']) + rule_string_N+=" "+operator + rules['C']=rule_string_C + rules['B']=rule_string_B + rules['H']=rule_string_H + rules['N']=rule_string_N + iterations = random.choice(range(0,4)) + ls = LSystemDecoder(axiom,rules,iterations) + return ls From b919749886929bacafd817047a4b31a59af69208 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:05:19 +0200 Subject: [PATCH 33/47] updated l-system fully functional --- examples/_display_robot_from_json.py | 127 ++++ examples/graph.json | 94 +++ examples/lsystem_graph.json | 131 ++++ examples/mygraph.json | 44 ++ examples/mygraph2.json | 54 ++ examples/offspring1.json | 54 ++ examples/offspring2.json | 164 +++++ examples/parent1-mutated.json | 74 ++ examples/parent1.json | 104 +++ examples/parent2-mutated.json | 44 ++ examples/parent2.json | 54 ++ .../decoders/l_system_genotype.py | 651 +++++++++++------- src/ariel/ec/ec_l_system.py | 20 +- 13 files changed, 1347 insertions(+), 268 deletions(-) create mode 100644 examples/_display_robot_from_json.py create mode 100644 examples/graph.json create mode 100755 examples/lsystem_graph.json create mode 100644 examples/mygraph.json create mode 100644 examples/mygraph2.json create mode 100644 examples/offspring1.json create mode 100644 examples/offspring2.json create mode 100644 examples/parent1-mutated.json create mode 100644 examples/parent1.json create mode 100644 examples/parent2-mutated.json create mode 100644 examples/parent2.json diff --git a/examples/_display_robot_from_json.py b/examples/_display_robot_from_json.py new file mode 100644 index 00000000..f42be292 --- /dev/null +++ b/examples/_display_robot_from_json.py @@ -0,0 +1,127 @@ +"""TODO(jmdm): description of script. + +Author: jmdm +Date: 2025-06-25 +Py Ver: 3.12 +OS: macOS Sequoia 15.3.1 +Hardware: M4 Pro +Status: Completed ✅ +""" + +# Standard library +from pathlib import Path +from typing import TYPE_CHECKING, Any + +# Third-party libraries +import mujoco +import numpy as np +from mujoco import viewer +from rich.console import Console + +# Local libraries +from ariel.body_phenotypes.robogen_lite.config import ( + NUM_OF_FACES, + NUM_OF_ROTATIONS, + NUM_OF_TYPES_OF_MODULES, +) +from ariel.body_phenotypes.robogen_lite.constructor import ( + construct_mjspec_from_graph, +) +from ariel.body_phenotypes.robogen_lite.decoders.l_system_genotype import ( + load_graph_from_json, +) + + +from ariel.body_phenotypes.robogen_lite.modules.core import CoreModule +from ariel.simulation.environments import SimpleFlatWorld +from ariel.utils.renderers import single_frame_renderer + +if TYPE_CHECKING: + from networkx import DiGraph + + +# Global constants +SCRIPT_NAME = __file__.split("/")[-1][:-3] +CWD = Path.cwd() +DATA = Path(CWD / "__data__" / SCRIPT_NAME) +DATA.mkdir(exist_ok=True) +SEED = 40 + +# Global functions +console = Console() +RNG = np.random.default_rng(SEED) + + +def main() -> None: + """Entry point.""" + + graph_parent1 = load_graph_from_json("parent1.json") + graph_parent2 = load_graph_from_json("parent2.json") + graph_parent1_mutated = load_graph_from_json("parent1-mutated.json") + graph_parent2_mutated = load_graph_from_json("parent2-mutated.json") + graph_offspring1 = load_graph_from_json("offspring1.json") + graph_offspring2 = load_graph_from_json("offspring2.json") + + # Construct the robot from the graph + core_parent1 = construct_mjspec_from_graph(graph_parent1) + core_parent2 = construct_mjspec_from_graph(graph_parent2) + core_parent1_mutated = construct_mjspec_from_graph(graph_parent1_mutated) + core_parent2_mutated = construct_mjspec_from_graph(graph_parent2_mutated) + core_offspring1 = construct_mjspec_from_graph(graph_offspring1) + core_offspring2 = construct_mjspec_from_graph(graph_offspring2) + # Simulate the robot + run(core_parent1, with_viewer=True) + run(core_parent2, with_viewer=True) + run(core_parent1_mutated, with_viewer=True) + run(core_parent2_mutated, with_viewer=True) + run(core_offspring1, with_viewer=True) + run(core_offspring2, with_viewer=True) + +def run( + robot: CoreModule, + *, + with_viewer: bool = False, +) -> None: + """Entry point.""" + # MuJoCo configuration + viz_options = mujoco.MjvOption() # visualization of various elements + + # Visualization of the corresponding model or decoration element + viz_options.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] = True + viz_options.flags[mujoco.mjtVisFlag.mjVIS_ACTUATOR] = True + viz_options.flags[mujoco.mjtVisFlag.mjVIS_BODYBVH] = True + + # MuJoCo basics + world = SimpleFlatWorld() + + # Set random colors for geoms + for i in range(len(robot.spec.geoms)): + robot.spec.geoms[i].rgba[-1] = 0.5 + # Spawn the robot at the world + world.spawn(robot.spec) + + # Compile the model + model = world.spec.compile() + data = mujoco.MjData(model) + + # Save the model to XML + xml = world.spec.to_xml() + with (DATA / f"{SCRIPT_NAME}.xml").open("w", encoding="utf-8") as f: + f.write(xml) + + # Number of actuators and DoFs + console.log(f"DoF (model.nv): {model.nv}, Actuators (model.nu): {model.nu}") + + # Reset state and time of simulation + mujoco.mj_resetData(model, data) + + # Render + single_frame_renderer(model, data, steps=10) + + # View + if with_viewer: + viewer.launch(model=model, data=data) + + +if __name__ == "__main__": + main() diff --git a/examples/graph.json b/examples/graph.json new file mode 100644 index 00000000..734a7db0 --- /dev/null +++ b/examples/graph.json @@ -0,0 +1,94 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "HINGE", + "rotation": "DEG_270", + "id": 1 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 2 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 3 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 4 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 5 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 6 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 7 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 8 + } + ], + "edges": [ + { + "face": "BACK", + "source": 0, + "target": 1 + }, + { + "face": "RIGHT", + "source": 0, + "target": 2 + }, + { + "face": "TOP", + "source": 0, + "target": 6 + }, + { + "face": "TOP", + "source": 0, + "target": 7 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 8 + }, + { + "face": "RIGHT", + "source": 2, + "target": 3 + }, + { + "face": "TOP", + "source": 2, + "target": 4 + }, + { + "face": "TOP", + "source": 2, + "target": 5 + } + ] +} \ No newline at end of file diff --git a/examples/lsystem_graph.json b/examples/lsystem_graph.json new file mode 100755 index 00000000..9fc96aea --- /dev/null +++ b/examples/lsystem_graph.json @@ -0,0 +1,131 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": 0, + "id": "C0" + }, + { + "type": "HINGE", + "rotation": 45, + "id": "H1" + }, + { + "type": "BRICK", + "rotation": 45, + "id": "B2" + }, + { + "type": "BRICK", + "rotation": 0, + "id": "B3" + }, + { + "type": "HINGE", + "rotation": 45, + "id": "H4" + }, + { + "type": "BRICK", + "rotation": 0, + "id": "B5" + }, + { + "type": "HINGE", + "rotation": 45, + "id": "H6" + }, + { + "type": "HINGE", + "rotation": 45, + "id": "H7" + }, + { + "type": "BRICK", + "rotation": 45, + "id": "B8" + }, + { + "type": "HINGE", + "rotation": 45, + "id": "H9" + }, + { + "type": "BRICK", + "rotation": 45, + "id": "B10" + }, + { + "type": "BRICK", + "rotation": 0, + "id": "B11" + }, + { + "type": "HINGE", + "rotation": 45, + "id": "H12" + }, + { + "type": "N", + "rotation": 0, + "id": "N13" + } + ], + "edges": [ + { + "source": "C0", + "target": "H1" + }, + { + "source": "C0", + "target": "B5" + }, + { + "source": "C0", + "target": "H9" + }, + { + "source": "C0", + "target": "N13" + }, + { + "source": "H1", + "target": "B2" + }, + { + "source": "B2", + "target": "B3" + }, + { + "source": "B3", + "target": "H4" + }, + { + "source": "B5", + "target": "H6" + }, + { + "source": "H6", + "target": "H7" + }, + { + "source": "H7", + "target": "B8" + }, + { + "source": "H9", + "target": "B10" + }, + { + "source": "B10", + "target": "B11" + }, + { + "source": "B11", + "target": "H12" + } + ] +} \ No newline at end of file diff --git a/examples/mygraph.json b/examples/mygraph.json new file mode 100644 index 00000000..231abd17 --- /dev/null +++ b/examples/mygraph.json @@ -0,0 +1,44 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": 0, + "rotation": 0, + "id": "C-0" + }, + { + "type": 3, + "rotation": 90, + "id": "N-1" + }, + { + "type": 3, + "rotation": 270, + "id": "N-2" + }, + { + "type": "BRICK", + "rotation": 270, + "id": "B-3" + } + ], + "edges": [ + { + "face": "RIGHT", + "source": "C-0", + "target": "N-1" + }, + { + "face": "LEFT", + "source": "C-0", + "target": "N-2" + }, + { + "face": "TOP", + "source": "C-0", + "target": "B-3" + } + ] +} \ No newline at end of file diff --git a/examples/mygraph2.json b/examples/mygraph2.json new file mode 100644 index 00000000..4e2efb39 --- /dev/null +++ b/examples/mygraph2.json @@ -0,0 +1,54 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": 0, + "rotation": 0, + "id": "C-0" + }, + { + "type": "HINGE", + "rotation": 135, + "id": "H-1" + }, + { + "type": "BRICK", + "rotation": 270, + "id": "B-2" + }, + { + "type": 3, + "rotation": 45, + "id": "N-3" + }, + { + "type": "BRICK", + "rotation": 225, + "id": "B-4" + } + ], + "edges": [ + { + "face": "FRONT", + "source": "C-0", + "target": "H-1" + }, + { + "face": "RIGHT", + "source": "C-0", + "target": "B-2" + }, + { + "face": "TOP", + "source": "C-0", + "target": "N-3" + }, + { + "face": "BOTTOM", + "source": "C-0", + "target": "B-4" + } + ] +} \ No newline at end of file diff --git a/examples/offspring1.json b/examples/offspring1.json new file mode 100644 index 00000000..6007298f --- /dev/null +++ b/examples/offspring1.json @@ -0,0 +1,54 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "NONE", + "rotation": "DEG_135", + "id": 1 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 2 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 3 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 4 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "RIGHT", + "source": 0, + "target": 2 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 3 + }, + { + "face": "FRONT", + "source": 3, + "target": 4 + } + ] +} \ No newline at end of file diff --git a/examples/offspring2.json b/examples/offspring2.json new file mode 100644 index 00000000..0ca37be1 --- /dev/null +++ b/examples/offspring2.json @@ -0,0 +1,164 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "BRICK", + "rotation": "DEG_135", + "id": 1 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 2 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 3 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 4 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 5 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 6 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 7 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 8 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 9 + }, + { + "type": "NONE", + "rotation": "DEG_90", + "id": 10 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 11 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 12 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 13 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 14 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 15 + } + ], + "edges": [ + { + "face": "BACK", + "source": 0, + "target": 1 + }, + { + "face": "RIGHT", + "source": 0, + "target": 7 + }, + { + "face": "LEFT", + "source": 0, + "target": 8 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 15 + }, + { + "face": "FRONT", + "source": 1, + "target": 2 + }, + { + "face": "LEFT", + "source": 1, + "target": 3 + }, + { + "face": "TOP", + "source": 1, + "target": 4 + }, + { + "face": "TOP", + "source": 1, + "target": 5 + }, + { + "face": "BOTTOM", + "source": 1, + "target": 6 + }, + { + "face": "FRONT", + "source": 8, + "target": 9 + }, + { + "face": "RIGHT", + "source": 8, + "target": 10 + }, + { + "face": "LEFT", + "source": 8, + "target": 11 + }, + { + "face": "TOP", + "source": 8, + "target": 12 + }, + { + "face": "TOP", + "source": 8, + "target": 13 + }, + { + "face": "BOTTOM", + "source": 8, + "target": 14 + } + ] +} \ No newline at end of file diff --git a/examples/parent1-mutated.json b/examples/parent1-mutated.json new file mode 100644 index 00000000..1eceb952 --- /dev/null +++ b/examples/parent1-mutated.json @@ -0,0 +1,74 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "HINGE", + "rotation": "DEG_0", + "id": 1 + }, + { + "type": "HINGE", + "rotation": "DEG_0", + "id": 2 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 3 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 4 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 5 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 6 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "LEFT", + "source": 0, + "target": 3 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 5 + }, + { + "face": "FRONT", + "source": 1, + "target": 2 + }, + { + "face": "FRONT", + "source": 3, + "target": 4 + }, + { + "face": "FRONT", + "source": 5, + "target": 6 + } + ] +} \ No newline at end of file diff --git a/examples/parent1.json b/examples/parent1.json new file mode 100644 index 00000000..a93003c4 --- /dev/null +++ b/examples/parent1.json @@ -0,0 +1,104 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 1 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 2 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 3 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 4 + }, + { + "type": "HINGE", + "rotation": "DEG_225", + "id": 5 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 6 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 7 + }, + { + "type": "HINGE", + "rotation": "DEG_225", + "id": 8 + }, + { + "type": "HINGE", + "rotation": "DEG_270", + "id": 9 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "RIGHT", + "source": 0, + "target": 2 + }, + { + "face": "TOP", + "source": 0, + "target": 3 + }, + { + "face": "TOP", + "source": 0, + "target": 6 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 9 + }, + { + "face": "FRONT", + "source": 3, + "target": 4 + }, + { + "face": "BOTTOM", + "source": 3, + "target": 5 + }, + { + "face": "FRONT", + "source": 6, + "target": 7 + }, + { + "face": "BOTTOM", + "source": 6, + "target": 8 + } + ] +} \ No newline at end of file diff --git a/examples/parent2-mutated.json b/examples/parent2-mutated.json new file mode 100644 index 00000000..6c03569e --- /dev/null +++ b/examples/parent2-mutated.json @@ -0,0 +1,44 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "BRICK", + "rotation": "DEG_90", + "id": 1 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 2 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 3 + } + ], + "edges": [ + { + "face": "BACK", + "source": 0, + "target": 1 + }, + { + "face": "RIGHT", + "source": 0, + "target": 2 + }, + { + "face": "LEFT", + "source": 0, + "target": 3 + } + ] +} \ No newline at end of file diff --git a/examples/parent2.json b/examples/parent2.json new file mode 100644 index 00000000..78013907 --- /dev/null +++ b/examples/parent2.json @@ -0,0 +1,54 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 1 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 2 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 3 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 4 + } + ], + "edges": [ + { + "face": "BACK", + "source": 0, + "target": 1 + }, + { + "face": "RIGHT", + "source": 0, + "target": 2 + }, + { + "face": "TOP", + "source": 0, + "target": 3 + }, + { + "face": "TOP", + "source": 0, + "target": 4 + } + ] +} \ No newline at end of file diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py index 78fd282b..e5b3a160 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py @@ -34,7 +34,7 @@ # Local libraries -from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType +from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType, ModuleInstance,ModuleRotationsIdx SEED = 42 DPI = 300 @@ -95,28 +95,44 @@ def connect_to(self,side,obj,rotation): def has_element(self,side): has_element=False - if side in self.allowed_connection: - match side: - case 'TOP': - if self.top!=None: - has_element=True - case 'BOTTOM': - if self.bottom!=None: - has_element=True - case 'LEFT': - if self.left!=None: - has_element=True - case 'RIGHT': - if self.right!=None: - has_element=True - case 'FRONT': - if self.front!=None: - has_element=True - case 'BACK': - if self.back!=None: - has_element=True + match side: + case 'TOP': + if self.top!=None: + has_element=True + case 'BOTTOM': + if self.bottom!=None: + has_element=True + case 'LEFT': + if self.left!=None: + has_element=True + case 'RIGHT': + if self.right!=None: + has_element=True + case 'FRONT': + if self.front!=None: + has_element=True + case 'BACK': + if self.back!=None: + has_element=True return has_element + def is_allowed(self,side): + is_allowed=False + if side in self.allowed_connection: + is_allowed=True + return is_allowed + + def should_i_explore(self,side): + res=False + if self.is_allowed(side)==True: + if self.has_element(side)==False: + res = False + else: + res = True + else: + res = False + return res + class lsystem_block(lsystem_element): @@ -169,6 +185,9 @@ def __init__( axiom: str, rules: Dict[str, str], iterations: int = 2, + max_elements: int = 32, + max_depth: int = 8, + verbose: int = 0 ) -> None: """ Initialize the L-system decoder. @@ -180,6 +199,9 @@ def __init__( self.graph = nx.DiGraph() self.expanded_token=None self.structure = None + self.max_elements = max_elements + self.max_depth = max_depth + self.verbose=verbose def expand_lsystem(self): expanded_token = [] @@ -205,337 +227,434 @@ def get_rotation(self,cmd): return int(rotation) - def build_lsystem_structure(self,verbose=0): + def build_lsystem_structure(self): + if self.verbose==1: + print("Building L-system structure...") if self.expanded_token!=None and self.expanded_token[0]=='C': self.structure=lsystem_core() turtle = self.structure tk=1 - while tk < len(self.expanded_token): - if verbose==1: - print("token : ",tk) + add_elements=1 + while tk < len(self.expanded_token) and add_elementsself.max_depth or id>self.max_elements: + return id connection_map = [] for j in ['FRONT','BACK','RIGHT','LEFT','TOP','BOTTOM']: connection_map.append(element.has_element(j)) - if verbose==1: - print(element.name,"-",id," - ",connection_map) + print(element.name,"-",id," - ",connection_map) id_tmp = id - if element.has_element('FRONT')==True: - if verbose==1: - print(element.name,"-",id," links FRONT / ",element.front.rotation," degree to ",element.front.name,"-",id_tmp+1) - id_tmp=self.print_lsystem_element(element.front,id_tmp+1,verbose) + if element.should_i_explore('FRONT')==True: + print(element.name,"-",id," links FRONT / ",element.front.rotation," degree to ",element.front.name,"-",id_tmp+1) + id_tmp=self.print_lsystem_element(element.front,id_tmp+1,depth+1) if element.name=="C": - if element.has_element('BACK')==True: - if verbose==1: - print(element.name,"-",id," links BACK / ",element.back.rotation," degree to ",element.back.name,"-",id_tmp+1) - id_tmp=self.print_lsystem_element(element.back,id_tmp+1,verbose) - if element.has_element('LEFT')==True: - if verbose==1: - print(element.name,"-",id," links LEFT / ",element.left.rotation," degree to ",element.left.name,"-",id_tmp+1) - id_tmp=self.print_lsystem_element(element.left,id_tmp+1,verbose) - if element.has_element('RIGHT')==True: - if verbose==1: - print(element.name,"-",id," links RIGHT / ",element.right.rotation," degree to ",element.right.name,"-",id_tmp+1) - id_tmp=self.print_lsystem_element(element.right,id_tmp+1,verbose) - if element.has_element('TOP')==True: - if verbose==1: - print(element.name,"-",id," links TOP / ",element.top.rotation," degree to ",element.top.name,"-",id_tmp+1) - id_tmp=self.print_lsystem_element(element.top,id_tmp+1,verbose) - if element.has_element('BOTTOM')==True: - if verbose==1: - print(element.name,"-",id," links BOTTOM / ",element.bottom.rotation," degree to ",element.bottom.name,"-",id_tmp+1) - id_tmp=self.print_lsystem_element(element.bottom,id_tmp+1,verbose) + if element.should_i_explore('BACK')==True: + print(element.name,"-",id," links BACK / ",element.back.rotation," degree to ",element.back.name,"-",id_tmp+1) + id_tmp=self.print_lsystem_element(element.back,id_tmp+1,depth+1) + if element.should_i_explore('LEFT')==True: + print(element.name,"-",id," links LEFT / ",element.left.rotation," degree to ",element.left.name,"-",id_tmp+1) + id_tmp=self.print_lsystem_element(element.left,id_tmp+1,depth+1) + if element.should_i_explore('RIGHT')==True: + print(element.name,"-",id," links RIGHT / ",element.right.rotation," degree to ",element.right.name,"-",id_tmp+1) + id_tmp=self.print_lsystem_element(element.right,id_tmp+1,depth+1) + if element.should_i_explore('TOP')==True: + print(element.name,"-",id," links TOP / ",element.top.rotation," degree to ",element.top.name,"-",id_tmp+1) + id_tmp=self.print_lsystem_element(element.top,id_tmp+1,depth+1) + if element.should_i_explore('BOTTOM')==True: + print(element.name,"-",id," links BOTTOM / ",element.bottom.rotation," degree to ",element.bottom.name,"-",id_tmp+1) + id_tmp=self.print_lsystem_element(element.bottom,id_tmp+1,depth+1) return id_tmp - def print_lsystem_structure(self,verbose=0): - self.print_lsystem_element(self.structure,0,verbose) + def print_lsystem_structure(self): + self.print_lsystem_element(self.structure,0,0) - def generate_lsystem_graph_element(self,element,id,verbose=0): + def generate_lsystem_graph_element(self,element,id,depth): + if depth>self.max_depth or id>=self.max_elements: + return id if id==0: - self.graph.add_node( - element.name+"-"+str(id), - type=ModuleType.CORE.value, - rotation=0, - ) + self.graph.add_node(id,type=ModuleType.CORE.name,rotation='DEG_0') id_tmp = id - if element.has_element('FRONT')==True: - eltype = ModuleType.NONE.value + if element.should_i_explore('FRONT')==True: + eltype = ModuleType.NONE.name match element.front.name: case 'B': - eltype = ModuleType.BRICK.value + eltype = ModuleType.BRICK.name case 'H': - eltype = ModuleType.HINGE.value - self.graph.add_node(element.front.name+"-"+str(id_tmp+1),type=eltype,rotation=element.front.rotation) - self.graph.add_edge(element.name+"-"+str(id),element.front.name+"-"+str(id_tmp+1),face='FRONT') - id_tmp=self.generate_lsystem_graph_element(element.front,id_tmp+1,verbose) + eltype = ModuleType.HINGE.name + rotation="DEG_"+str(element.front.rotation) + self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) + self.graph.add_edge(id,id_tmp+1,face='FRONT') + id_tmp=self.generate_lsystem_graph_element(element.front,id_tmp+1,depth+1) if element.name=="C": - if element.has_element('BACK')==True: - eltype = ModuleType.NONE.value + if element.should_i_explore('BACK')==True: + eltype = ModuleType.NONE.name match element.back.name: case 'B': - eltype = ModuleType.BRICK.value + eltype = ModuleType.BRICK.name case 'H': - eltype = ModuleType.HINGE.value - self.graph.add_node(element.back.name+"-"+str(id_tmp+1),type=eltype,rotation=element.back.rotation) - self.graph.add_edge(element.name+"-"+str(id),element.back.name+"-"+str(id_tmp+1),face='BACK') - id_tmp=self.generate_lsystem_graph_element(element.back,id_tmp+1,verbose) - if element.has_element('RIGHT')==True: - eltype = ModuleType.NONE.value + eltype = ModuleType.HINGE.name + rotation="DEG_"+str(element.back.rotation) + self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) + self.graph.add_edge(id,id_tmp+1,face='BACK') + id_tmp=self.generate_lsystem_graph_element(element.back,id_tmp+1,depth+1) + if element.should_i_explore('RIGHT')==True: + eltype = ModuleType.NONE.name match element.right.name: case 'B': - eltype = ModuleType.BRICK.value + eltype = ModuleType.BRICK.name case 'H': - eltype = ModuleType.HINGE.value - self.graph.add_node(element.right.name+"-"+str(id_tmp+1),type=eltype,rotation=element.right.rotation) - self.graph.add_edge(element.name+"-"+str(id),element.right.name+"-"+str(id_tmp+1),face='RIGHT') - id_tmp=self.generate_lsystem_graph_element(element.right,id_tmp+1,verbose) - if element.has_element('LEFT')==True: - eltype = ModuleType.NONE.value + eltype = ModuleType.HINGE.name + rotation="DEG_"+str(element.right.rotation) + self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) + self.graph.add_edge(id,id_tmp+1,face='RIGHT') + id_tmp=self.generate_lsystem_graph_element(element.right,id_tmp+1,depth+1) + if element.should_i_explore('LEFT')==True: + eltype = ModuleType.NONE.name match element.left.name: case 'B': - eltype = ModuleType.BRICK.value + eltype = ModuleType.BRICK.name case 'H': - eltype = ModuleType.HINGE.value - self.graph.add_node(element.left.name+"-"+str(id_tmp+1),type=eltype,rotation=element.left.rotation) - self.graph.add_edge(element.name+"-"+str(id),element.left.name+"-"+str(id_tmp+1),face='LEFT') - id_tmp=self.generate_lsystem_graph_element(element.left,id_tmp+1,verbose) - if element.has_element('TOP')==True: - eltype = ModuleType.NONE.value + eltype = ModuleType.HINGE.name + rotation="DEG_"+str(element.left.rotation) + self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) + self.graph.add_edge(id,id_tmp+1,face='LEFT') + id_tmp=self.generate_lsystem_graph_element(element.left,id_tmp+1,depth+1) + if element.should_i_explore('TOP')==True: + eltype = ModuleType.NONE.name match element.top.name: case 'B': - eltype = ModuleType.BRICK.value + eltype = ModuleType.BRICK.name case 'H': - eltype = ModuleType.HINGE.value - self.graph.add_node(element.top.name+"-"+str(id_tmp+1),type=eltype,rotation=element.top.rotation) - self.graph.add_edge(element.name+"-"+str(id),element.top.name+"-"+str(id_tmp+1),face='TOP') - id_tmp=self.generate_lsystem_graph_element(element.top,id_tmp+1,verbose) - if element.has_element('BOTTOM')==True: - eltype = ModuleType.NONE.value + eltype = ModuleType.HINGE.name + rotation="DEG_"+str(element.top.rotation) + self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) + self.graph.add_edge(id,id_tmp+1,face='TOP') + id_tmp=self.generate_lsystem_graph_element(element.top,id_tmp+1,depth+1) + if element.should_i_explore('TOP')==True: + eltype = ModuleType.NONE.name + match element.top.name: + case 'B': + eltype = ModuleType.BRICK.name + case 'H': + eltype = ModuleType.HINGE.name + rotation="DEG_"+str(element.top.rotation) + self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) + self.graph.add_edge(id,id_tmp+1,face='TOP') + id_tmp=self.generate_lsystem_graph_element(element.top,id_tmp+1,depth+1) + if element.should_i_explore('BOTTOM')==True: + eltype = ModuleType.NONE.name match element.bottom.name: case 'B': - eltype = ModuleType.BRICK.value + eltype = ModuleType.BRICK.name case 'H': - eltype = ModuleType.HINGE.value - self.graph.add_node(element.bottom.name+"-"+str(id_tmp+1),type=eltype,rotation=element.bottom.rotation) - self.graph.add_edge(element.name+"-"+str(id),element.bottom.name+"-"+str(id_tmp+1),face='BOTTOM') - id_tmp=self.generate_lsystem_graph_element(element.bottom,id_tmp+1,verbose) + eltype = ModuleType.HINGE.name + rotation="DEG_"+str(element.bottom.rotation) + self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) + self.graph.add_edge(id,id_tmp+1,face='BOTTOM') + id_tmp=self.generate_lsystem_graph_element(element.bottom,id_tmp+1,depth+1) return id_tmp - def generate_lsystem_graph(self,verbose=0): + def generate_lsystem_graph(self): self.graph = nx.DiGraph() - self.generate_lsystem_graph_element(self.structure,0,verbose) + self.generate_lsystem_graph_element(self.structure,0,0) def print_lsystem_expanded(self): print(self.expanded_token) @@ -548,6 +667,13 @@ def save_graph_as_json(self,save_file): with Path(save_file).open("w", encoding="utf-8") as f: f.write(json_string) + def refresh(self): + if self.verbose==1: + print("Refreshing L-system decoding...") + self.expand_lsystem() + self.build_lsystem_structure() + self.generate_lsystem_graph() + def draw_graph(self, title = "L-System Decoded Graph",save_file = None): """Draw the decoded graph using matplotlib and networkx.""" plt.figure() @@ -584,4 +710,13 @@ def draw_graph(self, title = "L-System Decoded Graph",save_file = None): else: plt.show() +def load_graph_from_json(load_file): + with Path(load_file).open("r", encoding="utf-8") as f: + data = json.load(f) + return json_graph.node_link_graph( + data, + directed=True, + multigraph=False, + edges="edges", + ) diff --git a/src/ariel/ec/ec_l_system.py b/src/ariel/ec/ec_l_system.py index 632f4ad0..42a78b49 100644 --- a/src/ariel/ec/ec_l_system.py +++ b/src/ariel/ec/ec_l_system.py @@ -146,11 +146,11 @@ def crossover_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): iter_offspring2+=lsystem_parent2.iterations iteration_offspring1=int(iter_offspring1/4) iteration_offspring2=int(iter_offspring2/4) - offspring1=LSystemDecoder(axiom_offspring1,rules_offspring1,iter_offspring1) - offspring2=LSystemDecoder(axiom_offspring2,rules_offspring2,iter_offspring2) + offspring1=LSystemDecoder(axiom_offspring1,rules_offspring1,iter_offspring1,lsystem_parent1.max_elements,lsystem_parent1.max_depth,lsystem_parent1.verbose) + offspring2=LSystemDecoder(axiom_offspring2,rules_offspring2,iter_offspring2,lsystem_parent2.max_elements,lsystem_parent2.max_depth,lsystem_parent2.verbose) return offspring1,offspring2 -def initialization_lsystem(): +def initialization_lsystem(max_elements=32,max_depth=8,add_temperature=0.5,none_temperature=0.2,verbose=0): axiom = "C" rules = {} nb_item_C=random.choice(range(6,10)) @@ -159,13 +159,13 @@ def initialization_lsystem(): nb_item_N=random.choice(range(2,10)) rule_string_C = "C" for i in range(0,nb_item_C): - what_to_add = random.choice(['add','mov']) + what_to_add = random.choices(['add','mov'],weights=[add_temperature,1-add_temperature])[0] match what_to_add: case 'add': operator = random.choice(['addf','addk','addl','addr','addb','addt']) rotation = random.choice([0,45,90,135,180,225,270]) op_to_add=operator+"("+str(rotation)+")" - item_to_add=random.choice(['B','H','N']) + item_to_add=random.choices(['B','H','N'],weights=[(1-none_temperature)/2,(1-none_temperature)/2,none_temperature])[0] rule_string_C+=" "+op_to_add+" "+item_to_add case 'mov': operator = random.choice(['movf','movk','movl','movr','movb','movt']) @@ -178,7 +178,7 @@ def initialization_lsystem(): operator = random.choice(['addf','addk','addl','addr','addb','addt']) rotation = random.choice([0,45,90,135,180,225,270]) op_to_add=operator+"("+str(rotation)+")" - item_to_add=random.choice(['B','H','N']) + item_to_add=random.choices(['B','H','N'],weights=[(1-none_temperature)/2,(1-none_temperature)/2,none_temperature])[0] rule_string_B+=" "+op_to_add+" "+item_to_add case 'mov': operator = random.choice(['movf','movk','movl','movr','movb','movt']) @@ -191,7 +191,7 @@ def initialization_lsystem(): operator = random.choice(['addf','addk','addl','addr','addb','addt']) rotation = random.choice([0,45,90,135,180,225,270]) op_to_add=operator+"("+str(rotation)+")" - item_to_add=random.choice(['B','H','N']) + item_to_add=random.choices(['B','H','N'],weights=[(1-none_temperature)/2,(1-none_temperature)/2,none_temperature])[0] rule_string_H+=" "+op_to_add+" "+item_to_add case 'mov': operator = random.choice(['movf','movk','movl','movr','movb','movt']) @@ -204,7 +204,7 @@ def initialization_lsystem(): operator = random.choice(['addf','addk','addl','addr','addb','addt']) rotation = random.choice([0,45,90,135,180,225,270]) op_to_add=operator+"("+str(rotation)+")" - item_to_add=random.choice(['B','H','N']) + item_to_add=random.choices(['B','H','N'],weights=[(1-none_temperature)/2,(1-none_temperature)/2,none_temperature])[0] rule_string_N+=" "+op_to_add+" "+item_to_add case 'mov': operator = random.choice(['movf','movk','movl','movr','movb','movt']) @@ -213,6 +213,6 @@ def initialization_lsystem(): rules['B']=rule_string_B rules['H']=rule_string_H rules['N']=rule_string_N - iterations = random.choice(range(0,4)) - ls = LSystemDecoder(axiom,rules,iterations) + iterations = random.choice(range(1,3)) + ls = LSystemDecoder(axiom,rules,iterations,max_elements=max_elements,max_depth=max_depth,verbose=verbose) return ls From 436c0acffec7ebd6c4ec6b595808a5501f5c6d86 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:48:58 +0200 Subject: [PATCH 34/47] added a more granular lsystem crossover (genes level and not rules level) --- examples/_display_robot_from_json.py | 6 + examples/offspring1.json | 48 +++- examples/offspring2.json | 104 ++------ examples/offspring3.json | 74 ++++++ examples/offspring4.json | 114 +++++++++ examples/parent1-mutated.json | 32 ++- examples/parent1.json | 142 +++++++++-- examples/parent2-mutated.json | 64 ++++- examples/parent2.json | 22 +- .../decoders/l_system_genotype.py | 11 - src/ariel/ec/ec_l_system.py | 223 +++++++++++++++++- 11 files changed, 676 insertions(+), 164 deletions(-) create mode 100644 examples/offspring3.json create mode 100644 examples/offspring4.json diff --git a/examples/_display_robot_from_json.py b/examples/_display_robot_from_json.py index f42be292..98f72228 100644 --- a/examples/_display_robot_from_json.py +++ b/examples/_display_robot_from_json.py @@ -61,6 +61,8 @@ def main() -> None: graph_parent2_mutated = load_graph_from_json("parent2-mutated.json") graph_offspring1 = load_graph_from_json("offspring1.json") graph_offspring2 = load_graph_from_json("offspring2.json") + graph_offspring3 = load_graph_from_json("offspring3.json") + graph_offspring4 = load_graph_from_json("offspring4.json") # Construct the robot from the graph core_parent1 = construct_mjspec_from_graph(graph_parent1) @@ -69,6 +71,8 @@ def main() -> None: core_parent2_mutated = construct_mjspec_from_graph(graph_parent2_mutated) core_offspring1 = construct_mjspec_from_graph(graph_offspring1) core_offspring2 = construct_mjspec_from_graph(graph_offspring2) + core_offspring3 = construct_mjspec_from_graph(graph_offspring3) + core_offspring4 = construct_mjspec_from_graph(graph_offspring4) # Simulate the robot run(core_parent1, with_viewer=True) run(core_parent2, with_viewer=True) @@ -76,6 +80,8 @@ def main() -> None: run(core_parent2_mutated, with_viewer=True) run(core_offspring1, with_viewer=True) run(core_offspring2, with_viewer=True) + run(core_offspring3, with_viewer=True) + run(core_offspring4, with_viewer=True) def run( robot: CoreModule, diff --git a/examples/offspring1.json b/examples/offspring1.json index 6007298f..41f28998 100644 --- a/examples/offspring1.json +++ b/examples/offspring1.json @@ -9,8 +9,8 @@ "id": 0 }, { - "type": "NONE", - "rotation": "DEG_135", + "type": "HINGE", + "rotation": "DEG_270", "id": 1 }, { @@ -20,35 +20,65 @@ }, { "type": "HINGE", - "rotation": "DEG_135", + "rotation": "DEG_180", "id": 3 }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 4 + }, + { + "type": "BRICK", + "rotation": "DEG_270", + "id": 5 + }, { "type": "NONE", "rotation": "DEG_225", - "id": 4 + "id": 6 + }, + { + "type": "NONE", + "rotation": "DEG_90", + "id": 7 } ], "edges": [ { - "face": "FRONT", + "face": "RIGHT", "source": 0, "target": 1 }, { - "face": "RIGHT", + "face": "LEFT", "source": 0, - "target": 2 + "target": 3 }, { - "face": "BOTTOM", + "face": "TOP", "source": 0, - "target": 3 + "target": 5 + }, + { + "face": "FRONT", + "source": 1, + "target": 2 }, { "face": "FRONT", "source": 3, "target": 4 + }, + { + "face": "TOP", + "source": 5, + "target": 6 + }, + { + "face": "BOTTOM", + "source": 5, + "target": 7 } ] } \ No newline at end of file diff --git a/examples/offspring2.json b/examples/offspring2.json index 0ca37be1..a129796b 100644 --- a/examples/offspring2.json +++ b/examples/offspring2.json @@ -14,74 +14,39 @@ "id": 1 }, { - "type": "BRICK", - "rotation": "DEG_0", + "type": "HINGE", + "rotation": "DEG_90", "id": 2 }, { - "type": "BRICK", - "rotation": "DEG_45", + "type": "NONE", + "rotation": "DEG_90", "id": 3 }, { "type": "NONE", - "rotation": "DEG_270", + "rotation": "DEG_225", "id": 4 }, { - "type": "NONE", - "rotation": "DEG_270", + "type": "HINGE", + "rotation": "DEG_135", "id": 5 }, { - "type": "NONE", - "rotation": "DEG_270", + "type": "HINGE", + "rotation": "DEG_90", "id": 6 }, { "type": "NONE", - "rotation": "DEG_270", + "rotation": "DEG_90", "id": 7 }, { - "type": "BRICK", - "rotation": "DEG_180", + "type": "HINGE", + "rotation": "DEG_225", "id": 8 - }, - { - "type": "BRICK", - "rotation": "DEG_0", - "id": 9 - }, - { - "type": "NONE", - "rotation": "DEG_90", - "id": 10 - }, - { - "type": "BRICK", - "rotation": "DEG_45", - "id": 11 - }, - { - "type": "NONE", - "rotation": "DEG_270", - "id": 12 - }, - { - "type": "NONE", - "rotation": "DEG_270", - "id": 13 - }, - { - "type": "BRICK", - "rotation": "DEG_45", - "id": 14 - }, - { - "type": "NONE", - "rotation": "DEG_270", - "id": 15 } ], "edges": [ @@ -90,20 +55,15 @@ "source": 0, "target": 1 }, - { - "face": "RIGHT", - "source": 0, - "target": 7 - }, { "face": "LEFT", "source": 0, - "target": 8 + "target": 7 }, { "face": "BOTTOM", "source": 0, - "target": 15 + "target": 8 }, { "face": "FRONT", @@ -120,45 +80,15 @@ "source": 1, "target": 4 }, - { - "face": "TOP", - "source": 1, - "target": 5 - }, { "face": "BOTTOM", "source": 1, - "target": 6 + "target": 5 }, { "face": "FRONT", - "source": 8, - "target": 9 - }, - { - "face": "RIGHT", - "source": 8, - "target": 10 - }, - { - "face": "LEFT", - "source": 8, - "target": 11 - }, - { - "face": "TOP", - "source": 8, - "target": 12 - }, - { - "face": "TOP", - "source": 8, - "target": 13 - }, - { - "face": "BOTTOM", - "source": 8, - "target": 14 + "source": 5, + "target": 6 } ] } \ No newline at end of file diff --git a/examples/offspring3.json b/examples/offspring3.json new file mode 100644 index 00000000..7c6cf971 --- /dev/null +++ b/examples/offspring3.json @@ -0,0 +1,74 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 1 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 2 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 3 + }, + { + "type": "HINGE", + "rotation": "DEG_270", + "id": 4 + }, + { + "type": "NONE", + "rotation": "DEG_0", + "id": 5 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 6 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "BACK", + "source": 0, + "target": 3 + }, + { + "face": "RIGHT", + "source": 0, + "target": 4 + }, + { + "face": "TOP", + "source": 0, + "target": 5 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 6 + }, + { + "face": "TOP", + "source": 1, + "target": 2 + } + ] +} \ No newline at end of file diff --git a/examples/offspring4.json b/examples/offspring4.json new file mode 100644 index 00000000..cd31576f --- /dev/null +++ b/examples/offspring4.json @@ -0,0 +1,114 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 1 + }, + { + "type": "BRICK", + "rotation": "DEG_135", + "id": 2 + }, + { + "type": "BRICK", + "rotation": "DEG_225", + "id": 3 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 4 + }, + { + "type": "HINGE", + "rotation": "DEG_180", + "id": 5 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 6 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 7 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 8 + }, + { + "type": "NONE", + "rotation": "DEG_90", + "id": 9 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 10 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "BACK", + "source": 0, + "target": 2 + }, + { + "face": "RIGHT", + "source": 0, + "target": 8 + }, + { + "face": "LEFT", + "source": 0, + "target": 9 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 10 + }, + { + "face": "RIGHT", + "source": 2, + "target": 3 + }, + { + "face": "LEFT", + "source": 2, + "target": 5 + }, + { + "face": "TOP", + "source": 2, + "target": 6 + }, + { + "face": "BOTTOM", + "source": 2, + "target": 7 + }, + { + "face": "FRONT", + "source": 3, + "target": 4 + } + ] +} \ No newline at end of file diff --git a/examples/parent1-mutated.json b/examples/parent1-mutated.json index 1eceb952..41f28998 100644 --- a/examples/parent1-mutated.json +++ b/examples/parent1-mutated.json @@ -10,38 +10,43 @@ }, { "type": "HINGE", - "rotation": "DEG_0", + "rotation": "DEG_270", "id": 1 }, { - "type": "HINGE", - "rotation": "DEG_0", + "type": "NONE", + "rotation": "DEG_270", "id": 2 }, { "type": "HINGE", - "rotation": "DEG_90", + "rotation": "DEG_180", "id": 3 }, { - "type": "NONE", - "rotation": "DEG_225", + "type": "BRICK", + "rotation": "DEG_0", "id": 4 }, { - "type": "HINGE", - "rotation": "DEG_135", + "type": "BRICK", + "rotation": "DEG_270", "id": 5 }, { "type": "NONE", "rotation": "DEG_225", "id": 6 + }, + { + "type": "NONE", + "rotation": "DEG_90", + "id": 7 } ], "edges": [ { - "face": "FRONT", + "face": "RIGHT", "source": 0, "target": 1 }, @@ -51,7 +56,7 @@ "target": 3 }, { - "face": "BOTTOM", + "face": "TOP", "source": 0, "target": 5 }, @@ -66,9 +71,14 @@ "target": 4 }, { - "face": "FRONT", + "face": "TOP", "source": 5, "target": 6 + }, + { + "face": "BOTTOM", + "source": 5, + "target": 7 } ] } \ No newline at end of file diff --git a/examples/parent1.json b/examples/parent1.json index a93003c4..7f6a3c84 100644 --- a/examples/parent1.json +++ b/examples/parent1.json @@ -9,8 +9,8 @@ "id": 0 }, { - "type": "HINGE", - "rotation": "DEG_45", + "type": "BRICK", + "rotation": "DEG_180", "id": 1 }, { @@ -20,38 +20,83 @@ }, { "type": "BRICK", - "rotation": "DEG_45", + "rotation": "DEG_180", "id": 3 }, { - "type": "HINGE", - "rotation": "DEG_135", + "type": "BRICK", + "rotation": "DEG_90", "id": 4 }, { - "type": "HINGE", - "rotation": "DEG_225", + "type": "BRICK", + "rotation": "DEG_135", "id": 5 }, { "type": "BRICK", - "rotation": "DEG_45", + "rotation": "DEG_90", "id": 6 }, { - "type": "HINGE", + "type": "BRICK", "rotation": "DEG_135", "id": 7 }, { - "type": "HINGE", - "rotation": "DEG_225", + "type": "BRICK", + "rotation": "DEG_90", "id": 8 }, { - "type": "HINGE", - "rotation": "DEG_270", + "type": "BRICK", + "rotation": "DEG_225", "id": 9 + }, + { + "type": "HINGE", + "rotation": "DEG_0", + "id": 10 + }, + { + "type": "BRICK", + "rotation": "DEG_90", + "id": 11 + }, + { + "type": "HINGE", + "rotation": "DEG_0", + "id": 12 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 13 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 14 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 15 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 16 + }, + { + "type": "HINGE", + "rotation": "DEG_0", + "id": 17 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 18 } ], "edges": [ @@ -63,42 +108,87 @@ { "face": "RIGHT", "source": 0, - "target": 2 + "target": 17 }, { "face": "TOP", "source": 0, - "target": 3 + "target": 18 }, { - "face": "TOP", - "source": 0, - "target": 6 + "face": "FRONT", + "source": 1, + "target": 2 }, { - "face": "BOTTOM", - "source": 0, - "target": 9 + "face": "FRONT", + "source": 2, + "target": 3 }, { - "face": "FRONT", - "source": 3, + "face": "RIGHT", + "source": 2, "target": 4 }, { - "face": "BOTTOM", - "source": 3, + "face": "TOP", + "source": 2, + "target": 16 + }, + { + "face": "FRONT", + "source": 4, "target": 5 }, + { + "face": "RIGHT", + "source": 4, + "target": 6 + }, + { + "face": "TOP", + "source": 4, + "target": 15 + }, { "face": "FRONT", "source": 6, "target": 7 }, { - "face": "BOTTOM", + "face": "RIGHT", "source": 6, "target": 8 + }, + { + "face": "TOP", + "source": 6, + "target": 14 + }, + { + "face": "TOP", + "source": 8, + "target": 9 + }, + { + "face": "BOTTOM", + "source": 8, + "target": 13 + }, + { + "face": "RIGHT", + "source": 9, + "target": 10 + }, + { + "face": "LEFT", + "source": 9, + "target": 11 + }, + { + "face": "BOTTOM", + "source": 9, + "target": 12 } ] } \ No newline at end of file diff --git a/examples/parent2-mutated.json b/examples/parent2-mutated.json index 6c03569e..a129796b 100644 --- a/examples/parent2-mutated.json +++ b/examples/parent2-mutated.json @@ -10,18 +10,43 @@ }, { "type": "BRICK", - "rotation": "DEG_90", + "rotation": "DEG_135", "id": 1 }, { - "type": "NONE", - "rotation": "DEG_270", + "type": "HINGE", + "rotation": "DEG_90", "id": 2 }, { - "type": "BRICK", - "rotation": "DEG_180", + "type": "NONE", + "rotation": "DEG_90", "id": 3 + }, + { + "type": "NONE", + "rotation": "DEG_225", + "id": 4 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 5 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 6 + }, + { + "type": "NONE", + "rotation": "DEG_90", + "id": 7 + }, + { + "type": "HINGE", + "rotation": "DEG_225", + "id": 8 } ], "edges": [ @@ -31,14 +56,39 @@ "target": 1 }, { - "face": "RIGHT", + "face": "LEFT", + "source": 0, + "target": 7 + }, + { + "face": "BOTTOM", "source": 0, + "target": 8 + }, + { + "face": "FRONT", + "source": 1, "target": 2 }, { "face": "LEFT", - "source": 0, + "source": 1, "target": 3 + }, + { + "face": "TOP", + "source": 1, + "target": 4 + }, + { + "face": "BOTTOM", + "source": 1, + "target": 5 + }, + { + "face": "FRONT", + "source": 5, + "target": 6 } ] } \ No newline at end of file diff --git a/examples/parent2.json b/examples/parent2.json index 78013907..def0eef4 100644 --- a/examples/parent2.json +++ b/examples/parent2.json @@ -9,18 +9,18 @@ "id": 0 }, { - "type": "HINGE", - "rotation": "DEG_90", + "type": "NONE", + "rotation": "DEG_180", "id": 1 }, { - "type": "HINGE", - "rotation": "DEG_135", + "type": "BRICK", + "rotation": "DEG_180", "id": 2 }, { - "type": "HINGE", - "rotation": "DEG_45", + "type": "BRICK", + "rotation": "DEG_180", "id": 3 }, { @@ -31,23 +31,23 @@ ], "edges": [ { - "face": "BACK", + "face": "RIGHT", "source": 0, "target": 1 }, { - "face": "RIGHT", + "face": "TOP", "source": 0, "target": 2 }, { "face": "TOP", - "source": 0, + "source": 2, "target": 3 }, { - "face": "TOP", - "source": 0, + "face": "BOTTOM", + "source": 2, "target": 4 } ] diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py index e5b3a160..7f4588e5 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py @@ -627,17 +627,6 @@ def generate_lsystem_graph_element(self,element,id,depth): self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) self.graph.add_edge(id,id_tmp+1,face='TOP') id_tmp=self.generate_lsystem_graph_element(element.top,id_tmp+1,depth+1) - if element.should_i_explore('TOP')==True: - eltype = ModuleType.NONE.name - match element.top.name: - case 'B': - eltype = ModuleType.BRICK.name - case 'H': - eltype = ModuleType.HINGE.name - rotation="DEG_"+str(element.top.rotation) - self.graph.add_node(id_tmp+1,type=eltype,rotation=rotation) - self.graph.add_edge(id,id_tmp+1,face='TOP') - id_tmp=self.generate_lsystem_graph_element(element.top,id_tmp+1,depth+1) if element.should_i_explore('BOTTOM')==True: eltype = ModuleType.NONE.name match element.bottom.name: diff --git a/src/ariel/ec/ec_l_system.py b/src/ariel/ec/ec_l_system.py index 42a78b49..a554e2a3 100644 --- a/src/ariel/ec/ec_l_system.py +++ b/src/ariel/ec/ec_l_system.py @@ -97,7 +97,7 @@ def mutate_one_point_lsystem(lsystem,mut_rate,add_temperature=0.5): lsystem.rules[list(rules.keys())[rule_to_change]]=lsystem.rules[list(rules.keys())[rule_to_change]] return op_completed -def crossover_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): +def crossover_uniform_rules_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): axiom_offspring1="C" axiom_offspring2="C" rules_offspring1={} @@ -146,6 +146,225 @@ def crossover_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): iter_offspring2+=lsystem_parent2.iterations iteration_offspring1=int(iter_offspring1/4) iteration_offspring2=int(iter_offspring2/4) + offspring1=LSystemDecoder(axiom_offspring1,rules_offspring1,iteration_offspring1,lsystem_parent1.max_elements,lsystem_parent1.max_depth,lsystem_parent1.verbose) + offspring2=LSystemDecoder(axiom_offspring2,rules_offspring2,iteration_offspring2,lsystem_parent2.max_elements,lsystem_parent2.max_depth,lsystem_parent2.verbose) + return offspring1,offspring2 + + +def crossover_uniform_genes_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): + axiom_offspring1="C" + axiom_offspring2="C" + rules_offspring1={} + rules_offspring2={} + iter_offspring1=0 + iter_offspring2=0 + + rules_parent1 = lsystem_parent1.rules["C"].split() + rules_parent2 = lsystem_parent2.rules["C"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='C': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='C': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['C']=r_offspring1 + rules_offspring2['C']=r_offspring2 + + rules_parent1 = lsystem_parent1.rules["B"].split() + rules_parent2 = lsystem_parent2.rules["B"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='B': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='B': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['B']=r_offspring1 + rules_offspring2['B']=r_offspring2 + + rules_parent1 = lsystem_parent1.rules["H"].split() + rules_parent2 = lsystem_parent2.rules["H"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='H': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='H': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['H']=r_offspring1 + rules_offspring2['H']=r_offspring2 + + rules_parent1 = lsystem_parent1.rules["N"].split() + rules_parent2 = lsystem_parent2.rules["N"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='N': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='N': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['N']=r_offspring1 + rules_offspring2['N']=r_offspring2 + + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations offspring1=LSystemDecoder(axiom_offspring1,rules_offspring1,iter_offspring1,lsystem_parent1.max_elements,lsystem_parent1.max_depth,lsystem_parent1.verbose) offspring2=LSystemDecoder(axiom_offspring2,rules_offspring2,iter_offspring2,lsystem_parent2.max_elements,lsystem_parent2.max_depth,lsystem_parent2.verbose) return offspring1,offspring2 @@ -213,6 +432,6 @@ def initialization_lsystem(max_elements=32,max_depth=8,add_temperature=0.5,none_ rules['B']=rule_string_B rules['H']=rule_string_H rules['N']=rule_string_N - iterations = random.choice(range(1,3)) + iterations = random.choice(range(2,4)) ls = LSystemDecoder(axiom,rules,iterations,max_elements=max_elements,max_depth=max_depth,verbose=verbose) return ls From 6dd4eb8105d60949a5a2ad581b921cbae33e8e57 Mon Sep 17 00:00:00 2001 From: Lukas Bierling Date: Mon, 27 Oct 2025 23:30:07 +0100 Subject: [PATCH 35/47] added morphological descriptor and example script for tree genome --- .gitignore | 6 + examples/.DS_Store | Bin 6148 -> 6148 bytes examples/tree_genome_vis.py | 477 ++++++++++++ .../robogen_lite/decoders/tree_decoder.py | 8 +- src/ariel/ec/genotypes/tree/__init__.py | 3 + src/ariel/ec/genotypes/tree/tree_genome.py | 2 +- src/ariel/utils/morphological_descriptor.py | 737 ++++++++++++++++++ 7 files changed, 1228 insertions(+), 5 deletions(-) create mode 100644 examples/tree_genome_vis.py create mode 100644 src/ariel/ec/genotypes/tree/__init__.py create mode 100644 src/ariel/utils/morphological_descriptor.py diff --git a/.gitignore b/.gitignore index 0dd47dc6..b1a9cccb 100644 --- a/.gitignore +++ b/.gitignore @@ -149,6 +149,9 @@ cython_debug/ ### VSCODE ###################################################################### .vscode .vscode/* +.idea/* +.idea +.idea* # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions @@ -263,3 +266,6 @@ RECYCLE.BIN/ *.msp # Windows shortcuts *.lnk + + +CLAUDE.md diff --git a/examples/.DS_Store b/examples/.DS_Store index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0b73d529939ac7af19d98371d81465116c9085eb 100644 GIT binary patch delta 126 zcmZoMXfc=|&e%3FQEZ}~q9`K+0|O8XFfgPt9 Kn;k{=GXnt4;2L}Y delta 67 zcmZoMXfc=|&Zs)EP;8=}A_oHyFfuR*Y}^>eKJh@*W_At%4o20D8^1G8<`+@q1WGX^ TfYeMj;Zfe4AhLvcVgm~RE=&+7 diff --git a/examples/tree_genome_vis.py b/examples/tree_genome_vis.py new file mode 100644 index 00000000..76aed5f6 --- /dev/null +++ b/examples/tree_genome_vis.py @@ -0,0 +1,477 @@ +"""Assignment 3 template code.""" + +# Standard library +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +import matplotlib.pyplot as plt +import mujoco as mj +import numpy as np +import numpy.typing as npt +from mujoco import viewer + +# Local libraries +from ariel import console +from ariel.body_phenotypes.robogen_lite.constructor import ( + construct_mjspec_from_graph, +) +from ariel.body_phenotypes.robogen_lite.decoders.hi_prob_decoding import ( + HighProbabilityDecoder, + save_graph_as_json, +) +from ariel.ec.genotypes.nde import NeuralDevelopmentalEncoding +from ariel.simulation.controllers.controller import Controller +from ariel.simulation.environments import OlympicArena +from ariel.utils.renderers import single_frame_renderer, video_renderer +from ariel.utils.runners import simple_runner +from ariel.utils.tracker import Tracker +from ariel.utils.video_recorder import VideoRecorder +from ariel import console +from ariel.body_phenotypes.robogen_lite.constructor import construct_mjspec_from_graph +from ariel.body_phenotypes.robogen_lite.decoders.tree_decoder import to_digraph +from ariel.ec.a000 import TreeGenerator +from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode +from ariel.body_phenotypes.robogen_lite import config +from ariel.simulation.controllers.controller import Controller +from ariel.simulation.environments import OlympicArena +from ariel.utils.renderers import single_frame_renderer, video_renderer +from ariel.utils.runners import simple_runner +from ariel.utils.tracker import Tracker +from ariel.utils.video_recorder import VideoRecorder +from ariel.utils.morphological_descriptor import MorphologicalMeasures + + +# Type Checking +if TYPE_CHECKING: + from networkx import DiGraph + +# Type Aliases +type ViewerTypes = Literal["launcher", "video", "simple", "no_control", "frame"] + +# --- RANDOM GENERATOR SETUP --- # +SEED = 42 +RNG = np.random.default_rng(SEED) + +# --- DATA SETUP --- +SCRIPT_NAME = __file__.split("/")[-1][:-3] +CWD = Path.cwd() +DATA = CWD / "__data__" / SCRIPT_NAME +DATA.mkdir(exist_ok=True) + +# Global variables +SPAWN_POS = [-0.8, 0, 0.1] +NUM_OF_MODULES = 30 +TARGET_POSITION = [5, 0, 0.5] + + +def fitness_function(history: list[float]) -> float: + xt, yt, zt = TARGET_POSITION + xc, yc, zc = history[-1] + + # Minimize the distance --> maximize the negative distance + cartesian_distance = np.sqrt( + (xt - xc) ** 2 + (yt - yc) ** 2 + (zt - zc) ** 2, + ) + return -cartesian_distance + + +def show_xpos_history(history: list[float]) -> None: + # Create a tracking camera + camera = mj.MjvCamera() + camera.type = mj.mjtCamera.mjCAMERA_FREE + camera.lookat = [2.5, 0, 0] + camera.distance = 10 + camera.azimuth = 0 + camera.elevation = -90 + + # Initialize world to get the background + mj.set_mjcb_control(None) + world = OlympicArena() + model = world.spec.compile() + data = mj.MjData(model) + save_path = str(DATA / "background.png") + single_frame_renderer( + model, + data, + camera=camera, + save_path=save_path, + save=True, + ) + + # Setup background image + img = plt.imread(save_path) + _, ax = plt.subplots() + ax.imshow(img) + w, h, _ = img.shape + + # Convert list of [x,y,z] positions to numpy array + pos_data = np.array(history) + + # Calculate initial position + x0, y0 = int(h * 0.483), int(w * 0.815) + xc, yc = int(h * 0.483), int(w * 0.9205) + ym0, ymc = 0, SPAWN_POS[0] + + # Convert position data to pixel coordinates + pixel_to_dist = -((ymc - ym0) / (yc - y0)) + pos_data_pixel = [[xc, yc]] + for i in range(len(pos_data) - 1): + xi, yi, _ = pos_data[i] + xj, yj, _ = pos_data[i + 1] + xd, yd = (xj - xi) / pixel_to_dist, (yj - yi) / pixel_to_dist + xn, yn = pos_data_pixel[i] + pos_data_pixel.append([xn + int(xd), yn + int(yd)]) + pos_data_pixel = np.array(pos_data_pixel) + + # Plot x,y trajectory + ax.plot(x0, y0, "kx", label="[0, 0, 0]") + ax.plot(xc, yc, "go", label="Start") + ax.plot(pos_data_pixel[:, 0], pos_data_pixel[:, 1], "b-", label="Path") + ax.plot(pos_data_pixel[-1, 0], pos_data_pixel[-1, 1], "ro", label="End") + + # Add labels and title + ax.set_xlabel("X Position") + ax.set_ylabel("Y Position") + ax.legend() + + # Title + plt.title("Robot Path in XY Plane") + + # Show results + plt.show() + +def create_custom_genome() -> TreeGenome: + """Create your custom tree genome here. + + Modify this function to define the robot structure you want to visualize. + You can use the TreeGenerator methods or manually build the tree. + """ + # Option 1: Use predefined generators + # return TreeGenerator.star_shape(num_arms=4) + # return TreeGenerator.binary_tree(depth=3) + # return TreeGenerator.random_tree(max_depth=3, branching_prob=0.6) + + return TreeGenerator.random_tree(max_depth=10) + + #return TreeGenome.default_init() + # Option 2: Build manually + genome = TreeGenome.default_init() # Starts with CORE module + + # Add a brick to the front + front_brick = TreeNode( + config.ModuleInstance( + type=config.ModuleType.BRICK, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + ) + ) + genome.root.front = front_brick + + # Add a hinge to the right of the core + right_hinge = TreeNode( + config.ModuleInstance( + type=config.ModuleType.HINGE, + rotation=config.ModuleRotationsIdx.DEG_90, + links={} + ) + ) + genome.root.right = right_hinge + + # Add another brick to the front of the right hinge + hinge_front_brick = TreeNode( + config.ModuleInstance( + type=config.ModuleType.BRICK, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + ) + ) + right_hinge.front = hinge_front_brick + + return genome + +def create_multi_limb_robot(): + # Build a complex robot manually + genome = TreeGenome.default_init() # Core + + # Add main branches from core + for face in [config.ModuleFaces.FRONT, config.ModuleFaces.BACK, + config.ModuleFaces.LEFT, config.ModuleFaces.RIGHT]: + # Add brick to each main direction + main_brick = TreeNode(config.ModuleInstance( + type=config.ModuleType.BRICK, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + )) + setattr(genome.root, face.name.lower(), main_brick) + + # Add sub-branches from each main brick + for sub_face in [config.ModuleFaces.FRONT, config.ModuleFaces.LEFT, config.ModuleFaces.RIGHT]: + if sub_face in config.ALLOWED_FACES[config.ModuleType.BRICK]: + # Add hinge for articulation + hinge = TreeNode(config.ModuleInstance( + type=config.ModuleType.HINGE, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + )) + try: + setattr(main_brick, sub_face.name.lower(), hinge) + + # Add end effector brick + end_brick = TreeNode(config.ModuleInstance( + type=config.ModuleType.BRICK, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + )) + hinge.front = end_brick + except ValueError: + # Face already occupied, skip + pass + return genome + +def create_max_limb_robot(): + print("\n" + "=" * 50) + print("Testing MAXIMUM LIMBS robot:") + + # Build a robot that maximizes the number of limbs + genome = TreeGenome.default_init() # Core + + # Core has 6 faces: FRONT, BACK, LEFT, RIGHT, TOP, BOTTOM + # Each can have a brick with 5 faces: FRONT, LEFT, RIGHT, TOP, BOTTOM + # Each brick face can have a hinge with 1 face: FRONT + # Each hinge can have a final brick (limb endpoint) + + core_faces = [config.ModuleFaces.FRONT, config.ModuleFaces.BACK, + config.ModuleFaces.LEFT, config.ModuleFaces.RIGHT, + config.ModuleFaces.TOP, config.ModuleFaces.BOTTOM] + + limb_count = 0 + + for core_face in core_faces: + # Add brick to each core face + main_brick = TreeNode(config.ModuleInstance( + type=config.ModuleType.BRICK, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + )) + setattr(genome.root, core_face.name.lower(), main_brick) + + # Each brick can have limbs on all its available faces + brick_faces = config.ALLOWED_FACES[config.ModuleType.BRICK] + + for brick_face in brick_faces: + # Add hinge for articulation (limb joint) + hinge = TreeNode(config.ModuleInstance( + type=config.ModuleType.HINGE, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + )) + + try: + setattr(main_brick, brick_face.name.lower(), hinge) + + # Add end effector brick (limb endpoint) + end_brick = TreeNode(config.ModuleInstance( + type=config.ModuleType.BRICK, + rotation=config.ModuleRotationsIdx.DEG_0, + links={} + )) + hinge.front = end_brick # Hinge only has FRONT face + limb_count += 1 + + except ValueError: + # Face already occupied, skip + pass + + return genome + +def nn_controller( + model: mj.MjModel, + data: mj.MjData, +) -> npt.NDArray[np.float64]: + # Simple 3-layer neural network + input_size = len(data.qpos) + hidden_size = 8 + output_size = model.nu + + # Initialize the networks weights randomly + # Normally, you would use the genes of an individual as the weights, + # Here we set them randomly for simplicity. + w1 = RNG.normal(loc=0.0138, scale=0.5, size=(input_size, hidden_size)) + w2 = RNG.normal(loc=0.0138, scale=0.5, size=(hidden_size, hidden_size)) + w3 = RNG.normal(loc=0.0138, scale=0.5, size=(hidden_size, output_size)) + + # Get inputs, in this case the positions of the actuator motors (hinges) + inputs = data.qpos + + # Run the inputs through the lays of the network. + layer1 = np.tanh(np.dot(inputs, w1)) + layer2 = np.tanh(np.dot(layer1, w2)) + outputs = np.tanh(np.dot(layer2, w3)) + + # Scale the outputs + return outputs * np.pi + + +def morph_descriptors(robot_graph: Any): + # Analyze morphology using the phenotype graph + measures = MorphologicalMeasures(robot_graph) + + print(f"\nMorphological measures:") + print(f" Number of modules: {measures.num_modules}") + print(f" Number of bricks: {measures.num_bricks}") + print(f" Number of active hinges: {measures.num_active_hinges}") + print(f" Bounding box: {measures.bounding_box_depth}x{measures.bounding_box_width}x{measures.bounding_box_height}") + print(f" Coverage: {measures.coverage:.3f}") + print(f" Branching: {measures.branching:.3f}") + print(f" Limbs: {measures.limbs:.3f}") + print(f" Length of limbs: {measures.length_of_limbs:.3f}") + print(f" Symmetry: {measures.symmetry:.3f}") + print(f" Joints: {measures.J: 3f}") + print(f" Is 2D: {measures.is_2d}") + print(f" Size: {measures.size:.3f}") + + +def simple_controller(model: mj.MjModel, data: mj.MjData) -> np.ndarray: + """Simple oscillating controller for robot movement.""" + time = data.time + frequency = 2.0 + amplitude = 0.8 + + controls = np.zeros(model.nu) + for i in range(model.nu): + phase_offset = i * np.pi / 4 # Different phase for each joint + controls[i] = amplitude * np.sin(frequency * time + phase_offset) + + return controls + + +def experiment( + robot: Any, + controller: Controller, + duration: int = 15, + mode: ViewerTypes = "viewer", +) -> None: + """Run the simulation with random movements.""" + # ==================================================================== # + # Initialise controller to controller to None, always in the beginning. + mj.set_mjcb_control(None) # DO NOT REMOVE + + # Initialise world + # Import environments from ariel.simulation.environments + world = OlympicArena() + + # Spawn robot in the world + # Check docstring for spawn conditions + world.spawn(robot.spec, spawn_position=SPAWN_POS) + + # Generate the model and data + # These are standard parts of the simulation USE THEM AS IS, DO NOT CHANGE + model = world.spec.compile() + data = mj.MjData(model) + + # Reset state and time of simulation + mj.mj_resetData(model, data) + + # Pass the model and data to the tracker + if controller.tracker is not None: + controller.tracker.setup(world.spec, data) + + # Set the control callback function + # This is called every time step to get the next action. + args: list[Any] = [] # IF YOU NEED MORE ARGUMENTS ADD THEM HERE! + kwargs: dict[Any, Any] = {} # IF YOU NEED MORE ARGUMENTS ADD THEM HERE! + + mj.set_mjcb_control( + lambda m, d: controller.set_control(m, d, *args, **kwargs), + ) + + # ------------------------------------------------------------------ # + match mode: + case "simple": + # This disables visualisation (fastest option) + simple_runner( + model, + data, + duration=duration, + ) + case "frame": + # Render a single frame (for debugging) + save_path = str(DATA / "robot.png") + single_frame_renderer(model, data, save=True, save_path=save_path) + case "video": + # This records a video of the simulation + path_to_video_folder = str(DATA / "videos") + video_recorder = VideoRecorder(output_folder=path_to_video_folder) + + # Render with video recorder + video_renderer( + model, + data, + duration=duration, + video_recorder=video_recorder, + ) + case "launcher": + # This opens a liver viewer of the simulation + viewer.launch( + model=model, + data=data, + ) + case "no_control": + # If mj.set_mjcb_control(None), you can control the limbs manually. + mj.set_mjcb_control(None) + viewer.launch( + model=model, + data=data, + ) + # ==================================================================== # + + +def main() -> None: + """Entry point.""" + # ? ------------------------------------------------------------------ # + + tree_genome = create_max_limb_robot() + tree_genome = TreeGenerator.binary_tree(10) + + robot_graph = to_digraph(tree_genome) + + morph_descriptors(robot_graph) + + # ? ------------------------------------------------------------------ # + # Save the graph to a file + save_graph_as_json( + robot_graph, + DATA / "robot_graph.json", + ) + + # ? ------------------------------------------------------------------ # + # Print all nodes + core = construct_mjspec_from_graph(robot_graph) + + # ? ------------------------------------------------------------------ # + mujoco_type_to_find = mj.mjtObj.mjOBJ_GEOM + name_to_bind = "core" + tracker = Tracker( + mujoco_obj_to_find=mujoco_type_to_find, + name_to_bind=name_to_bind, + ) + + # ? ------------------------------------------------------------------ # + # Simulate the robot + ctrl = Controller( + controller_callback_function=simple_controller, + # controller_callback_function=random_move, + tracker=tracker, + ) + + experiment(robot=core, controller=ctrl, mode="launcher") + + show_xpos_history(tracker.history["xpos"][0]) + + fitness = fitness_function(tracker.history["xpos"][0]) + msg = f"Fitness of generated robot: {fitness}" + console.log(msg) + + +if __name__ == "__main__": + main() diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py index a4ea4fc9..2f638d95 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py @@ -2,10 +2,10 @@ from typing import Optional, Dict, Callable import networkx as nx -from ariel.ec.genotypes.tree.tree_genome import TreeNode, TreeGenome -from ariel.body_phenotypes.robogen_lite import config +from ....ec.genotypes.tree.tree_genome import TreeNode, TreeGenome +from ...robogen_lite import config -def to_digraph(genome: TreeGenome, use_node_ids: bool = True) -> nx.DiGraph: +def to_digraph(genome: TreeGenome, use_node_ids: bool = False) -> nx.DiGraph: """ Convert this genome (rooted at `genome.root`) to a NetworkX directed graph. @@ -104,4 +104,4 @@ def test(): # Test code if __name__ == "__main__": - test() \ No newline at end of file + test() diff --git a/src/ariel/ec/genotypes/tree/__init__.py b/src/ariel/ec/genotypes/tree/__init__.py new file mode 100644 index 00000000..cca3fb9c --- /dev/null +++ b/src/ariel/ec/genotypes/tree/__init__.py @@ -0,0 +1,3 @@ +from .tree_genome import TreeGenome, TreeNode + +__all__ = ["TreeGenome", "TreeNode"] diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index 5ab916f9..b582db8f 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -19,7 +19,7 @@ def __init__(self, root: TreeNode | None = None): def default_init(cls, *args, **kwargs): """Default instantiation with a core root.""" return cls(root=TreeNode(config.ModuleInstance(type=config.ModuleType.CORE, - rotation=config.ModuleRotationsIdx.DEG_90, + rotation=config.ModuleRotationsIdx.DEG_0, links={}))) @property diff --git a/src/ariel/utils/morphological_descriptor.py b/src/ariel/utils/morphological_descriptor.py new file mode 100644 index 00000000..0a25cab4 --- /dev/null +++ b/src/ariel/utils/morphological_descriptor.py @@ -0,0 +1,737 @@ +""" +MorphologicalMeasures class for robot phenotype digraph analysis. +Mostly based on the revolve implementation: https://github.com/ci-group/revolve2/blob/master/standards/revolve2/standards/morphological_measures.py +""" + +from itertools import product +from typing import Generic, TypeVar, Any + +import numpy as np +from numpy.typing import NDArray +import networkx as nx + +import ariel.body_phenotypes.robogen_lite.config as config + +TModule = TypeVar("TModule", bound=np.generic) + + +class MorphologicalMeasures(Generic[TModule]): + """ + Modular robot morphological measures for robot phenotype digraph. + + Works with a NetworkX directed graph representation of a robot. + Only works for robot with only right angle module rotations (90 degrees). + Some measures only work for 2d robots, which is noted in their docstring. + + The measures are based on the following paper: + Miras, K., Haasdijk, E., Glette, K., Eiben, A.E. (2018). + Search Space Analysis of Evolvable Robot Morphologies. + In: Sim, K., Kaufmann, P. (eds) Applications of Evolutionary Computation. + EvoApplications 2018. Lecture Notes in Computer Science(), vol 10784. Springer, Cham. + https://doi.org/10.1007/978-3-319-77538-8_47 + """ + + """Represents the modules of a body in a 3D tensor.""" + grid: NDArray[TModule] + symmetry_grid: NDArray[TModule] + """Position of the core in 'body_as_grid'.""" + core_grid_position: np.ndarray + + """If the robot is two dimensional, i.e. all module rotations are 0 degrees.""" + is_2d: bool + + """The robot graph structure.""" + graph: nx.DiGraph + core_node: Any + modules: list[Any] + bricks: list[Any] + active_hinges: list[Any] + + """If all slots of the core are filled with other modules.""" + core_is_filled: bool + + """Bricks which have all slots filled with other modules.""" + filled_bricks: list[Any] + + """Active hinges which have all slots filled with other modules.""" + filled_active_hinges: list[Any] + + """ + Modules that only connect to one other module. + + This includes children and parents. + """ + single_neighbour_modules: list[Any] + + """ + Bricks that are only connected to one other module. + + Both children and parent are counted. + """ + single_neighbour_bricks: list[Any] + + """ + Bricks that are connected to exactly two other modules. + + Both children and parent are counted. + """ + double_neighbour_bricks: list[Any] + + """ + Active hinges that are connected to exactly two other modules. + + Both children and parent are counted. + """ + double_neighbour_active_hinges: list[Any] + + """ + X/Y-plane symmetry according to the paper but in 3D. + + X-axis is defined as forward/backward for the core module + Y-axis is defined as left/right for the core module. + """ + xy_symmetry: float + + """ + X/Z-plane symmetry according to the paper but in 3D. + + X-axis is defined as forward/backward for the core module + Z-axis is defined as up/down for the core module. + """ + xz_symmetry: float + + """ + Y/Z-plane symmetry according to the paper but in 3D. + + Y-axis is defined as left/right for the core module. + Z-axis is defined as up/down for the core module. + """ + yz_symmetry: float + + def __init__(self, robot_graph: nx.DiGraph) -> None: + """ + Initialize this object. + + :param robot_graph: The NetworkX directed graph representing the robot phenotype. + Expected to have node attributes 'type' and 'rotation'. + Expected to have edge attributes 'face'. + """ + if robot_graph.number_of_nodes() == 0: + raise ValueError("Cannot analyze empty robot graph") + + self.graph = robot_graph + self.grid, self.core_grid_position = self._graph_to_grid(robot_graph) + self.core_node = self._find_core_node() + self.is_2d = self._calculate_is_2d() + self.modules = list(robot_graph.nodes()) + self.bricks = self._get_nodes_by_type("BRICK") + self.active_hinges = self._get_nodes_by_type("HINGE") + self.core_is_filled = self._calculate_core_is_filled() + self.filled_bricks = self._calculate_filled_bricks() + self.filled_active_hinges = self._calculate_filled_active_hinges() + self.single_neighbour_bricks = self._calculate_single_neighbour_bricks() + self.single_neighbour_modules = self._calculate_single_neighbour_modules() + self.double_neighbour_bricks = self._calculate_double_neighbour_bricks() + self.double_neighbour_active_hinges = ( + self._calculate_double_neighbour_active_hinges() + ) + + self._pad_grid() + self.xy_symmetry = self._calculate_xy_symmetry() + self.xz_symmetry = self._calculate_xz_symmetry() + self.yz_symmetry = self._calculate_yz_symmetry() + + def _find_core_node(self) -> Any: + """Find the core node (root of the tree) in the graph.""" + # Find node with no predecessors (root) + roots = [node for node in self.graph.nodes() if self.graph.in_degree(node) == 0] + if len(roots) != 1: + raise ValueError(f"Expected exactly one root node, found {len(roots)}") + return roots[0] + + def _get_nodes_by_type(self, module_type: str) -> list[Any]: + """Get all nodes of a specific module type.""" + return [node for node in self.graph.nodes() + if self.graph.nodes[node].get('type') == module_type] + + def _calculate_is_2d(self) -> bool: + """Check if all modules use only 90-degree rotations.""" + valid_rotations = {"DEG_0", "DEG_90", "DEG_180", "DEG_270"} + return all( + self.graph.nodes[node].get('rotation', 'DEG_0') in valid_rotations + for node in self.graph.nodes() + ) + + def _get_node_type(self, node: Any) -> str: + """Get the module type of a node.""" + return self.graph.nodes[node].get('type', 'UNKNOWN') + + def _get_allowed_faces(self, node: Any) -> list[str]: + """Get allowed faces for a node based on its type.""" + module_type = self._get_node_type(node) + if module_type == "CORE": + return ["FRONT", "BACK", "RIGHT", "LEFT", "TOP", "BOTTOM"] + elif module_type == "BRICK": + return ["FRONT", "RIGHT", "LEFT", "TOP", "BOTTOM"] + elif module_type == "HINGE": + return ["FRONT"] + else: + return [] + + def _get_node_connections(self, node: Any) -> list[str]: + """Get the faces that are connected for a node.""" + connected_faces = [] + # Check outgoing edges (children) + for successor in self.graph.successors(node): + edge_data = self.graph.get_edge_data(node, successor) + if edge_data and 'face' in edge_data: + connected_faces.append(edge_data['face']) + return connected_faces + + def _count_neighbors(self, node: Any) -> int: + """Count total neighbors (predecessors + successors).""" + return self.graph.in_degree(node) + self.graph.out_degree(node) + + def _graph_to_grid(self, robot_graph: nx.DiGraph) -> tuple[NDArray[TModule], np.ndarray]: + """Convert robot graph to 3D grid representation.""" + if robot_graph.number_of_nodes() == 0: + raise ValueError("Cannot convert empty robot graph to grid") + + # Calculate positions of all nodes relative to core + positions = {} + core_node = self._find_core_node() + self._calculate_graph_positions(core_node, positions, np.array([0, 0, 0])) + + # Find bounds + if not positions: + # Single core only + positions[core_node] = np.array([0, 0, 0]) + + pos_array = np.array(list(positions.values())) + min_pos = pos_array.min(axis=0) + max_pos = pos_array.max(axis=0) + + # Create grid with proper size + grid_size = max_pos - min_pos + 1 + grid = np.full(grid_size, None, dtype=object) + + # Place nodes in grid + core_pos = positions[core_node] - min_pos + for node in robot_graph.nodes(): + node_pos = positions[node] - min_pos + grid[tuple(node_pos)] = node + + return grid, core_pos + + def _calculate_graph_positions(self, node: Any, positions: dict, pos: np.ndarray) -> None: + """Recursively calculate 3D positions of all nodes in the graph.""" + positions[node] = pos.copy() + + # Define face direction vectors (assuming standard orientation) + face_directions = { + "FRONT": np.array([1, 0, 0]), + "BACK": np.array([-1, 0, 0]), + "RIGHT": np.array([0, 1, 0]), + "LEFT": np.array([0, -1, 0]), + "TOP": np.array([0, 0, 1]), + "BOTTOM": np.array([0, 0, -1]), + } + + # Process children (successors in the graph) + for child in self.graph.successors(node): + edge_data = self.graph.get_edge_data(node, child) + if edge_data and 'face' in edge_data: + face = edge_data['face'] + if face in face_directions: + child_pos = pos + face_directions[face] + if child not in positions: # Avoid cycles + self._calculate_graph_positions(child, positions, child_pos) + + def _calculate_core_is_filled(self) -> bool: + """Check if the core has all its allowed faces filled.""" + allowed_faces = self._get_allowed_faces(self.core_node) + connected_faces = self._get_node_connections(self.core_node) + return len(connected_faces) == len(allowed_faces) + + def _calculate_filled_bricks(self) -> list[Any]: + """Get bricks that have all their allowed faces filled.""" + return [ + brick + for brick in self.bricks + if len(self._get_node_connections(brick)) == len(self._get_allowed_faces(brick)) + ] + + def _calculate_filled_active_hinges(self) -> list[Any]: + """Get active hinges that have all their allowed faces filled.""" + return [ + hinge + for hinge in self.active_hinges + if len(self._get_node_connections(hinge)) == len(self._get_allowed_faces(hinge)) + ] + + def _calculate_single_neighbour_bricks(self) -> list[Any]: + """Get bricks that have no children (leaf nodes).""" + return [ + brick + for brick in self.bricks + if self.graph.out_degree(brick) == 0 + ] + + def _calculate_single_neighbour_modules(self) -> list[Any]: + """Get non-core modules that have only one neighbor (leaf nodes).""" + non_core_modules = [node for node in self.modules if self._get_node_type(node) != "CORE"] + return [ + module + for module in non_core_modules + if self._count_neighbors(module) == 1 + ] + + def _calculate_double_neighbour_bricks(self) -> list[Any]: + """Get bricks that have exactly one child (connecting two modules).""" + return [ + brick + for brick in self.bricks + if self.graph.out_degree(brick) == 1 + ] + + def _calculate_double_neighbour_active_hinges(self) -> list[Any]: + """Get active hinges that have exactly one child (connecting two modules).""" + return [ + hinge + for hinge in self.active_hinges + if self.graph.out_degree(hinge) == 1 + ] + + def _pad_grid(self) -> None: + x, y, z = self.grid.shape + xoffs, yoffs, zoffs = self.core_grid_position + self.symmetry_grid = np.empty( + shape=(x + xoffs, y + yoffs, z + zoffs), dtype=object + ) + self.symmetry_grid.fill(None) + self.symmetry_grid[:x, :y, :z] = self.grid + + def _calculate_xy_symmetry(self) -> float: + """Calculate XY-plane symmetry.""" + num_along_plane = 0 + num_symmetrical = 0 + for x, y, z in product( + range(self.bounding_box_depth), + range(self.bounding_box_width), + range(1, (self.bounding_box_height - 1) // 2), + ): + if self.symmetry_grid[x, y, self.core_grid_position[2]] is not None: + num_along_plane += 1 + pos_z = self.symmetry_grid[x, y, self.core_grid_position[2] + z] + neg_z = self.symmetry_grid[x, y, self.core_grid_position[2] - z] + if pos_z is not None and neg_z is not None: + # Check if module types match + if self._get_node_type(pos_z) == self._get_node_type(neg_z): + num_symmetrical += 2 + + difference = self.num_modules - num_along_plane + return num_symmetrical / difference if difference > 0.0 else 0.0 + + def _calculate_xz_symmetry(self) -> float: + """Calculate XZ-plane symmetry.""" + num_along_plane = 0 + num_symmetrical = 0 + for x, y, z in product( + range(self.bounding_box_depth), + range(1, (self.bounding_box_width - 1) // 2), + range(self.bounding_box_height), + ): + if self.symmetry_grid[x, self.core_grid_position[1], z] is not None: + num_along_plane += 1 + pos_y = self.symmetry_grid[x, self.core_grid_position[1] + y, z] + neg_y = self.symmetry_grid[x, self.core_grid_position[1] - y, z] + if pos_y is not None and neg_y is not None: + # Check if module types match + if self._get_node_type(pos_y) == self._get_node_type(neg_y): + num_symmetrical += 2 + difference = self.num_modules - num_along_plane + return num_symmetrical / difference if difference > 0.0 else 0.0 + + def _calculate_yz_symmetry(self) -> float: + """Calculate YZ-plane symmetry.""" + num_along_plane = 0 + num_symmetrical = 0 + for x, y, z in product( + range(1, (self.bounding_box_depth - 1) // 2), + range(self.bounding_box_width), + range(self.bounding_box_height), + ): + if self.symmetry_grid[self.core_grid_position[0], y, z] is not None: + num_along_plane += 1 + pos_x = self.symmetry_grid[self.core_grid_position[0] + x, y, z] + neg_x = self.symmetry_grid[self.core_grid_position[0] - x, y, z] + if pos_x is not None and neg_x is not None: + # Check if module types match + if self._get_node_type(pos_x) == self._get_node_type(neg_x): + num_symmetrical += 2 + difference = self.num_modules - num_along_plane + return num_symmetrical / difference if difference > 0.0 else 0.0 + + @property + def bounding_box_depth(self) -> int: + """ + Get the depth of the bounding box around the body. + + Forward/backward axis for the core module. + + :returns: The depth. + """ + return self.grid.shape[0] + + @property + def bounding_box_width(self) -> int: + """ + Get the width of the bounding box around the body. + + Right/left axis for the core module. + + :returns: The width. + """ + return self.grid.shape[1] + + @property + def bounding_box_height(self) -> int: + """ + Get the height of the bounding box around the body. + + Up/down axis for the core module. + + :returns: The height. + """ + return self.grid.shape[2] + + @property + def num_modules(self) -> int: + """ + Get the number of modules. + + :returns: The number of modules. + """ + return len(self.modules) + + @property + def num_bricks(self) -> int: + """ + Get the number of bricks. + + :returns: The number of bricks. + """ + return len(self.bricks) + + @property + def num_active_hinges(self) -> int: + """ + Get the number of active hinges. + + :returns: The number of active hinges. + """ + return len(self.active_hinges) + + @property + def num_filled_bricks(self) -> int: + """ + Get the number of bricks which have all slots filled with other modules. + + :returns: The number of bricks. + """ + return len(self.filled_bricks) + + @property + def num_filled_active_hinges(self) -> int: + """ + Get the number of bricks which have all slots filled with other modules. + + :returns: The number of bricks. + """ + return len(self.filled_active_hinges) + + @property + def num_filled_modules(self) -> int: + """ + Get the number of modules which have all slots filled with other modules, including the core. + + :returns: The number of modules. + """ + return ( + self.num_filled_bricks + + self.num_filled_active_hinges + + (1 if self.core_is_filled else 0) + ) + + @property + def max_potentionally_filled_core_and_bricks(self) -> int: + """ + Get the maximum number of core and bricks that could potentially be filled with this set of modules if rearranged in an optimal way. + + This calculates 'b_max' from the paper. + + :returns: The calculated number. + """ + pot_max_filled = max(0, (self.num_modules - 2) // 3) + pot_max_filled = min(pot_max_filled, 1 + self.num_bricks) + return pot_max_filled + + @property + def filled_core_and_bricks_proportion(self) -> float: + """ + Get the ratio between filled cores and bricks and how many that potentially could have been if this set of modules was rearranged in an optimal way. + + This calculates 'branching' from the paper. + + :returns: The proportion. + """ + if self.max_potentionally_filled_core_and_bricks == 0: + return 0.0 + + return ( + len(self.filled_bricks) + (1 if self.core_is_filled else 0) + ) / self.max_potentionally_filled_core_and_bricks + + @property + def num_single_neighbour_modules(self) -> int: + """ + Get the number of bricks that are only connected to one other module. + + Both children and parent are counted. + + :returns: The number of bricks. + """ + return len(self.single_neighbour_modules) + + @property + def max_potential_single_neighbour_modules(self) -> int: + """ + Get the maximum number of bricks that could potentially have only one neighbour if this set of modules was rearranged in an optimal way. + + This calculates "l_max" from the paper. + + :returns: The calculated number. + """ + return self.num_modules - 1 - max(0, (self.num_modules - 3) // 3) + + @property + def num_double_neighbour_bricks(self) -> int: + """ + Get the number of bricks that are connected to exactly two other modules. + + Both children and parent are counted. + + :returns: The number of bricks. + """ + return len(self.double_neighbour_bricks) + + @property + def num_double_neighbour_active_hinges(self) -> int: + """ + Get the number of active hinges that are connected to exactly two other modules. + + Both children and parent are counted. + + :returns: The number of active hinges. + """ + return len(self.double_neighbour_active_hinges) + + @property + def potential_double_neighbour_bricks_and_active_hinges(self) -> int: + """ + Get the maximum number of bricks and active hinges that could potentially have exactly two neighbours if this set of modules was rearranged in an optimal way. + + This calculates e_max from the paper. + + :returns: The calculated number. + """ + return max(0, self.num_bricks + self.num_active_hinges - 1) + + @property + def double_neighbour_brick_and_active_hinge_proportion(self) -> float: + """ + Get the ratio between the number of bricks and active hinges with exactly two neighbours and how many that could potentially have been if this set of modules was rearranged in an optimal way. + + This calculate length of limbs proportion(extensiveness) from the paper. + + :returns: The proportion. + """ + if self.potential_double_neighbour_bricks_and_active_hinges == 0: + return 0.0 + + return ( + self.num_double_neighbour_bricks + self.num_double_neighbour_active_hinges + ) / self.potential_double_neighbour_bricks_and_active_hinges + + @property + def bounding_box_volume(self) -> int: + """ + Get the volume of the bounding box. + + This calculates m_area from the paper. + + :returns: The volume. + """ + return ( + self.bounding_box_width * self.bounding_box_height * self.bounding_box_depth + ) + + @property + def bounding_box_volume_coverage(self) -> float: + """ + Get the proportion of the bounding box that is filled with modules. + + This calculates 'coverage' from the paper. + + :returns: The proportion. + """ + return self.num_modules / self.bounding_box_volume + + @property + def branching(self) -> float: + """ + Get the 'branching' measurement from the paper. + + Alias for filled_core_and_bricks_proportion. + + :returns: Branching measurement. + """ + return self.filled_core_and_bricks_proportion + + @property + def limbs(self) -> float: + """ + Get the 'limbs' measurement from the paper. + + Alias for single_neighbour_brick_proportion. + + :returns: Limbs measurement. + """ + if self.max_potential_single_neighbour_modules == 0: + return 0.0 + return ( + self.num_single_neighbour_modules + / self.max_potential_single_neighbour_modules + ) + + @property + def length_of_limbs(self) -> float: + """ + Get the 'length of limbs' measurement from the paper. + + Alias for double_neighbour_brick_and_active_hinge_proportion. + + :returns: Length of limbs measurement. + """ + return self.double_neighbour_brick_and_active_hinge_proportion + + @property + def coverage(self) -> float: + """ + Get the 'coverage' measurement from the paper. + + Alias for bounding_box_volume_coverage. + + :returns: Coverage measurement. + """ + return self.bounding_box_volume_coverage + + @property + def proportion_2d(self) -> float: + """ + Get the 'proportion' measurement from the paper. + + Only for 2d robots. + + :returns: Proportion measurement. + """ + assert self.is_2d + + return min(self.bounding_box_depth, self.bounding_box_width) / max( + self.bounding_box_depth, self.bounding_box_width + ) + + @property + def symmetry(self) -> float: + """ + Get the 'symmetry' measurement from the paper, but extended to 3d. + + :returns: Symmetry measurement. + """ + return max(self.xy_symmetry, self.xz_symmetry, self.yz_symmetry) + + @property + def num_joints(self) -> int: + """Number of joints (active hinges).""" + return self.num_active_hinges + + @property + def max_potential_joints(self) -> int: + """Maximum possible joints (if every connection were a hinge).""" + return max(0, self.num_modules - 1) + + @property + def joints(self) -> float: + """Get the 'number of joints' measurement J = j / j_max.""" + if self.max_potential_joints == 0: + return 0.0 + return self.num_joints / self.max_potential_joints + + @property + def size(self) -> float: + #TODO check if m_max is fine like this!! + """Size S = m / m_max (proportion of occupied volume). + + m = number of modules + m_max = bounding box volume (max possible occupancy) + + Equivalent to 'coverage' in Miras et al. (2018) if volume is used as reference. + """ + if self.bounding_box_volume == 0: + return 0.0 + return self.num_modules / self.bounding_box_volume + + @property + def proportion(self) -> float: + """Proportion P = p_s / p_l (only valid for 2D morphologies).""" + return self.proportion_2d + + @property + def B(self) -> float: + """Branching B = b / b_max.""" + return self.branching + + @property + def L(self) -> float: + """Length of limbs L = e / e_max.""" + return self.limbs + + @property + def S(self) -> float: + """Symmetry S = s.""" + return self.symmetry + + @property + def C(self) -> float: + """Coverage C = c.""" + return self.coverage + + @property + def J(self) -> float: + """Joints J = j / j_max.""" + return self.joints + + @property + def E(self) -> float: + """Extensiveness E = e / e_max.""" + return self.length_of_limbs + + @property + def J(self): + """Joints J = j / j_max.""" + return self.joints + + @property + def S(self): + """Symmetry S = s.""" + return self.size From 0818e2d33ae3d206047c75e80f547c706f41f700 Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 29 Oct 2025 00:05:39 +0100 Subject: [PATCH 36/47] Untested evolution script --- examples/config.toml | 55 ++++++ examples/evolve.py | 214 +++++++++++++++++++++ src/ariel/ec/a000.py | 78 +++++++- src/ariel/ec/a005.py | 78 ++++++-- src/ariel/ec/genotypes/genotype.py | 29 +++ src/ariel/ec/genotypes/tree/tree_genome.py | 17 ++ 6 files changed, 451 insertions(+), 20 deletions(-) create mode 100644 examples/config.toml create mode 100644 examples/evolve.py create mode 100644 src/ariel/ec/genotypes/genotype.py diff --git a/examples/config.toml b/examples/config.toml new file mode 100644 index 00000000..41a0d892 --- /dev/null +++ b/examples/config.toml @@ -0,0 +1,55 @@ +# ========================= +# Evolutionary Algorithm — config.toml +# ========================= + +[run] +# Choose which genotype profile to use for this run. +# One of: "integers", "tree", "lsystem", "cppn" +genotype = "tree" + +# (Optional) Override the selected profile's default operators here. +# If omitted, the defaults under [genotypes..defaults] are used. +# mutation = "swap_mutation" +# crossover = "pmx" + +# ========================= +# Global EC settings (match EASettings fields) +# ========================= +[ec] +quiet = false +is_maximisation = true +first_generation_id = 0 +num_of_generations = 100 +target_population_size = 100 + +# ========================= +# Data config +# ========================= +[data] +# Paths can be absolute or relative to the working directory +output_folder = "__data__" +db_file_name = "database.db" +# db_handling modes: "delete" | "reuse" | "migrate" (adjust to your app’s enum) +db_handling = "delete" + +# ========================= +# Genotype registry +# Each genotype declares: +# - allowed_mutations / allowed_crossovers: for validation/help +# - defaults: operators used if not overridden in [run] +# - (Optional) operator params (e.g., rates, alphas) +# ========================= + +[genotypes.tree] +allowed_mutations = ["random_subtree_replacement"] +allowed_crossovers = ["koza_default", "normal"] + +[genotypes.tree.defaults] +mutation = "random_subtree_replacement" +crossover = "koza_default" + +[genotypes.tree.params] +max_depth = 8 +mutation_rate = 0.1 +crossover_rate = 0.9 + diff --git a/examples/evolve.py b/examples/evolve.py new file mode 100644 index 00000000..0cd120ef --- /dev/null +++ b/examples/evolve.py @@ -0,0 +1,214 @@ +# Standard library +import random +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +import tomllib +from typing import Literal, cast +from argparse import ArgumentParser + +# Third-party libraries +import numpy as np +from pydantic_settings import BaseSettings +from rich.console import Console +from rich.progress import track +from rich.traceback import install +from sqlalchemy import create_engine +from sqlmodel import Session, SQLModel, col, select + +# Local libraries +from ariel.ec.a000 import IntegerMutator, IntegersGenerator, Mutation, TreeMutator +from ariel.ec.a001 import Individual +from ariel.ec.a004 import EAStep, EA +from ariel.ec.a005 import Crossover, IntegerCrossover, TreeCrossover +from ariel.ec.genotypes.genotype import GenotypeEnum + +# Global constants +SEED = 42 +DB_HANDLING_MODES = Literal["delete", "halt"] + +# Global functions +install() +console = Console() +RNG = np.random.default_rng(SEED) + +# Type Aliases +type Population = list[Individual] +type PopulationFunc = Callable[[Population], Population] + +class EASettings(BaseSettings): + quiet: bool = False + + # EC mechanisms + is_maximisation: bool = True + first_generation_id: int = 0 + num_of_generations: int = 100 + target_population_size: int = 100 + genotype: GenotypeEnum + mutation: Mutation + crossover: Crossover + + # Data config + output_folder: Path = Path.cwd() / "__data__" + db_file_name: str = "database.db" + db_file_path: Path = output_folder / db_file_name + db_handling: DB_HANDLING_MODES = "delete" + +# ------------------------ EA STEPS ------------------------ # +def parent_selection(population: Population, config: EASettings) -> Population: + random.shuffle(population) + for idx in range(0, len(population) - 1, 2): + ind_i = population[idx] + ind_j = population[idx + 1] + + # Compare fitness values + if ind_i.fitness > ind_j.fitness and config.is_maximisation: + ind_i.tags = {"ps": True} + ind_j.tags = {"ps": False} + else: + ind_i.tags = {"ps": False} + ind_j.tags = {"ps": True} + return population + + +def crossover(population: Population, config: EASettings) -> Population: + parents = [ind for ind in population if ind.tags.get("ps", False)] + for idx in range(0, len(parents), 2): + parent_i = parents[idx] + parent_j = parents[idx] + genotype_i, genotype_j = config.crossover( + parent_i.genotype, + parent_j.genotype, + ) + + # First child + child_i = Individual() + child_i.genotype = genotype_i + child_i.tags = {"mut": True} + child_i.requires_eval = True + + # Second child + child_j = Individual() + child_j.genotype = genotype_j + child_j.tags = {"mut": True} + child_j.requires_eval = True + + population.extend([child_i, child_j]) + return population + + +def mutation(population: Population, config: EASettings) -> Population: + for ind in population: + if ind.tags.get("mut", False): + genes = ind.genotype + mutated = config.mutation( + individual=genes, + span=1, + mutation_probability=0.5, + ) + ind.genotype = mutated + ind.requires_eval = True + return population + + +def evaluate(population: Population) -> Population: + pass + + +def survivor_selection(population: Population, config: EASettings) -> Population: + random.shuffle(population) + current_pop_size = len(population) + for idx in range(len(population)): + ind_i = population[idx] + ind_j = population[idx + 1] + + # Kill worse individual + if ind_i.fitness > ind_j.fitness and config.is_maximisation: + ind_j.alive = False + else: + ind_i.alive = False + + # Termination condition + current_pop_size -= 1 + if current_pop_size <= config.target_population_size: + break + return population + + +def create_individual(config: EASettings) -> Individual: + ind = Individual() + ind.genotype = config.genotype.value.create_individual() + return ind + +def read_config_file() -> EASettings: + cfg = tomllib.loads(Path("config.toml").read_text()) + + # Resolve the active operators from the chosen genotype profile + gname = cfg["run"]["genotype"] + gblock = cfg["genotypes"][gname] + mutation_name = cfg["run"].get("mutation", gblock["defaults"]["mutation"]) + crossover_name = cfg["run"].get("crossover", gblock["defaults"]["crossover"]) + + if gname == 'tree': + genotype = GenotypeEnum.TREE + mutation = TreeMutator() + mutation.which_mutation = mutation_name + crossover = TreeCrossover() + crossover.which_crossover = crossover_name + elif gname == 'integers': + pass + + settings = EASettings( + quiet=cfg["ec"]["quiet"], + is_maximisation=cfg["ec"]["is_maximisation"], + first_generation_id=cfg["ec"]["first_generation_id"], + num_of_generations=cfg["ec"]["num_of_generations"], + target_population_size=cfg["ec"]["target_population_size"], + genotype=genotype, + mutation=mutation, + crossover=crossover, + output_folder=Path(cfg["data"]["output_folder"]), + db_file_name=cfg["data"]["db_file_name"], + db_handling=cfg["data"]["db_handling"], + db_file_path=Path(cfg["data"]["output_folder"]) / cfg["data"]["db_file_name"], + ) + return settings + + +def main() -> None: + """Entry point.""" + config = read_config_file() + # Create initial population + population_list = [create_individual(config) for _ in range(10)] + population_list = evaluate(population_list) + + # Create EA steps + ops = [ + EAStep("parent_selection", parent_selection), + EAStep("crossover", crossover), + EAStep("mutation", mutation), + EAStep("evaluation", evaluate), + EAStep("survivor_selection", survivor_selection), + ] + + # Initialize EA + ea = EA( + population_list, + operations=ops, + num_of_generations=100, + ) + + ea.run() + + best = ea.get_solution(only_alive=False) + console.log(best) + + median = ea.get_solution("median", only_alive=False) + console.log(median) + + worst = ea.get_solution("worst", only_alive=False) + console.log(worst) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 11404c45..7e0695a6 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -1,6 +1,7 @@ """TODO(jmdm): description of script.""" # Standard library +from abc import ABC, abstractmethod from collections.abc import Sequence from pathlib import Path from typing import cast @@ -11,6 +12,7 @@ from rich.console import Console from rich.traceback import install import copy +from ariel.ec.genotypes.genotype import Genotype from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode import ariel.body_phenotypes.robogen_lite.config as pheno_config @@ -84,15 +86,59 @@ def choice( ) return cast("Integers", generated_values.astype(int).tolist()) - -class IntegerMutator: +class Mutation(ABC): + which_mutation: str = "" + + @abstractmethod + @classmethod + def __call__( + cls, + individual: Genotype, + **kwargs: dict, + ) -> Genotype: + """Perform crossover on two genotypes. + + Parameters + ---------- + parent_i : Genotype + The first parent genotype (list or nested list of integers). + parent_j : Genotype + The second parent genotype (list or nested list of integers). + + Returns + ------- + tuple[Genotype, Genotype] + Two child genotypes resulting from the crossover. + """ + pass + +class IntegerMutator(Mutation): + @classmethod + def __call__( + cls, + individual: Genotype, + **kwargs: dict, + ) -> Genotype: + if cls.which_mutation == "random_swap": + return cls.random_swap( + individual=individual, + **kwargs, + ) + elif cls.which_mutation == "integer_creep": + return cls.integer_creep( + individual=individual, + **kwargs, + ) + else: + raise ValueError(f"Unknown mutation type: {cls.which_mutation}") + @staticmethod def random_swap( - individual: Integers, + individual: Genotype, low: int, high: int, mutation_probability: float, - ) -> Integers: + ) -> Genotype: shape = np.asarray(individual).shape mutator = RNG.integers( low=low, @@ -110,10 +156,10 @@ def random_swap( @staticmethod def integer_creep( - individual: Integers, + individual: Genotype, span: int, mutation_probability: float, - ) -> Integers: + ) -> Genotype: # Prep ind_arr = np.array(individual) shape = ind_arr.shape @@ -242,13 +288,27 @@ def random_tree(max_depth: int = 4, branching_prob: float = 0.7) -> TreeGenome: genome.root._set_face(face, subtree) return genome -class TreeMutator: +class TreeMutator(Mutation): + @classmethod + def __call__( + cls, + individual: Genotype, + **kwargs: dict, + ) -> Genotype: + if cls.which_mutation == "random_subtree_replacement": + return cls.random_subtree_replacement( + individual=individual, + **kwargs, + ) + else: + raise ValueError(f"Unknown mutation type: {cls.which_mutation}") + @staticmethod def random_subtree_replacement( - individual: TreeGenome, + individual: Genotype, max_subtree_depth: int = 1, branching_prob: float = 0.5, - ) -> TreeGenome: + ) -> Genotype: """Replace a random subtree with a new random subtree.""" if individual.root is None: return individual diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index b9da75ab..4f1eea7d 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -1,6 +1,7 @@ """TODO(jmdm): description of script.""" # Standard library +from abc import ABC, abstractmethod from pathlib import Path # Third-party libraries @@ -9,6 +10,7 @@ from rich.traceback import install # Local libraries +from ariel.ec.genotypes.genotype import Genotype from ariel.ec.genotypes.tree.tree_genome import TreeGenome from ariel.ec.a000 import IntegersGenerator from ariel.ec.a001 import JSONIterable @@ -27,13 +29,52 @@ console = Console() RNG = np.random.default_rng(SEED) +class Crossover(ABC): + which_crossover: str = "" + + @abstractmethod + @classmethod + def __call__( + cls, + parent_i: Genotype, + parent_j: Genotype, + **kwargs: dict, + ) -> tuple[Genotype, Genotype]: + """Perform crossover on two genotypes. + + Parameters + ---------- + parent_i : Genotype + The first parent genotype (list or nested list of integers). + parent_j : Genotype + The second parent genotype (list or nested list of integers). + + Returns + ------- + tuple[Genotype, Genotype] + Two child genotypes resulting from the crossover. + """ + pass + +class IntegerCrossover(Crossover): + @classmethod + def __call__( + cls, + parent_i: Genotype, + parent_j: Genotype, + **kwargs: dict, + ) -> tuple[Genotype, Genotype]: + if cls.which_crossover == "one_point": + return cls.one_point(parent_i, parent_j, **kwargs) + else: + msg = f"Crossover type '{cls.which_crossover}' not recognized." + raise ValueError(msg) -class Crossover: @staticmethod def one_point( - parent_i: JSONIterable, - parent_j: JSONIterable, - ) -> tuple[JSONIterable, JSONIterable]: + parent_i: Genotype, + parent_j: Genotype, + ) -> tuple[Genotype, Genotype]: # Prep parent_i_arr_shape = np.array(parent_i).shape parent_j_arr_shape = np.array(parent_j).shape @@ -61,13 +102,28 @@ def one_point( child2 = child2.reshape(parent_j_arr_shape).astype(int).tolist() return child1, child2 -class TreeCrossover: +class TreeCrossover(Crossover): + @classmethod + def __call__( + cls, + parent_i: Genotype, + parent_j: Genotype, + **kwargs: dict, + ) -> tuple[Genotype, Genotype]: + if cls.which_crossover == "koza_default": + return cls.koza_default(parent_i, parent_j, **kwargs) + elif cls.which_crossover == "normal": + return cls.normal(parent_i, parent_j, **kwargs) + else: + msg = f"Crossover type '{cls.which_crossover}' not recognized." + raise ValueError(msg) + @staticmethod def koza_default( - parent_i: TreeGenome, - parent_j: TreeGenome, + parent_i: Genotype, + parent_j: Genotype, koza_internal_node_prob: float = 0.9, - ) -> tuple[TreeGenome, TreeGenome]: + ) -> tuple[Genotype, Genotype]: """ Koza default: - In Parent A: choose an internal node with high probability (e.g., 90%) excluding root. @@ -107,9 +163,9 @@ def koza_default( @staticmethod def normal( - parent_i: TreeGenome, - parent_j: TreeGenome, - ) -> tuple[TreeGenome, TreeGenome]: + parent_i: Genotype, + parent_j: Genotype, + ) -> tuple[Genotype, Genotype]: """ Normal tree crossover: - Pick a random node from Parent A (uniform over all nodes). diff --git a/src/ariel/ec/genotypes/genotype.py b/src/ariel/ec/genotypes/genotype.py new file mode 100644 index 00000000..ad10c57f --- /dev/null +++ b/src/ariel/ec/genotypes/genotype.py @@ -0,0 +1,29 @@ +from abc import ABC +from enum import Enum +from ariel.ec.a000 import Mutation +from ariel.ec.a005 import Crossover +from ariel.ec.genotypes.tree.tree_genome import TreeGenome + +class GenotypeEnum(Enum): + TREE = TreeGenome + #LSYSTEM = LSystemGenome # Future implementation + +class Genotype(ABC): + """Interface for different genotype types.""" + + @staticmethod + def get_crossover_object() -> "Crossover": + """Return the crossover operator for this genotype type.""" + raise NotImplementedError("Crossover operator not implemented for this genotype type.") + + @staticmethod + def get_mutator_object() -> "Mutation": + """Return the mutator operator for this genotype type.""" + raise NotImplementedError("Mutator operator not implemented for this genotype type.") + + @staticmethod + def create_individual() -> "Genotype": + """Generate a new individual of this genotype type.""" + raise NotImplementedError("Individual generation not implemented for this genotype type.") + + \ No newline at end of file diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index b582db8f..c96ccc93 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -3,6 +3,8 @@ import contextlib from collections import deque import copy +from ariel.ec.a000 import TreeMutator +from ariel.ec.a005 import TreeCrossover from jedi.inference.gradual.typing import Callable from functools import reduce @@ -15,6 +17,21 @@ class TreeGenome: def __init__(self, root: TreeNode | None = None): self._root = root + @staticmethod + def get_crossover_object() -> TreeCrossover: + """Return the crossover operator for tree genomes.""" + return TreeCrossover() + + @staticmethod + def get_mutator_object() -> TreeMutator: + """Return the mutator operator for tree genomes.""" + return TreeMutator() + + @staticmethod + def create_individual() -> TreeGenome: + """Generate a new TreeGenome individual.""" + return TreeGenome.default_init() + @classmethod def default_init(cls, *args, **kwargs): """Default instantiation with a core root.""" From bf88ec776bc0740eb2a6241c602a4b72bc30507e Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 29 Oct 2025 23:59:59 +0100 Subject: [PATCH 37/47] Created EA for a generic genome --- examples/0_render_single_frame.py | 2 +- examples/__init__.py | 0 examples/_graph_to_robot.py | 1 + examples/config.toml | 8 +- examples/evolve.py | 82 ++-- examples/morphology_fitness_analysis.py | 428 ++++++++++++++++++++ examples/target_robots/large_robot_25.json | 254 ++++++++++++ examples/target_robots/medium_robot_15.json | 154 +++++++ examples/target_robots/small_robot_8.json | 84 ++++ examples/tree_genome_vis.py | 14 +- src/ariel/ec/a000.py | 246 +++++------ src/ariel/ec/a001.py | 48 +-- src/ariel/ec/a004.py | 1 - src/ariel/ec/a005.py | 35 +- src/ariel/ec/genotypes/genotype.py | 19 +- src/ariel/ec/genotypes/tree/__init__.py | 2 - src/ariel/ec/genotypes/tree/tree_genome.py | 117 +++++- src/ariel/utils/graph_ops.py | 52 +++ src/ariel/utils/morphological_descriptor.py | 14 +- 19 files changed, 1356 insertions(+), 205 deletions(-) create mode 100644 examples/__init__.py create mode 100644 examples/morphology_fitness_analysis.py create mode 100644 examples/target_robots/large_robot_25.json create mode 100644 examples/target_robots/medium_robot_15.json create mode 100644 examples/target_robots/small_robot_8.json create mode 100644 src/ariel/utils/graph_ops.py diff --git a/examples/0_render_single_frame.py b/examples/0_render_single_frame.py index 1f1af790..4d0577c2 100644 --- a/examples/0_render_single_frame.py +++ b/examples/0_render_single_frame.py @@ -38,7 +38,7 @@ def main() -> None: data = mujoco.MjData(model) # Render a single frame - single_frame_renderer(model, data, steps=10_000) + single_frame_renderer(model, data, steps=10_000, save=True) if __name__ == "__main__": diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/_graph_to_robot.py b/examples/_graph_to_robot.py index 1ba2690f..7b457097 100644 --- a/examples/_graph_to_robot.py +++ b/examples/_graph_to_robot.py @@ -31,6 +31,7 @@ HighProbabilityDecoder, save_graph_as_json, ) +#from ariel.body_phenotypes.robogen_lite.morphology import Morphology from ariel.body_phenotypes.robogen_lite.modules.core import CoreModule from ariel.simulation.environments import SimpleFlatWorld from ariel.utils.renderers import single_frame_renderer diff --git a/examples/config.toml b/examples/config.toml index 41a0d892..4071c8e8 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -7,10 +7,10 @@ # One of: "integers", "tree", "lsystem", "cppn" genotype = "tree" -# (Optional) Override the selected profile's default operators here. -# If omitted, the defaults under [genotypes..defaults] are used. -# mutation = "swap_mutation" -# crossover = "pmx" +task = "evolve_to_copy" + +[task.evolve_to_copy] +target_robot_path = "small_robot_8.json" # ========================= # Global EC settings (match EASettings fields) diff --git a/examples/evolve.py b/examples/evolve.py index 0cd120ef..9c4f8e57 100644 --- a/examples/evolve.py +++ b/examples/evolve.py @@ -1,27 +1,26 @@ # Standard library +from __future__ import annotations import random from collections.abc import Callable from dataclasses import dataclass from pathlib import Path import tomllib -from typing import Literal, cast -from argparse import ArgumentParser +from typing import Literal, cast, TYPE_CHECKING +from functools import partial # Third-party libraries import numpy as np from pydantic_settings import BaseSettings from rich.console import Console -from rich.progress import track from rich.traceback import install -from sqlalchemy import create_engine -from sqlmodel import Session, SQLModel, col, select # Local libraries -from ariel.ec.a000 import IntegerMutator, IntegersGenerator, Mutation, TreeMutator from ariel.ec.a001 import Individual from ariel.ec.a004 import EAStep, EA -from ariel.ec.a005 import Crossover, IntegerCrossover, TreeCrossover +from ariel.ec.a000 import Mutation +from ariel.ec.a005 import Crossover from ariel.ec.genotypes.genotype import GenotypeEnum +from morphology_fitness_analysis import compute_6d_descriptor, load_target_robot, compute_fitness_scores # Global constants SEED = 42 @@ -48,11 +47,14 @@ class EASettings(BaseSettings): mutation: Mutation crossover: Crossover + task: str = "evolve_to_copy" + target_robot_file_path: Path | None = Path("examples/target_robots/small_robot_8.json") + # Data config output_folder: Path = Path.cwd() / "__data__" db_file_name: str = "database.db" db_file_path: Path = output_folder / db_file_name - db_handling: DB_HANDLING_MODES = "delete" + db_handling: DB_HANDLING_MODES = "delete" # ------------------------ EA STEPS ------------------------ # def parent_selection(population: Population, config: EASettings) -> Population: @@ -77,19 +79,19 @@ def crossover(population: Population, config: EASettings) -> Population: parent_i = parents[idx] parent_j = parents[idx] genotype_i, genotype_j = config.crossover( - parent_i.genotype, - parent_j.genotype, + config.genotype.value.from_json(parent_i.genotype), + config.genotype.value.from_json(parent_j.genotype), ) # First child child_i = Individual() - child_i.genotype = genotype_i + child_i.genotype = genotype_i.to_json() child_i.tags = {"mut": True} child_i.requires_eval = True # Second child child_j = Individual() - child_j.genotype = genotype_j + child_j.genotype = genotype_j.to_json() child_j.tags = {"mut": True} child_j.requires_eval = True @@ -100,19 +102,30 @@ def crossover(population: Population, config: EASettings) -> Population: def mutation(population: Population, config: EASettings) -> Population: for ind in population: if ind.tags.get("mut", False): - genes = ind.genotype + genes = config.genotype.value.from_json(ind.genotype) mutated = config.mutation( individual=genes, - span=1, - mutation_probability=0.5, + # span=1, + # mutation_probability=0.5, ) - ind.genotype = mutated + ind.genotype = mutated.to_json() ind.requires_eval = True return population -def evaluate(population: Population) -> Population: - pass +def evaluate(population: Population, config: EASettings) -> Population: + if config.task == "evolve_to_copy": + target_descriptor = load_target_robot(Path("examples/target_robots/" + str(config.target_robot_file_path))) + + for ind in population: + genotype = config.genotype.value.from_json(ind.genotype) + # Convert to digraph + ind_digraph = genotype.to_digraph(genotype) + # Compute the morphological descriptors + measures = compute_6d_descriptor(ind_digraph) + fitness = compute_fitness_scores(target_descriptor, measures) + ind.fitness = fitness + return population def survivor_selection(population: Population, config: EASettings) -> Population: @@ -137,27 +150,33 @@ def survivor_selection(population: Population, config: EASettings) -> Population def create_individual(config: EASettings) -> Individual: ind = Individual() - ind.genotype = config.genotype.value.create_individual() + ind.genotype = config.genotype.value.create_individual().to_json() return ind def read_config_file() -> EASettings: - cfg = tomllib.loads(Path("config.toml").read_text()) + cfg = tomllib.loads(Path("examples/config.toml").read_text()) # Resolve the active operators from the chosen genotype profile gname = cfg["run"]["genotype"] gblock = cfg["genotypes"][gname] mutation_name = cfg["run"].get("mutation", gblock["defaults"]["mutation"]) crossover_name = cfg["run"].get("crossover", gblock["defaults"]["crossover"]) + task = cfg["run"]["task"] + + target_robot_path = cfg["task"]["evolve_to_copy"]["target_robot_path"] if task == "evolve_to_copy" else None if gname == 'tree': genotype = GenotypeEnum.TREE - mutation = TreeMutator() - mutation.which_mutation = mutation_name - crossover = TreeCrossover() - crossover.which_crossover = crossover_name + elif gname == 'lsystem': + pass elif gname == 'integers': pass + mutation = genotype.value.get_mutator_object() + mutation.set_which_mutation(mutation_name) + crossover = genotype.value.get_crossover_object() + crossover.set_which_crossover(crossover_name) + settings = EASettings( quiet=cfg["ec"]["quiet"], is_maximisation=cfg["ec"]["is_maximisation"], @@ -167,6 +186,8 @@ def read_config_file() -> EASettings: genotype=genotype, mutation=mutation, crossover=crossover, + task=task, + target_robot_file_path=Path(target_robot_path), output_folder=Path(cfg["data"]["output_folder"]), db_file_name=cfg["data"]["db_file_name"], db_handling=cfg["data"]["db_handling"], @@ -180,15 +201,16 @@ def main() -> None: config = read_config_file() # Create initial population population_list = [create_individual(config) for _ in range(10)] - population_list = evaluate(population_list) + population_list = evaluate(population_list, config) # Create EA steps + ops = [ - EAStep("parent_selection", parent_selection), - EAStep("crossover", crossover), - EAStep("mutation", mutation), - EAStep("evaluation", evaluate), - EAStep("survivor_selection", survivor_selection), + EAStep("parent_selection", partial(parent_selection, config=config)), + EAStep("crossover", partial(crossover, config=config)), + EAStep("mutation", partial(mutation, config=config)), + EAStep("evaluation", partial(evaluate, config=config)), + EAStep("survivor_selection", partial(survivor_selection, config=config)), ] # Initialize EA diff --git a/examples/morphology_fitness_analysis.py b/examples/morphology_fitness_analysis.py new file mode 100644 index 00000000..c056bb51 --- /dev/null +++ b/examples/morphology_fitness_analysis.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +""" +Morphological fitness analysis and visualization. + +This script: +1. Loads target robot JSONs and computes their 6D morphological descriptors +2. Generates random robots using tree genome +3. Computes fitness as distance to target descriptors +4. Visualizes fitness landscapes using PCA and various plots +""" + +import numpy as np +import matplotlib.pyplot as plt +#import seaborn as sns +from sklearn.decomposition import PCA +from sklearn.manifold import TSNE +from typing import List, Tuple +import json +from pathlib import Path + +# Import ARIEL modules +from ariel.utils.graph_ops import load_robot_json_file +from ariel.utils.morphological_descriptor import MorphologicalMeasures +from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode +from ariel.body_phenotypes.robogen_lite.decoders.tree_decoder import to_digraph +import ariel.body_phenotypes.robogen_lite.config as config + +# Set random seed for reproducibility +np.random.seed(42) + +def compute_6d_descriptor(robot_graph) -> np.ndarray: + """Compute 6D morphological descriptor vector.""" + + measures = MorphologicalMeasures(robot_graph) + # Handle potential division by zero or missing P for non-2D robots + try: + P = measures.P if measures.is_2d else 0.0 + except: + P = 0.0 + + descriptor = np.array([ + measures.B, # Branching + measures.L, # Limbs + measures.E, # Extensiveness (length of limbs) + measures.S, # Symmetry + P, # Proportion (2D only) + measures.J # Joints + ]) + return descriptor + + +def load_target_robot(json_path: str): + """Load target robots and compute their descriptors.""" + try: + robot_graph = load_robot_json_file(json_path) + descriptor = compute_6d_descriptor(robot_graph) + + # Extract name from path + name = Path(json_path).stem + print(f"Loaded {name}: {descriptor}") + return descriptor + + except Exception as e: + print(f"Error loading {json_path}: {e}") + + +def compute_fitness_scores(individual_descriptors, target_descriptors): + """Compute fitness scores as mean of distances in each dimension to each target.""" + + fitness_scores = [] + + # Compute absolute differences in each dimension + dimension_distances = np.abs(individual_descriptors - target_descriptors) + # Take mean across dimensions to get fitness score + mean_distances = np.mean(dimension_distances) + # Convert to fitness (higher is better, so use negative distance) + fitness = -mean_distances + fitness_scores.append(fitness) + + return np.array(fitness_scores) + + +class MorphologyAnalyzer: + """Analyze and visualize morphological fitness landscapes.""" + + def __init__(self): + self.target_descriptors = [] + self.target_names = [] + self.random_descriptors = [] + self.fitness_scores = [] + + def compute_6d_descriptor(self, robot_graph) -> np.ndarray: + """Compute 6D morphological descriptor vector.""" + + measures = MorphologicalMeasures(robot_graph) + # Handle potential division by zero or missing P for non-2D robots + try: + P = measures.P if measures.is_2d else 0.0 + except: + P = 0.0 + + descriptor = np.array([ + measures.B, # Branching + measures.L, # Limbs + measures.E, # Extensiveness (length of limbs) + measures.S, # Symmetry + P, # Proportion (2D only) + measures.J # Joints + ]) + return descriptor + + + def load_target_robots(self, json_paths: List[str]): + """Load target robots and compute their descriptors.""" + self.target_descriptors = [] + self.target_names = [] + + for json_path in json_paths: + try: + robot_graph = load_robot_json_file(json_path) + descriptor = self.compute_6d_descriptor(robot_graph) + self.target_descriptors.append(descriptor) + + # Extract name from path + name = Path(json_path).stem + self.target_names.append(name) + + print(f"Loaded {name}: {descriptor}") + + except Exception as e: + print(f"Error loading {json_path}: {e}") + + self.target_descriptors = np.array(self.target_descriptors) + + def generate_random_robot(self, max_depth: int = 3, branch_prob: float = 0.6) -> TreeGenome: + """Generate a random robot using tree genome.""" + # Create root with CORE + root = TreeNode( + module_type=config.ModuleType.CORE, + module_rotation=config.ModuleRotationsIdx.DEG_0 + ) + + # Add random children + self._add_random_children(root, max_depth, branch_prob) + + genome = TreeGenome(root) + return genome + + def _add_random_children(self, node: TreeNode, max_depth: int, branch_prob: float): + """Recursively add random children to a node.""" + if max_depth <= 0: + return + + available_faces = node.available_faces() + + for face in available_faces: + if np.random.random() < branch_prob: + # Choose random module type (excluding CORE and NONE) + module_types = [mt for mt in config.ModuleType + if mt not in {config.ModuleType.CORE, config.ModuleType.NONE}] + module_type = np.random.choice(module_types) + + # Choose random rotation + rotation = np.random.choice(list(config.ModuleRotationsIdx)) + + # Create child node + child = TreeNode(module_type=module_type, module_rotation=rotation) + node._set_face(face, child) + + # Recursively add children with reduced depth + self._add_random_children(child, max_depth - 1, branch_prob * 0.7) + + def generate_random_population(self, n_robots: int = 100) -> List[np.ndarray]: + """Generate a population of random robots and compute their descriptors.""" + print(f"Generating {n_robots} random robots...") + descriptors = [] + + for i in range(n_robots): + if i % 20 == 0: + print(f"Generated {i}/{n_robots} robots") + + try: + # Generate random robot + genome = self.generate_random_robot() + + # Decode to graph + robot_graph = to_digraph(genome) + + # Compute descriptor + descriptor = self.compute_6d_descriptor(robot_graph) + descriptors.append(descriptor) + + except Exception as e: + print(f"Error generating robot {i}: {e}") + # Add zero descriptor for failed robots + descriptors.append(np.zeros(6)) + + self.random_descriptors = np.array(descriptors) + return self.random_descriptors + + def compute_fitness_scores(self): + """Compute fitness scores as mean of distances in each dimension to each target.""" + if len(self.target_descriptors) == 0 or len(self.random_descriptors) == 0: + raise ValueError("Need both target and random descriptors") + + self.fitness_scores = [] + + for target_desc in self.target_descriptors: + # Compute absolute differences in each dimension + dimension_distances = np.abs(self.random_descriptors - target_desc) + # Take mean across dimensions to get fitness score + mean_distances = np.mean(dimension_distances, axis=1) + # Convert to fitness (higher is better, so use negative distance) + fitness = -mean_distances + self.fitness_scores.append(fitness) + + self.fitness_scores = np.array(self.fitness_scores) + + def plot_target_descriptors_pca(self): + """Plot target robots in PCA-reduced space.""" + if len(self.target_descriptors) == 0: + return + + fig, axes = plt.subplots(1, 2, figsize=(15, 6)) + + # Combine all descriptors for PCA fitting + all_desc = np.vstack([self.target_descriptors, self.random_descriptors]) + pca = PCA(n_components=2) + all_pca = pca.fit_transform(all_desc) + + target_pca = all_pca[:len(self.target_descriptors)] + random_pca = all_pca[len(self.target_descriptors):] + + # Plot 1: PCA visualization + axes[0].scatter(random_pca[:, 0], random_pca[:, 1], + alpha=0.3, c='lightgray', s=20, label='Random robots') + + colors = ['red', 'blue', 'green', 'orange', 'purple'] + for i, (target, name) in enumerate(zip(target_pca, self.target_names)): + color = colors[i % len(colors)] + axes[0].scatter(target[0], target[1], + c=color, s=100, marker='*', + label=f'Target: {name}', edgecolors='black') + + axes[0].set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%} variance)') + axes[0].set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%} variance)') + axes[0].set_title('Morphological Space (PCA)') + axes[0].legend() + axes[0].grid(True, alpha=0.3) + + # Plot 2: Feature importance + feature_names = ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + + pc1_importance = np.abs(pca.components_[0]) + pc2_importance = np.abs(pca.components_[1]) + + x = np.arange(len(feature_names)) + width = 0.35 + + axes[1].bar(x - width/2, pc1_importance, width, label='PC1', alpha=0.8) + axes[1].bar(x + width/2, pc2_importance, width, label='PC2', alpha=0.8) + + axes[1].set_xlabel('Morphological Features') + axes[1].set_ylabel('Absolute Component Weight') + axes[1].set_title('PCA Feature Importance') + axes[1].set_xticks(x) + axes[1].set_xticklabels(feature_names, rotation=45) + axes[1].legend() + axes[1].grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() + + def plot_fitness_landscapes(self): + """Plot fitness landscapes for each target.""" + if len(self.fitness_scores) == 0: + self.compute_fitness_scores() + + # Use PCA for dimensionality reduction + all_desc = np.vstack([self.target_descriptors, self.random_descriptors]) + pca = PCA(n_components=2) + all_pca = pca.fit_transform(all_desc) + + target_pca = all_pca[:len(self.target_descriptors)] + random_pca = all_pca[len(self.target_descriptors):] + + n_targets = len(self.target_names) + fig, axes = plt.subplots(2, (n_targets + 1) // 2, figsize=(15, 10)) + if n_targets == 1: + axes = [axes] + axes = axes.flatten() if n_targets > 1 else axes + + for i, (target_name, fitness) in enumerate(zip(self.target_names, self.fitness_scores)): + ax = axes[i] + + # Create scatter plot with fitness as color + scatter = ax.scatter(random_pca[:, 0], random_pca[:, 1], + c=fitness, cmap='viridis', alpha=0.6, s=30) + + # Mark target location + ax.scatter(target_pca[i, 0], target_pca[i, 1], + c='red', s=200, marker='*', + edgecolors='black', linewidths=2, + label=f'Target: {target_name}') + + ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%})') + ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%})') + ax.set_title(f'Fitness Landscape: {target_name}') + ax.legend() + ax.grid(True, alpha=0.3) + + # Add colorbar + plt.colorbar(scatter, ax=ax, label='Fitness') + + # Hide unused subplots + for j in range(n_targets, len(axes)): + axes[j].set_visible(False) + + plt.tight_layout() + plt.show() + + def plot_fitness_distributions(self): + """Plot fitness distributions for each target.""" + if len(self.fitness_scores) == 0: + self.compute_fitness_scores() + + fig, axes = plt.subplots(1, 2, figsize=(15, 6)) + + # Compute target scores (perfect match = 0 distance = 0 fitness) + target_scores = [0.0] * len(self.target_names) # Perfect match has 0 mean distance + + # Plot 1: Fitness distributions + for i, (target_name, fitness) in enumerate(zip(self.target_names, self.fitness_scores)): + # Get target score info for legend + target_score = target_scores[i] + best_fitness = np.max(fitness) + mean_fitness = np.mean(fitness) + + label = f'{target_name} (target: {target_score:.3f}, best: {best_fitness:.3f}, mean: {mean_fitness:.3f})' + axes[0].hist(fitness, bins=30, alpha=0.7, label=label, density=True) + + # Mark target score with vertical line + axes[0].axvline(target_score, color=f'C{i}', linestyle='--', linewidth=2, alpha=0.8) + + axes[0].set_xlabel('Fitness (negative mean distance)') + axes[0].set_ylabel('Density') + axes[0].set_title('Fitness Distributions') + axes[0].legend() + axes[0].grid(True, alpha=0.3) + + # Plot 2: Best fitness per target + best_fitness = [np.max(fitness) for fitness in self.fitness_scores] + mean_fitness = [np.mean(fitness) for fitness in self.fitness_scores] + + x = np.arange(len(self.target_names)) + width = 0.35 + + axes[1].bar(x - width/2, best_fitness, width, label='Best', alpha=0.8) + axes[1].bar(x + width/2, mean_fitness, width, label='Mean', alpha=0.8) + + axes[1].set_xlabel('Target Robot') + axes[1].set_ylabel('Fitness') + axes[1].set_title('Fitness Statistics') + axes[1].set_xticks(x) + axes[1].set_xticklabels(self.target_names) + axes[1].legend() + axes[1].grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() + + def analyze_morphological_diversity(self): + """Analyze diversity in the random population.""" + if len(self.random_descriptors) == 0: + return + + fig, axes = plt.subplots(2, 3, figsize=(18, 12)) + axes = axes.flatten() + + feature_names = ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + + for i, feature_name in enumerate(feature_names): + ax = axes[i] + + # Plot distribution of random robots + random_mean = np.mean(self.random_descriptors[:, i]) + random_std = np.std(self.random_descriptors[:, i]) + ax.hist(self.random_descriptors[:, i], bins=30, alpha=0.7, + density=True, label=f'Random robots (μ={random_mean:.3f}, σ={random_std:.3f})') + + # Mark target values + for j, target_name in enumerate(self.target_names): + target_value = self.target_descriptors[j, i] + ax.axvline(target_value, color=f'C{j+1}', linestyle='--', + linewidth=2, label=f'{target_name}: {target_value:.3f}') + + ax.set_xlabel(feature_name) + ax.set_ylabel('Density') + ax.set_title(f'{feature_name} Distribution') + ax.legend() + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() + + +def main(): + """Main analysis function.""" + # Define target robot paths + target_paths = [ + "examples/target_robots/small_robot_8.json", + "examples/target_robots/medium_robot_15.json", + "examples/target_robots/large_robot_25.json" + ] + + analyzer = MorphologyAnalyzer() + analyzer.load_target_robots(target_paths) + analyzer.generate_random_population(n_robots=200) + analyzer.compute_fitness_scores() + analyzer.plot_target_descriptors_pca() + analyzer.plot_fitness_landscapes() + analyzer.plot_fitness_distributions() + analyzer.analyze_morphological_diversity() + print("\nAnalysis complete!") + + +if __name__ == "__main__": + main() diff --git a/examples/target_robots/large_robot_25.json b/examples/target_robots/large_robot_25.json new file mode 100644 index 00000000..6bd9b51a --- /dev/null +++ b/examples/target_robots/large_robot_25.json @@ -0,0 +1,254 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 1 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 2 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 3 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 4 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 5 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 6 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 7 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 8 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 9 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 10 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 11 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 12 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 13 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 14 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 15 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 16 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 17 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 18 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 19 + }, + { + "type": "BRICK", + "rotation": "DEG_90", + "id": 20 + }, + { + "type": "BRICK", + "rotation": "DEG_90", + "id": 21 + }, + { + "type": "BRICK", + "rotation": "DEG_135", + "id": 22 + }, + { + "type": "BRICK", + "rotation": "DEG_135", + "id": 23 + }, + { + "type": "BRICK", + "rotation": "DEG_180", + "id": 24 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "BACK", + "source": 0, + "target": 2 + }, + { + "face": "LEFT", + "source": 0, + "target": 3 + }, + { + "face": "RIGHT", + "source": 0, + "target": 4 + }, + { + "face": "TOP", + "source": 0, + "target": 5 + }, + { + "face": "BOTTOM", + "source": 0, + "target": 6 + }, + { + "face": "FRONT", + "source": 1, + "target": 7 + }, + { + "face": "RIGHT", + "source": 1, + "target": 8 + }, + { + "face": "FRONT", + "source": 2, + "target": 9 + }, + { + "face": "LEFT", + "source": 2, + "target": 10 + }, + { + "face": "FRONT", + "source": 3, + "target": 11 + }, + { + "face": "FRONT", + "source": 6, + "target": 12 + }, + { + "face": "FRONT", + "source": 7, + "target": 13 + }, + { + "face": "FRONT", + "source": 8, + "target": 14 + }, + { + "face": "FRONT", + "source": 9, + "target": 15 + }, + { + "face": "FRONT", + "source": 10, + "target": 16 + }, + { + "face": "FRONT", + "source": 11, + "target": 17 + }, + { + "face": "RIGHT", + "source": 4, + "target": 18 + }, + { + "face": "LEFT", + "source": 4, + "target": 19 + }, + { + "face": "TOP", + "source": 5, + "target": 20 + }, + { + "face": "RIGHT", + "source": 5, + "target": 21 + }, + { + "face": "LEFT", + "source": 12, + "target": 22 + }, + { + "face": "RIGHT", + "source": 13, + "target": 23 + }, + { + "face": "TOP", + "source": 14, + "target": 24 + } + ] +} \ No newline at end of file diff --git a/examples/target_robots/medium_robot_15.json b/examples/target_robots/medium_robot_15.json new file mode 100644 index 00000000..7ab93174 --- /dev/null +++ b/examples/target_robots/medium_robot_15.json @@ -0,0 +1,154 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 1 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 2 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 3 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 4 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 5 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 6 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 7 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 8 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 9 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 10 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 11 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 12 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 13 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 14 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "BACK", + "source": 0, + "target": 2 + }, + { + "face": "LEFT", + "source": 0, + "target": 3 + }, + { + "face": "RIGHT", + "source": 0, + "target": 4 + }, + { + "face": "TOP", + "source": 0, + "target": 5 + }, + { + "face": "FRONT", + "source": 1, + "target": 6 + }, + { + "face": "FRONT", + "source": 2, + "target": 7 + }, + { + "face": "FRONT", + "source": 3, + "target": 8 + }, + { + "face": "FRONT", + "source": 5, + "target": 9 + }, + { + "face": "FRONT", + "source": 6, + "target": 10 + }, + { + "face": "FRONT", + "source": 7, + "target": 11 + }, + { + "face": "FRONT", + "source": 8, + "target": 12 + }, + { + "face": "RIGHT", + "source": 4, + "target": 13 + }, + { + "face": "LEFT", + "source": 9, + "target": 14 + } + ] +} \ No newline at end of file diff --git a/examples/target_robots/small_robot_8.json b/examples/target_robots/small_robot_8.json new file mode 100644 index 00000000..2ac44009 --- /dev/null +++ b/examples/target_robots/small_robot_8.json @@ -0,0 +1,84 @@ +{ + "directed": true, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "type": "CORE", + "rotation": "DEG_0", + "id": 0 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 1 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 2 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 3 + }, + { + "type": "HINGE", + "rotation": "DEG_90", + "id": 4 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 5 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 6 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 7 + } + ], + "edges": [ + { + "face": "FRONT", + "source": 0, + "target": 1 + }, + { + "face": "BACK", + "source": 0, + "target": 2 + }, + { + "face": "LEFT", + "source": 0, + "target": 3 + }, + { + "face": "RIGHT", + "source": 0, + "target": 4 + }, + { + "face": "FRONT", + "source": 3, + "target": 5 + }, + { + "face": "FRONT", + "source": 4, + "target": 6 + }, + { + "face": "FRONT", + "source": 1, + "target": 7 + } + ] +} \ No newline at end of file diff --git a/examples/tree_genome_vis.py b/examples/tree_genome_vis.py index 76aed5f6..7a26b8e7 100644 --- a/examples/tree_genome_vis.py +++ b/examples/tree_genome_vis.py @@ -39,7 +39,7 @@ from ariel.utils.tracker import Tracker from ariel.utils.video_recorder import VideoRecorder from ariel.utils.morphological_descriptor import MorphologicalMeasures - +from ariel.utils.graph_ops import robot_json_to_digraph, load_robot_json_file # Type Checking if TYPE_CHECKING: @@ -330,6 +330,8 @@ def morph_descriptors(robot_graph: Any): print(f" Is 2D: {measures.is_2d}") print(f" Size: {measures.size:.3f}") + return np.array([measures.B, measures.L, measures.E, measures.S, measures.P, measures.J]) + def simple_controller(model: mj.MjModel, data: mj.MjData) -> np.ndarray: """Simple oscillating controller for robot movement.""" @@ -434,15 +436,15 @@ def main() -> None: tree_genome = TreeGenerator.binary_tree(10) robot_graph = to_digraph(tree_genome) - + robot_graph = load_robot_json_file("examples/target_robots/large_robot_25.json") morph_descriptors(robot_graph) # ? ------------------------------------------------------------------ # # Save the graph to a file - save_graph_as_json( - robot_graph, - DATA / "robot_graph.json", - ) + #save_graph_as_json( + # robot_graph, + # DATA / "robot_graph.json", + #) # ? ------------------------------------------------------------------ # # Print all nodes diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index 7e0695a6..ace587fb 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -1,10 +1,10 @@ """TODO(jmdm): description of script.""" - +from __future__ import annotations # Standard library from abc import ABC, abstractmethod from collections.abc import Sequence from pathlib import Path -from typing import cast +from typing import cast, TYPE_CHECKING # Third-party libraries import numpy as np @@ -12,8 +12,8 @@ from rich.console import Console from rich.traceback import install import copy -from ariel.ec.genotypes.genotype import Genotype -from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode +if TYPE_CHECKING: + from ariel.ec.genotypes.genotype import Genotype import ariel.body_phenotypes.robogen_lite.config as pheno_config # Global constants @@ -89,8 +89,12 @@ def choice( class Mutation(ABC): which_mutation: str = "" - @abstractmethod @classmethod + def set_which_mutation(cls, mutation_type: str) -> None: + cls.which_mutation = mutation_type + + @classmethod + @abstractmethod def __call__( cls, individual: Genotype, @@ -130,8 +134,9 @@ def __call__( **kwargs, ) else: - raise ValueError(f"Unknown mutation type: {cls.which_mutation}") - + msg = f"Mutation type '{cls.which_mutation}' not recognized." + raise ValueError(msg) + @staticmethod def random_swap( individual: Genotype, @@ -189,106 +194,107 @@ def integer_creep( return cast("Integers", new_genotype.astype(int).tolist()) -class TreeGenerator: - @staticmethod - def __call__(*args, **kwargs) -> TreeGenome: - return TreeGenome.default_init() - - @staticmethod - def default(): - return TreeGenome.default_init() - - @staticmethod - def linear_chain(length: int = 3) -> TreeGenome: - """Generate a linear chain of modules (snake-like).""" - genome = TreeGenome.default_init() # Start with CORE - current_node = genome.root - - for i in range(length): - module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) - rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) - module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) - - # Always attach to FRONT face for linear chain - if pheno_config.ModuleFaces.FRONT in current_node.available_faces(): - child = TreeNode(module, depth=current_node._depth + 1) - current_node._set_face(pheno_config.ModuleFaces.FRONT, child) - current_node = child - - return genome - - @staticmethod - def star_shape(num_arms: int = 3) -> TreeGenome: - """Generate a star-shaped tree with arms radiating from center.""" - genome = TreeGenome.default_init() # Start with CORE - available_faces = genome.root.available_faces() - - # Limit arms to available faces - actual_arms = min(num_arms, len(available_faces)) - selected_faces = RNG.choice(available_faces, size=actual_arms, replace=False) - - for face in selected_faces: - module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) - rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) - module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) - - child = TreeNode(module, depth=1) - genome.root._set_face(face, child) - - return genome - - @staticmethod - def binary_tree(depth: int = 2) -> TreeGenome: - """Generate a binary-like tree structure.""" - def build_subtree(current_depth: int, max_depth: int) -> TreeNode | None: - if current_depth >= max_depth: - return None - - module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) - rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) - module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) - - node = TreeNode(module, depth=current_depth) - available_faces = node.available_faces() - - # Add 1-2 children randomly - if available_faces and current_depth < max_depth - 1: - num_children = RNG.integers(1, min(3, len(available_faces) + 1)) - selected_faces = RNG.choice(available_faces, size=num_children, replace=False) - - for face in selected_faces: - child = build_subtree(current_depth + 1, max_depth) - if child: - node._set_face(face, child) - - return node - - genome = TreeGenome.default_init() - - # Add children to root - available_faces = genome.root.available_faces() - if available_faces: - num_children = RNG.integers(1, min(3, len(available_faces) + 1)) - selected_faces = RNG.choice(available_faces, size=num_children, replace=False) - - for face in selected_faces: - child = build_subtree(1, depth) - if child: - genome.root._set_face(face, child) - - return genome - - @staticmethod - def random_tree(max_depth: int = 4, branching_prob: float = 0.7) -> TreeGenome: - """Generate a random tree with pheno_configurable branching probability.""" - genome = TreeGenome.default_init() # Start with CORE - face = RNG.choice(genome.root.available_faces()) - subtree = TreeNode.random_tree_node(max_depth=max_depth - 1, branch_prob=branching_prob) - if subtree: - genome.root._set_face(face, subtree) - return genome +# class TreeGenerator: +# @staticmethod +# def __call__(*args, **kwargs) -> TreeGenome: +# return TreeGenome.default_init() + +# @staticmethod +# def default(): +# return TreeGenome.default_init() + +# @staticmethod +# def linear_chain(length: int = 3) -> TreeGenome: +# """Generate a linear chain of modules (snake-like).""" +# genome = TreeGenome.default_init() # Start with CORE +# current_node = genome.root + +# for i in range(length): +# module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) +# rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) +# module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) + +# # Always attach to FRONT face for linear chain +# if pheno_config.ModuleFaces.FRONT in current_node.available_faces(): +# child = TreeNode(module, depth=current_node._depth + 1) +# current_node._set_face(pheno_config.ModuleFaces.FRONT, child) +# current_node = child + +# return genome + +# @staticmethod +# def star_shape(num_arms: int = 3) -> TreeGenome: +# """Generate a star-shaped tree with arms radiating from center.""" +# genome = TreeGenome.default_init() # Start with CORE +# available_faces = genome.root.available_faces() + +# # Limit arms to available faces +# actual_arms = min(num_arms, len(available_faces)) +# selected_faces = RNG.choice(available_faces, size=actual_arms, replace=False) + +# for face in selected_faces: +# module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) +# rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) +# module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) + +# child = TreeNode(module, depth=1) +# genome.root._set_face(face, child) + +# return genome + +# @staticmethod +# def binary_tree(depth: int = 2) -> TreeGenome: +# """Generate a binary-like tree structure.""" +# def build_subtree(current_depth: int, max_depth: int) -> TreeNode | None: +# if current_depth >= max_depth: +# return None + +# module_type = RNG.choice([pheno_config.ModuleType.BRICK, pheno_config.ModuleType.HINGE]) +# rotation = RNG.choice(list(pheno_config.ModuleRotationsIdx)) +# module = pheno_config.ModuleInstance(type=module_type, rotation=rotation, links={}) + +# node = TreeNode(module, depth=current_depth) +# available_faces = node.available_faces() + +# # Add 1-2 children randomly +# if available_faces and current_depth < max_depth - 1: +# num_children = RNG.integers(1, min(3, len(available_faces) + 1)) +# selected_faces = RNG.choice(available_faces, size=num_children, replace=False) + +# for face in selected_faces: +# child = build_subtree(current_depth + 1, max_depth) +# if child: +# node._set_face(face, child) + +# return node + +# genome = TreeGenome.default_init() + +# # Add children to root +# available_faces = genome.root.available_faces() +# if available_faces: +# num_children = RNG.integers(1, min(3, len(available_faces) + 1)) +# selected_faces = RNG.choice(available_faces, size=num_children, replace=False) + +# for face in selected_faces: +# child = build_subtree(1, depth) +# if child: +# genome.root._set_face(face, child) + +# return genome + +# @staticmethod +# def random_tree(max_depth: int = 4, branching_prob: float = 0.7) -> TreeGenome: +# """Generate a random tree with pheno_configurable branching probability.""" +# genome = TreeGenome.default_init() # Start with CORE +# face = RNG.choice(genome.root.available_faces()) +# subtree = TreeNode.random_tree_node(max_depth=max_depth - 1, branch_prob=branching_prob) +# if subtree: +# genome.root._set_face(face, subtree) +# return genome class TreeMutator(Mutation): + @classmethod def __call__( cls, @@ -301,32 +307,42 @@ def __call__( **kwargs, ) else: - raise ValueError(f"Unknown mutation type: {cls.which_mutation}") + msg = f"Mutation type '{cls.which_mutation}' not recognized." + raise ValueError(msg) + + @staticmethod + def _random_tree(max_depth: int = 2, branching_prob: float = 0.5) -> Genotype: + """Generate a random tree with pheno_configurable branching probability.""" + from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode + genome = TreeGenome.default_init() # Start with CORE + face = RNG.choice(genome.root.available_faces()) + subtree = TreeNode.random_tree_node(max_depth=max_depth - 1, branch_prob=branching_prob) + if subtree: + genome.root._set_face(face, subtree) + return genome @staticmethod def random_subtree_replacement( individual: Genotype, - max_subtree_depth: int = 1, + max_subtree_depth: int = 2, branching_prob: float = 0.5, ) -> Genotype: """Replace a random subtree with a new random subtree.""" - if individual.root is None: - return individual - + from ariel.ec.genotypes.tree.tree_genome import TreeNode new_individual = copy.copy(individual) # Collect all nodes in the tree all_nodes = new_individual.root.get_all_nodes(exclude_root=True) - # Select a random node to replace (excluding root) - if len(all_nodes) <= 1: - return new_individual - - node_to_replace = RNG.choice(all_nodes[1:]) # Avoid replacing root + if not all_nodes: + # print("Tree has no nodes to replace; generating a new random tree.") + return TreeMutator._random_tree(max_depth=max_subtree_depth, branching_prob=branching_prob) # Generate a new random subtree new_subtree = TreeNode.random_tree_node(max_depth=max_subtree_depth, branch_prob=branching_prob) + node_to_replace = RNG.choice(all_nodes) + with new_individual.root.enable_replacement(): new_individual.root.replace_node(node_to_replace, new_subtree) diff --git a/src/ariel/ec/a001.py b/src/ariel/ec/a001.py index 31c363c0..177afd61 100644 --- a/src/ariel/ec/a001.py +++ b/src/ariel/ec/a001.py @@ -12,7 +12,6 @@ from sqlmodel import Field, Session, SQLModel, create_engine # Local libraries -from ariel.ec.a000 import IntegersGenerator # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] @@ -121,29 +120,30 @@ def tags(self, tag: dict[JSONType, JSONType]) -> None: def main() -> None: - """Entry point.""" - # Initialize the database - engine = init_database() - - # Save data - with Session(engine) as session: - ind = Individual() - - # Generators - ind.genotype = IntegersGenerator.integers(low=0, high=10, size=5) - - # Tags - ind.tags = {"a": ["1", 2, 3]} - ind.tags = {"b": ("1", 2, 3)} - ind.tags = {"c": 1} - ind.tags = {"d": True} - - prnt(ind) - session.add(ind) - session.commit() - prnt(ind) - session.refresh(ind) - prnt(ind) + # """Entry point.""" + # # Initialize the database + # engine = init_database() + + # # Save data + # with Session(engine) as session: + # ind = Individual() + + # # Generators + # ind.genotype = IntegersGenerator.integers(low=0, high=10, size=5) + + # # Tags + # ind.tags = {"a": ["1", 2, 3]} + # ind.tags = {"b": ("1", 2, 3)} + # ind.tags = {"c": 1} + # ind.tags = {"d": True} + + # prnt(ind) + # session.add(ind) + # session.commit() + # prnt(ind) + # session.refresh(ind) + # prnt(ind) + pass if __name__ == "__main__": diff --git a/src/ariel/ec/a004.py b/src/ariel/ec/a004.py index 82e93a3f..e91125bb 100644 --- a/src/ariel/ec/a004.py +++ b/src/ariel/ec/a004.py @@ -24,7 +24,6 @@ # Local libraries from ariel.ec.a000 import IntegerMutator, IntegersGenerator from ariel.ec.a001 import Individual -from ariel.ec.a005 import Crossover # Global constants SEED = 42 diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 4f1eea7d..0d18acfe 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -1,4 +1,5 @@ """TODO(jmdm): description of script.""" +from __future__ import annotations # Standard library from abc import ABC, abstractmethod @@ -9,11 +10,12 @@ from rich.console import Console from rich.traceback import install +from typing import TYPE_CHECKING + # Local libraries -from ariel.ec.genotypes.genotype import Genotype -from ariel.ec.genotypes.tree.tree_genome import TreeGenome +if TYPE_CHECKING: + from ariel.ec.genotypes.genotype import Genotype from ariel.ec.a000 import IntegersGenerator -from ariel.ec.a001 import JSONIterable # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] @@ -32,8 +34,12 @@ class Crossover(ABC): which_crossover: str = "" - @abstractmethod @classmethod + def set_which_crossover(cls, crossover_type: str) -> None: + cls.which_crossover = crossover_type + + @classmethod + @abstractmethod def __call__( cls, parent_i: Genotype, @@ -134,6 +140,14 @@ def koza_default( (not just swapping a leaf for a leaf), while letting the other parent be unrestricted adds variety. """ parent_i_root, parent_j_root = parent_i.root, parent_j.root + + nodes_a = parent_i_root.get_all_nodes(exclude_root=True) + nodes_b = parent_j_root.get_all_nodes(exclude_root=True) + + # If either tree is just a root, return copies of parents + if not nodes_a or not nodes_b: + return parent_i.copy(), parent_j.copy() + parent_i_internal_nodes = parent_i_root.get_internal_nodes(mode="dfs", exclude_root=True) if RNG.random() > koza_internal_node_prob and parent_i_internal_nodes: @@ -176,14 +190,17 @@ def normal( """ parent_i_root, parent_j_root = parent_i.root, parent_j.root - # Uniformly choose any node (root, internal, or leaf) - node_a = RNG.choice(parent_i_root.get_all_nodes(exclude_root=True)) - node_b = RNG.choice(parent_j_root.get_all_nodes(exclude_root=True)) + nodes_a = parent_i_root.get_all_nodes(exclude_root=True) + nodes_b = parent_j_root.get_all_nodes(exclude_root=True) - if not node_a or not node_b: - # If either tree is just a root, return copies of parents + # If either tree is just a root, return copies of parents + if not nodes_a or not nodes_b: return parent_i.copy(), parent_j.copy() + # Uniformly choose any node (root, internal, or leaf) + node_a = RNG.choice(nodes_a) + node_b = RNG.choice(nodes_b) + # Preserve originals (same pattern as in koza_default) parent_i_old = parent_i.copy() parent_j_old = parent_j.copy() diff --git a/src/ariel/ec/genotypes/genotype.py b/src/ariel/ec/genotypes/genotype.py index ad10c57f..bfe29ca6 100644 --- a/src/ariel/ec/genotypes/genotype.py +++ b/src/ariel/ec/genotypes/genotype.py @@ -1,8 +1,13 @@ -from abc import ABC +from __future__ import annotations +from abc import ABC, abstractmethod from enum import Enum -from ariel.ec.a000 import Mutation -from ariel.ec.a005 import Crossover +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ariel.ec.a000 import Mutation + from ariel.ec.a005 import Crossover from ariel.ec.genotypes.tree.tree_genome import TreeGenome +import networkx as nx + class GenotypeEnum(Enum): TREE = TreeGenome @@ -12,18 +17,26 @@ class Genotype(ABC): """Interface for different genotype types.""" @staticmethod + @abstractmethod def get_crossover_object() -> "Crossover": """Return the crossover operator for this genotype type.""" raise NotImplementedError("Crossover operator not implemented for this genotype type.") @staticmethod + @abstractmethod def get_mutator_object() -> "Mutation": """Return the mutator operator for this genotype type.""" raise NotImplementedError("Mutator operator not implemented for this genotype type.") @staticmethod + @abstractmethod def create_individual() -> "Genotype": """Generate a new individual of this genotype type.""" raise NotImplementedError("Individual generation not implemented for this genotype type.") + @staticmethod + @abstractmethod + def to_digraph(robot_genotype: "Genotype", **kwargs: dict) -> nx.DiGraph: + """Convert the genotype to a directed graph representation.""" + raise NotImplementedError("Conversion to directed graph not implemented for this genotype type.") \ No newline at end of file diff --git a/src/ariel/ec/genotypes/tree/__init__.py b/src/ariel/ec/genotypes/tree/__init__.py index cca3fb9c..8b137891 100644 --- a/src/ariel/ec/genotypes/tree/__init__.py +++ b/src/ariel/ec/genotypes/tree/__init__.py @@ -1,3 +1 @@ -from .tree_genome import TreeGenome, TreeNode -__all__ = ["TreeGenome", "TreeNode"] diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index c96ccc93..17324b76 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -1,12 +1,13 @@ from __future__ import annotations +import json import ariel.body_phenotypes.robogen_lite.config as config import contextlib from collections import deque -import copy from ariel.ec.a000 import TreeMutator from ariel.ec.a005 import TreeCrossover +import networkx as nx -from jedi.inference.gradual.typing import Callable +from collections.abc import Callable from functools import reduce import numpy as np @@ -31,6 +32,48 @@ def get_mutator_object() -> TreeMutator: def create_individual() -> TreeGenome: """Generate a new TreeGenome individual.""" return TreeGenome.default_init() + + @staticmethod + def to_digraph(robot_genotype: TreeGenome, use_node_ids: bool = False) -> nx.DiGraph: + g = nx.DiGraph() + root = robot_genotype.root + if root is None: + return g + + # Stable mapping: either identity (node.id) or contiguous DFS ids. + if use_node_ids: + node_key = lambda n: n.id + else: + # Assign 0..N-1 in first-seen (DFS) order + seen: dict[int, int] = {} + next_id = 0 + def node_key(n: TreeNode) -> int: + nonlocal next_id + if n.id not in seen: + seen[n.id] = next_id + next_id += 1 + return seen[n.id] + + def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None) -> None: + cid = node_key(child) + # Add/update child node with attributes (use enum names for JSON-friendliness) + g.add_node( + cid, + type=child.module_type.name, + rotation=child.rotation.name, + raw_id=child.id, + ) + + if parent is not None: + pid = node_key(parent) + g.add_edge(pid, cid, face=via_face.name if via_face is not None else None) + + # Recurse over children (face -> subnode) + for face, sub in child.children.items(): + dfs(child, sub, face) + + dfs(None, root, None) + return g @classmethod def default_init(cls, *args, **kwargs): @@ -39,6 +82,18 @@ def default_init(cls, *args, **kwargs): rotation=config.ModuleRotationsIdx.DEG_0, links={}))) + @classmethod + def from_dict(cls, data: dict) -> "TreeGenome": + """Deserialize a genome from a dict produced by to_dict().""" + root_data = data.get("root") + root = None if root_data is None else TreeNode.from_dict(root_data) + return cls(root=root) + + @classmethod + def from_json(cls, s: str) -> "TreeGenome": + """Deserialize from a JSON string.""" + return cls.from_dict(json.loads(s)) + @property def root(self) -> TreeNode | None: return self._root @@ -134,6 +189,17 @@ def copy(self) -> 'TreeGenome': def __copy__(self) -> 'TreeGenome': """Support for copy.copy().""" return self.copy() + + # ---------- JSON / dict serialization ---------- + def to_dict(self) -> dict: + """Serialize the genome into a pure-Python dict (JSON-friendly).""" + return { + "root": None if self._root is None else self._root.to_dict() + } + + def to_json(self, *, indent: int | None = 2) -> str: + """Serialize to a JSON string.""" + return json.dumps(self.to_dict(), indent=indent) # TODO: Implement this # def __deepcopy__(self, memo) -> 'TreeGenome': @@ -470,6 +536,7 @@ def replace_node(self, node_to_remove: TreeNode, node_to_add: TreeNode): """ predicate_is_parent = lambda x: node_to_remove in set(x.children.values()) parent = self.find_all_nodes_dfs(predicate=predicate_is_parent) + # print("PARENTS LENGTH",len(parent)) if not parent or len(parent) > 1: raise RuntimeError("Father not found, are you sure node_to_remove is in subtree?") # We expect a list of len 1 in which there is the parent @@ -498,6 +565,52 @@ def copy(self) -> 'TreeNode': def __copy__(self) -> 'TreeNode': """Support for copy.copy() - creates deep copy for tree structures.""" return self.copy() + + # ---------- JSON / dict serialization ---------- + def to_dict(self) -> dict: + """ + Serialize this node recursively. + Enums are stored by name (string). Children are a mapping face->child. + """ + # Children as {face_name: child_dict} + children_dict = { + face.name: child.to_dict() + for face, child in self.children.items() + } + return { + "id": self._id, + "depth": self._depth, + "module_type": self.module_type.name, + "rotation": self.rotation.name, + "children": children_dict, + } + + @classmethod + def from_dict(cls, data: dict) -> "TreeNode": + """ + Rebuild a TreeNode (and its subtree) from dict. + Uses _set_face so that ModuleInstance.links remains consistent. + """ + node_id = data["id"] + depth = data["depth"] + module_type = config.ModuleType[data["module_type"]] + rotation = config.ModuleRotationsIdx[data["rotation"]] + + # Create the node with a fresh ModuleInstance and the preserved id/depth + node = cls( + module=config.ModuleInstance(type=module_type, rotation=rotation, links={}), + depth=depth, + node_id=node_id, + ) + + # Rebuild children through _set_face so links are updated properly. + children = data.get("children", {}) or {} + for face_name, child_payload in children.items(): + face = config.ModuleFaces[face_name] + child_node = cls.from_dict(child_payload) + node._set_face(face, child_node) + + return node diff --git a/src/ariel/utils/graph_ops.py b/src/ariel/utils/graph_ops.py new file mode 100644 index 00000000..5b8166b8 --- /dev/null +++ b/src/ariel/utils/graph_ops.py @@ -0,0 +1,52 @@ +""" +Graph operations for robot phenotype manipulation. + +Functions for converting between different graph representations. +""" + +import json +from typing import Union +import networkx as nx + + +def robot_json_to_digraph(json_data: Union[dict, str]) -> nx.DiGraph: + """ + Convert a robot JSON to a NetworkX directed graph. + + :param json_data: Robot data as dict or JSON string + :returns: NetworkX directed graph representation + """ + if isinstance(json_data, str): + robot_data = json.loads(json_data) + else: + robot_data = json_data + + graph = nx.DiGraph() + + # Add nodes with their attributes + for node in robot_data.get("nodes", []): + node_id = node["id"] + graph.add_node(node_id, + type=node["type"], + rotation=node["rotation"]) + + # Add edges with face information + for edge in robot_data.get("edges", []): + graph.add_edge(edge["source"], + edge["target"], + face=edge["face"]) + + return graph + + +def load_robot_json_file(file_path: str) -> nx.DiGraph: + """ + Load a robot JSON file and convert it to a NetworkX directed graph. + + :param file_path: Path to the JSON file + :returns: NetworkX directed graph representation + """ + with open(file_path, 'r') as f: + robot_data = json.load(f) + + return robot_json_to_digraph(robot_data) \ No newline at end of file diff --git a/src/ariel/utils/morphological_descriptor.py b/src/ariel/utils/morphological_descriptor.py index 0a25cab4..55ef26ac 100644 --- a/src/ariel/utils/morphological_descriptor.py +++ b/src/ariel/utils/morphological_descriptor.py @@ -727,11 +727,9 @@ def E(self) -> float: return self.length_of_limbs @property - def J(self): - """Joints J = j / j_max.""" - return self.joints - - @property - def S(self): - """Symmetry S = s.""" - return self.size + def P(self) -> float: + """Proportion P = p_s / p_l (only valid for 2D morphologies).""" + if self.is_2d: + return self.proportion_2d + else: + return 0.0 # Return 0 for 3D robots where proportion is not defined From 83ddbe781cd262c960cb3aea87d2a9e17cb12704 Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:05:01 +0100 Subject: [PATCH 38/47] Adding an upper class for Evoluationary Computing --- examples/offspring1.json | 73 +--------- examples/offspring2.json | 260 +++++++++++++++++++++++++++++++--- examples/offspring3.json | 48 ++----- examples/offspring4.json | 74 +++------- examples/parent1-mutated.json | 73 +--------- examples/parent1.json | 168 +--------------------- examples/parent2-mutated.json | 58 ++++---- examples/parent2.json | 28 ++-- src/ariel/ec/ec.py | 97 +++++++++++++ 9 files changed, 412 insertions(+), 467 deletions(-) create mode 100644 src/ariel/ec/ec.py diff --git a/examples/offspring1.json b/examples/offspring1.json index 41f28998..1adabd98 100644 --- a/examples/offspring1.json +++ b/examples/offspring1.json @@ -7,78 +7,7 @@ "type": "CORE", "rotation": "DEG_0", "id": 0 - }, - { - "type": "HINGE", - "rotation": "DEG_270", - "id": 1 - }, - { - "type": "NONE", - "rotation": "DEG_270", - "id": 2 - }, - { - "type": "HINGE", - "rotation": "DEG_180", - "id": 3 - }, - { - "type": "BRICK", - "rotation": "DEG_0", - "id": 4 - }, - { - "type": "BRICK", - "rotation": "DEG_270", - "id": 5 - }, - { - "type": "NONE", - "rotation": "DEG_225", - "id": 6 - }, - { - "type": "NONE", - "rotation": "DEG_90", - "id": 7 } ], - "edges": [ - { - "face": "RIGHT", - "source": 0, - "target": 1 - }, - { - "face": "LEFT", - "source": 0, - "target": 3 - }, - { - "face": "TOP", - "source": 0, - "target": 5 - }, - { - "face": "FRONT", - "source": 1, - "target": 2 - }, - { - "face": "FRONT", - "source": 3, - "target": 4 - }, - { - "face": "TOP", - "source": 5, - "target": 6 - }, - { - "face": "BOTTOM", - "source": 5, - "target": 7 - } - ] + "edges": [] } \ No newline at end of file diff --git a/examples/offspring2.json b/examples/offspring2.json index a129796b..d7695167 100644 --- a/examples/offspring2.json +++ b/examples/offspring2.json @@ -10,60 +10,175 @@ }, { "type": "BRICK", - "rotation": "DEG_135", + "rotation": "DEG_45", "id": 1 }, { - "type": "HINGE", - "rotation": "DEG_90", + "type": "BRICK", + "rotation": "DEG_45", "id": 2 }, { - "type": "NONE", - "rotation": "DEG_90", + "type": "BRICK", + "rotation": "DEG_45", "id": 3 }, { - "type": "NONE", - "rotation": "DEG_225", + "type": "BRICK", + "rotation": "DEG_45", "id": 4 }, { - "type": "HINGE", - "rotation": "DEG_135", + "type": "BRICK", + "rotation": "DEG_45", "id": 5 }, { - "type": "HINGE", - "rotation": "DEG_90", + "type": "BRICK", + "rotation": "DEG_45", "id": 6 }, { - "type": "NONE", - "rotation": "DEG_90", + "type": "BRICK", + "rotation": "DEG_45", "id": 7 }, { "type": "HINGE", - "rotation": "DEG_225", + "rotation": "DEG_180", "id": 8 + }, + { + "type": "HINGE", + "rotation": "DEG_270", + "id": 9 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 10 + }, + { + "type": "HINGE", + "rotation": "DEG_0", + "id": 11 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 12 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 13 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 14 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 15 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 16 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 17 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 18 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 19 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 20 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 21 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 22 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 23 + }, + { + "type": "BRICK", + "rotation": "DEG_0", + "id": 24 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 25 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 26 + }, + { + "type": "BRICK", + "rotation": "DEG_45", + "id": 27 + }, + { + "type": "NONE", + "rotation": "DEG_270", + "id": 28 + }, + { + "type": "HINGE", + "rotation": "DEG_0", + "id": 29 + }, + { + "type": "HINGE", + "rotation": "DEG_45", + "id": 30 } ], "edges": [ { - "face": "BACK", + "face": "FRONT", "source": 0, "target": 1 }, { "face": "LEFT", "source": 0, - "target": 7 + "target": 27 + }, + { + "face": "TOP", + "source": 0, + "target": 29 }, { "face": "BOTTOM", "source": 0, - "target": 8 + "target": 30 }, { "face": "FRONT", @@ -73,22 +188,127 @@ { "face": "LEFT", "source": 1, - "target": 3 + "target": 24 }, { - "face": "TOP", + "face": "BOTTOM", "source": 1, + "target": 26 + }, + { + "face": "FRONT", + "source": 2, + "target": 3 + }, + { + "face": "LEFT", + "source": 2, + "target": 21 + }, + { + "face": "BOTTOM", + "source": 2, + "target": 23 + }, + { + "face": "FRONT", + "source": 3, "target": 4 }, + { + "face": "LEFT", + "source": 3, + "target": 18 + }, { "face": "BOTTOM", - "source": 1, + "source": 3, + "target": 20 + }, + { + "face": "FRONT", + "source": 4, "target": 5 }, + { + "face": "LEFT", + "source": 4, + "target": 15 + }, + { + "face": "BOTTOM", + "source": 4, + "target": 17 + }, { "face": "FRONT", "source": 5, "target": 6 + }, + { + "face": "LEFT", + "source": 5, + "target": 12 + }, + { + "face": "BOTTOM", + "source": 5, + "target": 14 + }, + { + "face": "FRONT", + "source": 6, + "target": 7 + }, + { + "face": "LEFT", + "source": 6, + "target": 10 + }, + { + "face": "TOP", + "source": 6, + "target": 11 + }, + { + "face": "FRONT", + "source": 7, + "target": 8 + }, + { + "face": "TOP", + "source": 7, + "target": 9 + }, + { + "face": "BOTTOM", + "source": 12, + "target": 13 + }, + { + "face": "BOTTOM", + "source": 15, + "target": 16 + }, + { + "face": "BOTTOM", + "source": 18, + "target": 19 + }, + { + "face": "BOTTOM", + "source": 21, + "target": 22 + }, + { + "face": "BOTTOM", + "source": 24, + "target": 25 + }, + { + "face": "BOTTOM", + "source": 27, + "target": 28 } ] } \ No newline at end of file diff --git a/examples/offspring3.json b/examples/offspring3.json index 7c6cf971..f3cf286a 100644 --- a/examples/offspring3.json +++ b/examples/offspring3.json @@ -8,67 +8,37 @@ "rotation": "DEG_0", "id": 0 }, - { - "type": "BRICK", - "rotation": "DEG_0", - "id": 1 - }, - { - "type": "NONE", - "rotation": "DEG_225", - "id": 2 - }, { "type": "NONE", - "rotation": "DEG_270", - "id": 3 + "rotation": "DEG_45", + "id": 1 }, { "type": "HINGE", - "rotation": "DEG_270", - "id": 4 + "rotation": "DEG_0", + "id": 2 }, { "type": "NONE", "rotation": "DEG_0", - "id": 5 - }, - { - "type": "HINGE", - "rotation": "DEG_135", - "id": 6 + "id": 3 } ], "edges": [ - { - "face": "FRONT", - "source": 0, - "target": 1 - }, { "face": "BACK", "source": 0, - "target": 3 + "target": 1 }, { "face": "RIGHT", "source": 0, - "target": 4 - }, - { - "face": "TOP", - "source": 0, - "target": 5 + "target": 2 }, { - "face": "BOTTOM", + "face": "LEFT", "source": 0, - "target": 6 - }, - { - "face": "TOP", - "source": 1, - "target": 2 + "target": 3 } ] } \ No newline at end of file diff --git a/examples/offspring4.json b/examples/offspring4.json index cd31576f..2b7dcddf 100644 --- a/examples/offspring4.json +++ b/examples/offspring4.json @@ -9,54 +9,34 @@ "id": 0 }, { - "type": "NONE", - "rotation": "DEG_270", + "type": "HINGE", + "rotation": "DEG_90", "id": 1 }, { "type": "BRICK", - "rotation": "DEG_135", + "rotation": "DEG_180", "id": 2 }, { "type": "BRICK", - "rotation": "DEG_225", + "rotation": "DEG_180", "id": 3 }, { "type": "HINGE", - "rotation": "DEG_90", + "rotation": "DEG_0", "id": 4 }, { - "type": "HINGE", + "type": "BRICK", "rotation": "DEG_180", "id": 5 }, { - "type": "HINGE", - "rotation": "DEG_90", - "id": 6 - }, - { - "type": "NONE", - "rotation": "DEG_225", - "id": 7 - }, - { - "type": "NONE", - "rotation": "DEG_225", - "id": 8 - }, - { - "type": "NONE", - "rotation": "DEG_90", - "id": 9 - }, - { - "type": "NONE", + "type": "BRICK", "rotation": "DEG_225", - "id": 10 + "id": 6 } ], "edges": [ @@ -66,49 +46,29 @@ "target": 1 }, { - "face": "BACK", - "source": 0, - "target": 2 - }, - { - "face": "RIGHT", + "face": "LEFT", "source": 0, - "target": 8 + "target": 3 }, { - "face": "LEFT", + "face": "TOP", "source": 0, - "target": 9 + "target": 4 }, { "face": "BOTTOM", "source": 0, - "target": 10 - }, - { - "face": "RIGHT", - "source": 2, - "target": 3 - }, - { - "face": "LEFT", - "source": 2, - "target": 5 - }, - { - "face": "TOP", - "source": 2, "target": 6 }, { - "face": "BOTTOM", - "source": 2, - "target": 7 + "face": "FRONT", + "source": 1, + "target": 2 }, { "face": "FRONT", - "source": 3, - "target": 4 + "source": 4, + "target": 5 } ] } \ No newline at end of file diff --git a/examples/parent1-mutated.json b/examples/parent1-mutated.json index 41f28998..1adabd98 100644 --- a/examples/parent1-mutated.json +++ b/examples/parent1-mutated.json @@ -7,78 +7,7 @@ "type": "CORE", "rotation": "DEG_0", "id": 0 - }, - { - "type": "HINGE", - "rotation": "DEG_270", - "id": 1 - }, - { - "type": "NONE", - "rotation": "DEG_270", - "id": 2 - }, - { - "type": "HINGE", - "rotation": "DEG_180", - "id": 3 - }, - { - "type": "BRICK", - "rotation": "DEG_0", - "id": 4 - }, - { - "type": "BRICK", - "rotation": "DEG_270", - "id": 5 - }, - { - "type": "NONE", - "rotation": "DEG_225", - "id": 6 - }, - { - "type": "NONE", - "rotation": "DEG_90", - "id": 7 } ], - "edges": [ - { - "face": "RIGHT", - "source": 0, - "target": 1 - }, - { - "face": "LEFT", - "source": 0, - "target": 3 - }, - { - "face": "TOP", - "source": 0, - "target": 5 - }, - { - "face": "FRONT", - "source": 1, - "target": 2 - }, - { - "face": "FRONT", - "source": 3, - "target": 4 - }, - { - "face": "TOP", - "source": 5, - "target": 6 - }, - { - "face": "BOTTOM", - "source": 5, - "target": 7 - } - ] + "edges": [] } \ No newline at end of file diff --git a/examples/parent1.json b/examples/parent1.json index 7f6a3c84..00fc61de 100644 --- a/examples/parent1.json +++ b/examples/parent1.json @@ -8,187 +8,27 @@ "rotation": "DEG_0", "id": 0 }, - { - "type": "BRICK", - "rotation": "DEG_180", - "id": 1 - }, - { - "type": "BRICK", - "rotation": "DEG_180", - "id": 2 - }, - { - "type": "BRICK", - "rotation": "DEG_180", - "id": 3 - }, - { - "type": "BRICK", - "rotation": "DEG_90", - "id": 4 - }, - { - "type": "BRICK", - "rotation": "DEG_135", - "id": 5 - }, - { - "type": "BRICK", - "rotation": "DEG_90", - "id": 6 - }, { "type": "BRICK", "rotation": "DEG_135", - "id": 7 - }, - { - "type": "BRICK", - "rotation": "DEG_90", - "id": 8 - }, - { - "type": "BRICK", - "rotation": "DEG_225", - "id": 9 - }, - { - "type": "HINGE", - "rotation": "DEG_0", - "id": 10 - }, - { - "type": "BRICK", - "rotation": "DEG_90", - "id": 11 - }, - { - "type": "HINGE", - "rotation": "DEG_0", - "id": 12 - }, - { - "type": "HINGE", - "rotation": "DEG_45", - "id": 13 - }, - { - "type": "BRICK", - "rotation": "DEG_180", - "id": 14 - }, - { - "type": "BRICK", - "rotation": "DEG_180", - "id": 15 - }, - { - "type": "BRICK", - "rotation": "DEG_180", - "id": 16 + "id": 1 }, { "type": "HINGE", - "rotation": "DEG_0", - "id": 17 - }, - { - "type": "BRICK", - "rotation": "DEG_45", - "id": 18 + "rotation": "DEG_135", + "id": 2 } ], "edges": [ - { - "face": "FRONT", - "source": 0, - "target": 1 - }, { "face": "RIGHT", "source": 0, - "target": 17 + "target": 1 }, { "face": "TOP", "source": 0, - "target": 18 - }, - { - "face": "FRONT", - "source": 1, "target": 2 - }, - { - "face": "FRONT", - "source": 2, - "target": 3 - }, - { - "face": "RIGHT", - "source": 2, - "target": 4 - }, - { - "face": "TOP", - "source": 2, - "target": 16 - }, - { - "face": "FRONT", - "source": 4, - "target": 5 - }, - { - "face": "RIGHT", - "source": 4, - "target": 6 - }, - { - "face": "TOP", - "source": 4, - "target": 15 - }, - { - "face": "FRONT", - "source": 6, - "target": 7 - }, - { - "face": "RIGHT", - "source": 6, - "target": 8 - }, - { - "face": "TOP", - "source": 6, - "target": 14 - }, - { - "face": "TOP", - "source": 8, - "target": 9 - }, - { - "face": "BOTTOM", - "source": 8, - "target": 13 - }, - { - "face": "RIGHT", - "source": 9, - "target": 10 - }, - { - "face": "LEFT", - "source": 9, - "target": 11 - }, - { - "face": "BOTTOM", - "source": 9, - "target": 12 } ] } \ No newline at end of file diff --git a/examples/parent2-mutated.json b/examples/parent2-mutated.json index a129796b..68dd6888 100644 --- a/examples/parent2-mutated.json +++ b/examples/parent2-mutated.json @@ -9,18 +9,18 @@ "id": 0 }, { - "type": "BRICK", - "rotation": "DEG_135", + "type": "HINGE", + "rotation": "DEG_180", "id": 1 }, { "type": "HINGE", - "rotation": "DEG_90", + "rotation": "DEG_270", "id": 2 }, { - "type": "NONE", - "rotation": "DEG_90", + "type": "HINGE", + "rotation": "DEG_225", "id": 3 }, { @@ -30,40 +30,45 @@ }, { "type": "HINGE", - "rotation": "DEG_135", + "rotation": "DEG_0", "id": 5 }, { - "type": "HINGE", - "rotation": "DEG_90", + "type": "BRICK", + "rotation": "DEG_225", "id": 6 }, { "type": "NONE", - "rotation": "DEG_90", + "rotation": "DEG_0", "id": 7 - }, - { - "type": "HINGE", - "rotation": "DEG_225", - "id": 8 } ], "edges": [ { - "face": "BACK", + "face": "FRONT", "source": 0, "target": 1 }, + { + "face": "BACK", + "source": 0, + "target": 3 + }, { "face": "LEFT", "source": 0, - "target": 7 + "target": 4 + }, + { + "face": "TOP", + "source": 0, + "target": 5 }, { "face": "BOTTOM", "source": 0, - "target": 8 + "target": 6 }, { "face": "FRONT", @@ -72,23 +77,8 @@ }, { "face": "LEFT", - "source": 1, - "target": 3 - }, - { - "face": "TOP", - "source": 1, - "target": 4 - }, - { - "face": "BOTTOM", - "source": 1, - "target": 5 - }, - { - "face": "FRONT", - "source": 5, - "target": 6 + "source": 6, + "target": 7 } ] } \ No newline at end of file diff --git a/examples/parent2.json b/examples/parent2.json index def0eef4..a7445b90 100644 --- a/examples/parent2.json +++ b/examples/parent2.json @@ -9,46 +9,56 @@ "id": 0 }, { - "type": "NONE", - "rotation": "DEG_180", + "type": "BRICK", + "rotation": "DEG_45", "id": 1 }, { "type": "BRICK", - "rotation": "DEG_180", + "rotation": "DEG_135", "id": 2 }, { "type": "BRICK", - "rotation": "DEG_180", + "rotation": "DEG_135", "id": 3 }, { "type": "HINGE", "rotation": "DEG_45", "id": 4 + }, + { + "type": "HINGE", + "rotation": "DEG_135", + "id": 5 } ], "edges": [ { - "face": "RIGHT", + "face": "BACK", "source": 0, "target": 1 }, { - "face": "TOP", + "face": "RIGHT", "source": 0, "target": 2 }, { - "face": "TOP", - "source": 2, + "face": "LEFT", + "source": 0, "target": 3 }, { "face": "BOTTOM", - "source": 2, + "source": 0, "target": 4 + }, + { + "face": "FRONT", + "source": 4, + "target": 5 } ] } \ No newline at end of file diff --git a/src/ariel/ec/ec.py b/src/ariel/ec/ec.py new file mode 100644 index 00000000..c8ab9180 --- /dev/null +++ b/src/ariel/ec/ec.py @@ -0,0 +1,97 @@ +class EC: + Decoder = None + population = [] + population_size = 100 + population_fitness = [] + selected_parents = [] + fitness_selected_parents = [] + offsprings = [] + fitness_offsprings = [] + generation = 100 + has_crossover = True + has_mutation = True + + def __init__(individuals=100,generations=100,has_crossover=True,has_mutation=True): + self.generation=geneartions + self.population_size = individuals + self.initialize_population() + self.has_crossover = has_crossover + self.has_mutation = has_mutation + + def initialize_individual(): + return None + + def initialize_population(): + self.population=[] + for i in range(0.self.population_size): + self.population=np.append(self.population,initialize_individual()) + + def evaluate_individual(indice) + return 0 + + def evalutate_population(): + self.population_fitness=[] + for i in range(0,self.population_size): + self.population_fitness=np.append(self.population_fitness,evalute_individual(i)) + + def parent_selection(): + self.selected_parents = [] + self.fitness_selected_parents = [] + for i in range(0,self.population_size): + parent_1_id = np.choice(range(0,self.population_size)) + parent_2_id = np.choice(range(0,self.population_size)) + + parent_1 = population(parent_1_id) + fitness_parent_1 = population_fitness(parent_1_id) + parent_2 = population(parent_2_id) + fitness_parent_2 = population_fitness(parent_2_id) + + if fitness_parent_1>=fitness_parent_2: + self.selected_parents=np.append(self.selected_parents,parent_1) + self.fitness_selected_parents=np.append(self.fitness_selected_parents,fitness_parent_1) + else: + self.selected_parents=np.append(self.selected_parents,parent_2) + self.fitness_selected_parents=np.append(self.fitness_selected_parents,fitness_parent_2) + + def crossover_individual(parent_1,parent_2): + return parent_1,parent_2 + + def evaluate_offspring(offspring): + return 0 + + def crossover_population(): + self.offsprings=[] + self.fitness_offstrings=[] + for i in range(0,self.selected_parents,2): + parent_1=self.selected_parents[i] + parent_2=self.selected_parents[2] + offspring1,offspring2=self.crossover_individual(parent1,parent2) + self.offsprings=np.append(self.offsprings,offspring1) + self.offsprings=np.append(self.offsprings,offspring2) + self.fitness_offsprings=np.append(self.fitness_offsprings,offspring1) + self.fitness_offsprings=np.append(self.fitness_offsprings,offspring2) + + def selection_survivors(): + for i in range(0,self.population_size): + self.population=np.append(self.population,self.offsprings[i]) + self.population_fitness=np.append(self.population_fitness,self.fitness_offsprings[i]) + order = np.argsort(self.population_fitness) + self.population=self.population[order] + self.population_fitness=self.population_fitness[order] + self.population=self.population[:100] + self.population_fitness=self.population_fitness[0:100] + + def mutate_individual(i): + return self.offsprings[i] + + def mutate_offspring(): + for i in range(0,self.population_size): + self.offsprings[i]=self.mutate_offspring(i) + + def run_ec(): + self.initialize_population() + for i in range(0,self.generation): + self.parent_selection() + self.crossover_population() + self.mutate_offspring() + self.selection_survivors() From d097661a5b9500f95480d73b173c488b8f98ea4b Mon Sep 17 00:00:00 2001 From: Olivier Moulin <196188894+omoulin@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:10:54 +0100 Subject: [PATCH 39/47] Update l_system_genotype.py --- .../robogen_lite/decoders/l_system_genotype.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py index 7f4588e5..b04fde43 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py +++ b/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py @@ -172,9 +172,7 @@ def __init__(self): self.back = None self.left = None self.right = None - self.top = None - self.bottom = None - self.allowed_connection=['TOP','BOTTOM','LEFT','RIGHT','FRONT','BACK'] + self.allowed_connection=['LEFT','RIGHT','FRONT','BACK'] self.name='C' class LSystemDecoder: From d393e22cae0c2966a32b439e450eb21ca2428970 Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 30 Oct 2025 12:53:11 +0100 Subject: [PATCH 40/47] Evolution now works for a generic genotype --- examples/evolve.py | 21 +++++++++++++++++++-- src/ariel/ec/a004.py | 2 +- src/ariel/ec/genotypes/tree/tree_genome.py | 10 +++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/examples/evolve.py b/examples/evolve.py index 9c4f8e57..6033f6b6 100644 --- a/examples/evolve.py +++ b/examples/evolve.py @@ -7,6 +7,7 @@ import tomllib from typing import Literal, cast, TYPE_CHECKING from functools import partial +import matplotlib.pyplot as plt # Third-party libraries import numpy as np @@ -75,9 +76,9 @@ def parent_selection(population: Population, config: EASettings) -> Population: def crossover(population: Population, config: EASettings) -> Population: parents = [ind for ind in population if ind.tags.get("ps", False)] - for idx in range(0, len(parents), 2): + for idx in range(0, len(parents)-1, 2): parent_i = parents[idx] - parent_j = parents[idx] + parent_j = parents[idx + 1] genotype_i, genotype_j = config.crossover( config.genotype.value.from_json(parent_i.genotype), config.genotype.value.from_json(parent_j.genotype), @@ -231,6 +232,22 @@ def main() -> None: worst = ea.get_solution("worst", only_alive=False) console.log(worst) + fitnesses = [] + + for i in range(100): + ea.fetch_population(only_alive=False, best_comes=None, custom_logic=[Individual.time_of_birth==i]) + individuals = ea.population + avg_fitness = sum(ind.fitness for ind in individuals) / len(individuals) if individuals else 0 + console.log(f"Generation {i}: Avg Fitness = {avg_fitness}") + fitnesses.append(avg_fitness) + + # Line plot of the fitness + plt.plot(range(100), fitnesses, marker='o') + plt.title('Average Fitness Over Generations') + plt.xlabel('Generation') + plt.ylabel('Average Fitness') + plt.savefig('average_fitness_over_generations.png') + plt.show() if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/ariel/ec/a004.py b/src/ariel/ec/a004.py index e91125bb..a1e77adf 100644 --- a/src/ariel/ec/a004.py +++ b/src/ariel/ec/a004.py @@ -209,7 +209,7 @@ def fetch_population( (Individual.requires_eval != already_evaluated), ) if custom_logic is not None: - statement.where( + statement = statement.where( *custom_logic, ) diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index 17324b76..1d4a506b 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -212,7 +212,7 @@ def to_json(self, *, indent: int | None = 2) -> str: class TreeNode: - def __init__(self, module: config.ModuleInstance | None = None, depth: int = 0, node_id: int | None = None, + def __init__(self, module: config.ModuleInstance | None = None, depth: int = 0, module_type: config.ModuleType | None = None, module_rotation: config.ModuleRotationsIdx | None = None): if module is None: assert module_type is not None, "Module type cannot be None if module is not specified" @@ -232,7 +232,7 @@ def __init__(self, module: config.ModuleInstance | None = None, depth: int = 0, self._enable_replacement: bool = False - self._id = id(self) if node_id is None else node_id + self._id = id(self)# if node_id is None else node_id @property def id(self) -> int: @@ -553,7 +553,7 @@ def copy(self) -> 'TreeNode': ) # Create new node - new_node = TreeNode(new_module, depth=self._depth, node_id=self._id) + new_node = TreeNode(new_module, depth=self._depth,) # Recursively copy children for face, child in self.children.items(): @@ -591,7 +591,7 @@ def from_dict(cls, data: dict) -> "TreeNode": Rebuild a TreeNode (and its subtree) from dict. Uses _set_face so that ModuleInstance.links remains consistent. """ - node_id = data["id"] + #node_id = data["id"] depth = data["depth"] module_type = config.ModuleType[data["module_type"]] rotation = config.ModuleRotationsIdx[data["rotation"]] @@ -600,7 +600,7 @@ def from_dict(cls, data: dict) -> "TreeNode": node = cls( module=config.ModuleInstance(type=module_type, rotation=rotation, links={}), depth=depth, - node_id=node_id, + #node_id=node_id, ) # Rebuild children through _set_face so links are updated properly. From 65df189b6522fcc81ad336fce3a4b6f882a486cc Mon Sep 17 00:00:00 2001 From: DavidePasero Date: Tue, 4 Nov 2025 15:24:25 +0100 Subject: [PATCH 41/47] Merged Lsystem within new ec logic --- src/ariel/ec/a000.py | 122 +++++-- src/ariel/ec/a005.py | 335 ++++++++++++++++-- src/ariel/ec/ec.py | 97 ----- src/ariel/ec/ec_l_system.py | 3 +- src/ariel/ec/genotypes/lsystem/__init__.py | 0 .../genotypes/lsystem}/l_system_genotype.py | 28 +- src/ariel/ec/genotypes/tree/tree_genome.py | 7 +- 7 files changed, 419 insertions(+), 173 deletions(-) delete mode 100644 src/ariel/ec/ec.py create mode 100644 src/ariel/ec/genotypes/lsystem/__init__.py rename src/ariel/{body_phenotypes/robogen_lite/decoders => ec/genotypes/lsystem}/l_system_genotype.py (97%) diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/a000.py index ace587fb..48ce48c8 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/a000.py @@ -4,7 +4,8 @@ from abc import ABC, abstractmethod from collections.abc import Sequence from pathlib import Path -from typing import cast, TYPE_CHECKING +from typing import cast, TYPE_CHECKING, List +import random # Third-party libraries import numpy as np @@ -87,14 +88,22 @@ def choice( return cast("Integers", generated_values.astype(int).tolist()) class Mutation(ABC): + mutations_mapping: dict[str, function] = NotImplemented which_mutation: str = "" @classmethod def set_which_mutation(cls, mutation_type: str) -> None: cls.which_mutation = mutation_type + def __init_subclass__(cls): + super().__init_subclass__() + cls.mutations_mapping = { + name: getattr(cls, name) + for name, val in cls.__dict__.items() + if isinstance(val, staticmethod) + } + @classmethod - @abstractmethod def __call__( cls, individual: Genotype, @@ -114,29 +123,17 @@ def __call__( tuple[Genotype, Genotype] Two child genotypes resulting from the crossover. """ - pass - -class IntegerMutator(Mutation): - @classmethod - def __call__( - cls, - individual: Genotype, - **kwargs: dict, - ) -> Genotype: - if cls.which_mutation == "random_swap": - return cls.random_swap( - individual=individual, - **kwargs, - ) - elif cls.which_mutation == "integer_creep": - return cls.integer_creep( - individual=individual, - **kwargs, + if cls.which_mutation in cls.mutations_mapping: + return cls.mutations_mapping[cls.which_mutation]( + individual, + **kwargs ) else: msg = f"Mutation type '{cls.which_mutation}' not recognized." raise ValueError(msg) +class IntegerMutator(Mutation): + @staticmethod def random_swap( individual: Genotype, @@ -294,21 +291,6 @@ def integer_creep( # return genome class TreeMutator(Mutation): - - @classmethod - def __call__( - cls, - individual: Genotype, - **kwargs: dict, - ) -> Genotype: - if cls.which_mutation == "random_subtree_replacement": - return cls.random_subtree_replacement( - individual=individual, - **kwargs, - ) - else: - msg = f"Mutation type '{cls.which_mutation}' not recognized." - raise ValueError(msg) @staticmethod def _random_tree(max_depth: int = 2, branching_prob: float = 0.5) -> Genotype: @@ -347,6 +329,76 @@ def random_subtree_replacement( new_individual.root.replace_node(node_to_replace, new_subtree) return new_individual + +class LSystemMutator(Mutation): + + @staticmethod + def mutate_one_point_lsystem(lsystem,mut_rate,add_temperature=0.5): + op_completed = "" + if random.random()=0: + splitted_rules.pop(gene_to_change-1) + splitted_rules.pop(gene_to_change-1) + elif splitted_rules[gene_to_change][:4] in ['movf','movk','movl','movr','movt','movb']: + op_completed="REMOVED : "+splitted_rules[gene_to_change] + splitted_rules.pop(gene_to_change) + new_rule = "" + for j in range(0,len(splitted_rules)): + new_rule+=splitted_rules[j]+" " + if new_rule!="": + lsystem.rules[list(rules.keys())[rule_to_change]]=new_rule + else: + lsystem.rules[list(rules.keys())[rule_to_change]]=lsystem.rules[list(rules.keys())[rule_to_change]] + return op_completed + def test() -> None: """Entry point.""" diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 9287fed2..12077898 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -4,6 +4,7 @@ # Standard library from abc import ABC, abstractmethod from pathlib import Path +import random # Third-party libraries import numpy as np @@ -15,6 +16,8 @@ # Local libraries if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype + from ariel.ec.genotypes.tree.tree_genome import TreeGenome + from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder from ariel.ec.a000 import IntegersGenerator # Global constants @@ -32,14 +35,22 @@ RNG = np.random.default_rng(SEED) class Crossover(ABC): + crossovers_mapping: dict[str, function] = NotImplemented which_crossover: str = "" @classmethod def set_which_crossover(cls, crossover_type: str) -> None: cls.which_crossover = crossover_type + def __init_subclass__(cls): + super().__init_subclass__() + cls.crossovers_mapping = { + name: getattr(cls, name) + for name, val in cls.__dict__.items() + if isinstance(val, staticmethod) + } + @classmethod - @abstractmethod def __call__( cls, parent_i: Genotype, @@ -60,22 +71,17 @@ def __call__( tuple[Genotype, Genotype] Two child genotypes resulting from the crossover. """ - pass - -class IntegerCrossover(Crossover): - @classmethod - def __call__( - cls, - parent_i: Genotype, - parent_j: Genotype, - **kwargs: dict, - ) -> tuple[Genotype, Genotype]: - if cls.which_crossover == "one_point": - return cls.one_point(parent_i, parent_j, **kwargs) + if cls.which_crossover in cls.crossovers_mapping: + return cls.crossovers_mapping[cls.which_crossover]( + parent_i, + parent_j, + **kwargs + ) else: msg = f"Crossover type '{cls.which_crossover}' not recognized." raise ValueError(msg) +class IntegerCrossover(Crossover): @staticmethod def one_point( parent_i: Genotype, @@ -109,27 +115,12 @@ def one_point( return child1, child2 class TreeCrossover(Crossover): - @classmethod - def __call__( - cls, - parent_i: Genotype, - parent_j: Genotype, - **kwargs: dict, - ) -> tuple[Genotype, Genotype]: - if cls.which_crossover == "koza_default": - return cls.koza_default(parent_i, parent_j, **kwargs) - elif cls.which_crossover == "normal": - return cls.normal(parent_i, parent_j, **kwargs) - else: - msg = f"Crossover type '{cls.which_crossover}' not recognized." - raise ValueError(msg) - @staticmethod def koza_default( - parent_i: Genotype, - parent_j: Genotype, + parent_i: TreeGenome, + parent_j: TreeGenome, koza_internal_node_prob: float = 0.9, - ) -> tuple[Genotype, Genotype]: + ) -> tuple[TreeGenome, TreeGenome]: """ Koza default: - In Parent A: choose an internal node with high probability (e.g., 90%) excluding root. @@ -177,9 +168,9 @@ def koza_default( @staticmethod def normal( - parent_i: Genotype, - parent_j: Genotype, - ) -> tuple[Genotype, Genotype]: + parent_i: TreeGenome, + parent_j: TreeGenome, + ) -> tuple[TreeGenome, TreeGenome]: """ Normal tree crossover: - Pick a random node from Parent A (uniform over all nodes). @@ -217,6 +208,282 @@ def normal( parent_i = parent_i_old parent_j = parent_j_old return child1, child2 + +class LSystemCrossover(Crossover): + @staticmethod + def crossover_uniform_rules_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): + from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder + axiom_offspring1="C" + axiom_offspring2="C" + rules_offspring1={} + rules_offspring2={} + iter_offspring1=0 + iter_offspring2=0 + if random.random()>mutation_rate: + rules_offspring1['C']=lsystem_parent2.rules['C'] + rules_offspring2['C']=lsystem_parent1.rules['C'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['C']=lsystem_parent1.rules['C'] + rules_offspring2['C']=lsystem_parent2.rules['C'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + if random.random()>mutation_rate: + rules_offspring1['B']=lsystem_parent2.rules['B'] + rules_offspring2['B']=lsystem_parent1.rules['B'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['B']=lsystem_parent1.rules['B'] + rules_offspring2['B']=lsystem_parent2.rules['B'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + if random.random()>mutation_rate: + rules_offspring1['H']=lsystem_parent2.rules['H'] + rules_offspring2['H']=lsystem_parent1.rules['H'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['H']=lsystem_parent1.rules['H'] + rules_offspring2['H']=lsystem_parent2.rules['H'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + if random.random()>mutation_rate: + rules_offspring1['N']=lsystem_parent2.rules['N'] + rules_offspring2['N']=lsystem_parent1.rules['N'] + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + else: + rules_offspring1['N']=lsystem_parent1.rules['N'] + rules_offspring2['N']=lsystem_parent2.rules['N'] + iter_offspring1+=lsystem_parent1.iterations + iter_offspring2+=lsystem_parent2.iterations + iteration_offspring1=int(iter_offspring1/4) + iteration_offspring2=int(iter_offspring2/4) + offspring1=LSystemDecoder(axiom_offspring1,rules_offspring1,iteration_offspring1,lsystem_parent1.max_elements,lsystem_parent1.max_depth,lsystem_parent1.verbose) + offspring2=LSystemDecoder(axiom_offspring2,rules_offspring2,iteration_offspring2,lsystem_parent2.max_elements,lsystem_parent2.max_depth,lsystem_parent2.verbose) + return offspring1,offspring2 + + @staticmethod + def crossover_uniform_genes_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): + from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder + axiom_offspring1="C" + axiom_offspring2="C" + rules_offspring1={} + rules_offspring2={} + iter_offspring1=0 + iter_offspring2=0 + + rules_parent1 = lsystem_parent1.rules["C"].split() + rules_parent2 = lsystem_parent2.rules["C"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='C': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='C': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['C']=r_offspring1 + rules_offspring2['C']=r_offspring2 + + rules_parent1 = lsystem_parent1.rules["B"].split() + rules_parent2 = lsystem_parent2.rules["B"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='B': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='B': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['B']=r_offspring1 + rules_offspring2['B']=r_offspring2 + + rules_parent1 = lsystem_parent1.rules["H"].split() + rules_parent2 = lsystem_parent2.rules["H"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='H': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='H': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['H']=r_offspring1 + rules_offspring2['H']=r_offspring2 + + rules_parent1 = lsystem_parent1.rules["N"].split() + rules_parent2 = lsystem_parent2.rules["N"].split() + enh_parent1 = [] + enh_parent2 = [] + i = 0 + while i < len(rules_parent1): + if rules_parent1[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent1[i] + " " + rules_parent1[i+1] + enh_parent1.append(new_token) + i+=1 + if rules_parent1[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent1.append(rules_parent1[i]) + if rules_parent1[i]=='N': + enh_parent1.append(rules_parent1[i]) + i+=1 + i = 0 + while i < len(rules_parent2): + if rules_parent2[i][:4] in ['addf','addk','addl','addr','addb','addt']: + new_token= rules_parent2[i] + " " + rules_parent2[i+1] + enh_parent2.append(new_token) + i+=1 + if rules_parent2[i][:4] in ['movf','movk','movl','movr','movb','movt']: + enh_parent2.append(rules_parent2[i]) + if rules_parent2[i]=='N': + enh_parent2.append(rules_parent2[i]) + i+=1 + r_offspring1="" + r_offspring2="" + le_common = min(len(enh_parent1),len(enh_parent2)) + for i in range(0,le_common): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[i]+" " + r_offspring2+=enh_parent1[i]+" " + else: + r_offspring1+=enh_parent1[i]+" " + r_offspring2+=enh_parent2[i]+" " + if len(enh_parent1)>le_common: + for j in range(le_common,len(enh_parent1)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent1[j]+" " + else: + r_offspring2+=enh_parent1[j]+" " + if len(enh_parent2)>le_common: + for j in range(le_common,len(enh_parent2)): + if random.random()>mutation_rate: + r_offspring1+=enh_parent2[j]+" " + else: + r_offspring2+=enh_parent2[j]+" " + rules_offspring1['N']=r_offspring1 + rules_offspring2['N']=r_offspring2 + + iter_offspring1+=lsystem_parent2.iterations + iter_offspring2+=lsystem_parent1.iterations + offspring1=LSystemDecoder(axiom_offspring1,rules_offspring1,iter_offspring1,lsystem_parent1.max_elements,lsystem_parent1.max_depth,lsystem_parent1.verbose) + offspring2=LSystemDecoder(axiom_offspring2,rules_offspring2,iter_offspring2,lsystem_parent2.max_elements,lsystem_parent2.max_depth,lsystem_parent2.verbose) + return offspring1,offspring2 def tree_main(): diff --git a/src/ariel/ec/ec.py b/src/ariel/ec/ec.py deleted file mode 100644 index c8ab9180..00000000 --- a/src/ariel/ec/ec.py +++ /dev/null @@ -1,97 +0,0 @@ -class EC: - Decoder = None - population = [] - population_size = 100 - population_fitness = [] - selected_parents = [] - fitness_selected_parents = [] - offsprings = [] - fitness_offsprings = [] - generation = 100 - has_crossover = True - has_mutation = True - - def __init__(individuals=100,generations=100,has_crossover=True,has_mutation=True): - self.generation=geneartions - self.population_size = individuals - self.initialize_population() - self.has_crossover = has_crossover - self.has_mutation = has_mutation - - def initialize_individual(): - return None - - def initialize_population(): - self.population=[] - for i in range(0.self.population_size): - self.population=np.append(self.population,initialize_individual()) - - def evaluate_individual(indice) - return 0 - - def evalutate_population(): - self.population_fitness=[] - for i in range(0,self.population_size): - self.population_fitness=np.append(self.population_fitness,evalute_individual(i)) - - def parent_selection(): - self.selected_parents = [] - self.fitness_selected_parents = [] - for i in range(0,self.population_size): - parent_1_id = np.choice(range(0,self.population_size)) - parent_2_id = np.choice(range(0,self.population_size)) - - parent_1 = population(parent_1_id) - fitness_parent_1 = population_fitness(parent_1_id) - parent_2 = population(parent_2_id) - fitness_parent_2 = population_fitness(parent_2_id) - - if fitness_parent_1>=fitness_parent_2: - self.selected_parents=np.append(self.selected_parents,parent_1) - self.fitness_selected_parents=np.append(self.fitness_selected_parents,fitness_parent_1) - else: - self.selected_parents=np.append(self.selected_parents,parent_2) - self.fitness_selected_parents=np.append(self.fitness_selected_parents,fitness_parent_2) - - def crossover_individual(parent_1,parent_2): - return parent_1,parent_2 - - def evaluate_offspring(offspring): - return 0 - - def crossover_population(): - self.offsprings=[] - self.fitness_offstrings=[] - for i in range(0,self.selected_parents,2): - parent_1=self.selected_parents[i] - parent_2=self.selected_parents[2] - offspring1,offspring2=self.crossover_individual(parent1,parent2) - self.offsprings=np.append(self.offsprings,offspring1) - self.offsprings=np.append(self.offsprings,offspring2) - self.fitness_offsprings=np.append(self.fitness_offsprings,offspring1) - self.fitness_offsprings=np.append(self.fitness_offsprings,offspring2) - - def selection_survivors(): - for i in range(0,self.population_size): - self.population=np.append(self.population,self.offsprings[i]) - self.population_fitness=np.append(self.population_fitness,self.fitness_offsprings[i]) - order = np.argsort(self.population_fitness) - self.population=self.population[order] - self.population_fitness=self.population_fitness[order] - self.population=self.population[:100] - self.population_fitness=self.population_fitness[0:100] - - def mutate_individual(i): - return self.offsprings[i] - - def mutate_offspring(): - for i in range(0,self.population_size): - self.offsprings[i]=self.mutate_offspring(i) - - def run_ec(): - self.initialize_population() - for i in range(0,self.generation): - self.parent_selection() - self.crossover_population() - self.mutate_offspring() - self.selection_survivors() diff --git a/src/ariel/ec/ec_l_system.py b/src/ariel/ec/ec_l_system.py index a554e2a3..d4e1d064 100644 --- a/src/ariel/ec/ec_l_system.py +++ b/src/ariel/ec/ec_l_system.py @@ -25,8 +25,7 @@ # Local libraries -from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType -from ariel.body_phenotypes.robogen_lite.decoders.l_system_genotype import LSystemDecoder +from ariel.src.ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder SEED = 42 DPI = 300 diff --git a/src/ariel/ec/genotypes/lsystem/__init__.py b/src/ariel/ec/genotypes/lsystem/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py similarity index 97% rename from src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py rename to src/ariel/ec/genotypes/lsystem/l_system_genotype.py index b04fde43..803d7246 100644 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/l_system_genotype.py +++ b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py @@ -19,11 +19,11 @@ """ # Standard library - +from __future__ import annotations import json import re from pathlib import Path -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, TYPE_CHECKING from enum import Enum # Third-party libraries @@ -35,6 +35,11 @@ # Local libraries from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType, ModuleInstance,ModuleRotationsIdx +from ariel.ec.a000 import LSystemMutator +from ariel.ec.a005 import LSystemCrossover + +if TYPE_CHECKING: + from ariel.ec.genotypes.genotype import Genotype SEED = 42 DPI = 300 @@ -175,7 +180,7 @@ def __init__(self): self.allowed_connection=['LEFT','RIGHT','FRONT','BACK'] self.name='C' -class LSystemDecoder: +class LSystemDecoder(Genotype): """Implements an L-system-based decoder for modular robot graphs.""" def __init__( @@ -201,6 +206,23 @@ def __init__( self.max_depth = max_depth self.verbose=verbose + @staticmethod + def get_crossover_object() -> LSystemCrossover: + return LSystemCrossover() + + @staticmethod + def get_mutator_object() -> LSystemMutator: + return LSystemMutator() + + @staticmethod + def create_individual(): + pass # Implementation + + @staticmethod + def to_digraph(robot: LSystemDecoder): + robot.generate_lsystem_graph() + return robot.graph + def expand_lsystem(self): expanded_token = [] expanded_token.append(self.axiom) diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index 1d4a506b..a74faf80 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -6,15 +6,18 @@ from ariel.ec.a000 import TreeMutator from ariel.ec.a005 import TreeCrossover import networkx as nx - from collections.abc import Callable from functools import reduce import numpy as np +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ariel.ec.genotypes.genotype import Genotype SEED = 42 RNG = np.random.default_rng(SEED) -class TreeGenome: +class TreeGenome(Genotype): def __init__(self, root: TreeNode | None = None): self._root = root From c0086b98fc65ccf6c0ff95c00000c15a1e0986e3 Mon Sep 17 00:00:00 2001 From: DavidePasero Date: Tue, 4 Nov 2025 15:47:52 +0100 Subject: [PATCH 42/47] Merged and code refactored --- examples/evolve.py | 9 ++------- requirements.txt | 15 +++++++++++++++ src/ariel/ec/a005.py | 1 + src/ariel/ec/genotypes/genotype.py | 7 +------ src/ariel/ec/genotypes/genotype_mapping.py | 7 +++++++ 5 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 requirements.txt create mode 100644 src/ariel/ec/genotypes/genotype_mapping.py diff --git a/examples/evolve.py b/examples/evolve.py index 6033f6b6..8454448b 100644 --- a/examples/evolve.py +++ b/examples/evolve.py @@ -20,7 +20,7 @@ from ariel.ec.a004 import EAStep, EA from ariel.ec.a000 import Mutation from ariel.ec.a005 import Crossover -from ariel.ec.genotypes.genotype import GenotypeEnum +from ariel.ec.genotypes.genotype_mapping import GENOTYPES_MAPPING from morphology_fitness_analysis import compute_6d_descriptor, load_target_robot, compute_fitness_scores # Global constants @@ -166,12 +166,7 @@ def read_config_file() -> EASettings: target_robot_path = cfg["task"]["evolve_to_copy"]["target_robot_path"] if task == "evolve_to_copy" else None - if gname == 'tree': - genotype = GenotypeEnum.TREE - elif gname == 'lsystem': - pass - elif gname == 'integers': - pass + genotype = GENOTYPES_MAPPING[gname] mutation = genotype.value.get_mutator_object() mutation.set_which_mutation(mutation_name) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..3d9939d7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +matplotlib==3.10.7 +mypyc==0.1.0 +networkx==3.5 +nicegui==3.2.0 +noise==1.2.2 +nox==2025.10.16 +numpy==2.3.4 +Pillow==12.0.0 +pydantic==2.12.3 +pydantic_settings==2.11.0 +rich==14.2.0 +scikit_learn==1.7.2 +setuptools==80.9.0 +SQLAlchemy==2.0.44 +sqlmodel==0.0.27 diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index 12077898..ce4e19ef 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -20,6 +20,7 @@ from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder from ariel.ec.a000 import IntegersGenerator + # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] CWD = Path.cwd() diff --git a/src/ariel/ec/genotypes/genotype.py b/src/ariel/ec/genotypes/genotype.py index bfe29ca6..c853c043 100644 --- a/src/ariel/ec/genotypes/genotype.py +++ b/src/ariel/ec/genotypes/genotype.py @@ -2,17 +2,12 @@ from abc import ABC, abstractmethod from enum import Enum from typing import TYPE_CHECKING + if TYPE_CHECKING: from ariel.ec.a000 import Mutation from ariel.ec.a005 import Crossover -from ariel.ec.genotypes.tree.tree_genome import TreeGenome import networkx as nx - -class GenotypeEnum(Enum): - TREE = TreeGenome - #LSYSTEM = LSystemGenome # Future implementation - class Genotype(ABC): """Interface for different genotype types.""" diff --git a/src/ariel/ec/genotypes/genotype_mapping.py b/src/ariel/ec/genotypes/genotype_mapping.py new file mode 100644 index 00000000..4d401175 --- /dev/null +++ b/src/ariel/ec/genotypes/genotype_mapping.py @@ -0,0 +1,7 @@ +from ariel.ec.genotypes.tree.tree_genome import TreeGenome +from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder + +GENOTYPES_MAPPING = { + 'tree': TreeGenome, + 'lsystem': LSystemDecoder, +} \ No newline at end of file From 022bdc1db7409eef3974ee27de7fd0b6551da13e Mon Sep 17 00:00:00 2001 From: DavidePasero Date: Tue, 4 Nov 2025 18:15:43 +0100 Subject: [PATCH 43/47] Refactored circular import --- src/ariel/ec/a005.py | 5 +---- src/ariel/ec/genotypes/lsystem/l_system_genotype.py | 6 ++++-- src/ariel/ec/genotypes/tree/tree_genome.py | 6 ++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/a005.py index ce4e19ef..6ac3bc79 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/a005.py @@ -14,13 +14,12 @@ from typing import TYPE_CHECKING # Local libraries +from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype from ariel.ec.genotypes.tree.tree_genome import TreeGenome - from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder from ariel.ec.a000 import IntegersGenerator - # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] CWD = Path.cwd() @@ -213,7 +212,6 @@ def normal( class LSystemCrossover(Crossover): @staticmethod def crossover_uniform_rules_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): - from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder axiom_offspring1="C" axiom_offspring2="C" rules_offspring1={} @@ -268,7 +266,6 @@ def crossover_uniform_rules_lsystem(lsystem_parent1,lsystem_parent2,mutation_rat @staticmethod def crossover_uniform_genes_lsystem(lsystem_parent1,lsystem_parent2,mutation_rate): - from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder axiom_offspring1="C" axiom_offspring2="C" rules_offspring1={} diff --git a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py index 803d7246..b03574fc 100644 --- a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py +++ b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py @@ -35,11 +35,11 @@ # Local libraries from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType, ModuleInstance,ModuleRotationsIdx -from ariel.ec.a000 import LSystemMutator -from ariel.ec.a005 import LSystemCrossover if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype + from ariel.ec.a000 import LSystemMutator + from ariel.ec.a005 import LSystemCrossover SEED = 42 DPI = 300 @@ -208,10 +208,12 @@ def __init__( @staticmethod def get_crossover_object() -> LSystemCrossover: + from ariel.ec.a005 import LSystemCrossover return LSystemCrossover() @staticmethod def get_mutator_object() -> LSystemMutator: + from ariel.ec.a000 import LSystemMutator return LSystemMutator() @staticmethod diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index a74faf80..ddc09221 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -3,8 +3,6 @@ import ariel.body_phenotypes.robogen_lite.config as config import contextlib from collections import deque -from ariel.ec.a000 import TreeMutator -from ariel.ec.a005 import TreeCrossover import networkx as nx from collections.abc import Callable from functools import reduce @@ -13,6 +11,8 @@ if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype + from ariel.ec.a005 import TreeCrossover + from ariel.ec.a000 import TreeMutator SEED = 42 RNG = np.random.default_rng(SEED) @@ -23,11 +23,13 @@ def __init__(self, root: TreeNode | None = None): @staticmethod def get_crossover_object() -> TreeCrossover: + from ariel.ec.a005 import TreeCrossover """Return the crossover operator for tree genomes.""" return TreeCrossover() @staticmethod def get_mutator_object() -> TreeMutator: + from ariel.ec.a000 import TreeMutator """Return the mutator operator for tree genomes.""" return TreeMutator() From 26aa86d2d808436c828ed2fbf5165662f4cb682a Mon Sep 17 00:00:00 2001 From: DavidePasero Date: Wed, 5 Nov 2025 14:40:40 +0100 Subject: [PATCH 44/47] Create individual for lsystem as well --- examples/config.toml | 7 ++ examples/evolve.py | 4 +- src/ariel/ec/a002.py | 2 +- src/ariel/ec/a003.py | 2 +- src/ariel/ec/a004.py | 2 +- src/ariel/ec/{a005.py => crossovers.py} | 2 +- src/ariel/ec/genotypes/genotype.py | 6 +- .../ec/genotypes/integers/integers_genome.py | 64 +++++++++++++++++++ .../ec/genotypes/lsystem/l_system_genotype.py | 27 ++++++-- src/ariel/ec/genotypes/tree/tree_genome.py | 10 +-- src/ariel/ec/{a000.py => mutations.py} | 56 ---------------- 11 files changed, 106 insertions(+), 76 deletions(-) rename src/ariel/ec/{a005.py => crossovers.py} (99%) create mode 100644 src/ariel/ec/genotypes/integers/integers_genome.py rename src/ariel/ec/{a000.py => mutations.py} (91%) diff --git a/examples/config.toml b/examples/config.toml index 4071c8e8..84fa4a28 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -53,3 +53,10 @@ max_depth = 8 mutation_rate = 0.1 crossover_rate = 0.9 +[genotypes.lsystem] +allowed_mutations = ["mutate_one_point_lsystem"] +allowed_crossovers = ["crossover_uniform_rules_lsystem", "crossover_uniform_genes_lsystem"] + +[genotypes.tree.defaults] +mutation = "mutate_one_point_lsystem" +crossover = "crossover_uniform_rules_lsystem" \ No newline at end of file diff --git a/examples/evolve.py b/examples/evolve.py index 8454448b..05010317 100644 --- a/examples/evolve.py +++ b/examples/evolve.py @@ -18,8 +18,8 @@ # Local libraries from ariel.ec.a001 import Individual from ariel.ec.a004 import EAStep, EA -from ariel.ec.a000 import Mutation -from ariel.ec.a005 import Crossover +from ariel.ec.mutations import Mutation +from ariel.ec.crossovers import Crossover from ariel.ec.genotypes.genotype_mapping import GENOTYPES_MAPPING from morphology_fitness_analysis import compute_6d_descriptor, load_target_robot, compute_fitness_scores diff --git a/src/ariel/ec/a002.py b/src/ariel/ec/a002.py index 25727c57..47152517 100644 --- a/src/ariel/ec/a002.py +++ b/src/ariel/ec/a002.py @@ -13,7 +13,7 @@ from sqlmodel import Session, select # Local libraries -from ariel.ec.a000 import IntegersGenerator +from ariel.src.ariel.ec.mutations import IntegersGenerator from ariel.ec.a001 import Individual, init_database # Global constants diff --git a/src/ariel/ec/a003.py b/src/ariel/ec/a003.py index b0a8ae62..4239f00a 100644 --- a/src/ariel/ec/a003.py +++ b/src/ariel/ec/a003.py @@ -19,7 +19,7 @@ from sqlmodel import Session, SQLModel, col, select # Local libraries -from ariel.ec.a000 import IntegersGenerator +from ariel.src.ariel.ec.mutations import IntegersGenerator from ariel.ec.a001 import Individual # Third-party libraries diff --git a/src/ariel/ec/a004.py b/src/ariel/ec/a004.py index a1e77adf..f272c34d 100644 --- a/src/ariel/ec/a004.py +++ b/src/ariel/ec/a004.py @@ -22,7 +22,7 @@ from sqlmodel import Session, SQLModel, col, select # Local libraries -from ariel.ec.a000 import IntegerMutator, IntegersGenerator +from ariel.src.ariel.ec.mutations import IntegerMutator, IntegersGenerator from ariel.ec.a001 import Individual # Global constants diff --git a/src/ariel/ec/a005.py b/src/ariel/ec/crossovers.py similarity index 99% rename from src/ariel/ec/a005.py rename to src/ariel/ec/crossovers.py index 6ac3bc79..695e5682 100644 --- a/src/ariel/ec/a005.py +++ b/src/ariel/ec/crossovers.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype from ariel.ec.genotypes.tree.tree_genome import TreeGenome -from ariel.ec.a000 import IntegersGenerator +from ariel.src.ariel.ec.mutations import IntegersGenerator # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] diff --git a/src/ariel/ec/genotypes/genotype.py b/src/ariel/ec/genotypes/genotype.py index c853c043..d77932db 100644 --- a/src/ariel/ec/genotypes/genotype.py +++ b/src/ariel/ec/genotypes/genotype.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from ariel.ec.a000 import Mutation - from ariel.ec.a005 import Crossover + from ariel.src.ariel.ec.mutations import Mutation + from ariel.src.ariel.ec.crossovers import Crossover import networkx as nx class Genotype(ABC): @@ -25,7 +25,7 @@ def get_mutator_object() -> "Mutation": @staticmethod @abstractmethod - def create_individual() -> "Genotype": + def create_individual(**kwargs: dict) -> "Genotype": """Generate a new individual of this genotype type.""" raise NotImplementedError("Individual generation not implemented for this genotype type.") diff --git a/src/ariel/ec/genotypes/integers/integers_genome.py b/src/ariel/ec/genotypes/integers/integers_genome.py new file mode 100644 index 00000000..4a06b1bc --- /dev/null +++ b/src/ariel/ec/genotypes/integers/integers_genome.py @@ -0,0 +1,64 @@ +from collections.abc import Sequence +from typing import cast +import numpy as np +from ariel.ec.genotypes.genotype import Genotype +from ariel.ec.mutations import IntegerMutator +from ariel.ec.crossovers import IntegerCrossover +from pydantic import BaseSettings + +SEED = 42 +RNG = np.random.default_rng(SEED) +# Type Aliases +type Integers = Sequence[int] +type Floats = Sequence[float] + +class IntegersGenome(Genotype): + + @staticmethod + def get_crossover_object() -> "IntegerCrossover": + return IntegerCrossover() + + @staticmethod + def get_mutator_object() -> "IntegerMutator": + return IntegerMutator() + + @staticmethod + def create_individual( + low: int, + high: int, + size: int | Sequence[int] | None = 1, + *, + endpoint: bool | None = None, + ) -> Integers: + endpoint = endpoint + generated_values = RNG.integers( + low=low, + high=high, + size=size, + endpoint=endpoint, + ) + return cast("Integers", generated_values.astype(int).tolist()) + + @staticmethod + def choice( + value_set: int | Integers, + size: int | Sequence[int] | None = 1, + probabilities: Sequence[float] | None = None, + axis: int = 0, + *, + replace: bool | None = None, + shuffle: bool | None = None, + ) -> Integers: + replace = replace + shuffle = shuffle + generated_values = np.array( + RNG.choice( + a=value_set, + size=size, + replace=replace, + p=probabilities, + axis=axis, + shuffle=shuffle, + ), + ) + return cast("Integers", generated_values.astype(int).tolist()) diff --git a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py index b03574fc..0cc68fab 100644 --- a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py +++ b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py @@ -38,8 +38,8 @@ if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype - from ariel.ec.a000 import LSystemMutator - from ariel.ec.a005 import LSystemCrossover + from ariel.src.ariel.ec.mutations import LSystemMutator + from ariel.src.ariel.ec.crossovers import LSystemCrossover SEED = 42 DPI = 300 @@ -208,17 +208,32 @@ def __init__( @staticmethod def get_crossover_object() -> LSystemCrossover: - from ariel.ec.a005 import LSystemCrossover + from ariel.src.ariel.ec.crossovers import LSystemCrossover return LSystemCrossover() @staticmethod def get_mutator_object() -> LSystemMutator: - from ariel.ec.a000 import LSystemMutator + from ariel.src.ariel.ec.mutations import LSystemMutator return LSystemMutator() @staticmethod - def create_individual(): - pass # Implementation + def create_individual( + iterations: int = 2, + max_elements: int = 32, + max_depth: int = 8, + verbose: int = 0 + ) -> LSystemDecoder: + indiv = LSystemDecoder( + axiom="C", + rules={}, + iterations=iterations, + max_elements=max_elements, + max_depth=max_depth, + verbose=verbose, + ) + # Materialize expanded_token, structure, and graph + indiv.refresh() + return indiv @staticmethod def to_digraph(robot: LSystemDecoder): diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index ddc09221..bfbba2a8 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -11,8 +11,8 @@ if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype - from ariel.ec.a005 import TreeCrossover - from ariel.ec.a000 import TreeMutator + from ariel.src.ariel.ec.crossovers import TreeCrossover + from ariel.src.ariel.ec.mutations import TreeMutator SEED = 42 RNG = np.random.default_rng(SEED) @@ -23,18 +23,18 @@ def __init__(self, root: TreeNode | None = None): @staticmethod def get_crossover_object() -> TreeCrossover: - from ariel.ec.a005 import TreeCrossover + from ariel.src.ariel.ec.crossovers import TreeCrossover """Return the crossover operator for tree genomes.""" return TreeCrossover() @staticmethod def get_mutator_object() -> TreeMutator: - from ariel.ec.a000 import TreeMutator + from ariel.src.ariel.ec.mutations import TreeMutator """Return the mutator operator for tree genomes.""" return TreeMutator() @staticmethod - def create_individual() -> TreeGenome: + def create_individual(**kwargs: dict) -> TreeGenome: """Generate a new TreeGenome individual.""" return TreeGenome.default_init() diff --git a/src/ariel/ec/a000.py b/src/ariel/ec/mutations.py similarity index 91% rename from src/ariel/ec/a000.py rename to src/ariel/ec/mutations.py index 48ce48c8..a14f6c82 100644 --- a/src/ariel/ec/a000.py +++ b/src/ariel/ec/mutations.py @@ -31,62 +31,6 @@ console = Console() RNG = np.random.default_rng(SEED) -# Type Aliases -type Integers = Sequence[int] -type Floats = Sequence[float] - - -class IntegersGeneratorSettings(BaseSettings): - integers_endpoint: bool = True - choice_replace: bool = True - choice_shuffle: bool = False - - -config = IntegersGeneratorSettings() - - -class IntegersGenerator: - @staticmethod - def integers( - low: int, - high: int, - size: int | Sequence[int] | None = 1, - *, - endpoint: bool | None = None, - ) -> Integers: - endpoint = endpoint or config.integers_endpoint - generated_values = RNG.integers( - low=low, - high=high, - size=size, - endpoint=endpoint, - ) - return cast("Integers", generated_values.astype(int).tolist()) - - @staticmethod - def choice( - value_set: int | Integers, - size: int | Sequence[int] | None = 1, - probabilities: Sequence[float] | None = None, - axis: int = 0, - *, - replace: bool | None = None, - shuffle: bool | None = None, - ) -> Integers: - replace = replace or config.choice_replace - shuffle = shuffle or config.choice_shuffle - generated_values = np.array( - RNG.choice( - a=value_set, - size=size, - replace=replace, - p=probabilities, - axis=axis, - shuffle=shuffle, - ), - ) - return cast("Integers", generated_values.astype(int).tolist()) - class Mutation(ABC): mutations_mapping: dict[str, function] = NotImplemented which_mutation: str = "" From e4284ae434d39bebbae1859eda14ae1195ff43ee Mon Sep 17 00:00:00 2001 From: Davide Date: Wed, 5 Nov 2025 15:59:35 +0100 Subject: [PATCH 45/47] Better config, better merged code --- examples/config.toml | 22 ++-- examples/evolve.py | 34 +++--- examples/morphology_fitness_analysis.py | 3 +- .../robogen_lite/decoders/tree_decoder.py | 107 ------------------ src/ariel/ec/a002.py | 1 - src/ariel/ec/a003.py | 1 - src/ariel/ec/a004.py | 2 +- src/ariel/ec/crossovers.py | 1 - src/ariel/ec/ec_l_system.py | 2 +- src/ariel/ec/genotypes/genotype.py | 16 ++- .../ec/genotypes/lsystem/l_system_genotype.py | 39 ++++++- src/ariel/ec/genotypes/tree/tree_genome.py | 47 ++++---- src/ariel/ec/mutations.py | 4 +- 13 files changed, 113 insertions(+), 166 deletions(-) delete mode 100644 src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py diff --git a/examples/config.toml b/examples/config.toml index 84fa4a28..ea6ac356 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -5,7 +5,7 @@ [run] # Choose which genotype profile to use for this run. # One of: "integers", "tree", "lsystem", "cppn" -genotype = "tree" +genotype = "lsystem" task = "evolve_to_copy" @@ -48,15 +48,23 @@ allowed_crossovers = ["koza_default", "normal"] mutation = "random_subtree_replacement" crossover = "koza_default" -[genotypes.tree.params] -max_depth = 8 -mutation_rate = 0.1 -crossover_rate = 0.9 +[genotypes.tree.mutation.params] +max_subtree_depth = 2 +branching_prob = 0.5 + +[genotypes.tree.crossover.params] +koza_internal_node_prob = 0.9 [genotypes.lsystem] allowed_mutations = ["mutate_one_point_lsystem"] allowed_crossovers = ["crossover_uniform_rules_lsystem", "crossover_uniform_genes_lsystem"] -[genotypes.tree.defaults] +[genotypes.lsystem.defaults] mutation = "mutate_one_point_lsystem" -crossover = "crossover_uniform_rules_lsystem" \ No newline at end of file +crossover = "crossover_uniform_rules_lsystem" + +[genotypes.lsystem.mutation.params] +mutation_rate = 0.5 + +[genotypes.lsystem.crossover.params] +mutation_rate = 0.5 \ No newline at end of file diff --git a/examples/evolve.py b/examples/evolve.py index 05010317..a7e44e1a 100644 --- a/examples/evolve.py +++ b/examples/evolve.py @@ -23,6 +23,8 @@ from ariel.ec.genotypes.genotype_mapping import GENOTYPES_MAPPING from morphology_fitness_analysis import compute_6d_descriptor, load_target_robot, compute_fitness_scores +from ariel.ec.genotypes.genotype import Genotype + # Global constants SEED = 42 DB_HANDLING_MODES = Literal["delete", "halt"] @@ -44,9 +46,11 @@ class EASettings(BaseSettings): first_generation_id: int = 0 num_of_generations: int = 100 target_population_size: int = 100 - genotype: GenotypeEnum + genotype: type[Genotype] mutation: Mutation + mutation_params: dict = {} crossover: Crossover + crossover_params: dict = {} task: str = "evolve_to_copy" target_robot_file_path: Path | None = Path("examples/target_robots/small_robot_8.json") @@ -80,19 +84,20 @@ def crossover(population: Population, config: EASettings) -> Population: parent_i = parents[idx] parent_j = parents[idx + 1] genotype_i, genotype_j = config.crossover( - config.genotype.value.from_json(parent_i.genotype), - config.genotype.value.from_json(parent_j.genotype), + config.genotype.from_json(parent_i.genotype), + config.genotype.from_json(parent_j.genotype), + **config.crossover_params, ) # First child child_i = Individual() - child_i.genotype = genotype_i.to_json() + child_i.genotype = config.genotype.to_json(genotype_i) child_i.tags = {"mut": True} child_i.requires_eval = True # Second child child_j = Individual() - child_j.genotype = genotype_j.to_json() + child_j.genotype = config.genotype.to_json(genotype_j) child_j.tags = {"mut": True} child_j.requires_eval = True @@ -103,13 +108,12 @@ def crossover(population: Population, config: EASettings) -> Population: def mutation(population: Population, config: EASettings) -> Population: for ind in population: if ind.tags.get("mut", False): - genes = config.genotype.value.from_json(ind.genotype) + genes = config.genotype.from_json(ind.genotype) mutated = config.mutation( individual=genes, - # span=1, - # mutation_probability=0.5, + **config.mutation_params, ) - ind.genotype = mutated.to_json() + ind.genotype = config.genotype.to_json(mutated) ind.requires_eval = True return population @@ -119,7 +123,7 @@ def evaluate(population: Population, config: EASettings) -> Population: target_descriptor = load_target_robot(Path("examples/target_robots/" + str(config.target_robot_file_path))) for ind in population: - genotype = config.genotype.value.from_json(ind.genotype) + genotype = config.genotype.from_json(ind.genotype) # Convert to digraph ind_digraph = genotype.to_digraph(genotype) # Compute the morphological descriptors @@ -151,7 +155,7 @@ def survivor_selection(population: Population, config: EASettings) -> Population def create_individual(config: EASettings) -> Individual: ind = Individual() - ind.genotype = config.genotype.value.create_individual().to_json() + ind.genotype = config.genotype.to_json(config.genotype.create_individual()) return ind def read_config_file() -> EASettings: @@ -163,14 +167,16 @@ def read_config_file() -> EASettings: mutation_name = cfg["run"].get("mutation", gblock["defaults"]["mutation"]) crossover_name = cfg["run"].get("crossover", gblock["defaults"]["crossover"]) task = cfg["run"]["task"] + mutation_params = gblock.get("mutation", {}).get("params", {}) + crossover_params = gblock.get("crossover", {}).get("params", {}) target_robot_path = cfg["task"]["evolve_to_copy"]["target_robot_path"] if task == "evolve_to_copy" else None genotype = GENOTYPES_MAPPING[gname] - mutation = genotype.value.get_mutator_object() + mutation = genotype.get_mutator_object() mutation.set_which_mutation(mutation_name) - crossover = genotype.value.get_crossover_object() + crossover = genotype.get_crossover_object() crossover.set_which_crossover(crossover_name) settings = EASettings( @@ -181,7 +187,9 @@ def read_config_file() -> EASettings: target_population_size=cfg["ec"]["target_population_size"], genotype=genotype, mutation=mutation, + mutation_params=mutation_params, crossover=crossover, + crossover_params=crossover_params, task=task, target_robot_file_path=Path(target_robot_path), output_folder=Path(cfg["data"]["output_folder"]), diff --git a/examples/morphology_fitness_analysis.py b/examples/morphology_fitness_analysis.py index c056bb51..1eb5b413 100644 --- a/examples/morphology_fitness_analysis.py +++ b/examples/morphology_fitness_analysis.py @@ -22,7 +22,6 @@ from ariel.utils.graph_ops import load_robot_json_file from ariel.utils.morphological_descriptor import MorphologicalMeasures from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode -from ariel.body_phenotypes.robogen_lite.decoders.tree_decoder import to_digraph import ariel.body_phenotypes.robogen_lite.config as config # Set random seed for reproducibility @@ -184,7 +183,7 @@ def generate_random_population(self, n_robots: int = 100) -> List[np.ndarray]: genome = self.generate_random_robot() # Decode to graph - robot_graph = to_digraph(genome) + robot_graph = genome.to_digraph() #! THIS DOES NOT WORK, to_digraph is a static method that needs a genome as an argument # Compute descriptor descriptor = self.compute_6d_descriptor(robot_graph) diff --git a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py b/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py deleted file mode 100644 index 2f638d95..00000000 --- a/src/ariel/body_phenotypes/robogen_lite/decoders/tree_decoder.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import annotations - -from typing import Optional, Dict, Callable -import networkx as nx -from ....ec.genotypes.tree.tree_genome import TreeNode, TreeGenome -from ...robogen_lite import config - -def to_digraph(genome: TreeGenome, use_node_ids: bool = False) -> nx.DiGraph: - """ - Convert this genome (rooted at `genome.root`) to a NetworkX directed graph. - - The graph has a parent→child edge for each attachment in the tree. By default, - nodes in the graph are keyed by the `TreeNode.id` values maintained in your - structure. Optionally, you can request contiguous integer IDs (0..N-1) in DFS - order via `use_node_ids=False`. - - Node attributes - ---------------- - type : str - The module type (enum `.name`). - rotation : str - The rotation (enum `.name`). - depth : int - The tree depth stored on the node (`node._depth`). - raw_id : int - The original `TreeNode.id` (always present, even when `use_node_ids=False`). - - Edge attributes - ---------------- - face : str - The face label (enum `.name`) used to attach the child to its parent. - - Parameters - ---------- - use_node_ids : bool, optional (default: True) - If True, graph nodes are keyed by `TreeNode.id`. If False, assigns - contiguous integer IDs in DFS order starting at 0. - - Returns - ------- - nx.DiGraph - A directed graph representation of the tree. If the genome is empty - (`self.root is None`), returns an empty graph. - """ - g = nx.DiGraph() - root = genome.root - if root is None: - return g - - # Stable mapping: either identity (node.id) or contiguous DFS ids. - node_key: Callable[[TreeNode], int] - if use_node_ids: - node_key = lambda n: n.id - else: - # Assign 0..N-1 in first-seen (DFS) order - seen: Dict[int, int] = {} - next_id = 0 - def node_key(n: TreeNode) -> int: - nonlocal next_id - if n.id not in seen: - seen[n.id] = next_id - next_id += 1 - return seen[n.id] - - def dfs(parent: TreeNode | None, child: TreeNode, via_face: config.ModuleFaces | None) -> None: - cid = node_key(child) - # Add/update child node with attributes (use enum names for JSON-friendliness) - g.add_node( - cid, - type=child.module_type.name, - rotation=child.rotation.name, - raw_id=child.id, - ) - - if parent is not None: - pid = node_key(parent) - g.add_edge(pid, cid, face=via_face.name if via_face is not None else None) - - # Recurse over children (face -> subnode) - for face, sub in child.children.items(): - dfs(child, sub, face) - - dfs(None, root, None) - return g - -def test(): - # Create a simple tree genome for testing - genome = TreeGenome() - genome.root = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_90, links={})) - genome.root.front = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - genome.root.left = TreeNode(config.ModuleInstance(type=config.ModuleType.BRICK, rotation=config.ModuleRotationsIdx.DEG_45, links={})) - - # Convert to directed graph - digraph = to_digraph(genome, use_node_ids=False) - - # Print the graph nodes and edges with attributes - print("Nodes:") - for node, attrs in digraph.nodes(data=True): - print(f" {node}: {attrs}") - - print("\nEdges:") - for u, v, attrs in digraph.edges(data=True): - print(f" {u} -> {v}: {attrs}") - -# Test code -if __name__ == "__main__": - test() diff --git a/src/ariel/ec/a002.py b/src/ariel/ec/a002.py index 47152517..aefa607e 100644 --- a/src/ariel/ec/a002.py +++ b/src/ariel/ec/a002.py @@ -13,7 +13,6 @@ from sqlmodel import Session, select # Local libraries -from ariel.src.ariel.ec.mutations import IntegersGenerator from ariel.ec.a001 import Individual, init_database # Global constants diff --git a/src/ariel/ec/a003.py b/src/ariel/ec/a003.py index 4239f00a..e3fb216a 100644 --- a/src/ariel/ec/a003.py +++ b/src/ariel/ec/a003.py @@ -19,7 +19,6 @@ from sqlmodel import Session, SQLModel, col, select # Local libraries -from ariel.src.ariel.ec.mutations import IntegersGenerator from ariel.ec.a001 import Individual # Third-party libraries diff --git a/src/ariel/ec/a004.py b/src/ariel/ec/a004.py index f272c34d..b71e09d2 100644 --- a/src/ariel/ec/a004.py +++ b/src/ariel/ec/a004.py @@ -22,7 +22,7 @@ from sqlmodel import Session, SQLModel, col, select # Local libraries -from ariel.src.ariel.ec.mutations import IntegerMutator, IntegersGenerator +from ariel.ec.mutations import IntegerMutator from ariel.ec.a001 import Individual # Global constants diff --git a/src/ariel/ec/crossovers.py b/src/ariel/ec/crossovers.py index 695e5682..5f1e32ee 100644 --- a/src/ariel/ec/crossovers.py +++ b/src/ariel/ec/crossovers.py @@ -18,7 +18,6 @@ if TYPE_CHECKING: from ariel.ec.genotypes.genotype import Genotype from ariel.ec.genotypes.tree.tree_genome import TreeGenome -from ariel.src.ariel.ec.mutations import IntegersGenerator # Global constants SCRIPT_NAME = __file__.split("/")[-1][:-3] diff --git a/src/ariel/ec/ec_l_system.py b/src/ariel/ec/ec_l_system.py index d4e1d064..5bd7e5dd 100644 --- a/src/ariel/ec/ec_l_system.py +++ b/src/ariel/ec/ec_l_system.py @@ -25,7 +25,7 @@ # Local libraries -from ariel.src.ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder +from ariel.ec.genotypes.lsystem.l_system_genotype import LSystemDecoder SEED = 42 DPI = 300 diff --git a/src/ariel/ec/genotypes/genotype.py b/src/ariel/ec/genotypes/genotype.py index d77932db..4969e0b2 100644 --- a/src/ariel/ec/genotypes/genotype.py +++ b/src/ariel/ec/genotypes/genotype.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from ariel.src.ariel.ec.mutations import Mutation - from ariel.src.ariel.ec.crossovers import Crossover + from ariel.ec.mutations import Mutation + from ariel.ec.crossovers import Crossover import networkx as nx class Genotype(ABC): @@ -34,4 +34,16 @@ def create_individual(**kwargs: dict) -> "Genotype": def to_digraph(robot_genotype: "Genotype", **kwargs: dict) -> nx.DiGraph: """Convert the genotype to a directed graph representation.""" raise NotImplementedError("Conversion to directed graph not implemented for this genotype type.") + + @staticmethod + @abstractmethod + def to_json(robot_genotype: "Genotype", **kwargs: dict) -> str: + """Convert the genotype to a JSON-serializable dictionary.""" + raise NotImplementedError("Conversion to JSON not implemented for this genotype type.") + + @staticmethod + @abstractmethod + def from_json(json_data: str, **kwargs: dict) -> "Genotype": + """Create a genotype instance from a JSON-serializable dictionary.""" + raise NotImplementedError("Creation from JSON not implemented for this genotype type.") \ No newline at end of file diff --git a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py index 0cc68fab..ac612ac2 100644 --- a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py +++ b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py @@ -36,10 +36,10 @@ # Local libraries from ariel.body_phenotypes.robogen_lite.config import ModuleFaces, ModuleRotationsTheta, ModuleType, ModuleInstance,ModuleRotationsIdx +from ariel.ec.genotypes.genotype import Genotype if TYPE_CHECKING: - from ariel.ec.genotypes.genotype import Genotype - from ariel.src.ariel.ec.mutations import LSystemMutator - from ariel.src.ariel.ec.crossovers import LSystemCrossover + from ariel.ec.mutations import LSystemMutator + from ariel.ec.crossovers import LSystemCrossover SEED = 42 DPI = 300 @@ -208,12 +208,12 @@ def __init__( @staticmethod def get_crossover_object() -> LSystemCrossover: - from ariel.src.ariel.ec.crossovers import LSystemCrossover + from ariel.ec.crossovers import LSystemCrossover return LSystemCrossover() @staticmethod def get_mutator_object() -> LSystemMutator: - from ariel.src.ariel.ec.mutations import LSystemMutator + from ariel.ec.mutations import LSystemMutator return LSystemMutator() @staticmethod @@ -221,7 +221,8 @@ def create_individual( iterations: int = 2, max_elements: int = 32, max_depth: int = 8, - verbose: int = 0 + verbose: int = 0, + **kwargs: dict ) -> LSystemDecoder: indiv = LSystemDecoder( axiom="C", @@ -234,6 +235,32 @@ def create_individual( # Materialize expanded_token, structure, and graph indiv.refresh() return indiv + + @staticmethod + def to_json(robot_genotype: LSystemDecoder, *, indent: int = 2) -> str: + return json.dumps({ + "axiom": robot_genotype.axiom, + "rules": robot_genotype.rules, + "iterations": robot_genotype.iterations, + "max_elements": robot_genotype.max_elements, + "max_depth": robot_genotype.max_depth, + "verbose": robot_genotype.verbose, + }, indent=indent) + + @staticmethod + def from_json(json_data: str) -> LSystemDecoder: + data = json.loads(json_data) + indiv = LSystemDecoder( + axiom=data["axiom"], + rules=data["rules"], + iterations=data["iterations"], + max_elements=data["max_elements"], + max_depth=data["max_depth"], + verbose=data.get("verbose", 0), + ) + # Materialize expanded_token, structure, and graph + indiv.refresh() + return indiv @staticmethod def to_digraph(robot: LSystemDecoder): diff --git a/src/ariel/ec/genotypes/tree/tree_genome.py b/src/ariel/ec/genotypes/tree/tree_genome.py index bfbba2a8..7c1802ac 100644 --- a/src/ariel/ec/genotypes/tree/tree_genome.py +++ b/src/ariel/ec/genotypes/tree/tree_genome.py @@ -9,10 +9,10 @@ import numpy as np from typing import TYPE_CHECKING +from ariel.ec.genotypes.genotype import Genotype if TYPE_CHECKING: - from ariel.ec.genotypes.genotype import Genotype - from ariel.src.ariel.ec.crossovers import TreeCrossover - from ariel.src.ariel.ec.mutations import TreeMutator + from ariel.ec.crossovers import TreeCrossover + from ariel.ec.mutations import TreeMutator SEED = 42 RNG = np.random.default_rng(SEED) @@ -23,13 +23,13 @@ def __init__(self, root: TreeNode | None = None): @staticmethod def get_crossover_object() -> TreeCrossover: - from ariel.src.ariel.ec.crossovers import TreeCrossover + from ariel.ec.crossovers import TreeCrossover """Return the crossover operator for tree genomes.""" return TreeCrossover() @staticmethod def get_mutator_object() -> TreeMutator: - from ariel.src.ariel.ec.mutations import TreeMutator + from ariel.ec.mutations import TreeMutator """Return the mutator operator for tree genomes.""" return TreeMutator() @@ -87,18 +87,6 @@ def default_init(cls, *args, **kwargs): rotation=config.ModuleRotationsIdx.DEG_0, links={}))) - @classmethod - def from_dict(cls, data: dict) -> "TreeGenome": - """Deserialize a genome from a dict produced by to_dict().""" - root_data = data.get("root") - root = None if root_data is None else TreeNode.from_dict(root_data) - return cls(root=root) - - @classmethod - def from_json(cls, s: str) -> "TreeGenome": - """Deserialize from a JSON string.""" - return cls.from_dict(json.loads(s)) - @property def root(self) -> TreeNode | None: return self._root @@ -195,16 +183,31 @@ def __copy__(self) -> 'TreeGenome': """Support for copy.copy().""" return self.copy() - # ---------- JSON / dict serialization ---------- - def to_dict(self) -> dict: + # ---------- JSON serialization ---------- + + @staticmethod + def from_dict(data: dict) -> "TreeGenome": + """Deserialize a genome from a dict produced by to_dict().""" + root_data = data.get("root") + root = None if root_data is None else TreeNode.from_dict(root_data) + return TreeGenome(root=root) + + @staticmethod + def from_json(json_str: str, **kwargs) -> "TreeGenome": + """Deserialize from a JSON string.""" + return TreeGenome.from_dict(json.loads(json_str)) + + @staticmethod + def to_dict(robot_genome: "TreeGenome") -> dict: """Serialize the genome into a pure-Python dict (JSON-friendly).""" return { - "root": None if self._root is None else self._root.to_dict() + "root": None if robot_genome._root is None else robot_genome._root.to_dict() } - def to_json(self, *, indent: int | None = 2) -> str: + @staticmethod + def to_json(robot_genome: "TreeGenome", *, indent: int | None = 2) -> str: """Serialize to a JSON string.""" - return json.dumps(self.to_dict(), indent=indent) + return json.dumps(TreeGenome.to_dict(robot_genome), indent=indent) # TODO: Implement this # def __deepcopy__(self, memo) -> 'TreeGenome': diff --git a/src/ariel/ec/mutations.py b/src/ariel/ec/mutations.py index a14f6c82..afcc5125 100644 --- a/src/ariel/ec/mutations.py +++ b/src/ariel/ec/mutations.py @@ -277,9 +277,9 @@ def random_subtree_replacement( class LSystemMutator(Mutation): @staticmethod - def mutate_one_point_lsystem(lsystem,mut_rate,add_temperature=0.5): + def mutate_one_point_lsystem(lsystem,mutation_rate,add_temperature=0.5): op_completed = "" - if random.random() Date: Wed, 5 Nov 2025 17:36:33 +0100 Subject: [PATCH 46/47] Lsystem finally integrated --- examples/evolve.py | 2 +- .../ec/genotypes/lsystem/l_system_genotype.py | 4 +++- src/ariel/ec/mutations.py | 22 +++++++++---------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/evolve.py b/examples/evolve.py index a7e44e1a..cc924a66 100644 --- a/examples/evolve.py +++ b/examples/evolve.py @@ -249,7 +249,7 @@ def main() -> None: plt.title('Average Fitness Over Generations') plt.xlabel('Generation') plt.ylabel('Average Fitness') - plt.savefig('average_fitness_over_generations.png') + plt.savefig('average_fitness_over_generations_lsystem.png') plt.show() if __name__ == "__main__": diff --git a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py index ac612ac2..37b0aa33 100644 --- a/src/ariel/ec/genotypes/lsystem/l_system_genotype.py +++ b/src/ariel/ec/genotypes/lsystem/l_system_genotype.py @@ -224,9 +224,11 @@ def create_individual( verbose: int = 0, **kwargs: dict ) -> LSystemDecoder: + base_rules = {"C": "C", "B": "B", "H": "H", "N": "N"} + indiv = LSystemDecoder( axiom="C", - rules={}, + rules=base_rules, iterations=iterations, max_elements=max_elements, max_depth=max_depth, diff --git a/src/ariel/ec/mutations.py b/src/ariel/ec/mutations.py index afcc5125..155b4266 100644 --- a/src/ariel/ec/mutations.py +++ b/src/ariel/ec/mutations.py @@ -278,7 +278,7 @@ class LSystemMutator(Mutation): @staticmethod def mutate_one_point_lsystem(lsystem,mutation_rate,add_temperature=0.5): - op_completed = "" + # op_completed = "" if random.random()=0: splitted_rules.pop(gene_to_change-1) splitted_rules.pop(gene_to_change-1) elif splitted_rules[gene_to_change][:4] in ['movf','movk','movl','movr','movt','movb']: - op_completed="REMOVED : "+splitted_rules[gene_to_change] + # op_completed="REMOVED : "+splitted_rules[gene_to_change] splitted_rules.pop(gene_to_change) new_rule = "" for j in range(0,len(splitted_rules)): @@ -341,7 +341,7 @@ def mutate_one_point_lsystem(lsystem,mutation_rate,add_temperature=0.5): lsystem.rules[list(rules.keys())[rule_to_change]]=new_rule else: lsystem.rules[list(rules.keys())[rule_to_change]]=lsystem.rules[list(rules.keys())[rule_to_change]] - return op_completed + return lsystem def test() -> None: From cafdff4d1880e29e4d68adebb54a0cc92adb3c24 Mon Sep 17 00:00:00 2001 From: Lukas Bierling Date: Wed, 5 Nov 2025 23:59:09 +0100 Subject: [PATCH 47/47] added dashboard --- examples/config.toml | 4 +- examples/evolution_dashboard.py | 267 ++++++++++++++ examples/evolve.py | 52 ++- examples/morphology_fitness_analysis.py | 273 ++++++++++++--- examples/plotly_morphology_analysis.py | 439 ++++++++++++++++++++++++ examples/tree_genome_vis.py | 43 ++- generic_ea_settings.py | 134 ++++++++ src/ariel/ec/mutations.py | 4 +- 8 files changed, 1153 insertions(+), 63 deletions(-) create mode 100644 examples/evolution_dashboard.py create mode 100644 examples/plotly_morphology_analysis.py create mode 100644 generic_ea_settings.py diff --git a/examples/config.toml b/examples/config.toml index ea6ac356..317cf00e 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -10,7 +10,7 @@ genotype = "lsystem" task = "evolve_to_copy" [task.evolve_to_copy] -target_robot_path = "small_robot_8.json" +target_robot_path = "examples/target_robots/small_robot_8.json" # ========================= # Global EC settings (match EASettings fields) @@ -67,4 +67,4 @@ crossover = "crossover_uniform_rules_lsystem" mutation_rate = 0.5 [genotypes.lsystem.crossover.params] -mutation_rate = 0.5 \ No newline at end of file +mutation_rate = 0.5 diff --git a/examples/evolution_dashboard.py b/examples/evolution_dashboard.py new file mode 100644 index 00000000..bab50824 --- /dev/null +++ b/examples/evolution_dashboard.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +""" +Interactive Evolution Dashboard + +This module provides an interactive web dashboard for visualizing evolutionary +computation results using Plotly Dash. It displays morphological analysis +plots from MorphologyAnalyzer with generation selection capabilities. +""" + +import dash +from dash import dcc, html, Input, Output, callback +import plotly.graph_objects as go +import numpy as np +import pandas as pd +from typing import List, Callable, Any + +from plotly_morphology_analysis import PlotlyMorphologyAnalyzer +from ariel.ec.a001 import Individual + +type Population = List[Individual] + + +class EvolutionDashboard: + """Interactive dashboard for evolution visualization.""" + + def __init__(self, populations: List[Population], decoder: Callable, config: Any): + """Initialize dashboard with evolution data. + + Args: + populations: List of populations per generation + decoder: Function to decode Individual genotype to robot graph + config: Evolution configuration object + """ + self.populations = populations + self.decoder = decoder + self.config = config + self.analyzer = PlotlyMorphologyAnalyzer() + + # Load target robots + if hasattr(config, 'target_robot_file_path') and config.target_robot_file_path: + self.analyzer.load_target_robots(str(config.target_robot_file_path)) + + # Pre-compute fitness timeline + self._compute_fitness_timeline() + + # Cache for computed descriptors per generation + self._descriptor_cache = {} + + # Initialize Dash app + self.app = dash.Dash(__name__) + self._setup_layout() + self._setup_callbacks() + + def _compute_fitness_timeline(self): + """Compute average fitness for each generation.""" + self.fitness_timeline = [] + + for gen_idx, population in enumerate(self.populations): + if population: + avg_fitness = sum(ind.fitness for ind in population) / len(population) + self.fitness_timeline.append({ + 'generation': gen_idx, + 'avg_fitness': avg_fitness, + 'best_fitness': max(ind.fitness for ind in population), + 'worst_fitness': min(ind.fitness for ind in population) + }) + + def _get_generation_data(self, generation: int): + """Get or compute morphological data for a specific generation.""" + if generation in self._descriptor_cache: + return self._descriptor_cache[generation] + + if generation >= len(self.populations): + generation = len(self.populations) - 1 + + # Load population data into analyzer + population = self.populations[generation] + self.analyzer.load_population(population, self.decoder) + self.analyzer.compute_fitness_scores() + + # Cache the results + self._descriptor_cache[generation] = { + 'descriptors': self.analyzer.descriptors.copy(), + 'fitness_scores': self.analyzer.fitness_scores.copy() + } + + return self._descriptor_cache[generation] + + def _setup_layout(self): + """Setup the dashboard layout.""" + max_generation = len(self.populations) - 1 + + self.app.layout = html.Div([ + html.H1("Evolution Dashboard", style={'textAlign': 'center', 'marginBottom': 30}), + + # Generation control section + html.Div([ + html.Label("Select Generation:", style={'fontWeight': 'bold', 'marginBottom': 10}), + dcc.Slider( + id='generation-slider', + min=0, + max=max_generation, + step=1, + value=max_generation, + marks={i: str(i) for i in range(0, max_generation + 1, max(1, max_generation // 10))}, + tooltip={"placement": "bottom", "always_visible": True} + ) + ], style={'margin': '20px', 'marginBottom': 40}), + + # Fitness timeline (always visible) + html.Div([ + html.H3("Fitness Over Generations"), + dcc.Graph(id='fitness-timeline') + ], style={'margin': '20px', 'marginBottom': 40}), + + # Tabbed plots section + html.Div([ + dcc.Tabs(id='plot-tabs', value='pca-tab', children=[ + dcc.Tab(label='PCA Analysis', value='pca-tab'), + dcc.Tab(label='Fitness Landscapes', value='landscape-tab'), + dcc.Tab(label='Fitness Distributions', value='distribution-tab'), + dcc.Tab(label='Morphological Diversity', value='diversity-tab'), + dcc.Tab(label='Pairwise Features', value='pairwise-tab'), + ]), + html.Div(id='tab-content') + ], style={'margin': '20px'}) + ]) + + def _setup_callbacks(self): + """Setup dashboard callbacks.""" + + @self.app.callback( + Output('fitness-timeline', 'figure'), + Input('generation-slider', 'value') + ) + def update_fitness_timeline(selected_generation): + """Update fitness timeline with highlighted generation.""" + if not self.fitness_timeline: + return go.Figure() + + df = pd.DataFrame(self.fitness_timeline) + + fig = go.Figure() + + # Add average fitness line + fig.add_trace(go.Scatter( + x=df['generation'], + y=df['avg_fitness'], + mode='lines+markers', + name='Average Fitness', + line=dict(color='blue', width=2) + )) + + # Add best fitness line + fig.add_trace(go.Scatter( + x=df['generation'], + y=df['best_fitness'], + mode='lines+markers', + name='Best Fitness', + line=dict(color='green', width=2) + )) + + # Highlight selected generation + if selected_generation < len(df): + selected_row = df.iloc[selected_generation] + fig.add_trace(go.Scatter( + x=[selected_row['generation']], + y=[selected_row['avg_fitness']], + mode='markers', + name=f'Generation {selected_generation}', + marker=dict(color='red', size=12, symbol='circle-open', line=dict(width=3)) + )) + + fig.update_layout( + title='Fitness Evolution Over Generations', + xaxis_title='Generation', + yaxis_title='Fitness', + height=400, + showlegend=True + ) + + return fig + + @self.app.callback( + Output('tab-content', 'children'), + [Input('plot-tabs', 'value'), + Input('generation-slider', 'value')] + ) + def update_tab_content(active_tab, selected_generation): + """Update tab content based on selection.""" + # Get data for selected generation + gen_data = self._get_generation_data(selected_generation) + + if active_tab == 'pca-tab': + return self._create_pca_plot(selected_generation) + elif active_tab == 'landscape-tab': + return self._create_landscape_plot(selected_generation) + elif active_tab == 'distribution-tab': + return self._create_distribution_plot(selected_generation) + elif active_tab == 'diversity-tab': + return self._create_diversity_plot(selected_generation) + elif active_tab == 'pairwise-tab': + return self._create_pairwise_plot(selected_generation) + + return html.Div("Select a tab to view plots") + + def _create_pca_plot(self, generation: int): + """Create PCA analysis plot using PlotlyMorphologyAnalyzer.""" + self._get_generation_data(generation) # Load data into analyzer + fig = self.analyzer.plot_target_descriptors_pca() + fig.update_layout(title_text=f"PCA Analysis - Generation {generation}") + return dcc.Graph(figure=fig) + + def _create_landscape_plot(self, generation: int): + """Create fitness landscape plot using PlotlyMorphologyAnalyzer.""" + self._get_generation_data(generation) # Load data into analyzer + fig = self.analyzer.plot_fitness_landscapes() + fig.update_layout(title_text=f"Fitness Landscapes - Generation {generation}") + return dcc.Graph(figure=fig) + + def _create_distribution_plot(self, generation: int): + """Create fitness distribution plot using PlotlyMorphologyAnalyzer.""" + self._get_generation_data(generation) # Load data into analyzer + fig = self.analyzer.plot_fitness_distributions() + fig.update_layout(title_text=f"Fitness Distributions - Generation {generation}") + return dcc.Graph(figure=fig) + + def _create_diversity_plot(self, generation: int): + """Create morphological diversity analysis plot using PlotlyMorphologyAnalyzer.""" + self._get_generation_data(generation) # Load data into analyzer + fig = self.analyzer.analyze_morphological_diversity() + fig.update_layout(title_text=f"Morphological Diversity - Generation {generation}") + return dcc.Graph(figure=fig) + + def _create_pairwise_plot(self, generation: int): + """Create pairwise feature landscape plot using PlotlyMorphologyAnalyzer.""" + self._get_generation_data(generation) # Load data into analyzer + fig = self.analyzer.plot_pairwise_feature_landscapes() + fig.update_layout(title_text=f"Pairwise Feature Landscapes - Generation {generation}") + return dcc.Graph(figure=fig) + + def run(self, host='127.0.0.1', port=8050, debug=True): + """Run the dashboard server.""" + print(f"Starting Evolution Dashboard at http://{host}:{port}") + print("Press Ctrl+C to stop the server") + self.app.run(host=host, port=port, debug=debug) + + +def run_dashboard(populations: List[Population], decoder: Callable, config: Any, + host='127.0.0.1', port=8050, debug=True): + """Run the evolution dashboard. + + Args: + populations: List of populations per generation from evolution + decoder: Function to decode Individual genotype to robot graph + config: Evolution configuration object + host: Server host address + port: Server port + debug: Enable debug mode + """ + dashboard = EvolutionDashboard(populations, decoder, config) + dashboard.run(host=host, port=port, debug=debug) + + +if __name__ == "__main__": + # Example usage - this would be called from evolve.py + print("Evolution Dashboard module - import and call run_dashboard() with your evolution data") \ No newline at end of file diff --git a/examples/evolve.py b/examples/evolve.py index cc924a66..abaa29fe 100644 --- a/examples/evolve.py +++ b/examples/evolve.py @@ -21,7 +21,8 @@ from ariel.ec.mutations import Mutation from ariel.ec.crossovers import Crossover from ariel.ec.genotypes.genotype_mapping import GENOTYPES_MAPPING -from morphology_fitness_analysis import compute_6d_descriptor, load_target_robot, compute_fitness_scores +from morphology_fitness_analysis import compute_6d_descriptor, load_target_robot, compute_fitness_scores, MorphologyAnalyzer +from evolution_dashboard import run_dashboard from ariel.ec.genotypes.genotype import Genotype @@ -120,7 +121,7 @@ def mutation(population: Population, config: EASettings) -> Population: def evaluate(population: Population, config: EASettings) -> Population: if config.task == "evolve_to_copy": - target_descriptor = load_target_robot(Path("examples/target_robots/" + str(config.target_robot_file_path))) + target_descriptor = load_target_robot(Path(str(config.target_robot_file_path))) for ind in population: genotype = config.genotype.from_json(ind.genotype) @@ -169,7 +170,7 @@ def read_config_file() -> EASettings: task = cfg["run"]["task"] mutation_params = gblock.get("mutation", {}).get("params", {}) crossover_params = gblock.get("crossover", {}).get("params", {}) - + target_robot_path = cfg["task"]["evolve_to_copy"]["target_robot_path"] if task == "evolve_to_copy" else None genotype = GENOTYPES_MAPPING[gname] @@ -198,7 +199,26 @@ def read_config_file() -> EASettings: db_file_path=Path(cfg["data"]["output_folder"]) / cfg["data"]["db_file_name"], ) return settings - + + +# Example usage +def analyze_evolution_videos(analyzer, populations, decoder): + """Create evolution videos for different visualizations.""" + + analyzer.create_evolution_video( + populations=populations, + decoder=decoder, + plot_method_name="plot_fitness_distributions", + video_filename="videos/fitness_distributions.mp4" + ) + + analyzer.create_evolution_video( + populations=populations, + decoder=decoder, + plot_method_name="plot_pairwise_feature_landscapes", + video_filename="videos/pairwise_feature_landscapes.mp4" + ) + def main() -> None: """Entry point.""" @@ -208,7 +228,7 @@ def main() -> None: population_list = evaluate(population_list, config) # Create EA steps - + ops = [ EAStep("parent_selection", partial(parent_selection, config=config)), EAStep("crossover", partial(crossover, config=config)), @@ -237,9 +257,11 @@ def main() -> None: fitnesses = [] + populations = [] for i in range(100): ea.fetch_population(only_alive=False, best_comes=None, custom_logic=[Individual.time_of_birth==i]) individuals = ea.population + populations.append(individuals) avg_fitness = sum(ind.fitness for ind in individuals) / len(individuals) if individuals else 0 console.log(f"Generation {i}: Avg Fitness = {avg_fitness}") fitnesses.append(avg_fitness) @@ -252,5 +274,23 @@ def main() -> None: plt.savefig('average_fitness_over_generations_lsystem.png') plt.show() + morphology_analyzer = MorphologyAnalyzer() + morphology_analyzer.load_target_robots(config.target_robot_file_path) + + #analyze_evolution_videos(morphology_analyzer, populations, lambda x: config.genotype.to_digraph(config.genotype.from_json(x))) + + # Launch interactive dashboard + print("\nLaunching Evolution Dashboard...") + + decoder = lambda individual: config.genotype.to_digraph(config.genotype.from_json(individual.genotype)) + run_dashboard(populations, decoder, config) + + #morphology_analyzer.load_population(individuals, lambda x: config.genotype.to_digraph(config.genotype.from_json(x))) ##???? why not juse make a method of the tree genome? + #morphology_analyzer.compute_fitness_scores() + #morphology_analyzer.plot_fitness_distributions() + #morphology_analyzer.plot_target_descriptors_pca() + #morphology_analyzer.plot_fitness_landscapes() + #morphology_analyzer.analyze_morphological_diversity() + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/morphology_fitness_analysis.py b/examples/morphology_fitness_analysis.py index 1eb5b413..4a012451 100644 --- a/examples/morphology_fitness_analysis.py +++ b/examples/morphology_fitness_analysis.py @@ -8,26 +8,32 @@ 3. Computes fitness as distance to target descriptors 4. Visualizes fitness landscapes using PCA and various plots """ - import numpy as np import matplotlib.pyplot as plt #import seaborn as sns from sklearn.decomposition import PCA from sklearn.manifold import TSNE -from typing import List, Tuple +from typing import List, Tuple, Callable, Any import json from pathlib import Path +import imageio.v2 as imageio # Import ARIEL modules from ariel.utils.graph_ops import load_robot_json_file from ariel.utils.morphological_descriptor import MorphologicalMeasures from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode + import ariel.body_phenotypes.robogen_lite.config as config +from ariel.ec.a001 import Individual +import networkx as nx +import matplotlib.animation as animation + +type Population = List[Individual] # Set random seed for reproducibility np.random.seed(42) -def compute_6d_descriptor(robot_graph) -> np.ndarray: +def compute_6d_descriptor(robot_graph: nx.DiGraph) -> np.ndarray: """Compute 6D morphological descriptor vector.""" measures = MorphologicalMeasures(robot_graph) @@ -85,7 +91,7 @@ class MorphologyAnalyzer: def __init__(self): self.target_descriptors = [] self.target_names = [] - self.random_descriptors = [] + self.descriptors = [] self.fitness_scores = [] def compute_6d_descriptor(self, robot_graph) -> np.ndarray: @@ -109,7 +115,7 @@ def compute_6d_descriptor(self, robot_graph) -> np.ndarray: return descriptor - def load_target_robots(self, json_paths: List[str]): + def load_target_robots(self, *json_paths: str): """Load target robots and compute their descriptors.""" self.target_descriptors = [] self.target_names = [] @@ -169,8 +175,20 @@ def _add_random_children(self, node: TreeNode, max_depth: int, branch_prob: floa # Recursively add children with reduced depth self._add_random_children(child, max_depth - 1, branch_prob * 0.7) + def load_population(self, population: Population, decoder: Callable[[Individual], nx.DiGraph]): + """Load a population of robots and compute their descriptors.""" + self.descriptors = [] + descriptors = [] + for ind in population: + robot_graph: nx.DiGraph = decoder(ind.genotype) + descriptor = self.compute_6d_descriptor(robot_graph) + descriptors.append(descriptor) + + self.descriptors = np.array(descriptors) + def generate_random_population(self, n_robots: int = 100) -> List[np.ndarray]: """Generate a population of random robots and compute their descriptors.""" + self.descriptors = [] print(f"Generating {n_robots} random robots...") descriptors = [] @@ -178,35 +196,31 @@ def generate_random_population(self, n_robots: int = 100) -> List[np.ndarray]: if i % 20 == 0: print(f"Generated {i}/{n_robots} robots") - try: - # Generate random robot - genome = self.generate_random_robot() + # Generate random robot + genome = self.generate_random_robot() - # Decode to graph - robot_graph = genome.to_digraph() #! THIS DOES NOT WORK, to_digraph is a static method that needs a genome as an argument + # Decode to graph + robot_graph = genome.to_digraph(genome) #! THIS DOES NOT WORK, to_digraph is a static method that needs a genome as an argument + + # Compute descriptor + descriptor = self.compute_6d_descriptor(robot_graph) + descriptors.append(descriptor) - # Compute descriptor - descriptor = self.compute_6d_descriptor(robot_graph) - descriptors.append(descriptor) - except Exception as e: - print(f"Error generating robot {i}: {e}") - # Add zero descriptor for failed robots - descriptors.append(np.zeros(6)) - self.random_descriptors = np.array(descriptors) - return self.random_descriptors + self.descriptors = np.array(descriptors) + return self.descriptors def compute_fitness_scores(self): """Compute fitness scores as mean of distances in each dimension to each target.""" - if len(self.target_descriptors) == 0 or len(self.random_descriptors) == 0: + if len(self.target_descriptors) == 0 or len(self.descriptors) == 0: raise ValueError("Need both target and random descriptors") self.fitness_scores = [] for target_desc in self.target_descriptors: # Compute absolute differences in each dimension - dimension_distances = np.abs(self.random_descriptors - target_desc) + dimension_distances = np.abs(self.descriptors - target_desc) # Take mean across dimensions to get fitness score mean_distances = np.mean(dimension_distances, axis=1) # Convert to fitness (higher is better, so use negative distance) @@ -215,7 +229,7 @@ def compute_fitness_scores(self): self.fitness_scores = np.array(self.fitness_scores) - def plot_target_descriptors_pca(self): + def plot_target_descriptors_pca(self, return_fig: bool = False): """Plot target robots in PCA-reduced space.""" if len(self.target_descriptors) == 0: return @@ -223,7 +237,7 @@ def plot_target_descriptors_pca(self): fig, axes = plt.subplots(1, 2, figsize=(15, 6)) # Combine all descriptors for PCA fitting - all_desc = np.vstack([self.target_descriptors, self.random_descriptors]) + all_desc = np.vstack([self.target_descriptors, self.descriptors]) pca = PCA(n_components=2) all_pca = pca.fit_transform(all_desc) @@ -232,9 +246,9 @@ def plot_target_descriptors_pca(self): # Plot 1: PCA visualization axes[0].scatter(random_pca[:, 0], random_pca[:, 1], - alpha=0.3, c='lightgray', s=20, label='Random robots') + alpha=0.3, c='purple', s=20, label='Evolved robots') - colors = ['red', 'blue', 'green', 'orange', 'purple'] + colors = ['red', 'blue', 'green', 'orange'] for i, (target, name) in enumerate(zip(target_pca, self.target_names)): color = colors[i % len(colors)] axes[0].scatter(target[0], target[1], @@ -268,32 +282,37 @@ def plot_target_descriptors_pca(self): axes[1].grid(True, alpha=0.3) plt.tight_layout() - plt.show() - def plot_fitness_landscapes(self): + if return_fig: + return fig + else: + plt.show() + return fig + + def plot_fitness_landscapes(self, return_fig: bool = False): """Plot fitness landscapes for each target.""" if len(self.fitness_scores) == 0: self.compute_fitness_scores() # Use PCA for dimensionality reduction - all_desc = np.vstack([self.target_descriptors, self.random_descriptors]) + all_desc = np.vstack([self.target_descriptors, self.descriptors]) pca = PCA(n_components=2) all_pca = pca.fit_transform(all_desc) target_pca = all_pca[:len(self.target_descriptors)] - random_pca = all_pca[len(self.target_descriptors):] + evolved_pca = all_pca[len(self.target_descriptors):] n_targets = len(self.target_names) fig, axes = plt.subplots(2, (n_targets + 1) // 2, figsize=(15, 10)) if n_targets == 1: axes = [axes] - axes = axes.flatten() if n_targets > 1 else axes + axes = axes.flatten() if n_targets > 1 else axes[0] for i, (target_name, fitness) in enumerate(zip(self.target_names, self.fitness_scores)): ax = axes[i] # Create scatter plot with fitness as color - scatter = ax.scatter(random_pca[:, 0], random_pca[:, 1], + scatter = ax.scatter(evolved_pca[:, 0], evolved_pca[:, 1], c=fitness, cmap='viridis', alpha=0.6, s=30) # Mark target location @@ -316,9 +335,14 @@ def plot_fitness_landscapes(self): axes[j].set_visible(False) plt.tight_layout() - plt.show() - def plot_fitness_distributions(self): + if return_fig: + return fig + else: + plt.show() + return fig + + def plot_fitness_distributions(self, return_fig: bool = False): """Plot fitness distributions for each target.""" if len(self.fitness_scores) == 0: self.compute_fitness_scores() @@ -366,11 +390,16 @@ def plot_fitness_distributions(self): axes[1].grid(True, alpha=0.3) plt.tight_layout() - plt.show() - def analyze_morphological_diversity(self): + if return_fig: + return fig + else: + plt.show() + return fig + + def analyze_morphological_diversity(self, return_fig: bool = False): """Analyze diversity in the random population.""" - if len(self.random_descriptors) == 0: + if len(self.descriptors) == 0: return fig, axes = plt.subplots(2, 3, figsize=(18, 12)) @@ -382,9 +411,9 @@ def analyze_morphological_diversity(self): ax = axes[i] # Plot distribution of random robots - random_mean = np.mean(self.random_descriptors[:, i]) - random_std = np.std(self.random_descriptors[:, i]) - ax.hist(self.random_descriptors[:, i], bins=30, alpha=0.7, + random_mean = np.mean(self.descriptors[:, i]) + random_std = np.std(self.descriptors[:, i]) + ax.hist(self.descriptors[:, i], bins=30, alpha=0.7, density=True, label=f'Random robots (μ={random_mean:.3f}, σ={random_std:.3f})') # Mark target values @@ -400,7 +429,162 @@ def analyze_morphological_diversity(self): ax.grid(True, alpha=0.3) plt.tight_layout() - plt.show() + + if return_fig: + return fig + else: + plt.show() + return fig + + def plot_pairwise_feature_landscapes(self, features: List[str] = None, return_fig: bool = False): + """Plot fitness landscapes for pairwise combinations of selected morphological features. + + Parameters: + ----------- + features : List[str], optional + List of feature names to include. By default includes all features: + ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + return_fig : bool, optional + Whether to return the figure instead of showing it + """ + if len(self.fitness_scores) == 0: + self.compute_fitness_scores() + + # All available features with their indices + all_feature_names = ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + + # Use all features by default + if features is None: + features = all_feature_names.copy() + + # Get indices of selected features + feature_indices = [] + selected_feature_names = [] + for feature in features: + if feature in all_feature_names: + idx = all_feature_names.index(feature) + feature_indices.append(idx) + selected_feature_names.append(feature) + else: + print(f"Warning: Feature '{feature}' not recognized. Available features: {all_feature_names}") + + n_features = len(feature_indices) + if n_features < 2: + print("Error: Need at least 2 features for pairwise plots") + return None + + # Create all pairwise combinations + pairs = [] + pair_names = [] + for i in range(n_features): + for j in range(i + 1, n_features): + pairs.append((feature_indices[i], feature_indices[j])) + pair_names.append((selected_feature_names[i], selected_feature_names[j])) + + n_pairs = len(pairs) + + # Calculate optimal subplot grid + if n_pairs <= 3: + rows, cols = 1, n_pairs + elif n_pairs <= 6: + rows, cols = 2, 3 + elif n_pairs <= 12: + rows, cols = 3, 4 + else: + rows, cols = 3, 5 # For 15 pairs (all features) + + # Create subplot grid + fig, axes = plt.subplots(rows, cols, figsize=(5*cols, 5*rows)) + if n_pairs == 1: + axes = [axes] + else: + axes = axes.flatten() if rows > 1 or cols > 1 else [axes] + + # Use the first target for fitness coloring (could be extended for multiple targets) + fitness_values = self.fitness_scores[0] if len(self.fitness_scores) > 0 else np.zeros(len(self.descriptors)) + + for idx, ((i, j), (name_i, name_j)) in enumerate(zip(pairs, pair_names)): + if idx >= len(axes): + break + + ax = axes[idx] + + # Extract feature pairs + x_values = self.descriptors[:, i] + y_values = self.descriptors[:, j] + + # Create scatter plot with fitness as color + scatter = ax.scatter(x_values, y_values, c=fitness_values, + cmap='viridis', alpha=0.6, s=30) + # Add target locations for all targets + colors = ['red', 'blue', 'green', 'orange', 'purple'] + for target_idx, (target_desc, target_name) in enumerate(zip(self.target_descriptors, self.target_names)): + color = colors[target_idx % len(colors)] + ax.scatter(target_desc[i], target_desc[j], + c=color, s=200, marker='*', + edgecolors='black', linewidths=2, + label=f'Target: {target_name}') + + ax.set_xlabel(name_i) + ax.set_ylabel(name_j) + ax.set_title(f'{name_i} vs {name_j}') + ax.grid(True, alpha=0.3) + + # Add legend only to first subplot to avoid clutter + if idx == 0: + ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left') + + # Add colorbar to each subplot + plt.colorbar(scatter, ax=ax, label='Fitness', shrink=0.8) + + # Hide unused subplots + for idx in range(n_pairs, len(axes)): + axes[idx].set_visible(False) + + plt.tight_layout() + + if return_fig: + return fig + else: + plt.show() + return fig + + def create_evolution_video( + self, + populations, + decoder, + plot_method_name: str, + video_filename: str = "evolution_video.mp4", + fps: int = 2, + dpi: int = 100, + **plot_kwargs + ): + """Create video by converting each plotted figure into an RGB array.""" + plot_method = getattr(self, plot_method_name) + assert self.target_descriptors is not None, "Target descriptors not loaded" + frames = [] + + for generation_idx, current_pop in enumerate(populations): + # Compute current data + self.load_population(current_pop, decoder) + self.compute_fitness_scores() + + # Generate figure (your existing plot method already returns fig) + fig = plot_method(return_fig=True, **plot_kwargs) + fig.suptitle(f"Generation {generation_idx}", fontsize=16) + + fig.canvas.draw() + + # --- Convert figure to numpy array --- + buf = np.asarray(fig.canvas.buffer_rgba()) + frame = buf[..., :3].copy() # drop alpha channel + frames.append(frame) + plt.close(fig) + + # --- Write video --- + print("Saving {len(frames)} frames to {video_filename}...") + imageio.mimsave(video_filename, frames, fps=fps, quality=8) + print(" Saved: {video_filename}") def main(): @@ -413,15 +597,14 @@ def main(): ] analyzer = MorphologyAnalyzer() - analyzer.load_target_robots(target_paths) + analyzer.load_target_robots(*target_paths) analyzer.generate_random_population(n_robots=200) analyzer.compute_fitness_scores() - analyzer.plot_target_descriptors_pca() - analyzer.plot_fitness_landscapes() - analyzer.plot_fitness_distributions() - analyzer.analyze_morphological_diversity() + analyzer.plot_pairwise_feature_landscapes() print("\nAnalysis complete!") if __name__ == "__main__": main() + + diff --git a/examples/plotly_morphology_analysis.py b/examples/plotly_morphology_analysis.py new file mode 100644 index 00000000..8136172d --- /dev/null +++ b/examples/plotly_morphology_analysis.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +""" +Plotly-based Morphological Analysis + +This module provides interactive morphological analysis and visualization +using Plotly for web dashboards. It mirrors the functionality of +MorphologyAnalyzer but returns Plotly figures instead of matplotlib plots. +""" + +import numpy as np +import plotly.graph_objects as go +import plotly.express as px +from plotly.subplots import make_subplots +from sklearn.decomposition import PCA +from sklearn.manifold import TSNE +from typing import List, Tuple, Callable, Any +from pathlib import Path +import networkx as nx + +# Import ARIEL modules +from ariel.utils.graph_ops import load_robot_json_file +from ariel.utils.morphological_descriptor import MorphologicalMeasures +from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode +import ariel.body_phenotypes.robogen_lite.config as config +from ariel.ec.a001 import Individual + +type Population = List[Individual] + + +class PlotlyMorphologyAnalyzer: + """Analyze and visualize morphological fitness landscapes using Plotly.""" + + def __init__(self): + self.target_descriptors = [] + self.target_names = [] + self.descriptors = [] + self.fitness_scores = [] + + def compute_6d_descriptor(self, robot_graph) -> np.ndarray: + """Compute 6D morphological descriptor vector.""" + measures = MorphologicalMeasures(robot_graph) + # Handle potential division by zero or missing P for non-2D robots + try: + P = measures.P if measures.is_2d else 0.0 + except: + P = 0.0 + + descriptor = np.array([ + measures.B, # Branching + measures.L, # Limbs + measures.E, # Extensiveness (length of limbs) + measures.S, # Symmetry + P, # Proportion (2D only) + measures.J # Joints + ]) + return descriptor + + def load_target_robots(self, *json_paths: str): + """Load target robots and compute their descriptors.""" + self.target_descriptors = [] + self.target_names = [] + + for json_path in json_paths: + try: + robot_graph = load_robot_json_file(json_path) + descriptor = self.compute_6d_descriptor(robot_graph) + self.target_descriptors.append(descriptor) + + # Extract name from path + name = Path(json_path).stem + self.target_names.append(name) + + print(f"Loaded {name}: {descriptor}") + + except Exception as e: + print(f"Error loading {json_path}: {e}") + + self.target_descriptors = np.array(self.target_descriptors) + + def load_population(self, population: Population, decoder: Callable[[Individual], nx.DiGraph]): + """Load a population of robots and compute their descriptors.""" + self.descriptors = [] + descriptors = [] + for ind in population: + robot_graph: nx.DiGraph = decoder(ind) + descriptor = self.compute_6d_descriptor(robot_graph) + descriptors.append(descriptor) + + self.descriptors = np.array(descriptors) + + def compute_fitness_scores(self): + """Compute fitness scores as mean of distances in each dimension to each target.""" + if len(self.target_descriptors) == 0 or len(self.descriptors) == 0: + raise ValueError("Need both target and random descriptors") + + self.fitness_scores = [] + + for target_desc in self.target_descriptors: + # Compute absolute differences in each dimension + dimension_distances = np.abs(self.descriptors - target_desc) + # Take mean across dimensions to get fitness score + mean_distances = np.mean(dimension_distances, axis=1) + # Convert to fitness (higher is better, so use negative distance) + fitness = -mean_distances + self.fitness_scores.append(fitness) + + self.fitness_scores = np.array(self.fitness_scores) + + def plot_target_descriptors_pca(self) -> go.Figure: + """Plot target robots in PCA-reduced space using Plotly.""" + if len(self.target_descriptors) == 0: + return go.Figure() + + # Combine all descriptors for PCA fitting + all_desc = np.vstack([self.target_descriptors, self.descriptors]) + pca = PCA(n_components=2) + all_pca = pca.fit_transform(all_desc) + + target_pca = all_pca[:len(self.target_descriptors)] + random_pca = all_pca[len(self.target_descriptors):] + + fig = make_subplots( + rows=1, cols=2, + subplot_titles=['Morphological Space (PCA)', 'PCA Feature Importance'] + ) + + # Plot 1: PCA visualization + fig.add_trace( + go.Scatter( + x=random_pca[:, 0], + y=random_pca[:, 1], + mode='markers', + name='Evolved robots', + marker=dict(color='purple', size=8, opacity=0.6) + ), + row=1, col=1 + ) + + colors = ['red', 'blue', 'green', 'orange'] + for i, (target, name) in enumerate(zip(target_pca, self.target_names)): + color = colors[i % len(colors)] + fig.add_trace( + go.Scatter( + x=[target[0]], + y=[target[1]], + mode='markers', + name=f'Target: {name}', + marker=dict(color=color, size=15, symbol='star', line=dict(width=2, color='black')) + ), + row=1, col=1 + ) + + # Plot 2: Feature importance + feature_names = ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + pc1_importance = np.abs(pca.components_[0]) + pc2_importance = np.abs(pca.components_[1]) + + fig.add_trace( + go.Bar(x=feature_names, y=pc1_importance, name='PC1', opacity=0.8), + row=1, col=2 + ) + fig.add_trace( + go.Bar(x=feature_names, y=pc2_importance, name='PC2', opacity=0.8), + row=1, col=2 + ) + + fig.update_xaxes(title_text=f'PC1 ({pca.explained_variance_ratio_[0]:.2%} variance)', row=1, col=1) + fig.update_yaxes(title_text=f'PC2 ({pca.explained_variance_ratio_[1]:.2%} variance)', row=1, col=1) + fig.update_xaxes(title_text='Morphological Features', row=1, col=2) + fig.update_yaxes(title_text='Absolute Component Weight', row=1, col=2) + + fig.update_layout(height=600, title_text="PCA Analysis") + + return fig + + def plot_fitness_landscapes(self) -> go.Figure: + """Plot fitness landscapes for each target using Plotly.""" + if len(self.fitness_scores) == 0: + self.compute_fitness_scores() + + # Use PCA for dimensionality reduction + all_desc = np.vstack([self.target_descriptors, self.descriptors]) + pca = PCA(n_components=2) + all_pca = pca.fit_transform(all_desc) + + target_pca = all_pca[:len(self.target_descriptors)] + evolved_pca = all_pca[len(self.target_descriptors):] + + n_targets = len(self.target_names) + fig = make_subplots( + rows=1, cols=n_targets, + subplot_titles=[f'Fitness: {name}' for name in self.target_names] + ) + + colors = ['red', 'blue', 'green', 'orange'] + for i, (target_name, fitness) in enumerate(zip(self.target_names, self.fitness_scores)): + col = i + 1 + + # Create scatter plot with fitness as color + fig.add_trace( + go.Scatter( + x=evolved_pca[:, 0], + y=evolved_pca[:, 1], + mode='markers', + marker=dict( + color=fitness, + colorscale='Viridis', + size=8, + opacity=0.7, + colorbar=dict(title='Fitness') if i == 0 else None + ), + name=f'Population - {target_name}', + showlegend=False + ), + row=1, col=col + ) + + # Mark target location + color = colors[i % len(colors)] + fig.add_trace( + go.Scatter( + x=[target_pca[i, 0]], + y=[target_pca[i, 1]], + mode='markers', + name=f'Target: {target_name}', + marker=dict(color=color, size=15, symbol='star', line=dict(width=2, color='black')), + showlegend=(i == 0) + ), + row=1, col=col + ) + + fig.update_layout(height=500, title_text="Fitness Landscapes") + + return fig + + def plot_fitness_distributions(self) -> go.Figure: + """Plot fitness distributions for each target using Plotly.""" + if len(self.fitness_scores) == 0: + self.compute_fitness_scores() + + fig = make_subplots( + rows=1, cols=2, + subplot_titles=['Fitness Distributions', 'Fitness Statistics'] + ) + + # Compute target scores (perfect match = 0 distance = 0 fitness) + target_scores = [0.0] * len(self.target_names) + + # Plot 1: Fitness distributions + for i, (target_name, fitness) in enumerate(zip(self.target_names, self.fitness_scores)): + target_score = target_scores[i] + best_fitness = np.max(fitness) + mean_fitness = np.mean(fitness) + + fig.add_trace( + go.Histogram( + x=fitness, + name=f'{target_name} (target: {target_score:.3f}, best: {best_fitness:.3f}, mean: {mean_fitness:.3f})', + opacity=0.7, + nbinsx=30 + ), + row=1, col=1 + ) + + # Mark target score with vertical line + colors = ['red', 'blue', 'green', 'orange', 'purple'] + fig.add_vline( + x=target_score, + line=dict(color=colors[i % len(colors)], dash='dash', width=2), + row=1, col=1 + ) + + # Plot 2: Best fitness per target + best_fitness = [np.max(fitness) for fitness in self.fitness_scores] + mean_fitness = [np.mean(fitness) for fitness in self.fitness_scores] + + fig.add_trace( + go.Bar(x=self.target_names, y=best_fitness, name='Best', opacity=0.8), + row=1, col=2 + ) + fig.add_trace( + go.Bar(x=self.target_names, y=mean_fitness, name='Mean', opacity=0.8), + row=1, col=2 + ) + + fig.update_xaxes(title_text='Fitness (negative mean distance)', row=1, col=1) + fig.update_yaxes(title_text='Count', row=1, col=1) + fig.update_xaxes(title_text='Target Robot', row=1, col=2) + fig.update_yaxes(title_text='Fitness', row=1, col=2) + + fig.update_layout(height=500, title_text="Fitness Distributions") + + return fig + + def analyze_morphological_diversity(self) -> go.Figure: + """Analyze diversity in the population using Plotly.""" + if len(self.descriptors) == 0: + return go.Figure() + + feature_names = ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + + fig = make_subplots( + rows=2, cols=3, + subplot_titles=feature_names + ) + + for i, feature_name in enumerate(feature_names): + row = (i // 3) + 1 + col = (i % 3) + 1 + + # Plot distribution of evolved robots + feature_data = self.descriptors[:, i] + random_mean = np.mean(feature_data) + random_std = np.std(feature_data) + + fig.add_trace( + go.Histogram( + x=feature_data, + name=f'Evolved robots (μ={random_mean:.3f}, σ={random_std:.3f})', + opacity=0.7, + nbinsx=30, + showlegend=(i == 0) + ), + row=row, col=col + ) + + # Mark target values + colors = ['red', 'blue', 'green', 'orange'] + for j, target_name in enumerate(self.target_names): + target_value = self.target_descriptors[j, i] + fig.add_vline( + x=target_value, + line=dict(color=colors[j % len(colors)], dash='dash', width=2), + row=row, col=col + ) + + fig.update_layout(height=600, title_text="Morphological Diversity Analysis") + + return fig + + def plot_pairwise_feature_landscapes(self, features: List[str] = None) -> go.Figure: + """Plot fitness landscapes for pairwise combinations of selected morphological features using Plotly. + + Parameters: + ----------- + features : List[str], optional + List of feature names to include. By default includes all features: + ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + """ + if len(self.fitness_scores) == 0: + self.compute_fitness_scores() + + # All available features with their indices + all_feature_names = ['Branching', 'Limbs', 'Extensiveness', 'Symmetry', 'Proportion', 'Joints'] + + # Use all features by default + if features is None: + features = all_feature_names.copy() + + # Get indices of selected features + feature_indices = [] + selected_feature_names = [] + for feature in features: + if feature in all_feature_names: + idx = all_feature_names.index(feature) + feature_indices.append(idx) + selected_feature_names.append(feature) + else: + print(f"Warning: Feature '{feature}' not recognized. Available features: {all_feature_names}") + + n_features = len(feature_indices) + if n_features < 2: + print("Error: Need at least 2 features for pairwise plots") + return go.Figure() + + # Create all pairwise combinations - limit to 6 key pairs for display + key_pairs = [(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (4, 5)] + key_pairs = [(feature_indices[i], feature_indices[j]) for i, j in key_pairs + if i < len(feature_indices) and j < len(feature_indices)][:6] + + pair_names = [(all_feature_names[i], all_feature_names[j]) for i, j in key_pairs] + + fig = make_subplots( + rows=2, cols=3, + subplot_titles=[f'{name_i} vs {name_j}' for name_i, name_j in pair_names] + ) + + # Use the first target for fitness coloring + fitness_values = self.fitness_scores[0] if len(self.fitness_scores) > 0 else np.zeros(len(self.descriptors)) + + for idx, ((i, j), (name_i, name_j)) in enumerate(zip(key_pairs, pair_names)): + row = (idx // 3) + 1 + col = (idx % 3) + 1 + + # Extract feature pairs + x_values = self.descriptors[:, i] + y_values = self.descriptors[:, j] + + # Create scatter plot with fitness as color + fig.add_trace( + go.Scatter( + x=x_values, + y=y_values, + mode='markers', + marker=dict( + color=fitness_values, + colorscale='Viridis', + size=6, + opacity=0.7, + colorbar=dict(title='Fitness') if idx == 0 else None + ), + name='Population', + showlegend=(idx == 0) + ), + row=row, col=col + ) + + # Add target locations for all targets + colors = ['red', 'blue', 'green', 'orange', 'purple'] + for target_idx, (target_desc, target_name) in enumerate(zip(self.target_descriptors, self.target_names)): + color = colors[target_idx % len(colors)] + fig.add_trace( + go.Scatter( + x=[target_desc[i]], + y=[target_desc[j]], + mode='markers', + name=f'Target: {target_name}' if idx == 0 else None, + marker=dict(color=color, size=12, symbol='star', line=dict(width=2, color='black')), + showlegend=(idx == 0) + ), + row=row, col=col + ) + + fig.update_xaxes(title_text=name_i, row=row, col=col) + fig.update_yaxes(title_text=name_j, row=row, col=col) + + fig.update_layout(height=600, title_text="Pairwise Feature Landscapes") + + return fig \ No newline at end of file diff --git a/examples/tree_genome_vis.py b/examples/tree_genome_vis.py index 7a26b8e7..73a8c944 100644 --- a/examples/tree_genome_vis.py +++ b/examples/tree_genome_vis.py @@ -28,8 +28,6 @@ from ariel.utils.video_recorder import VideoRecorder from ariel import console from ariel.body_phenotypes.robogen_lite.constructor import construct_mjspec_from_graph -from ariel.body_phenotypes.robogen_lite.decoders.tree_decoder import to_digraph -from ariel.ec.a000 import TreeGenerator from ariel.ec.genotypes.tree.tree_genome import TreeGenome, TreeNode from ariel.body_phenotypes.robogen_lite import config from ariel.simulation.controllers.controller import Controller @@ -41,6 +39,7 @@ from ariel.utils.morphological_descriptor import MorphologicalMeasures from ariel.utils.graph_ops import robot_json_to_digraph, load_robot_json_file + # Type Checking if TYPE_CHECKING: from networkx import DiGraph @@ -352,6 +351,7 @@ def experiment( controller: Controller, duration: int = 15, mode: ViewerTypes = "viewer", + camera_view: str = "default" # Add camera parameter ) -> None: """Run the simulation with random movements.""" # ==================================================================== # @@ -397,9 +397,36 @@ def experiment( duration=duration, ) case "frame": - # Render a single frame (for debugging) - save_path = str(DATA / "robot.png") - single_frame_renderer(model, data, save=True, save_path=save_path) + # Create custom camera based on view type + camera = mj.MjvCamera() + camera.type = mj.mjtCamera.mjCAMERA_FREE + + if camera_view == "front": + camera.lookat = [0, 0, 0.5] + camera.distance = 3.0 + camera.azimuth = 0 + camera.elevation = -20 + elif camera_view == "side": + camera.lookat = [0, 0, 0.5] + camera.distance = 3.0 + camera.azimuth = 90 + camera.elevation = -20 + elif camera_view == "isometric": + camera.lookat = [0, 0, 0.5] + camera.distance = 4.0 + camera.azimuth = 45 + camera.elevation = -30 + else: # default + camera = None + + save_path = str(DATA / f"robot_{camera_view}.png") + single_frame_renderer( + model, + data, + save=True, + save_path=save_path, + camera=camera + ) case "video": # This records a video of the simulation path_to_video_folder = str(DATA / "videos") @@ -433,9 +460,8 @@ def main() -> None: # ? ------------------------------------------------------------------ # tree_genome = create_max_limb_robot() - tree_genome = TreeGenerator.binary_tree(10) - robot_graph = to_digraph(tree_genome) + robot_graph = tree_genome.to_digraph(tree_genome) robot_graph = load_robot_json_file("examples/target_robots/large_robot_25.json") morph_descriptors(robot_graph) @@ -466,7 +492,8 @@ def main() -> None: tracker=tracker, ) - experiment(robot=core, controller=ctrl, mode="launcher") + experiment(robot=core, controller=ctrl, mode="frame", + camera_view="isometric") show_xpos_history(tracker.history["xpos"][0]) diff --git a/generic_ea_settings.py b/generic_ea_settings.py new file mode 100644 index 00000000..62eaf9a9 --- /dev/null +++ b/generic_ea_settings.py @@ -0,0 +1,134 @@ +""" +Generic EA Settings configuration using TypeVars for genome types and operators. +""" + +from typing import TypeVar, Generic, Callable, Any +from pathlib import Path +from pydantic import BaseSettings + +# Define type variables +TGenome = TypeVar('TGenome') # Generic genome type +TMutation = TypeVar('TMutation', bound=Callable[[TGenome], TGenome]) # Mutation callable +TCrossover = TypeVar('TCrossover', bound=Callable[[TGenome, TGenome], tuple[TGenome, TGenome]]) # Crossover callable + +# Database handling modes +DB_HANDLING_MODES = ["delete", "append", "error"] + + +class EASettings(BaseSettings, Generic[TGenome]): + """ + Generic EA Settings class that can work with any genome type. + + TGenome: The genome type (e.g., TreeGenome, ListGenome, etc.) + """ + quiet: bool = False + + # EC mechanisms + is_maximisation: bool = True + first_generation_id: int = 0 + num_of_generations: int = 100 + target_population_size: int = 100 + + # Generic operators - these should be callables that work with TGenome + mutation_operator: Callable[[TGenome], TGenome] + crossover_operator: Callable[[TGenome, TGenome], tuple[TGenome, TGenome]] + + # Individual creation function + create_individual: Callable[[], TGenome] + + # Task configuration + task: str = "evolve_to_copy" + target_robot_file_path: Path | None = Path("examples/target_robots/small_robot_8.json") + + # Data config + output_folder: Path = Path.cwd() / "__data__" + db_file_name: str = "database.db" + db_file_path: Path = output_folder / db_file_name + db_handling: str = "delete" # One of DB_HANDLING_MODES + + class Config: + # Allow arbitrary types for the callable fields + arbitrary_types_allowed = True + + +# Example usage with TreeGenome +from ariel.ec.genotypes.tree.tree_genome import TreeGenome + +class TreeEASettings(EASettings[TreeGenome]): + """Concrete EA settings for TreeGenome.""" + + def __init__(self, **kwargs): + # Set default operators for TreeGenome + if 'mutation_operator' not in kwargs: + kwargs['mutation_operator'] = self._default_tree_mutation + if 'crossover_operator' not in kwargs: + kwargs['crossover_operator'] = self._default_tree_crossover + if 'create_individual' not in kwargs: + kwargs['create_individual'] = TreeGenome.create_individual + + super().__init__(**kwargs) + + @staticmethod + def _default_tree_mutation(genome: TreeGenome) -> TreeGenome: + """Default mutation for TreeGenome.""" + mutator = TreeGenome.get_mutator_object() + return mutator.mutate(genome) + + @staticmethod + def _default_tree_crossover(parent1: TreeGenome, parent2: TreeGenome) -> tuple[TreeGenome, TreeGenome]: + """Default crossover for TreeGenome.""" + crossover = TreeGenome.get_crossover_object() + return crossover.crossover(parent1, parent2) + + +# Alternative approach: Factory function for creating EA settings +def create_ea_settings( + genome_type: type[TGenome], + mutation_func: Callable[[TGenome], TGenome], + crossover_func: Callable[[TGenome, TGenome], tuple[TGenome, TGenome]], + create_func: Callable[[], TGenome], + **kwargs +) -> EASettings[TGenome]: + """ + Factory function to create EA settings for any genome type. + + Args: + genome_type: The genome class (e.g., TreeGenome) + mutation_func: Function that takes a genome and returns a mutated genome + crossover_func: Function that takes two genomes and returns two offspring + create_func: Function that creates a new random genome + **kwargs: Additional settings to override defaults + """ + settings_data = { + 'mutation_operator': mutation_func, + 'crossover_operator': crossover_func, + 'create_individual': create_func, + **kwargs + } + + return EASettings[genome_type](**settings_data) + + +# Example usage: +if __name__ == "__main__": + # Using the concrete TreeEASettings class + tree_settings = TreeEASettings( + num_of_generations=50, + target_population_size=50, + target_robot_file_path=Path("examples/target_robots/medium_robot_15.json") + ) + + print(f"Settings for {tree_settings.num_of_generations} generations") + print(f"Population size: {tree_settings.target_population_size}") + print(f"Mutation operator: {tree_settings.mutation_operator}") + print(f"Crossover operator: {tree_settings.crossover_operator}") + + # Using the factory function + tree_settings2 = create_ea_settings( + genome_type=TreeGenome, + mutation_func=TreeGenome.get_mutator_object().mutate, + crossover_func=TreeGenome.get_crossover_object().crossover, + create_func=TreeGenome.create_individual, + num_of_generations=100, + target_population_size=100 + ) \ No newline at end of file diff --git a/src/ariel/ec/mutations.py b/src/ariel/ec/mutations.py index 155b4266..7a3339ba 100644 --- a/src/ariel/ec/mutations.py +++ b/src/ariel/ec/mutations.py @@ -235,7 +235,7 @@ def integer_creep( # return genome class TreeMutator(Mutation): - + @staticmethod def _random_tree(max_depth: int = 2, branching_prob: float = 0.5) -> Genotype: """Generate a random tree with pheno_configurable branching probability.""" @@ -273,7 +273,7 @@ def random_subtree_replacement( new_individual.root.replace_node(node_to_replace, new_subtree) return new_individual - + class LSystemMutator(Mutation): @staticmethod