diff --git a/experiments/__init__.py b/experiments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/experiments/__pycache__/__init__.cpython-312.pyc b/experiments/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e0b42b2 Binary files /dev/null and b/experiments/__pycache__/__init__.cpython-312.pyc differ diff --git a/experiments/phenotype_genotype_map/myg.json b/experiments/phenotype_genotype_map/myg.json new file mode 100644 index 0000000..354a447 --- /dev/null +++ b/experiments/phenotype_genotype_map/myg.json @@ -0,0 +1,96 @@ +{ + "cortex": { + "id": 0.8543980322495442, + "sensor_ids": [ + 5.685640276474605e-10 + ], + "actuator_ids": [ + 5.685640276474598e-10 + ], + "neuron_ids": [ + 0.15322756084303968, + 0.7120745036860967, + 0.024186022052107514 + ] + }, + "sensor": { + "id": 5.685640276474605e-10, + "name": "xor_GetInput", + "vector_length": 2, + "cx_id": 0.8543980322495442, + "fanout_ids": [ + 0.15322756084303968, + 0.7120745036860967 + ] + }, + "actuator": { + "id": 5.685640276474598e-10, + "name": "xor_SendOutput", + "vector_length": 1, + "cx_id": 0.8543980322495442, + "fanin_ids": [ + 0.024186022052107514 + ] + }, + "neurons": [ + { + "id": 0.15322756084303968, + "layer_index": 0, + "cx_id": 0.8543980322495442, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685640276474605e-10, + "weights": [ + -1.463374592873151, + -0.941866321100616 + ] + } + ], + "output_ids": [ + 0.32112185541392557 + ] + }, + { + "id": 0.7120745036860967, + "layer_index": 0, + "cx_id": 0.8543980322495442, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685640276474605e-10, + "weights": [ + 1.8732878918890417, + 1.4545727537289697 + ] + } + ], + "output_ids": [ + 0.32112185541392557 + ] + }, + { + "id": 0.024186022052107514, + "layer_index": 1, + "cx_id": 0.8543980322495442, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 0.15322756084303968, + "weights": [ + -1.3198353764897197 + ] + }, + { + "input_id": 0.7120745036860967, + "weights": [ + -1.8234720470310797 + ] + } + ], + "output_ids": [ + 5.685640276474598e-10 + ] + } + ] +} \ No newline at end of file diff --git a/experiments/phenotype_genotype_map/network.dot b/experiments/phenotype_genotype_map/network.dot new file mode 100644 index 0000000..90862df --- /dev/null +++ b/experiments/phenotype_genotype_map/network.dot @@ -0,0 +1,22 @@ +digraph G { + rankdir=TB; + node [fontsize=10]; + subgraph cluster_sensors { label="Sensors"; style=dashed; + "S0.000000" [label="xor_GetInput", shape=box]; + } + subgraph cluster_L0 { label="Layer 0"; style=rounded; + "N0.153228" [label="N0", shape=circle]; + "N0.712075" [label="N0", shape=circle]; + } + subgraph cluster_L1 { label="Layer 1"; style=rounded; + "N0.024186" [label="N1", shape=circle]; + } + subgraph cluster_actuators { label="Actuators"; style=dashed; + "A0.000000" [label="xor_SendOutput", shape=diamond]; + } + "S0.000000" -> "N0.153228" [label="w×2"]; + "S0.000000" -> "N0.712075" [label="w×2"]; + "N0.153228" -> "N0.024186" [label="w×1"]; + "N0.712075" -> "N0.024186" [label="w×1"]; + "N0.024186" -> "A0.000000" [label="out"]; +} \ No newline at end of file diff --git a/experiments/phenotype_genotype_map/network.png b/experiments/phenotype_genotype_map/network.png new file mode 100644 index 0000000..06da007 Binary files /dev/null and b/experiments/phenotype_genotype_map/network.png differ diff --git a/experiments/phenotype_genotype_map/network_new.png b/experiments/phenotype_genotype_map/network_new.png new file mode 100644 index 0000000..20dc217 Binary files /dev/null and b/experiments/phenotype_genotype_map/network_new.png differ diff --git a/experiments/phenotype_genotype_map/visualizer.py b/experiments/phenotype_genotype_map/visualizer.py new file mode 100644 index 0000000..a7cbccc --- /dev/null +++ b/experiments/phenotype_genotype_map/visualizer.py @@ -0,0 +1,72 @@ +import json + + +def load_geno(p="myg.json"): + with open(p) as f: + return json.load(f) + + +def to_dot(geno: dict) -> str: + sensors = [] + if "sensors" in geno: + sensors = geno["sensors"] + elif "sensor" in geno: + sensors = [geno["sensor"]] + + actuators = [] + if "actuators" in geno: + actuators = geno["actuators"] + elif "actuator" in geno: + actuators = [geno["actuator"]] + + neurons = geno["neurons"] + + lines = [] + lines.append('digraph G {') + lines.append(' rankdir=TB;') # top-to-bottom + lines.append(' node [fontsize=10];') + + lines.append(' subgraph cluster_sensors { label="Sensors"; style=dashed;') + for s in sensors: + lines.append(f' "S{float(s["id"]):.6f}" [label="{s.get("name", "sensor")}", shape=box];') + lines.append(' }') + + by_layer = {} + for n in neurons: + by_layer.setdefault(n["layer_index"], []).append(n) + + for li in sorted(by_layer.keys()): + lines.append(f' subgraph cluster_L{li} {{ label="Layer {li}"; style=rounded;') + for n in by_layer[li]: + lines.append(f' "N{float(n["id"]):.6f}" [label="N{li}", shape=circle];') + lines.append(' }') + + lines.append(' subgraph cluster_actuators { label="Actuators"; style=dashed;') + for a in actuators: + lines.append(f' "A{float(a["id"]):.6f}" [label="{a.get("name", "act")}", shape=diamond];') + lines.append(' }') + + for n in neurons: + to_id = f'N{float(n["id"]):.6f}' + for iw in n["input_weights"]: + src = iw["input_id"] + wcount = len(iw["weights"]) + is_sensor = any(abs(src - s["id"]) < 1e-9 for s in sensors) + from_id = f'S{float(src):.6f}' if is_sensor else f'N{float(src):.6f}' + lines.append(f' "{from_id}" -> "{to_id}" [label="w×{wcount}"];') + + for a in actuators: + to_id = f'A{float(a["id"]):.6f}' + for src in a.get("fanin_ids", []): + lines.append(f' "N{float(src):.6f}" -> "{to_id}" [label="out"];') + + lines.append('}') + return "\n".join(lines) + + +if __name__ == "__main__": + geno = load_geno("myg.json") + dot = to_dot(geno) + with open("network.dot", "w") as f: + f.write(dot) + print("Wrote network.dot") diff --git a/experiments/stochastic_hillclimber/__init__.py b/experiments/stochastic_hillclimber/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/experiments/stochastic_hillclimber/__pycache__/__init__.cpython-312.pyc b/experiments/stochastic_hillclimber/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..5b6d234 Binary files /dev/null and b/experiments/stochastic_hillclimber/__pycache__/__init__.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors.zip b/experiments/stochastic_hillclimber/actors.zip new file mode 100644 index 0000000..e5dffed Binary files /dev/null and b/experiments/stochastic_hillclimber/actors.zip differ diff --git a/experiments/stochastic_hillclimber/actors/__init__.py b/experiments/stochastic_hillclimber/actors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/__init__.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c0f0480 Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/__init__.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/actor.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/actor.cpython-312.pyc new file mode 100644 index 0000000..0ba08da Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/actor.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/actuator.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/actuator.cpython-312.pyc new file mode 100644 index 0000000..764b4ee Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/actuator.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/cortex.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/cortex.cpython-312.pyc new file mode 100644 index 0000000..cd6e28d Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/cortex.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/exoself.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/exoself.cpython-312.pyc new file mode 100644 index 0000000..bfb5e11 Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/exoself.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/genotype.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/genotype.cpython-312.pyc new file mode 100644 index 0000000..41d8f81 Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/genotype.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/morphology.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/morphology.cpython-312.pyc new file mode 100644 index 0000000..f6c0844 Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/morphology.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/neuron.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/neuron.cpython-312.pyc new file mode 100644 index 0000000..9538f45 Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/neuron.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/scape.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/scape.cpython-312.pyc new file mode 100644 index 0000000..10b2fec Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/scape.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/sensor.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/sensor.cpython-312.pyc new file mode 100644 index 0000000..633c2bf Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/sensor.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/__pycache__/trainer.cpython-312.pyc b/experiments/stochastic_hillclimber/actors/__pycache__/trainer.cpython-312.pyc new file mode 100644 index 0000000..cd284cf Binary files /dev/null and b/experiments/stochastic_hillclimber/actors/__pycache__/trainer.cpython-312.pyc differ diff --git a/experiments/stochastic_hillclimber/actors/actor.py b/experiments/stochastic_hillclimber/actors/actor.py new file mode 100644 index 0000000..f3fd26f --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/actor.py @@ -0,0 +1,13 @@ +import asyncio + + +class Actor: + def __init__(self, name: str): + self.name = name + self.inbox = asyncio.Queue() + + async def send(self, msg): + await self.inbox.put(msg) + + async def run(self): + raise NotImplementedError diff --git a/experiments/stochastic_hillclimber/actors/actuator.py b/experiments/stochastic_hillclimber/actors/actuator.py new file mode 100644 index 0000000..604eab3 --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/actuator.py @@ -0,0 +1,55 @@ +# actors/actuator.py +import asyncio + +from actor import Actor + + +class Actuator(Actor): + def __init__(self, aid, cx_pid, name, fanin_ids, expect_count, scape=None): + super().__init__(f"Actuator-{aid}") + self.aid = aid + self.cx_pid = cx_pid + self.aname = name + self.fanin_ids = fanin_ids + self.expect = expect_count + self.received = {} + self.scape = scape + self.scape_inbox = asyncio.Queue() + + async def run(self): + + while True: + msg = await self.inbox.get() + tag = msg[0] + + if tag == "forward": + _, from_id, vec = msg + self.received[from_id] = vec + + if len(self.received) == self.expect: + print("ACTUATOR: collected all signals...") + output = [] + for fid in self.fanin_ids: + output.extend(self.received[fid]) + + if self.aname == "pts": + print(f"Actuator output: {output}") + fitness, halt_flag = 1.0, 0 + elif self.aname == "xor_SendOutput" and self.scape: + print("ACTUATOR: sending action to scape...") + await self.scape.send(("action", output, self)) + while True: + resp = await self.inbox.get() + if resp[0] == "result": + print("ACTUATOR: got scape response: ", resp) + fitness, halt_flag = resp[1], resp[2] + break + else: + fitness, halt_flag = 0.0, 0 + + await self.cx_pid.send(("sync", self.aid, fitness, halt_flag)) + print("ACTUATOR: sent sync message to cortex.") + self.received.clear() + + elif tag == "terminate": + return diff --git a/experiments/stochastic_hillclimber/actors/best.json b/experiments/stochastic_hillclimber/actors/best.json new file mode 100644 index 0000000..fd0196d --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/best.json @@ -0,0 +1,96 @@ +{ + "cortex": { + "id": 0.5107239887106383, + "sensor_ids": [ + 5.685637947817566e-10 + ], + "actuator_ids": [ + 5.685637947817563e-10 + ], + "neuron_ids": [ + 0.3776999353275148, + 0.7030052650887313, + 0.9936497204289092 + ] + }, + "sensor": { + "id": 5.685637947817566e-10, + "name": "xor_GetInput", + "vector_length": 2, + "cx_id": 0.5107239887106383, + "fanout_ids": [ + 0.3776999353275148, + 0.7030052650887313 + ] + }, + "actuator": { + "id": 5.685637947817563e-10, + "name": "xor_SendOutput", + "vector_length": 1, + "cx_id": 0.5107239887106383, + "fanin_ids": [ + 0.9936497204289092 + ] + }, + "neurons": [ + { + "id": 0.3776999353275148, + "layer_index": 0, + "cx_id": 0.5107239887106383, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685637947817566e-10, + "weights": [ + -1.7328567115118854, + 0.31546591460152307 + ] + } + ], + "output_ids": [ + 0.24149710385676537 + ] + }, + { + "id": 0.7030052650887313, + "layer_index": 0, + "cx_id": 0.5107239887106383, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685637947817566e-10, + "weights": [ + 1.507492385500833, + -1.5181033637128052 + ] + } + ], + "output_ids": [ + 0.24149710385676537 + ] + }, + { + "id": 0.9936497204289092, + "layer_index": 1, + "cx_id": 0.5107239887106383, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 0.3776999353275148, + "weights": [ + 0.9998252528454215 + ] + }, + { + "input_id": 0.7030052650887313, + "weights": [ + -1.7243886895741118 + ] + } + ], + "output_ids": [ + 5.685637947817563e-10 + ] + } + ] +} \ No newline at end of file diff --git a/experiments/stochastic_hillclimber/actors/cortex.py b/experiments/stochastic_hillclimber/actors/cortex.py new file mode 100644 index 0000000..9197530 --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/cortex.py @@ -0,0 +1,119 @@ +# actors/cortex.py +import time +from actor import Actor + + +class Cortex(Actor): + def __init__(self, cid, exoself_pid, sensor_pids, neuron_pids, actuator_pids): + super().__init__(f"Cortex-{cid}") + self.cid = cid + self.sensors = sensor_pids + self.neurons = neuron_pids + self.actuators = actuator_pids + self.exoself_pid = exoself_pid + + self.awaiting_sync = set() # AIDs, von denen wir im aktuellen Schritt noch Sync erwarten + self.fitness_acc = 0.0 # akkumulierte Fitness über die Episode + self.ef_acc = 0 # akkumulierte EndFlags im aktuellen Schritt/Episode + self.cycle_acc = 0 # Anzahl abgeschlossener Sense-Think-Act Zyklen + self.active = False # aktiv/inaktiv (wartet auf reactivate) + self._t0 = None # Startzeit der Episode + + async def _kick_sensors(self): + for s in self.sensors: + await s.send(("sync",)) + + def _reset_for_new_cycle(self): + self.awaiting_sync = set(a.aid for a in self.actuators) + + def _reset_for_new_episode(self): + self.fitness_acc = 0.0 + self.ef_acc = 0 + self.cycle_acc = 0 + self._reset_for_new_cycle() + self._t0 = time.perf_counter() + self.active = True + + async def run(self): + # Initialisierung: falls Actuatoren schon gesetzt sind, Episode starten + if self.actuators: + self._reset_for_new_episode() + await self._kick_sensors() + + while True: + msg = await self.inbox.get() + tag = msg[0] + + # Optional: Exoself kann AIDs registrieren, bevor wir starten + if tag == "register_actuators": + _, aids = msg + # Map aids auf echte Actuatoren falls nötig; hier übernehmen wir sie direkt + self.awaiting_sync = set(aids) + if not self.active: + self._reset_for_new_episode() + await self._kick_sensors() + continue + + if tag == "sync" and self.active: + print("CORTEX: got sync message: ", msg) + # Aktuator meldet Schrittabschluss + _t, aid, fitness, halt_flag = msg + + print("----------------") + print("_t:", _t) + print("aid:", aid) + print("fitness:", fitness) + print("halt_flag:", halt_flag) + print("----------------") + + # akkumulieren + self.fitness_acc += float(fitness) + self.ef_acc += int(halt_flag) + + # diesen Aktuator als "eingetroffen" markieren + if aid in self.awaiting_sync: + self.awaiting_sync.remove(aid) + + print("CORTEX: awaiting sync: ", self.awaiting_sync) + + # Wenn alle Aktuatoren gemeldet haben, ist ein Zyklus fertig + if not self.awaiting_sync: + print("CORTEX: cycle completed.") + self.cycle_acc += 1 + + # Episodenende, wenn irgendein Aktuator EndFlag setzt + if self.ef_acc > 0: + elapsed = time.perf_counter() - self._t0 + # An Exoself berichten + await self.exoself_pid.send(( + "evaluation_completed", + self.fitness_acc, + self.cycle_acc, + elapsed + )) + # inaktiv werden – warten auf reactivate + self.active = False + # Flags für nächste Episode werden erst bei reactivate gesetzt + else: + # Nächsten Zyklus starten + self.ef_acc = 0 + self._reset_for_new_cycle() + await self._kick_sensors() + + continue + + if tag == "reactivate": + # neue Episode starten + self._reset_for_new_episode() + await self._kick_sensors() + continue + + if tag == "terminate": + # geordnet alle Kinder beenden + for a in (self.sensors + self.actuators + self.neurons): + await a.send(("terminate",)) + return + + elif tag == "backup_from_neuron": + # vom Neuron an Cortex -> an Exoself weiterreichen + await self.exoself_pid.send(msg) diff --git a/experiments/stochastic_hillclimber/actors/exoself.py b/experiments/stochastic_hillclimber/actors/exoself.py new file mode 100644 index 0000000..fd97b98 --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/exoself.py @@ -0,0 +1,346 @@ +# exoself.py +import asyncio +import json +import math +import random +from collections import defaultdict +from typing import Any, Dict, List, Tuple, Optional + +from actor import Actor +from cortex import Cortex +from sensor import Sensor +from neuron import Neuron +from actuator import Actuator +from scape import XorScape + + +class Exoself(Actor): + """ + Exoself übersetzt den Genotyp (JSON) in einen laufenden Phenotyp (Actors) und + steuert das simple Neuroevolution-Training (Backup/Restore/Perturb + Reactivate). + """ + def __init__(self, genotype: Dict[str, Any], file_name: Optional[str] = None): + super().__init__("Exoself") + self.g = genotype + self.file_name = file_name + + self.cx_actor: Optional[Cortex] = None + self.sensor_actors: List[Sensor] = [] + self.neuron_actors: List[Neuron] = [] + self.actuator_actors: List[Actuator] = [] + + self.tasks: List[asyncio.Task] = [] + + # Training-Stats + self.highest_fitness = float("-inf") + self.eval_acc = 0 + self.cycle_acc = 0 + self.time_acc = 0.0 + self.attempt = 0 + self.MAX_ATTEMPTS = 50 + self.actuator_scape = None + + # zuletzt perturbierte Neuronen (für Restore) + self._perturbed: List[Neuron] = [] + + # ---------- Convenience ---------- + @staticmethod + def from_file(path: str) -> "Exoself": + with open(path, "r") as f: + g = json.load(f) + return Exoself(g, file_name=path) + + # ---------- Public API ---------- + async def run(self): + # 1) Netzwerk bauen + self._build_pid_map_and_spawn() + + # 2) Cortex verlinken + starten + self._link_cortex() + + # 3) Actors starten (Sensoren/Neuronen/Aktuatoren) + for a in self.sensor_actors + self.neuron_actors + self.actuator_actors + [self.actuator_scape]: + self.tasks.append(asyncio.create_task(a.run())) + + # 4) Hauptloop: auf Cortex-Events hören + while True: + msg = await self.inbox.get() + tag = msg[0] + + if tag == "evaluation_completed": + _, fitness, cycles, elapsed = msg + await self._on_evaluation_completed(fitness, cycles, elapsed) + + elif tag == "terminate": + await self._terminate_all() + return + + # in exoself.py, innerhalb der Klasse Exoself + + async def run_evaluation(self): + """ + Eine einzelne Episode/Evaluation: + - baut & verlinkt das Netz + - startet alle Actors + - wartet auf 'evaluation_completed' vom Cortex + - beendet alles und liefert (fitness, evals, cycles, elapsed) + """ + # 1) Netzwerk bauen & Cortex verlinken + print("build network and link...") + self._build_pid_map_and_spawn() + print("link cortex...") + self._link_cortex() + + # 2) Sensor/Neuron/Aktuator-Tasks starten (Cortex startete _link_cortex bereits) + for a in self.sensor_actors + self.neuron_actors + self.actuator_actors: + self.tasks.append(asyncio.create_task(a.run())) + + if self.actuator_scape: + self.tasks.append(asyncio.create_task(self.actuator_scape.run())) + + print("network actors are running...") + + # 3) Auf Abschluss warten + while True: + msg = await self.inbox.get() + print("message in exsoself: ", msg) + tag = msg[0] + if tag == "evaluation_completed": + _, fitness, cycles, elapsed = msg + # 4) Sauber terminieren + await self._terminate_all() + # Evals = 1 (eine Episode) + return float(fitness), 1, int(cycles), float(elapsed) + elif tag == "terminate": + await self._terminate_all() + # Falls vorzeitig terminiert wurde + return float("-inf"), 0, 0, 0.0 + + # ---------- Build ---------- + def _build_pid_map_and_spawn(self): + """ + Baut Cortex, dann alle Neuronen (mit cx_pid=self.cx_actor), dann verlinkt Outputs schichtweise, + dann Sensoren/Aktuatoren (mit cx_pid=self.cx_actor). Achtung: Reihenfolge wichtig. + """ + cx = self.g["cortex"] + # Cortex zuerst (damit wir cx_pid an Kinder übergeben können) + self.cx_actor = Cortex( + cid=cx["id"], + exoself_pid=self, + sensor_pids=[], # werden gleich gesetzt + neuron_pids=[], + actuator_pids=[] + ) + + self.actuator_scape = XorScape() + + # Neuronen nach Layer gruppieren (damit outputs korrekt gesetzt werden) + layers: Dict[int, List[Dict[str, Any]]] = defaultdict(list) + for n in self.g["neurons"]: + layers[n["layer_index"]].append(n) + ordered_layers = [layers[i] for i in sorted(layers)] + + # Platzhalter: wir benötigen später Referenzen nach ID + id2neuron_actor: Dict[Any, Neuron] = {} + + # Zuerst alle Neuronen erzeugen (ohne Outputs), damit wir Referenzen haben + for layer in ordered_layers: + for n in layer: + input_idps = [(iw["input_id"], iw["weights"]) for iw in n["input_weights"]] + neuron = Neuron( + nid=n["id"], + cx_pid=self.cx_actor, + af_name=n.get("activation_function", "tanh"), + input_idps=input_idps, + output_pids=[] # füllen wir gleich + ) + id2neuron_actor[n["id"]] = neuron + self.neuron_actors.append(neuron) + + # Jetzt Outputs pro Layer setzen: + # - für Nicht-Output-Layer: Outputs = Neuronen der nächsten Schicht + # - für Output-Layer: Outputs = Aktuator(en) (setzen wir nachdem Aktuatoren erzeugt sind) + for li in range(len(ordered_layers) - 1): + next_pids = [id2neuron_actor[nx["id"]] for nx in ordered_layers[li + 1]] + for n in ordered_layers[li]: + id2neuron_actor[n["id"]].outputs = next_pids + + # Aktuatoren anlegen (brauchen cx_pid und fanin_ids) + # Genotyp kann "actuator" (ein Objekt) oder "actuators" (Liste) haben – wir unterstützen beides. + actuators = self._get_actuators_block() + if not actuators: + raise ValueError("Genotype must include 'actuator' or 'actuators'.") + + for a in actuators: + fanin_ids = a.get("fanin_ids", []) + expect = len(fanin_ids) if fanin_ids else 0 + actuator = Actuator( + aid=a["id"], + cx_pid=self.cx_actor, + name=a["name"], + fanin_ids=fanin_ids, + expect_count=expect, + scape=self.actuator_scape + ) + self.actuator_actors.append(actuator) + + # Output-Layer Neuronen → Outputs = Aktuatoren + if ordered_layers: + last_layer = ordered_layers[-1] + out_targets = self.actuator_actors # Liste + for n in last_layer: + id2neuron_actor[n["id"]].outputs = out_targets + + # Sensor(en) anlegen (brauchen cx_pid und fanout auf erste Schicht) + sensors = self._get_sensors_block() + if not sensors: + raise ValueError("Genotype must include 'sensor' or 'sensors'.") + + first_layer = ordered_layers[0] if ordered_layers else [] + first_layer_pids = [id2neuron_actor[n["id"]] for n in first_layer] + + for s in sensors: + sensor = Sensor( + sid=s["id"], + cx_pid=self.cx_actor, + name=s["name"], + vector_length=s["vector_length"], + fanout_pids=first_layer_pids, + scape=self.actuator_scape + ) + self.sensor_actors.append(sensor) + + def _get_sensors_block(self) -> List[Dict[str, Any]]: + if "sensors" in self.g: + return list(self.g["sensors"]) + if "sensor" in self.g: + return [self.g["sensor"]] + return [] + + def _get_actuators_block(self) -> List[Dict[str, Any]]: + if "actuators" in self.g: + return list(self.g["actuators"]) + if "actuator" in self.g: + return [self.g["actuator"]] + return [] + + # ---------- Link ---------- + def _link_cortex(self): + """ + Übergibt dem Cortex die fertigen Listen und setzt awaiting_sync. + Startet dann den Cortex-Task. + """ + self.cx_actor.sensors = [a for a in self.sensor_actors if a] + self.cx_actor.neurons = [a for a in self.neuron_actors if a] + self.cx_actor.actuators = [a for a in self.actuator_actors if a] + + # Wichtig: vor Start die erwarteten AIDs setzen, + # damit der erste Sensor-Trigger nicht in eine leere awaiting_sync läuft. + self.cx_actor.awaiting_sync = set(a.aid for a in self.cx_actor.actuators) + + # Cortex starten + self.tasks.append(asyncio.create_task(self.cx_actor.run())) + + # ---------- Training-Loop Reaction ---------- + async def _on_evaluation_completed(self, fitness: float, cycles: int, elapsed: float): + self.eval_acc += 1 + self.cycle_acc += int(cycles) + self.time_acc += float(elapsed) + + print(f"[Exoself] evaluation_completed: fitness={fitness:.6f} cycles={cycles} time={elapsed:.3f}s") + + if fitness > self.highest_fitness: + self.highest_fitness = fitness + self.attempt = 0 + # Backup aller Neuronen + for n in self.neuron_actors: + await n.send(("weight_backup",)) + else: + self.attempt += 1 + # Restore nur der zuletzt perturbierten Neuronen + for n in self._perturbed: + await n.send(("weight_restore",)) + + # Stop-Kriterium? + if self.attempt >= self.MAX_ATTEMPTS: + print(f"[Exoself] STOP. Best fitness={self.highest_fitness:.6f} evals={self.eval_acc} cycles={self.cycle_acc}") + await self._backup_genotype() + await self._terminate_all() + return { + "best_fitness": self.highest_fitness, + "eval_acc": self.eval_acc, + "cycle_acc": self.cycle_acc, + "time_acc": self.time_acc, + } + + # Perturbiere Teilmenge der Neuronen + tot = len(self.neuron_actors) + mp = 1.0 / math.sqrt(max(1, tot)) + self._perturbed = [n for n in self.neuron_actors if random.random() < mp] + + for n in self._perturbed: + await n.send(("weight_perturb",)) + + # Nächste Episode starten + await self.cx_actor.send(("reactivate",)) + + # ---------- Backup Genotype ---------- + async def _backup_genotype(self): + """ + Holt von allen Neuronen die aktuellen Weights und schreibt sie in self.g. + Speichert optional in self.file_name. + """ + # 1) Request + remaining = len(self.neuron_actors) + for n in self.neuron_actors: + await n.send(("get_backup",)) + + # 2) Collect vom Cortex-Postfach (Neuronen senden an cx_pid → cx leitet an Exoself weiter + # oder du hast sie direkt an Exoself schicken lassen; falls direkt an Cortex, dann + # lausche hier stattdessen auf self.cx_actor.inbox. In deinem Neuron-Code geht es an cx_pid, + # und in deiner bisherigen Implementierung hast du aus dem Cortex-Postfach gelesen. + # Hier vereinfachen wir: Neuronen senden direkt an EXOSELF (passe Neuron ggf. an). + backups: List[Tuple[Any, List[Tuple[Any, List[float]]]]] = [] + + while remaining > 0: + msg = await self.inbox.get() + if msg[0] == "backup_from_neuron": + _, nid, idps = msg + backups.append((nid, idps)) + remaining -= 1 + + # 3) Update JSON + # exoself.py -> in _backup_genotype() + id2n = {n["id"]: n for n in self.g["neurons"]} + for nid, idps in backups: + if nid not in id2n: + continue + new_iw = [] + bias_val = None + for item in idps: + if isinstance(item[0], str) and item[0] == "bias": + bias_val = float(item[1]) if not isinstance(item[1], list) else float(item[1][0]) + else: + input_id, weights = item + new_iw.append({"input_id": input_id, "weights": list(weights)}) + id2n[nid]["input_weights"] = new_iw + # Bias mit abspeichern (Variante B): + if bias_val is not None: + id2n[nid].setdefault("input_weights", []).append({"input_id": "bias", "weights": [bias_val]}) + + # 4) Save + if self.file_name: + with open(self.file_name, "w") as f: + json.dump(self.g, f, indent=2) + print(f"[Exoself] Genotype updated → {self.file_name}") + + # ---------- Termination ---------- + async def _terminate_all(self): + for a in self.sensor_actors + self.neuron_actors + self.actuator_actors: + await a.send(("terminate",)) + if self.cx_actor: + await self.cx_actor.send(("terminate",)) + for t in self.tasks: + if not t.done(): + t.cancel() + self.tasks.clear() diff --git a/experiments/stochastic_hillclimber/actors/experimental.json b/experiments/stochastic_hillclimber/actors/experimental.json new file mode 100644 index 0000000..286fe5e --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/experimental.json @@ -0,0 +1,96 @@ +{ + "cortex": { + "id": 0.0873421806271496, + "sensor_ids": [ + 5.6856379409509e-10 + ], + "actuator_ids": [ + 5.685637940950896e-10 + ], + "neuron_ids": [ + 0.6384794610574114, + 0.6023131059017351, + 0.39277072802910173 + ] + }, + "sensor": { + "id": 5.6856379409509e-10, + "name": "xor_GetInput", + "vector_length": 2, + "cx_id": 0.0873421806271496, + "fanout_ids": [ + 0.6384794610574114, + 0.6023131059017351 + ] + }, + "actuator": { + "id": 5.685637940950896e-10, + "name": "xor_SendOutput", + "vector_length": 1, + "cx_id": 0.0873421806271496, + "fanin_ids": [ + 0.39277072802910173 + ] + }, + "neurons": [ + { + "id": 0.6384794610574114, + "layer_index": 0, + "cx_id": 0.0873421806271496, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.6856379409509e-10, + "weights": [ + -1.241701053140722, + -0.9643293453192259 + ] + } + ], + "output_ids": [ + 0.20086494757397733 + ] + }, + { + "id": 0.6023131059017351, + "layer_index": 0, + "cx_id": 0.0873421806271496, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.6856379409509e-10, + "weights": [ + -0.38795737506820904, + -0.5125123920689245 + ] + } + ], + "output_ids": [ + 0.20086494757397733 + ] + }, + { + "id": 0.39277072802910173, + "layer_index": 1, + "cx_id": 0.0873421806271496, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 0.6384794610574114, + "weights": [ + -0.7426574958298033 + ] + }, + { + "input_id": 0.6023131059017351, + "weights": [ + -0.9823211499227558 + ] + } + ], + "output_ids": [ + 5.685637940950896e-10 + ] + } + ] +} \ No newline at end of file diff --git a/experiments/stochastic_hillclimber/actors/genotype.py b/experiments/stochastic_hillclimber/actors/genotype.py new file mode 100644 index 0000000..188b81f --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/genotype.py @@ -0,0 +1,266 @@ +# genotype.py +import json +import math +import random +import time +from typing import Dict, List, Tuple, Any, Optional + + +# ----------------------------- +# Utilities / ID & Weights +# ----------------------------- +def generate_id() -> float: + return random.random() + + +def create_neural_weights(vector_length: int) -> List[float]: + return [random.uniform(-2.0, 2.0) for _ in range(vector_length)] + + +# ----------------------------- +# Morphology "Interface" +# ----------------------------- +""" +Erwartete Morphologie-API (duck-typed): + +- get_InitSensor(morphology) -> dict mit Feldern: + { + "id": , + "name": , + "vector_length": , + # optional: "scape": , + } + +- get_InitActuator(morphology) -> dict mit Feldern: + { + "id": , + "name": , + "vector_length": , + # optional: "scape": , + } + +Du kannst z.B. ein kleines Modul/Objekt übergeben, das diese Funktionen bereitstellt. +""" + + +# ----------------------------- +# Genotype-Erzeugung +# ----------------------------- +def construct( + morphology_module, + hidden_layer_densities: List[int], + file_name: Optional[str] = None, + *, + add_bias: bool = False, +) -> Dict[str, Any]: + """ + Baut einen Genotyp (JSON-kompatibles Dict) analog zur Erlang-Version: + + - wählt Initial-Sensor & -Aktuator aus Morphologie + - bestimmt Output-Layer-Dichte = actuator.vector_length + - erzeugt alle Neuronen-Schichten & Gewichte + - setzt fanout_ids/fanin_ids + - erstellt Cortex mit Listen der IDs + - speichert optional in file_name + + add_bias: Wenn True, hängt pro Neuron einen Bias als ('bias', b) an die Input-Liste an. + Dein bisheriges JSON nutzt KEIN Bias-Element. Standard=False für Kompatibilität. + """ + + # Seed wie im Buch + rnd_seed = time.time_ns() & 0xFFFFFFFF + random.seed(rnd_seed) + + # 1) Sensor & Aktuator aus Morphologie + S = morphology_module.get_InitSensor(morphology_module) + A = morphology_module.get_InitActuator(morphology_module) + + # Pflichtfelder normalisieren + sensor = { + "id": S.get("id", generate_id()), + "name": S["name"], + "vector_length": int(S["vector_length"]), + "cx_id": None, # wird später gesetzt + "fanout_ids": [], # wird später gesetzt + # optional: + # "scape": S.get("scape") + } + + actuator = { + "id": A.get("id", generate_id()), + "name": A["name"], + "vector_length": int(A["vector_length"]), + "cx_id": None, # wird später gesetzt + "fanin_ids": [], # wird später gesetzt + # optional: + # "scape": A.get("scape") + } + + # 2) Output-Layer-Dichte = actuator.vector_length (wie im Buch) + output_vl = actuator["vector_length"] + layer_densities = list(hidden_layer_densities) + [output_vl] + + # 3) Cortex-ID festlegen + cortex_id = generate_id() + + # 4) Neuronen-Schichten erzeugen + layers = _create_neuro_layers( + cx_id=cortex_id, + sensor=sensor, + actuator=actuator, + layer_densities=layer_densities, + add_bias=add_bias, + ) + + # 5) Sensor/Aktuator mit Cortex/Fanout/Fanin verbinden + input_layer = layers[0] + output_layer = layers[-1] + + sensor["cx_id"] = cortex_id + sensor["fanout_ids"] = [n["id"] for n in input_layer] + + actuator["cx_id"] = cortex_id + actuator["fanin_ids"] = [n["id"] for n in output_layer] + + # 6) Cortex-Objekt + neuron_ids = [n["id"] for layer in layers for n in layer] + cortex = { + "id": cortex_id, + "sensor_ids": [sensor["id"]], + "actuator_ids": [actuator["id"]], + "neuron_ids": neuron_ids, + } + + # 7) Genotyp zusammensetzen + genotype = { + "cortex": cortex, + "sensor": sensor, + "actuator": actuator, + "neurons": [n for layer in layers for n in layer], + } + + # 8) Optional speichern + if file_name: + save_genotype(file_name, genotype) + + return genotype + + +def _create_neuro_layers( + cx_id: float, + sensor: Dict[str, Any], + actuator: Dict[str, Any], + layer_densities: List[int], + *, + add_bias: bool, +) -> List[List[Dict[str, Any]]]: + """ + Baut alle Neuronen-Schichten. + - Erste Schicht erhält als Input den Sensor (Vektor). + - Mittlere Schichten erhalten skalare Inputs von voriger Schicht. + - Letzte Schicht verbindet zum Aktuator (output_ids = [actuator.id]). + """ + layers: List[List[Dict[str, Any]]] = [] + + # Platzhalter-Input: (input_id, vector_length) + input_idps: List[Tuple[float, int]] = [(sensor["id"], sensor["vector_length"])] + + for layer_index, layer_density in enumerate(layer_densities): + # Neuron-IDs generieren + neuron_ids = [generate_id() for _ in range(layer_density)] + + # output_ids: Standardmäßig IDs der nächsten Schicht (werden nach dem Layer bekannt) + if layer_index < len(layer_densities) - 1: + # IDs der nächste Schicht vorab erzeugen (für output_ids) + next_ids = [generate_id() for _ in range(layer_densities[layer_index + 1])] + # Aber: wir erzeugen hier *nur* die IDs für output_ids, nicht die Neuronen der nächsten Schicht + output_ids = next_ids + else: + # letzte Schicht → zum Aktuator + output_ids = [actuator["id"]] + + # Layer-Neuronen erzeugen + this_layer: List[Dict[str, Any]] = [] + for _nid in neuron_ids: + proper_input = _create_neural_input(input_idps, add_bias=add_bias) + neuron = { + "id": _nid, + "layer_index": layer_index, + "cx_id": cx_id, + "activation_function": "tanh", + # JSON-Konvention: [{"input_id": id, "weights": [...]}, ...] + "input_weights": [{"input_id": i, "weights": w} for (i, w) in proper_input if not _is_bias_tuple((i, w))], + "output_ids": output_ids[:], # Kopie + } + this_layer.append(neuron) + + layers.append(this_layer) + + # Inputs für nächste Schicht: jetzt sind es skalare Outputs der aktuellen Neuronen + input_idps = [(n["id"], 1) for n in this_layer] + + # WICHTIG: oben haben wir für mittlere Layer „künstliche“ next_ids erzeugt, damit output_ids befüllt waren. + # In deiner Laufzeit-Topologie ignorieren wir diese Platzhalter und verdrahten später im Exoself die + # tatsächlichen Actor-Referenzen schichtweise. Für den Genotyp sind die output_ids der Zwischenlayer + # nicht kritisch (du überschreibst sie ohnehin beim Phenotype-Mapping). Die letzte Schicht zeigt korrekt auf den Aktuator. + + return layers + + +def _is_bias_tuple(t: Tuple[Any, Any]) -> bool: + """Hilfsfunktion falls du später Bias im JSON aufnehmen willst.""" + key, _ = t + return isinstance(key, str) and key == "bias" + + +def _create_neural_input( + input_idps: List[Tuple[float, int]], + *, + add_bias: bool, +) -> List[Tuple[Any, List[float]]]: + """ + Wandelt Platzhalter (input_id, vector_length) in (input_id, weights[]) um. + Optional einen Bias als ('bias', [b]) anhängen (Erlang-Variante). + """ + proper: List[Tuple[Any, List[float]]] = [] + for input_id, vl in input_idps: + proper.append((input_id, create_neural_weights(vl))) + + if add_bias: + proper.append(("bias", [random.random() - 0.5])) + + return proper + + +# ----------------------------- +# File I/O & Print +# ----------------------------- +def save_genotype(file_name: str, genotype: Dict[str, Any]) -> None: + with open(file_name, "w") as f: + json.dump(genotype, f, indent=2) + + +def load_from_file(file_name: str) -> Dict[str, Any]: + with open(file_name, "r") as f: + return json.load(f) + + +def print_genotype(file_name: str) -> None: + g = load_from_file(file_name) + cx = g["cortex"] + print("[CORTEX]", cx) + sids = cx.get("sensor_ids", []) + nids = cx.get("neuron_ids", []) + aids = cx.get("actuator_ids", []) + + # Indexe für schnellen Zugriff + nid2n = {n["id"]: n for n in g.get("neurons", [])} + sid2s = {g["sensor"]["id"]: g["sensor"]} if "sensor" in g else {s["id"]: s for s in g.get("sensors", [])} + aid2a = {g["actuator"]["id"]: g["actuator"]} if "actuator" in g else {a["id"]: a for a in g.get("actuators", [])} + + for sid in sids: + print("[SENSOR]", sid2s.get(sid)) + for nid in nids: + print("[NEURON]", nid2n.get(nid)) + for aid in aids: + print("[ACTUATOR]", aid2a.get(aid)) diff --git a/experiments/stochastic_hillclimber/actors/morphology.py b/experiments/stochastic_hillclimber/actors/morphology.py new file mode 100644 index 0000000..688f409 --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/morphology.py @@ -0,0 +1,104 @@ +# morphology.py +import time +from typing import Any, Callable, Dict, List, Union + +MorphologyType = Union[str, Callable[[str], List[Dict[str, Any]]]] + + +def generate_id() -> float: + """ + Zeitbasierte float-ID (ähnlich wie im Buch). + """ + now = time.time() + return 1.0 / now + + +# ------------------------------------------------------------ +# Morphologie-API (duck-typed, wie im Erlang-Original) +# ------------------------------------------------------------ +def get_InitSensor(morphology: MorphologyType) -> Dict[str, Any]: + sensors = get_Sensors(morphology) + if not sensors: + raise ValueError("Morphology has no sensors.") + return sensors[0] + + +def get_InitActuator(morphology: MorphologyType) -> Dict[str, Any]: + actuators = get_Actuators(morphology) + if not actuators: + raise ValueError("Morphology has no actuators.") + return actuators[0] + + +def get_Sensors(morphology: MorphologyType) -> List[Dict[str, Any]]: + fn = _resolve_morphology(morphology) + return fn("sensors") + + +def get_Actuators(morphology: MorphologyType) -> List[Dict[str, Any]]: + fn = _resolve_morphology(morphology) + return fn("actuators") + + +def _resolve_morphology(morphology: MorphologyType) -> Callable[[str], List[Dict[str, Any]]]: + """ + Akzeptiert: + - eine Callable Morphologie-Funktion (z.B. xor_mimic) + - einen String (z.B. "xor_mimic") -> per Registry auflösen + - ein Modul-Objekt (z.B. import morphology) -> Standard 'xor_mimic' aus dem Modul + """ + # 1) Bereits eine Callable-Funktion? (z.B. xor_mimic) + if callable(morphology): + return morphology + + # 2) String -> Registry + if isinstance(morphology, str): + reg = { + "xor_mimic": xor_mimic, + # weitere Morphologien hier registrieren... + } + if morphology in reg: + return reg[morphology] + raise ValueError(f"Unknown morphology name: {morphology}") + + # 3) Modul-Objekt: versuche eine Standard-Morphologie-Funktion daraus zu nehmen + # Hier nehmen wir 'xor_mimic' als Default (du kannst das generalisieren). + try: + # Ist es ein Modul mit einer Funktion 'xor_mimic'? + if hasattr(morphology, "xor_mimic") and callable(getattr(morphology, "xor_mimic")): + return getattr(morphology, "xor_mimic") + except Exception: + pass + + raise TypeError("morphology must be a callable, a module with 'xor_mimic', or a registered string key") + + +# ------------------------------------------------------------ +# Beispiel-Morphologie: XOR (wie im Buch) +# ------------------------------------------------------------ +def xor_mimic(kind: str) -> List[Dict[str, Any]]: + """ + Liefert je nach 'kind' ('sensors' | 'actuators') die Definitionen. + Felder sind so benannt, dass sie direkt mit unserem Genotype/Phenotype-Code + kompatibel sind (vector_length statt 'vl', etc.). + """ + if kind == "sensors": + return [ + { + "id": generate_id(), + "name": "xor_GetInput", # Sensorfunktion (muss in deinem Sensor-Actor implementiert sein) + "vector_length": 2, + "scape": {"private": "xor_sim"} # optional, falls du Scapes nutzt + } + ] + elif kind == "actuators": + return [ + { + "id": generate_id(), + "name": "xor_SendOutput", # Aktuatorfunktion (muss in deinem Actuator-Actor implementiert sein) + "vector_length": 1, + "scape": {"private": "xor_sim"} # optional + } + ] + else: + raise ValueError(f"xor_mimic: unsupported kind '{kind}', expected 'sensors' or 'actuators'") diff --git a/experiments/stochastic_hillclimber/actors/mtest.py b/experiments/stochastic_hillclimber/actors/mtest.py new file mode 100644 index 0000000..775ce8c --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/mtest.py @@ -0,0 +1,39 @@ +import asyncio + +import morphology +from experiments.stochastic_hillclimber.actors.trainer import Trainer +from genotype import construct, save_genotype + + +def test_genotype_construction(): + """ + genotype_data = construct(morphology, hidden_layer_densities=[3, 2]) + + # Prüfen, ob Cortex, Sensor, Actuator und Neuronen existieren + assert "cortex" in genotype_data + assert len(genotype_data["neurons"]) == 3 + 2 + 1 # 3 in 1. HL, 2 in 2. HL, 1 Output + + print("Genotype construction OK") + print("Cortex:", genotype_data["cortex"]) + print("---------------------------------") + print("Neurons:", genotype_data["neurons"]) + print("---------------------------------") + print("Actuators:", genotype_data["actuator"]) + + save_genotype("test.json", genotype_data) + """ + + trainer = Trainer( + morphology_spec=morphology, # <— wichtig! callable oder "xor_mimic" + hidden_layer_densities=[2], # wie im Buchbeispiel + max_attempts=float("inf"), # MA=inf + eval_limit=float("inf"), # EL=inf + fitness_target=99.9, # FT=99.9 + experimental_file="experimental.json", + best_file="best.json", + exoself_steps_per_eval=0, # 0 = Scape/Cortex entscheiden über Halt + ) + asyncio.run(trainer.go()) + + +test_genotype_construction() diff --git a/experiments/stochastic_hillclimber/actors/neuron.py b/experiments/stochastic_hillclimber/actors/neuron.py new file mode 100644 index 0000000..14c488c --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/neuron.py @@ -0,0 +1,105 @@ +# actors/neuron.py +import math +import random + +from actor import Actor + + +def tanh(x): return math.tanh(x) + + +class Neuron(Actor): + def __init__(self, nid, cx_pid, af_name, input_idps, output_pids): + super().__init__(f"Neuron-{nid}") + self.nid = nid + self.cx_pid = cx_pid + self.af = tanh if af_name == "tanh" else tanh + # input_idps: [(input_id, [w1, w2, ...])] + self.inputs = {} + self.order = [] + for (inp_id, weights) in input_idps: + self.order.append(inp_id) + self.inputs[inp_id] = {"weights": list(weights), "got": False, "val": None} + # WICHTIG: Bias lernbar machen + self.bias = random.uniform(-2.0, 2.0) + # Für Backup/Restore + self._backup_inputs = None + self._backup_bias = None + + self.outputs = output_pids + + print(f"Neuron {nid}: inputs={list(self.inputs.keys())}, bias={self.bias}") + + async def run(self): + while True: + msg = await self.inbox.get() + tag = msg[0] + + if tag == "forward": + _, from_id, data = msg + if from_id not in self.inputs: + continue + slot = self.inputs[from_id] + slot["got"] = True + slot["val"] = data + + if all(self.inputs[i]["got"] for i in self.order): + acc = 0.0 + for i in self.order: + w = self.inputs[i]["weights"] + v = self.inputs[i]["val"] + if isinstance(v, list): + acc += sum(wj * vj for wj, vj in zip(w, v)) + else: + acc += w[0] * float(v) + out = self.af(acc + self.bias) + for pid in self.outputs: + await pid.send(("forward", self.nid, [out])) + for i in self.order: + self.inputs[i]["got"] = False + self.inputs[i]["val"] = None + print(f"Neuron {self.nid}: input_sum={acc + self.bias:.3f}, output={out:.3f}") + + elif tag == "get_backup": + # Packe Gewichte + Bias zurück + idps = [(i, self.inputs[i]["weights"]) for i in self.order] + idps.append(("bias", self.bias)) + await self.cx_pid.send(("backup_from_neuron", self.nid, idps)) + + elif tag == "weight_backup": + # Tiefkopie der Gewichte + Bias + print(f"Neuron {self.nid}: backing up weights") + self._backup_inputs = {k: {"weights": v["weights"][:]} for k, v in self.inputs.items()} + self._backup_bias = self.bias + + elif tag == "weight_restore": + if self._backup_inputs is not None: + for k in self.inputs: + self.inputs[k]["weights"] = self._backup_inputs[k]["weights"][:] + self.bias = self._backup_bias + + elif tag == "weight_perturb": + # In neuron.py weight_perturb, add: + print(f"Neuron {self.nid}: perturbing {len([w for i in self.order for w in self.inputs[i]['weights']])} weights") + # MP wie im Buch: 1/sqrt(TotalWeightsIncludingBias) + tot_w = sum(len(self.inputs[i]["weights"]) for i in self.order) + 1 + mp = 1 / math.sqrt(tot_w) + delta_mag = 2.0 * math.pi + sat_lim = 2.0 * math.pi + + # Gewichte perturbieren + for i in self.order: + ws = self.inputs[i]["weights"] + for j in range(len(ws)): + if random.random() < mp: + ws[j] = _sat(ws[j] + (random.random() - 0.5) * delta_mag, -sat_lim, sat_lim) + # Bias perturbieren + if random.random() < mp: + self.bias = _sat(self.bias + (random.random() - 0.5) * delta_mag, -sat_lim, sat_lim) + + elif tag == "terminate": + return + + +def _sat(val, lo, hi): + return lo if val < lo else (hi if val > hi else val) diff --git a/experiments/stochastic_hillclimber/actors/scape.py b/experiments/stochastic_hillclimber/actors/scape.py new file mode 100644 index 0000000..95afe57 --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/scape.py @@ -0,0 +1,58 @@ +# actors/scape.py +from actor import Actor +import math + + +class XorScape(Actor): + def __init__(self): + super().__init__("XorScape") + self.data = [ + ([-1, -1], [-1]), + ([1, -1], [1]), + ([-1, 1], [1]), + ([1, 1], [-1]) + ] + self.index = 0 + self.error_acc = 0.0 + self.last_actuator = None # <-- wer uns zuletzt "action" geschickt hat + + async def run(self): + while True: + msg = await self.inbox.get() + tag = msg[0] + + if tag == "sense": + print("SCAPE: got sensed by sensor...") + # Sensor fragt nach Input + _, sid, from_pid = msg + self.last_actuator = from_pid # merken, wer beteiligt ist + inputs, correct = self.data[self.index] + print("SENSOR input /correct: ", inputs, correct) + await from_pid.send(("percept", inputs)) + + elif tag == "action": + _, output, from_pid = msg + _, correct = self.data[self.index] + + # Calculate RMSE like the Erlang version + step_error = sum((o - c) ** 2 for o, c in zip(output, correct)) + step_rmse = math.sqrt(step_error) # This is the key change! + + print(f"XOR PATTERN: Input={self.data[self.index][0]} → Network={output} → Expected={correct}") + + self.index += 1 + + if self.index >= len(self.data): + # Episode finished - calculate final fitness + total_rmse = math.sqrt(self.error_acc + step_rmse) # Not step_error! + fitness = 1.0 / (total_rmse + 1e-5) + await from_pid.send(("result", fitness, 1)) + self.index = 0 + self.error_acc = 0.0 + else: + # Continue episode + self.error_acc += step_rmse # Add RMSE, not raw error + await from_pid.send(("result", 0.0, 0)) + + elif tag == "terminate": + return diff --git a/experiments/stochastic_hillclimber/actors/sensor.py b/experiments/stochastic_hillclimber/actors/sensor.py new file mode 100644 index 0000000..7ef72cb --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/sensor.py @@ -0,0 +1,45 @@ +# actors/sensor.py +from actor import Actor +import random + + +class Sensor(Actor): + def __init__(self, sid, cx_pid, name, vector_length, fanout_pids, scape=None): + super().__init__(f"Sensor-{sid}") + self.sid = sid + self.cx_pid = cx_pid + self.sname = name + self.vl = vector_length + self.fanout = fanout_pids + self.scape = scape + + async def run(self): + while True: + print("sensor running...") + msg = await self.inbox.get() + tag = msg[0] + + print("got sensor message: ", msg) + + if tag == "sync": + vec = await self._sense() + print("sensed vec: ", vec) + for pid in self.fanout: + await pid.send(("forward", self.sid, vec)) + + elif tag == "terminate": + return + + async def _sense(self): + if self.sname == "rng": + return [random.random() for _ in range(self.vl)] + elif self.sname == "xor_GetInput" and self.scape: + print("TODO") + await self.scape.send(("sense", self.sid, self)) + msg = await self.inbox.get() + if msg[0] == "percept": + return msg[1] + else: + return [0.0] * self.vl + else: + return [0.0] * self.vl diff --git a/experiments/stochastic_hillclimber/actors/test.json b/experiments/stochastic_hillclimber/actors/test.json new file mode 100644 index 0000000..959f5e1 --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/test.json @@ -0,0 +1,179 @@ +{ + "cortex": { + "id": 5.685689537782505e-10, + "sensor_ids": [ + 5.685689537782522e-10 + ], + "actuator_ids": [ + 5.685689537782516e-10 + ], + "neuron_ids": [ + 5.685689537782498e-10, + 5.685689537782496e-10, + 5.685689537782496e-10, + 5.685689537782457e-10, + 5.685689537782457e-10, + 5.685689537782437e-10 + ] + }, + "sensor": { + "id": 5.685689537782522e-10, + "name": "xor_GetInput", + "vector_length": 2, + "cx_id": 5.685689537782505e-10, + "fanout_ids": [ + 5.685689537782498e-10, + 5.685689537782496e-10, + 5.685689537782496e-10 + ] + }, + "actuator": { + "id": 5.685689537782516e-10, + "name": "xor_SendOutput", + "vector_length": 1, + "cx_id": 5.685689537782505e-10, + "fanin_ids": [ + 5.685689537782437e-10 + ] + }, + "neurons": [ + { + "id": 5.685689537782498e-10, + "layer_index": 0, + "cx_id": 5.685689537782505e-10, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685689537782522e-10, + "weights": [ + -0.16508088565795254, + -0.2129110085532152 + ] + } + ], + "output_ids": [ + 5.685689537782493e-10, + 5.685689537782493e-10 + ] + }, + { + "id": 5.685689537782496e-10, + "layer_index": 0, + "cx_id": 5.685689537782505e-10, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685689537782522e-10, + "weights": [ + -0.2097398020485577, + 0.39878180348846304 + ] + } + ], + "output_ids": [ + 5.685689537782493e-10, + 5.685689537782493e-10 + ] + }, + { + "id": 5.685689537782496e-10, + "layer_index": 0, + "cx_id": 5.685689537782505e-10, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685689537782522e-10, + "weights": [ + -0.29051024087142796, + 0.4799280128038572 + ] + } + ], + "output_ids": [ + 5.685689537782493e-10, + 5.685689537782493e-10 + ] + }, + { + "id": 5.685689537782457e-10, + "layer_index": 1, + "cx_id": 5.685689537782505e-10, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685689537782498e-10, + "weights": [ + -0.13139529896484625 + ] + }, + { + "input_id": 5.685689537782496e-10, + "weights": [ + 0.0922098630405015 + ] + }, + { + "input_id": 5.685689537782496e-10, + "weights": [ + 0.07506465172430998 + ] + } + ], + "output_ids": [ + 5.685689537782454e-10 + ] + }, + { + "id": 5.685689537782457e-10, + "layer_index": 1, + "cx_id": 5.685689537782505e-10, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685689537782498e-10, + "weights": [ + 0.01874150935077279 + ] + }, + { + "input_id": 5.685689537782496e-10, + "weights": [ + 0.1722685772992305 + ] + }, + { + "input_id": 5.685689537782496e-10, + "weights": [ + -0.07333424218267892 + ] + } + ], + "output_ids": [ + 5.685689537782454e-10 + ] + }, + { + "id": 5.685689537782437e-10, + "layer_index": 2, + "cx_id": 5.685689537782505e-10, + "activation_function": "tanh", + "input_weights": [ + { + "input_id": 5.685689537782457e-10, + "weights": [ + 0.47060152728694127 + ] + }, + { + "input_id": 5.685689537782457e-10, + "weights": [ + 0.33179877773351285 + ] + } + ], + "output_ids": [ + 5.685689537782516e-10 + ] + } + ] +} \ No newline at end of file diff --git a/experiments/stochastic_hillclimber/actors/trainer.py b/experiments/stochastic_hillclimber/actors/trainer.py new file mode 100644 index 0000000..24aeaca --- /dev/null +++ b/experiments/stochastic_hillclimber/actors/trainer.py @@ -0,0 +1,151 @@ +# trainer.py +import asyncio +import time +from typing import Any, Dict, List, Tuple, Optional + +import morphology # dein morphology.py +from genotype import construct, save_genotype, print_genotype +from exoself import Exoself # deine Actor-basierte Exoself-Implementierung + + +class Trainer: + """ + Stochastischer Hillclimber wie im Erlang-Original. + - morphology_spec: entweder das morphology-Modul, ein String-Key, oder eine Callable-Morphologie + - hidden_layer_densities: z.B. [4, 3] + - max_attempts / eval_limit / fitness_target: Stoppbedingungen + - experimental_file / best_file: Pfade, falls du wie im Buch speichern willst + """ + + def __init__( + self, + morphology_spec=morphology, + hidden_layer_densities: List[int] = None, + *, + max_attempts: int = 5, + eval_limit: float = float("inf"), + fitness_target: float = float("inf"), + experimental_file: Optional[str] = "experimental.json", + best_file: Optional[str] = "best.json", + exoself_steps_per_eval: int = 0, + ): + self.morphology_spec = morphology_spec + self.hds = hidden_layer_densities or [] + self.max_attempts = max_attempts + self.eval_limit = eval_limit + self.fitness_target = fitness_target + self.experimental_file = experimental_file + self.best_file = best_file + # Wenn deine Exoself/Cortex eine feste Anzahl Zyklen pro „Evaluation“ braucht, + # kannst du hier default 0 lassen (Cortex entscheidet über Halt-Flag), + # oder einen Wert setzen. + self.exoself_steps_per_eval = exoself_steps_per_eval + + # Laufende Akkus (wie im Erlang-Code) + self.best_fitness = float("-inf") + self.best_genotype: Optional[Dict[str, Any]] = None + + self.eval_acc = 0 + self.cycle_acc = 0 + self.time_acc = 0.0 + + async def _run_one_attempt(self) -> Tuple[float, int, int, float, Dict[str, Any]]: + """ + Ein Trainingsversuch: + - Genotyp konstruieren + - Exoself laufen lassen + - Fitness/Evals/Cycles/Time zurückgeben + den verwendeten Genotyp + """ + print("constructing genotype...") + geno = construct(self.morphology_spec, self.hds, file_name=self.experimental_file, add_bias=True) + + # Exoself starten und bis zum evaluation_completed warten + fitness, evals, cycles, elapsed = await self._evaluate_with_exoself(geno) + + return fitness, evals, cycles, elapsed, geno + + async def _evaluate_with_exoself(self, genotype: Dict[str, Any]) -> Tuple[float, int, int, float]: + """ + Startet Exoself (deine Actor-basierte Variante) und wartet, + bis der Cortex die Evaluation abgeschlossen hat. + Erwartete Rückgabe: fitness, evals, cycles, elapsed + """ + print("creating exoself...") + ex = Exoself(genotype) + # Exoself.run() sollte idealerweise einen Tuple (fitness, evals, cycles, time) + # liefern. Falls deine Version aktuell „backup“-Listen liefert, ersetze das hier + # mit der passenden Logik oder benutze das „evaluation_completed“-Signal aus dem Cortex. + fitness, evals, cycles, elapsed = await ex.run_evaluation() + return fitness, evals, cycles, elapsed + + async def go(self): + """ + Entspricht dem Erlang loop/…: + Wiederholt Versuche, bis Stoppbedingung erfüllt. + """ + attempt = 1 + while True: + print(".........") + print("current attempt: ", attempt) + print(".........") + # Stoppbedingung vor Versuch? + if attempt > self.max_attempts or self.eval_acc >= self.eval_limit or self.best_fitness >= self.fitness_target: + # Ausgabe wie im Buch + if self.best_genotype and self.best_file: + # bestes Genotypfile ausgeben/„drucken“ + save_genotype(self.best_file, self.best_genotype) + print_genotype(self.best_file) + print( + f" Morphology: {getattr(self.morphology_spec, '__name__', str(self.morphology_spec))} | " + f"Best Fitness: {self.best_fitness} | EvalAcc: {self.eval_acc}" + ) + # Optional: an „Benchmarker“ melden – bei dir vermutlich nicht nötig + return { + "best_fitness": self.best_fitness, + "eval_acc": self.eval_acc, + "cycle_acc": self.cycle_acc, + "time_acc": self.time_acc, + "best_file": self.best_file, + } + + print("RUN ONE ATTEMPT!") + # --- Ein Versuch --- + fitness, evals, cycles, elapsed, geno = await self._run_one_attempt() + + print("update akkus...") + + # Akkus updaten + self.eval_acc += evals + self.cycle_acc += cycles + self.time_acc += elapsed + + # Besser als bisher? + if fitness > self.best_fitness: + # „experimental.json“ → „best.json“ (semantisch wie file:rename(...)) + self.best_fitness = fitness + self.best_genotype = geno + if self.best_file: + save_genotype(self.best_file, geno) + # Reset Attempt-Zähler (wie Erlang: Attempt=0 nach Verbesserung) + attempt = 1 + else: + attempt += 1 + + +# -------------------------- +# Beispiel: ausführen +# -------------------------- +if __name__ == "__main__": + # Beispielkonfiguration (XOR-Morphologie, kleine Topologie) + trainer = Trainer( + morphology_spec=morphology, # oder morphology.xor_mimic + hidden_layer_densities=[2], # wie im Buch-Beispiel + max_attempts=1000, + eval_limit=float("inf"), + fitness_target=float("inf"), + experimental_file="experimental.json", + best_file="best.json", + exoself_steps_per_eval=0, # 0 => Cortex/Scape steuern Halt-Flag + ) + + asyncio.run(trainer.go()) diff --git a/experiments/stochastic_hillclimber/run.py b/experiments/stochastic_hillclimber/run.py new file mode 100644 index 0000000..e69de29