last workig state.
7
.env
Normal file
@@ -0,0 +1,7 @@
|
||||
NEO4J_PASSWORD=mathema2
|
||||
LOG_LEVEL=INFO
|
||||
LOG_TO_FILE=true
|
||||
LOG_FILE=logs/mathema.log
|
||||
STATS_DIR=stats/
|
||||
LOG_JSON=false
|
||||
WEIGHT_PERTURB_PROP=2.0
|
||||
3
.idea/neuroevolution.iml
generated
@@ -7,6 +7,9 @@
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (neuroevolution)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PackageRequirementsSettings">
|
||||
<option name="versionSpecifier" value="Don't specify version" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
|
||||
18
docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
neo4j:
|
||||
container_name: neo4j
|
||||
image: neo4j:latest
|
||||
ports:
|
||||
- 7474:7474
|
||||
- 7687:7687
|
||||
environment:
|
||||
- NEO4J_AUTH=neo4j/${NEO4J_PASSWORD}
|
||||
- NEO4J_apoc_export_file_enabled=true
|
||||
- NEO4J_apoc_import_file_enabled=true
|
||||
- NEO4J_apoc_import_file_use__neo4j__config=true
|
||||
- NEO4J_PLUGINS=["apoc", "graph-data-science"]
|
||||
volumes:
|
||||
- ./neo4j_db/data:/data
|
||||
- ./neo4j_db/logs:/logs
|
||||
- ./neo4j_db/import:/var/lib/neo4j/import
|
||||
- ./neo4j_db/plugins:/plugins
|
||||
@@ -6,6 +6,7 @@ obs, info = env.reset()
|
||||
for _ in range(1000):
|
||||
action = env.action_space.sample()
|
||||
_, reward, terminated, truncated, _ = env.step(action)
|
||||
|
||||
if terminated or truncated:
|
||||
obs, info = env.reset()
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class Sensor(Actor):
|
||||
if self.sname == "rng":
|
||||
vec = rng_vector(self.vl)
|
||||
else:
|
||||
# place for own sensors (this will be replaced by scapes down the road)
|
||||
# place for own sensors (this will be replaced by envs down the road)
|
||||
vec = [0.0] * self.vl
|
||||
# forward an alle Fanouts
|
||||
for pid in self.fanout:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# genotype.py
|
||||
# genotype.py.old
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
|
||||
@@ -1,37 +1,20 @@
|
||||
import asyncio
|
||||
|
||||
import morphology
|
||||
from experiments.stochastic_hillclimber.actors.trainer import Trainer
|
||||
from experiments.stochastic_hillclimber.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
|
||||
morphology_spec=morphology,
|
||||
hidden_layer_densities=[2],
|
||||
max_attempts=float("inf"),
|
||||
eval_limit=float("inf"),
|
||||
fitness_target=99.9,
|
||||
experimental_file="experimental.json",
|
||||
best_file="best.json",
|
||||
exoself_steps_per_eval=0, # 0 = Scape/Cortex entscheiden über Halt
|
||||
exoself_steps_per_eval=0,
|
||||
)
|
||||
asyncio.run(trainer.go())
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class Trainer:
|
||||
geno = construct(
|
||||
self.morphology_spec,
|
||||
self.hds,
|
||||
file_name=self.experimental_file, # <-- schreibt Startnetz nach experimental.json
|
||||
file_name=self.experimental_file,
|
||||
add_bias=True
|
||||
)
|
||||
fitness, evals, cycles, elapsed = await self._evaluate_with_exoself(geno)
|
||||
@@ -62,7 +62,7 @@ class Trainer:
|
||||
print(".........")
|
||||
|
||||
if attempt > self.max_attempts or self.eval_acc >= self.eval_limit or self.best_fitness >= self.fitness_target:
|
||||
# Abschlussausgabe wie im Buch
|
||||
|
||||
if self.best_file and os.path.exists(self.best_file):
|
||||
print_genotype(self.best_file)
|
||||
print(
|
||||
@@ -86,7 +86,6 @@ class Trainer:
|
||||
self.cycle_acc += cycles
|
||||
self.time_acc += elapsed
|
||||
|
||||
# Besser als bisher?
|
||||
if fitness > self.best_fitness:
|
||||
self.best_fitness = fitness
|
||||
if self.best_file and self.experimental_file and os.path.exists(self.experimental_file):
|
||||
@@ -96,7 +95,6 @@ class Trainer:
|
||||
attempt += 1
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
trainer = Trainer(
|
||||
morphology_spec=morphology,
|
||||
|
||||
BIN
mathema/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
mathema/__pycache__/viz_replay.cpython-312.pyc
Normal file
BIN
mathema/actors/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
mathema/actors/__pycache__/actor.cpython-312.pyc
Normal file
BIN
mathema/actors/__pycache__/actuator.cpython-312.pyc
Normal file
BIN
mathema/actors/__pycache__/cortex.cpython-312.pyc
Normal file
BIN
mathema/actors/__pycache__/neuron.cpython-312.pyc
Normal file
BIN
mathema/actors/__pycache__/sensor.cpython-312.pyc
Normal file
@@ -1,7 +1,9 @@
|
||||
# actors/actuator.py
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from actor import Actor
|
||||
from mathema.actors.actor import Actor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Actuator(Actor):
|
||||
@@ -27,7 +29,7 @@ class Actuator(Actor):
|
||||
self.received[from_id] = vec
|
||||
|
||||
if len(self.received) == self.expect:
|
||||
print("ACTUATOR: collected all signals...")
|
||||
log.debug("ACTUATOR: collected all signals...")
|
||||
output = []
|
||||
for fid in self.fanin_ids:
|
||||
output.extend(self.received[fid])
|
||||
@@ -36,19 +38,38 @@ class Actuator(Actor):
|
||||
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...")
|
||||
log.debug("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)
|
||||
log.debug("ACTUATOR: got scape response: %s", resp)
|
||||
fitness, halt_flag = resp[1], resp[2]
|
||||
break
|
||||
elif self.aname == "car_ApplyAction" and self.scape:
|
||||
y0 = float(output[0]) if len(output) > 0 else 0.0
|
||||
y1 = float(output[1]) if len(output) > 1 else 0.0
|
||||
y2 = float(output[2]) if len(output) > 2 else 0.0
|
||||
|
||||
steer = max(-1.0, min(1.0, y0))
|
||||
gas = max(0.0, min(1.0, 0.5 * (y1 + 1.0)))
|
||||
brake = max(0.0, min(1.0, 0.5 * (y2 + 1.0)))
|
||||
|
||||
action = [steer, gas, brake]
|
||||
|
||||
log.debug("ACTUATOR: sending action to car scape: %s", action)
|
||||
await self.scape.send(("action", action, self))
|
||||
while True:
|
||||
resp = await self.inbox.get()
|
||||
if resp[0] == "result":
|
||||
log.debug("ACTUATOR: got scape response: %s", 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.")
|
||||
log.debug("ACTUATOR: sent sync message to cortex.")
|
||||
self.received.clear()
|
||||
|
||||
elif tag == "terminate":
|
||||
@@ -1,5 +1,8 @@
|
||||
import time
|
||||
from actor import Actor
|
||||
import logging
|
||||
|
||||
from mathema.actors.actor import Actor
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Cortex(Actor):
|
||||
@@ -19,6 +22,10 @@ class Cortex(Actor):
|
||||
self._t0 = None
|
||||
|
||||
async def _kick_sensors(self):
|
||||
for n in self.neurons:
|
||||
await n.send(("cycle_start",))
|
||||
for n in self.neurons:
|
||||
await n.send(("tick",))
|
||||
for s in self.sensors:
|
||||
await s.send(("sync",))
|
||||
|
||||
@@ -51,15 +58,15 @@ class Cortex(Actor):
|
||||
continue
|
||||
|
||||
if tag == "sync" and self.active:
|
||||
print("CORTEX: got sync message: ", msg)
|
||||
log.debug("CORTEX: got sync message: ", msg)
|
||||
_t, aid, fitness, halt_flag = msg
|
||||
|
||||
print("----------------")
|
||||
print("_t:", _t)
|
||||
print("aid:", aid)
|
||||
print("fitness:", fitness)
|
||||
print("halt_flag:", halt_flag)
|
||||
print("----------------")
|
||||
log.debug("----------------")
|
||||
log.debug("_t:", _t)
|
||||
log.debug("aid:", aid)
|
||||
log.debug("fitness:", fitness)
|
||||
log.debug("halt_flag:", halt_flag)
|
||||
log.debug("----------------")
|
||||
|
||||
self.fitness_acc += float(fitness)
|
||||
self.ef_acc += int(halt_flag)
|
||||
@@ -67,10 +74,10 @@ class Cortex(Actor):
|
||||
if aid in self.awaiting_sync:
|
||||
self.awaiting_sync.remove(aid)
|
||||
|
||||
print("CORTEX: awaiting sync: ", self.awaiting_sync)
|
||||
log.debug("CORTEX: awaiting sync: ", self.awaiting_sync)
|
||||
|
||||
if not self.awaiting_sync:
|
||||
print("CORTEX: cycle completed.")
|
||||
log.debug("CORTEX: cycle completed.")
|
||||
self.cycle_acc += 1
|
||||
|
||||
if self.ef_acc > 0:
|
||||
162
mathema/actors/neuron.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import math
|
||||
import random
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from mathema.actors.actor import Actor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def tanh(x): return math.tanh(x)
|
||||
|
||||
|
||||
class Neuron(Actor):
|
||||
def __init__(self, nid, cx_pid, af_name, input_idps, output_pids, bias: Optional[float] = None):
|
||||
super().__init__(f"Neuron-{nid}")
|
||||
self.nid = nid
|
||||
self.cx_pid = cx_pid
|
||||
self.af = tanh if af_name == "tanh" else tanh
|
||||
|
||||
self.inputs = {}
|
||||
self.order = []
|
||||
self._has_recurrent = False
|
||||
|
||||
"""
|
||||
for (inp_id, weights) in input_idps:
|
||||
self.order.append(inp_id)
|
||||
self.inputs[inp_id] = {"weights": list(weights), "got": False, "val": None}
|
||||
"""
|
||||
|
||||
self.bias = float(bias) if bias is not None else 0.0
|
||||
for inp_id, weights, recurrent in input_idps:
|
||||
recurrent = bool(recurrent)
|
||||
if inp_id == "bias":
|
||||
self.bias = float(weights[0])
|
||||
else:
|
||||
self.order.append(inp_id)
|
||||
self.inputs[inp_id] = {
|
||||
"weights": list(weights),
|
||||
"got": False,
|
||||
"val": [],
|
||||
"recurrent": recurrent,
|
||||
"next_val": []
|
||||
}
|
||||
if recurrent:
|
||||
self._has_recurrent = True
|
||||
|
||||
self._backup_inputs = None
|
||||
self._backup_bias = None
|
||||
|
||||
self.outputs = output_pids
|
||||
|
||||
log.debug(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]
|
||||
if not isinstance(data, list):
|
||||
data = [float(data)]
|
||||
|
||||
if slot["recurrent"]:
|
||||
slot["next_val"] = data
|
||||
else:
|
||||
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 len(w) != len(v):
|
||||
raise ValueError(f"Lengths of weights and values must be equal")
|
||||
acc += sum(wj * vj for wj, vj in zip(w, 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"] = []
|
||||
log.debug(f"Neuron {self.nid}: input_sum={acc + self.bias:.3f}, output={out:.3f}")
|
||||
|
||||
elif tag == "tick":
|
||||
|
||||
if self.order and 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 len(w) != len(v):
|
||||
raise ValueError("Lengths of weights and values must be equal")
|
||||
acc += sum(wj * vj for wj, vj in zip(w, 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"] = []
|
||||
log.debug(f"Neuron {self.nid}: input_sum={acc + self.bias:.3f}, output={out:.3f}")
|
||||
|
||||
elif tag == "get_backup":
|
||||
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":
|
||||
log.debug(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":
|
||||
log.debug(
|
||||
f"Neuron {self.nid}: perturbing {len([w for i in self.order for w in self.inputs[i]['weights']])}"
|
||||
f"weights")
|
||||
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
|
||||
|
||||
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)
|
||||
if random.random() < mp:
|
||||
self.bias = _sat(self.bias + (random.random() - 0.5) * delta_mag, -sat_lim, sat_lim)
|
||||
|
||||
elif tag == "cycle_start":
|
||||
for i in self.order:
|
||||
slot = self.inputs[i]
|
||||
if slot["recurrent"]:
|
||||
nv = slot["next_val"]
|
||||
if not nv:
|
||||
w = slot["weights"]
|
||||
nv = [0.0] * max(1, len(w))
|
||||
slot["val"] = nv
|
||||
slot["got"] = True
|
||||
slot["next_val"] = []
|
||||
else:
|
||||
|
||||
slot["got"] = False
|
||||
slot["val"] = []
|
||||
|
||||
elif tag == "terminate":
|
||||
return
|
||||
|
||||
|
||||
def _sat(val, lo, hi):
|
||||
return lo if val < lo else (hi if val > hi else val)
|
||||
@@ -1,6 +1,8 @@
|
||||
# actors/sensor.py
|
||||
from actor import Actor
|
||||
import random
|
||||
import logging
|
||||
|
||||
from mathema.actors.actor import Actor
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Sensor(Actor):
|
||||
@@ -15,15 +17,14 @@ class Sensor(Actor):
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
print("sensor running...")
|
||||
msg = await self.inbox.get()
|
||||
tag = msg[0]
|
||||
|
||||
print("got sensor message: ", msg)
|
||||
log.debug(f"sensor {self.sid} got sensor message: %s", msg)
|
||||
|
||||
if tag == "sync":
|
||||
vec = await self._sense()
|
||||
print("sensed vec: ", vec)
|
||||
log.debug(f"sensor {self.sid} sensed vec: %s", vec)
|
||||
for pid in self.fanout:
|
||||
await pid.send(("forward", self.sid, vec))
|
||||
|
||||
@@ -34,12 +35,25 @@ class Sensor(Actor):
|
||||
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
|
||||
elif self.sname == "car_GetFeatures" and self.scape:
|
||||
await self.scape.send(("sense", self.sid, self))
|
||||
msg = await self.inbox.get()
|
||||
if msg[0] == "percept":
|
||||
vec = msg[1]
|
||||
|
||||
out = [max(-1.0, min(1.0, float(x))) for x in vec]
|
||||
if len(out) < self.vl:
|
||||
out = out + [0.0] * (self.vl - len(out))
|
||||
elif len(out) > self.vl:
|
||||
out = out[:self.vl]
|
||||
return out
|
||||
else:
|
||||
return [0.0] * self.vl
|
||||
else:
|
||||
return [0.0] * self.vl
|
||||
@@ -1,6 +1,5 @@
|
||||
# genotype.py
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
from typing import Dict, List, Tuple, Any, Optional
|
||||
848
mathema/archive/genotype_new.py.old
Normal file
@@ -0,0 +1,848 @@
|
||||
# genotype.py.old
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from typing import Any, Dict, List, Tuple, Optional, Callable
|
||||
|
||||
from mathema.core.db import Neo4jDB
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ID-Serialisierung im Stil des Buchs (als Strings)
|
||||
# ------------------------------------------------------------
|
||||
def erlang_id_cortex(uid: float) -> str:
|
||||
return f"{{{{origin,{uid}}},cortex}}"
|
||||
|
||||
|
||||
def erlang_id_neuron(layer_idx: int, uid: float) -> str:
|
||||
return f"{{{{{layer_idx},{uid}}},neuron}}"
|
||||
|
||||
|
||||
def erlang_id_sensor(uid: float) -> str:
|
||||
return f"{{{uid},sensor}}"
|
||||
|
||||
|
||||
def erlang_id_actuator(uid: float) -> str:
|
||||
return f"{{{uid},actuator}}"
|
||||
|
||||
|
||||
def now_unique() -> float:
|
||||
return time.time()
|
||||
|
||||
|
||||
def generate_ids(n: int) -> List[float]:
|
||||
return [now_unique() + i * 1e-6 for i in range(n)]
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# GenotypeBuilder
|
||||
# ------------------------------------------------------------
|
||||
class GenotypeBuilder:
|
||||
"""
|
||||
Async-Port von Erlangs genotype. Benötigt:
|
||||
- Neo4jDB
|
||||
- morphology.py mit get_InitSensors(morph) / get_InitActuators(morph)
|
||||
- optional: population_monitor mit async create_specie(pop_id, constraint, fingerprint) -> specie_id
|
||||
"""
|
||||
|
||||
def __init__(self, db: Neo4jDB, population_monitor: Optional[Any] = None):
|
||||
self.db = db
|
||||
self.population_monitor = population_monitor # erwartet .create_specie(...)
|
||||
|
||||
# ====================== Public API ======================
|
||||
|
||||
async def construct_Agent(self, specie_id: Any, agent_id: Any, speccon: Dict[str, Any]) -> Dict[str, Any]:
|
||||
random.seed(time.time())
|
||||
generation = 0
|
||||
cx_id, pattern = await self.construct_Cortex(agent_id, generation, speccon)
|
||||
|
||||
agent = {
|
||||
"id": agent_id,
|
||||
"cx_id": cx_id,
|
||||
"specie_id": specie_id,
|
||||
"constraint": speccon,
|
||||
"generation": generation,
|
||||
"pattern": pattern,
|
||||
"evo_hist": [],
|
||||
# default vals
|
||||
"population_id": None,
|
||||
"fingerprint": None,
|
||||
"fitness": None,
|
||||
"innovation_factor": 0
|
||||
}
|
||||
await self._write_agent(agent)
|
||||
await self.update_fingerprint(agent_id)
|
||||
return agent
|
||||
|
||||
async def construct_Cortex(self, agent_id: Any, generation: int, speccon: Dict[str, Any]) -> Tuple[
|
||||
Any, List[Tuple[int, List[Any]]]]:
|
||||
from importlib import import_module
|
||||
morphology_mod = import_module("morphology")
|
||||
|
||||
cx_uid = now_unique()
|
||||
cx_id = erlang_id_cortex(cx_uid)
|
||||
morphology_name = speccon["morphology"]
|
||||
|
||||
init_sensors = morphology_mod.get_InitSensor(morphology_name)
|
||||
init_actuators = morphology_mod.get_InitActuator(morphology_name)
|
||||
|
||||
sensors = []
|
||||
for S in init_sensors:
|
||||
uid = now_unique()
|
||||
sensors.append({
|
||||
**S,
|
||||
"id": erlang_id_sensor(uid),
|
||||
"cx_id": cx_id,
|
||||
"generation": generation,
|
||||
"fanout_ids": S.get("fanout_ids", [])
|
||||
})
|
||||
|
||||
actuators = []
|
||||
for A in init_actuators:
|
||||
uid = now_unique()
|
||||
actuators.append({
|
||||
**A,
|
||||
"id": erlang_id_actuator(uid),
|
||||
"cx_id": cx_id,
|
||||
"generation": generation,
|
||||
"fanin_ids": A.get("fanin_ids", [])
|
||||
})
|
||||
|
||||
neuron_ids = await self.construct_InitialNeuroLayer(cx_id, generation, speccon, sensors, actuators)
|
||||
|
||||
sensor_ids = [s["id"] for s in sensors]
|
||||
actuator_ids = [a["id"] for a in actuators]
|
||||
|
||||
cortex = {
|
||||
"id": cx_id,
|
||||
"agent_id": agent_id,
|
||||
"neuron_ids": neuron_ids,
|
||||
"sensor_ids": sensor_ids,
|
||||
"actuator_ids": actuator_ids
|
||||
}
|
||||
await self._write_cortex_and_io(cortex, sensors, actuators)
|
||||
pattern = [(0, neuron_ids)]
|
||||
return cx_id, pattern
|
||||
|
||||
async def construct_InitialNeuroLayer(
|
||||
self,
|
||||
cx_id: Any,
|
||||
generation: int,
|
||||
speccon: Dict[str, Any],
|
||||
sensors: List[Dict[str, Any]],
|
||||
actuators: List[Dict[str, Any]],
|
||||
) -> List[Any]:
|
||||
neuron_ids: List[Any] = []
|
||||
|
||||
for A in actuators:
|
||||
vl = int(A["vector_length"])
|
||||
n_uids = generate_ids(vl)
|
||||
n_ids = [erlang_id_neuron(0, u) for u in n_uids]
|
||||
|
||||
for n_id in n_ids:
|
||||
if random.random() >= 0.5:
|
||||
S = random.choice(sensors)
|
||||
input_specs = [(S["id"], int(S["vector_length"]))]
|
||||
S["fanout_ids"] = [n_id] + list(S.get("fanout_ids", []))
|
||||
else:
|
||||
input_specs = [(S["id"], int(S["vector_length"])) for S in sensors]
|
||||
for S in sensors:
|
||||
S["fanout_ids"] = [n_id] + list(S.get("fanout_ids", []))
|
||||
|
||||
await self.construct_Neuron(cx_id, generation, speccon, n_id, input_specs, [A["id"]])
|
||||
|
||||
A["fanin_ids"] = n_ids + list(A.get("fanin_ids", []))
|
||||
neuron_ids.extend(n_ids)
|
||||
|
||||
return neuron_ids
|
||||
|
||||
# Ersetze deine bisherigen _pack_inputs/_unpack_inputs durch diese Versionen:
|
||||
|
||||
"""
|
||||
helper functions. because we use list stuff from erlang.
|
||||
this is not needed anymore, if we start to use neo4j friendly
|
||||
data model.
|
||||
"""
|
||||
|
||||
def _pack_inputs(self, input_idps: list[dict]) -> tuple[list, list, list]:
|
||||
ids, wflat, lengths = [], [], []
|
||||
for e in input_idps:
|
||||
if "bias" in e:
|
||||
vals = list(e["bias"])
|
||||
ids.append("bias")
|
||||
else:
|
||||
vals = list(e["weights"])
|
||||
ids.append(e["id"])
|
||||
lengths.append(len(vals))
|
||||
wflat.extend(vals)
|
||||
return ids, wflat, lengths
|
||||
|
||||
def _unpack_inputs(self, input_ids: list, input_w: list, input_w_len: list) -> list[dict]:
|
||||
out, i = [], 0
|
||||
for iid, L in zip(input_ids or [], input_w_len or []):
|
||||
chunk = list(input_w[i:i + L]);
|
||||
i += L
|
||||
if iid == "bias":
|
||||
out.append({"bias": chunk})
|
||||
else:
|
||||
out.append({"id": iid, "weights": chunk})
|
||||
return out
|
||||
|
||||
def _normalize_scape(self, v: Any) -> Any:
|
||||
# erlaubt nur primitive Property-Typen
|
||||
if isinstance(v, (str, int, float, bool)) or v is None:
|
||||
return v
|
||||
if isinstance(v, dict) and "private" in v:
|
||||
return v["private"] # dein bisheriger Fall: {"private":"xor_sim"}
|
||||
return str(v)
|
||||
|
||||
async def construct_Neuron(
|
||||
self,
|
||||
cx_id: Any,
|
||||
generation: int,
|
||||
speccon: Dict[str, Any],
|
||||
n_id: Any,
|
||||
input_specs: List[Tuple[Any, int]],
|
||||
output_ids: List[Any],
|
||||
) -> None:
|
||||
input_idps = self._create_InputIdPs(input_specs)
|
||||
# expliziter Bias-Eintrag (Projektvorgabe)
|
||||
input_idps.append({"bias": [self._rand_weight()]})
|
||||
|
||||
neuron = {
|
||||
"id": n_id,
|
||||
"generation": generation,
|
||||
"cx_id": cx_id,
|
||||
"af": self._generate_NeuronAF(speccon.get("neural_afs", [])),
|
||||
"input_idps": input_idps,
|
||||
"output_ids": output_ids,
|
||||
"ro_ids": self._calculate_ROIds(n_id, output_ids),
|
||||
}
|
||||
await self._write_neuron(neuron)
|
||||
|
||||
async def update_fingerprint(self, agent_id: Any) -> None:
|
||||
import json
|
||||
|
||||
a = await self._read_agent(agent_id)
|
||||
if not a:
|
||||
return
|
||||
|
||||
cx = await self._read_cortex(a["cx_id"])
|
||||
if not cx:
|
||||
return
|
||||
|
||||
# --- IO lesen ---
|
||||
sensors = await self._read_sensors(cx.get("sensor_ids", []))
|
||||
actuators = await self._read_actuators(cx.get("actuator_ids", []))
|
||||
|
||||
# --- pattern robust aus Agent nehmen (durch _read_agent schon deserialisiert) ---
|
||||
raw_pat = a.get("pattern", []) or []
|
||||
gen_pattern: List[Tuple[int, int]] = []
|
||||
for item in raw_pat:
|
||||
if isinstance(item, (list, tuple)) and len(item) >= 2:
|
||||
li, ids_or_cnt = item[0], item[1]
|
||||
try:
|
||||
li = int(li)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(ids_or_cnt, (list, tuple)):
|
||||
cnt = len(ids_or_cnt)
|
||||
elif isinstance(ids_or_cnt, int):
|
||||
cnt = ids_or_cnt
|
||||
else:
|
||||
cnt = 0
|
||||
gen_pattern.append((li, cnt))
|
||||
|
||||
# --- Evo-Historie generalisieren (liefert primitive Strukturen) ---
|
||||
gen_evo = self._generalize_EvoHist(a.get("evo_hist", []))
|
||||
|
||||
# --- IO-Deskriptoren auf primitive Typen reduzieren ---
|
||||
def _vec_len(x):
|
||||
return int(x.get("vector_length", x.get("vector_length", 0)) or 0)
|
||||
|
||||
gen_s_desc = [(s.get("name"), _vec_len(s), self._normalize_scape(s.get("scape"))) for s in sensors]
|
||||
gen_a_desc = [(ac.get("name"), _vec_len(ac), self._normalize_scape(ac.get("scape"))) for ac in actuators]
|
||||
|
||||
# --- Fingerprint bauen und als JSON speichern ---
|
||||
fp_obj = {
|
||||
"pattern": gen_pattern,
|
||||
"evo": gen_evo,
|
||||
"sensors": gen_s_desc,
|
||||
"actuators": gen_a_desc,
|
||||
}
|
||||
fp_json = json.dumps(fp_obj, ensure_ascii=False)
|
||||
|
||||
await self._set_agent_fingerprint(agent_id, fp_json)
|
||||
|
||||
# ----------------- Ergänzungen aus Erlang -----------------
|
||||
|
||||
async def speciate(self, agent_id: Any) -> None:
|
||||
"""Port von speciate/1."""
|
||||
await self.update_fingerprint(agent_id)
|
||||
A = await self._read_agent(agent_id)
|
||||
if not A:
|
||||
return
|
||||
|
||||
# Test-Agent?
|
||||
if A["id"] == "test":
|
||||
await self._set_agent_fitness(agent_id, None)
|
||||
return
|
||||
|
||||
# Eltern-Specie und Population holen
|
||||
Parent_S = await self._read_specie(A["specie_id"])
|
||||
if not Parent_S:
|
||||
# ohne specie → keine Speziation möglich
|
||||
return
|
||||
P = await self._read_population(Parent_S["population_id"])
|
||||
if not P:
|
||||
return
|
||||
|
||||
# Spezies mit gleichem Fingerprint in der Population finden
|
||||
same: Optional[Any] = await self._find_specie_by_fingerprint(P["id"], A["fingerprint"])
|
||||
if same is None:
|
||||
# Neue Spezies erzeugen (via population_monitor)
|
||||
if not self.population_monitor or not hasattr(self.population_monitor, "create_specie"):
|
||||
raise RuntimeError(
|
||||
"speciate(): population_monitor.create_specie(pop_id, constraint, fingerprint) fehlt")
|
||||
new_specie_id = await self.population_monitor.create_specie(P["id"], A["constraint"], A["fingerprint"])
|
||||
S = await self._read_specie(new_specie_id)
|
||||
if not S:
|
||||
return
|
||||
# Agent updaten: neue specie_id, fitness=undefined
|
||||
await self._update_agent_fields(agent_id, {"specie_id": new_specie_id, "fitness": None})
|
||||
# Spezies-Agentliste erweitern (prepend wie im Buch reicht, Reihenfolge egal)
|
||||
await self._append_specie_agent(new_specie_id, agent_id, replace=False)
|
||||
else:
|
||||
# Bestehende Spezies updaten
|
||||
await self._update_agent_fields(agent_id, {"specie_id": same, "fitness": None})
|
||||
await self._append_specie_agent(same, agent_id, replace=False)
|
||||
|
||||
async def clone_Agent(self, agent_id: Any, clone_agent_id: Optional[Any] = None) -> Any:
|
||||
"""
|
||||
Port von clone_Agent/1,2 (vereinfacht – transaktional durch Reihenfolge).
|
||||
- erzeugt neue IDs nach Buchregeln
|
||||
- kopiert Nodes mit remappten IDs
|
||||
- schreibt Cortex/Agent-Knoten für den Klon
|
||||
"""
|
||||
if clone_agent_id is None:
|
||||
clone_agent_id = f"{{{now_unique()},agent}}"
|
||||
|
||||
A = await self._read_agent(agent_id)
|
||||
if not A:
|
||||
raise ValueError(f"agent not found: {agent_id}")
|
||||
Cx = await self._read_cortex(A["cx_id"])
|
||||
if not Cx:
|
||||
raise ValueError(f"cortex not found for agent: {agent_id}")
|
||||
|
||||
# 1) ID-Mapping erzeugen
|
||||
idmap: Dict[str, str] = {}
|
||||
# bias bleibt auf 'bias' → wir mappen nicht (wie im Erlang ETS bias->bias)
|
||||
idmap["bias"] = "bias"
|
||||
|
||||
# Agent
|
||||
idmap[str(agent_id)] = str(clone_agent_id)
|
||||
# Cortex-ID remappen: {{origin,uid},cortex} -> gleicher Layer-Tag 'origin'
|
||||
cx_new = erlang_id_cortex(now_unique())
|
||||
idmap[str(Cx["id"])] = cx_new
|
||||
|
||||
# Neuronen/Sensoren/Aktuatoren IDs remappen
|
||||
for nid in Cx.get("neuron_ids", []):
|
||||
# {{L,uid},neuron} -> {{L,new},neuron}
|
||||
layer = self._parse_layer(nid) or 0
|
||||
idmap[str(nid)] = erlang_id_neuron(layer, now_unique())
|
||||
for sid in Cx.get("sensor_ids", []):
|
||||
idmap[str(sid)] = erlang_id_sensor(now_unique())
|
||||
for aid in Cx.get("actuator_ids", []):
|
||||
idmap[str(aid)] = erlang_id_actuator(now_unique())
|
||||
|
||||
# 2) Originalelemente laden
|
||||
sensors = await self._read_sensors(Cx.get("sensor_ids", []))
|
||||
neurons = await self._read_neurons(Cx.get("neuron_ids", []))
|
||||
actuators = await self._read_actuators(Cx.get("actuator_ids", []))
|
||||
|
||||
# 3) Clones schreiben (Sensors/Actuators/Neurons)
|
||||
# (IDs, cx_id, *_ids, input_idps remappen)
|
||||
clone_sensors = []
|
||||
for S in sensors:
|
||||
clone_sensors.append({
|
||||
**S,
|
||||
"id": idmap[str(S["id"])],
|
||||
"cx_id": idmap[str(S["cx_id"])],
|
||||
"fanout_ids": [idmap.get(str(x), str(x)) for x in S.get("fanout_ids", [])],
|
||||
})
|
||||
clone_actuators = []
|
||||
for A0 in actuators:
|
||||
clone_actuators.append({
|
||||
**A0,
|
||||
"id": idmap[str(A0["id"])],
|
||||
"cx_id": idmap[str(A0["cx_id"])],
|
||||
"fanin_ids": [idmap.get(str(x), str(x)) for x in A0.get("fanin_ids", [])],
|
||||
})
|
||||
# Neuronen: input_idps, output_ids, ro_ids mappen
|
||||
clone_neurons = []
|
||||
for N in neurons:
|
||||
# Aus flachen Properties rekonstruieren
|
||||
orig_idps = self._unpack_inputs(N.get("input_ids"), N.get("input_w"), N.get("input_w_len"))
|
||||
|
||||
# IDs remappen
|
||||
remapped_idps = []
|
||||
for e in orig_idps:
|
||||
if "bias" in e:
|
||||
remapped_idps.append({"bias": e["bias"]})
|
||||
else:
|
||||
remapped_idps.append({"id": idmap.get(str(e["id"]), str(e["id"])), "weights": e["weights"]})
|
||||
|
||||
# Wieder flach packen
|
||||
input_ids, input_w, input_w_len = self._pack_inputs(remapped_idps)
|
||||
|
||||
clone_neurons.append({
|
||||
"id": idmap[str(N["id"])],
|
||||
"cx_id": idmap[str(N["cx_id"])],
|
||||
"generation": N.get("generation", 0),
|
||||
"af": N.get("af", "tanh"),
|
||||
"input_ids": input_ids,
|
||||
"input_w": input_w,
|
||||
"input_w_len": input_w_len,
|
||||
"output_ids": [idmap.get(str(x), str(x)) for x in N.get("output_ids", [])],
|
||||
"ro_ids": [idmap.get(str(x), str(x)) for x in N.get("ro_ids", [])],
|
||||
})
|
||||
|
||||
# 4) Cortex & Agent (Klon) schreiben
|
||||
clone_cortex = {
|
||||
"id": cx_new,
|
||||
"agent_id": idmap[str(agent_id)],
|
||||
"sensor_ids": [c["id"] for c in clone_sensors],
|
||||
"actuator_ids": [c["id"] for c in clone_actuators],
|
||||
"neuron_ids": [c["id"] for c in clone_neurons],
|
||||
}
|
||||
# zuerst Neuronen, dann Cortex + IO, damit HAS_* sofort auflöst
|
||||
for n in clone_neurons:
|
||||
await self._write_neuron(n)
|
||||
await self._write_cortex_and_io(clone_cortex, clone_sensors, clone_actuators)
|
||||
|
||||
# Agent-Klon: minimal id+cx_id updaten; Rest vom Original übernehmen
|
||||
clone_agent = {
|
||||
**A,
|
||||
"id": idmap[str(agent_id)],
|
||||
"cx_id": cx_new,
|
||||
}
|
||||
await self._write_agent(clone_agent)
|
||||
return clone_agent["id"]
|
||||
|
||||
async def test(self) -> None:
|
||||
"""Port von test/0 (vereinfacht, ohne Transaktion)."""
|
||||
Specie_Id = "test"
|
||||
Agent_Id = "test"
|
||||
CloneAgent_Id = "test_clone"
|
||||
SpecCon = {"morphology": "xor_mimic", "neural_afs": ["tanh", "cos", "gauss", "abs"]}
|
||||
|
||||
await self.construct_Agent(Specie_Id, Agent_Id, SpecCon)
|
||||
await self.clone_Agent(Agent_Id, CloneAgent_Id)
|
||||
await self.print(Agent_Id)
|
||||
await self.print(CloneAgent_Id)
|
||||
await self.delete_Agent(Agent_Id)
|
||||
await self.delete_Agent(CloneAgent_Id)
|
||||
|
||||
async def create_test(self) -> None:
|
||||
"""Port von create_test/0."""
|
||||
Specie_Id = "test"
|
||||
Agent_Id = "test"
|
||||
SpecCon = {"morphology": "xor_mimic", "neural_afs": ["tanh", "cos", "gauss", "abs"]}
|
||||
|
||||
a = await self._read_agent(Agent_Id)
|
||||
if a is None:
|
||||
await self.construct_Agent(Specie_Id, Agent_Id, SpecCon)
|
||||
await self.print(Agent_Id)
|
||||
else:
|
||||
await self.delete_Agent(Agent_Id)
|
||||
await self.construct_Agent(Specie_Id, Agent_Id, SpecCon)
|
||||
await self.print(Agent_Id)
|
||||
|
||||
# ====================== Helper ============================
|
||||
|
||||
def _create_InputIdPs(self, input_specs: List[Tuple[Any, int]]) -> List[Dict[str, Any]]:
|
||||
res: List[Dict[str, Any]] = []
|
||||
for input_id, vl in input_specs:
|
||||
weights = [self._rand_weight() for _ in range(int(vl))]
|
||||
res.append({"id": input_id, "weights": weights})
|
||||
return res
|
||||
|
||||
def _rand_weight(self) -> float:
|
||||
return random.random() - 0.5
|
||||
|
||||
def _generate_NeuronAF(self, afs: List[Any]) -> Any:
|
||||
if not afs:
|
||||
return "tanh"
|
||||
return random.choice(afs)
|
||||
|
||||
def _parse_layer(self, neuron_id: str) -> Optional[int]:
|
||||
try:
|
||||
if "neuron" not in neuron_id:
|
||||
return None
|
||||
inner = neuron_id.split("},neuron")[0]
|
||||
inner = inner.strip("{}").strip("{}")
|
||||
parts = inner.split(",")
|
||||
return int(parts[0])
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _calculate_ROIds(self, self_id: Any, output_ids: List[Any]) -> List[Any]:
|
||||
my_layer = self._parse_layer(str(self_id)) or 0
|
||||
acc: List[Any] = []
|
||||
for oid in output_ids:
|
||||
s = str(oid)
|
||||
if "actuator" in s:
|
||||
continue
|
||||
li = self._parse_layer(s)
|
||||
if li is not None and li <= my_layer:
|
||||
acc.append(oid)
|
||||
return acc
|
||||
|
||||
def _generalize_EvoHist(self, evo_hist: List[Any]) -> List[Any]:
|
||||
def strip_id(id_str: Any) -> Any:
|
||||
s = str(id_str)
|
||||
if "neuron" in s:
|
||||
try:
|
||||
inner = s.split("},neuron")[0].strip("{}").strip("{}")
|
||||
layer = int(inner.split(",")[0])
|
||||
return (layer, "neuron")
|
||||
except Exception:
|
||||
return ("?", "neuron")
|
||||
if "actuator" in s:
|
||||
return ("actuator",)
|
||||
if "sensor" in s:
|
||||
return ("sensor",)
|
||||
return ("?",)
|
||||
|
||||
generalized = []
|
||||
for item in evo_hist:
|
||||
if isinstance(item, (list, tuple)):
|
||||
g = []
|
||||
for el in item:
|
||||
if isinstance(el, (list, tuple)) and len(el) == 2:
|
||||
g.append((el[0], strip_id(el[1])))
|
||||
else:
|
||||
g.append(strip_id(el))
|
||||
generalized.append(tuple(g))
|
||||
else:
|
||||
generalized.append(item)
|
||||
return generalized
|
||||
|
||||
# =============== Neo4j Schreib-/Lese-Hilfen ===============
|
||||
|
||||
async def _write_agent(self, agent: Dict[str, Any]) -> None:
|
||||
cy = """
|
||||
MERGE (a:agent {id:$id})
|
||||
SET a.generation=$generation,
|
||||
a.population_id=COALESCE($population_id, a.population_id),
|
||||
a.specie_id=$specie_id,
|
||||
a.cx_id=$cx_id,
|
||||
a.fingerprint=COALESCE(a.fingerprint, $fingerprint),
|
||||
a.constraint_json=$constraint_json,
|
||||
a.evo_hist=$evo_hist,
|
||||
a.fitness=COALESCE($fitness, a.fitness),
|
||||
a.innovation_factor=COALESCE($innovation_factor, 0),
|
||||
a.pattern=$pattern_json
|
||||
"""
|
||||
payload = {
|
||||
**agent,
|
||||
"constraint_json": json.dumps(agent.get("constraint", {}), ensure_ascii=False),
|
||||
"pattern_json": json.dumps(agent.get("pattern", []), ensure_ascii=False),
|
||||
}
|
||||
await self.db.run_consume(cy, **payload)
|
||||
|
||||
async def _update_agent_fields(self, agent_id: Any, fields: Dict[str, Any]) -> None:
|
||||
sets = ", ".join([f"a.{k}=${k}" for k in fields.keys()])
|
||||
cy = f"MATCH (a:agent {{id:$id}}) SET {sets}"
|
||||
await (await self.db.run_read(cy, id=agent_id, **fields)).consume()
|
||||
|
||||
async def _set_agent_fingerprint(self, agent_id: Any, fingerprint: Any) -> None:
|
||||
cy = "MATCH (a:agent {id:$id}) SET a.fingerprint=$fp"
|
||||
await (await self.db.run_read(cy, id=agent_id, fp=fingerprint)).consume()
|
||||
|
||||
async def _set_agent_fitness(self, agent_id: Any, fitness: Any) -> None:
|
||||
cy = "MATCH (a:agent {id:$id}) SET a.fitness=$fitness"
|
||||
await (await self.db.run_read(cy, id=agent_id, fitness=fitness)).consume()
|
||||
|
||||
async def _read_agent(self, agent_id: Any) -> Optional[Dict[str, Any]]:
|
||||
cy = "MATCH (a:agent {id:$id}) RETURN a"
|
||||
rec = await self.db.read_single(cy, id=agent_id)
|
||||
if not rec:
|
||||
return None
|
||||
a = dict(rec["a"])
|
||||
|
||||
# robuste Deserialisierung
|
||||
import json
|
||||
|
||||
# constraint
|
||||
cj = a.get("constraint_json")
|
||||
if isinstance(cj, str):
|
||||
try:
|
||||
a["constraint"] = json.loads(cj)
|
||||
except Exception:
|
||||
a["constraint"] = {}
|
||||
elif "constraint" not in a or a["constraint"] is None:
|
||||
a["constraint"] = {}
|
||||
|
||||
# pattern – bevorzugt pattern_json; fallback: falls pattern als String gespeichert wurde
|
||||
pj = a.get("pattern_json")
|
||||
if isinstance(pj, str):
|
||||
try:
|
||||
a["pattern"] = json.loads(pj)
|
||||
except Exception:
|
||||
a["pattern"] = []
|
||||
else:
|
||||
p = a.get("pattern")
|
||||
if isinstance(p, str):
|
||||
try:
|
||||
a["pattern"] = json.loads(p)
|
||||
except Exception:
|
||||
a["pattern"] = []
|
||||
elif p is None:
|
||||
a["pattern"] = []
|
||||
|
||||
return a
|
||||
|
||||
async def _read_specie(self, specie_id: Any) -> Optional[Dict[str, Any]]:
|
||||
rec = await self.db.read_single("MATCH (s:specie {id:$id}) RETURN s", id=specie_id)
|
||||
return dict(rec["s"]) if rec else None
|
||||
|
||||
async def _read_population(self, population_id: Any) -> Optional[Dict[str, Any]]:
|
||||
rec = await self.db.read_single("MATCH (p:population {id:$id}) RETURN p", id=population_id)
|
||||
return dict(rec["p"]) if rec else None
|
||||
|
||||
async def _find_specie_by_fingerprint(self, population_id: Any, fingerprint: Any) -> Optional[Any]:
|
||||
"""Suche in Population eine Spezies mit exakt gleichem Fingerprint (wie im Buch)."""
|
||||
rec = await self.db.read_single("""
|
||||
MATCH (p:population {id:$pid})-[:HAS_SPECIE]->(s:specie)
|
||||
WHERE s.fingerprint = $fp
|
||||
RETURN s.id AS sid
|
||||
LIMIT 1
|
||||
""", pid=population_id, fp=fingerprint)
|
||||
return rec["sid"] if rec else None
|
||||
|
||||
async def _append_specie_agent(self, specie_id: Any, agent_id: Any, replace: bool = False) -> None:
|
||||
"""Fügt agent_id in specie.agent_ids ein; erstellt HAS_AGENT-Kante."""
|
||||
cy = """
|
||||
MATCH (s:specie {id:$sid})
|
||||
SET s.agent_ids = CASE WHEN $replace THEN [$aid] ELSE coalesce([$aid] + s.agent_ids, [$aid]) END
|
||||
"""
|
||||
await (await self.db.run_read(cy, sid=specie_id, aid=agent_id, replace=replace)).consume()
|
||||
# Beziehung:
|
||||
await (await self.db.run_read("""
|
||||
MATCH (s:specie {id:$sid}), (a:agent {id:$aid})
|
||||
MERGE (s)-[:HAS_AGENT]->(a)
|
||||
""", sid=specie_id, aid=agent_id)).consume()
|
||||
|
||||
async def _write_cortex_and_io(self, cortex: Dict[str, Any], sensors: List[Dict[str, Any]],
|
||||
actuators: List[Dict[str, Any]]) -> None:
|
||||
|
||||
sensors = [{**s, "scape": self._normalize_scape(s.get("scape"))} for s in sensors]
|
||||
actuators = [{**a, "scape": self._normalize_scape(a.get("scape"))} for a in actuators]
|
||||
|
||||
await self.db.run_consume("""
|
||||
MERGE (c:cortex {id:$id})
|
||||
SET c.agent_id=$agent_id, c.neuron_ids=$neuron_ids, c.sensor_ids=$sensor_ids, c.actuator_ids=$actuator_ids
|
||||
""", **cortex)
|
||||
|
||||
if sensors:
|
||||
await self.db.run_consume("""
|
||||
UNWIND $sensors AS s
|
||||
MERGE (x:sensor {id:s.id})
|
||||
SET x.name=s.name,
|
||||
x.cx_id=s.cx_id,
|
||||
x.scape=s.scape,
|
||||
x.vl=s.vl,
|
||||
x.fanout_ids=COALESCE(s.fanout_ids, []),
|
||||
x.generation=s.generation
|
||||
""", sensors=sensors)
|
||||
|
||||
if actuators:
|
||||
await self.db.run_consume("""
|
||||
UNWIND $actuators AS a
|
||||
MERGE (x:actuator {id:a.id})
|
||||
SET x.name=a.name,
|
||||
x.cx_id=a.cx_id,
|
||||
x.scape=a.scape,
|
||||
x.vl=a.vl,
|
||||
x.fanin_ids=COALESCE(a.fanin_ids, []),
|
||||
x.generation=a.generation
|
||||
""", actuators=actuators)
|
||||
|
||||
await self.db.run_consume("""
|
||||
MATCH (c:cortex {id:$cx})
|
||||
WITH c
|
||||
UNWIND c.sensor_ids AS sid
|
||||
MATCH (s:sensor {id:sid})
|
||||
MERGE (c)-[:HAS_SENSOR]->(s)
|
||||
""", cx=cortex["id"])
|
||||
|
||||
await self.db.run_consume("""
|
||||
MATCH (c:cortex {id:$cx})
|
||||
WITH c
|
||||
UNWIND c.actuator_ids AS aid
|
||||
MATCH (a:actuator {id:aid})
|
||||
MERGE (c)-[:HAS_ACTUATOR]->(a)
|
||||
""", cx=cortex["id"])
|
||||
|
||||
await self.db.run_consume("""
|
||||
MATCH (c:cortex {id:$cx})
|
||||
WITH c
|
||||
UNWIND c.neuron_ids AS nid
|
||||
MATCH (n:neuron {id:nid})
|
||||
MERGE (c)-[:HAS_NEURON]->(n)
|
||||
""", cx=cortex["id"])
|
||||
|
||||
await self.db.run_consume("""
|
||||
MATCH (c:cortex {id:$cx})
|
||||
WITH c
|
||||
UNWIND c.sensor_ids AS sid
|
||||
MATCH (s:sensor {id:sid})
|
||||
UNWIND s.fanout_ids AS nid
|
||||
MATCH (n:neuron {id:nid})
|
||||
MERGE (s)-[:FANOUT_TO]->(n)
|
||||
""", cx=cortex["id"])
|
||||
|
||||
await self.db.run_consume("""
|
||||
MATCH (c:cortex {id:$cx})
|
||||
WITH c
|
||||
UNWIND c.actuator_ids AS aid
|
||||
MATCH (a:actuator {id:aid})
|
||||
UNWIND a.fanin_ids AS nid
|
||||
MATCH (n:neuron {id:nid})
|
||||
MERGE (a)-[:FANIN_FROM]->(n)
|
||||
""", cx=cortex["id"])
|
||||
|
||||
# <<< HIER am Ende hinzufügen: OUTPUT_TO-Kanten für alle Neuronen dieses Cortex >>>
|
||||
await self.db.run_consume("""
|
||||
MATCH (c:cortex {id:$cx})
|
||||
UNWIND c.neuron_ids AS nid
|
||||
MATCH (n:neuron {id:nid})
|
||||
UNWIND n.output_ids AS oid
|
||||
CALL {
|
||||
WITH oid
|
||||
OPTIONAL MATCH (m:neuron {id:oid}) RETURN m AS dst
|
||||
UNION
|
||||
WITH oid
|
||||
OPTIONAL MATCH (a:actuator {id:oid}) RETURN a AS dst
|
||||
}
|
||||
WITH n, dst WHERE dst IS NOT NULL
|
||||
MERGE (n)-[:OUTPUT_TO]->(dst)
|
||||
""", cx=cortex["id"])
|
||||
|
||||
async def _write_neuron(self, neuron: Dict[str, Any]) -> None:
|
||||
# Eingabe vereinheitlichen
|
||||
if "input_idps" in neuron:
|
||||
input_ids, input_w, input_w_len = self._pack_inputs(neuron["input_idps"])
|
||||
else:
|
||||
# bereits flach angeliefert
|
||||
input_ids = neuron["input_ids"]
|
||||
input_w = neuron["input_w"]
|
||||
input_w_len = neuron["input_w_len"]
|
||||
|
||||
await self.db.run_consume("""
|
||||
MERGE (n:neuron {id:$id})
|
||||
SET n.generation=$generation,
|
||||
n.cx_id=$cx_id,
|
||||
n.af=$af,
|
||||
n.input_ids=$input_ids,
|
||||
n.input_w=$input_w,
|
||||
n.input_w_len=$input_w_len,
|
||||
n.output_ids=$output_ids,
|
||||
n.ro_ids=$ro_ids
|
||||
""",
|
||||
id=neuron["id"],
|
||||
generation=neuron["generation"],
|
||||
cx_id=neuron["cx_id"],
|
||||
af=neuron["af"],
|
||||
input_ids=input_ids,
|
||||
input_w=input_w,
|
||||
input_w_len=input_w_len,
|
||||
output_ids=neuron["output_ids"],
|
||||
ro_ids=neuron["ro_ids"],
|
||||
)
|
||||
|
||||
if neuron["ro_ids"]:
|
||||
await self.db.run_consume("""
|
||||
MATCH (n:neuron {id:$nid})
|
||||
UNWIND $ros AS rid
|
||||
MATCH (src:neuron {id:rid})
|
||||
MERGE (n)-[:READS_OUTPUT_OF]->(src)
|
||||
""", nid=neuron["id"], ros=neuron["ro_ids"])
|
||||
|
||||
async def _read_cortex(self, cx_id: Any) -> Optional[Dict[str, Any]]:
|
||||
rec = await self.db.read_single(
|
||||
"MATCH (c:cortex {id:$id}) RETURN c",
|
||||
id=cx_id,
|
||||
)
|
||||
return dict(rec["c"]) if rec else None
|
||||
|
||||
async def _read_sensors(self, ids: List[Any]) -> List[Dict[str, Any]]:
|
||||
if not ids:
|
||||
return []
|
||||
rows = await self.db.read_all("""
|
||||
UNWIND $ids AS sid
|
||||
MATCH (s:sensor {id:sid}) RETURN s
|
||||
""", ids=ids)
|
||||
return [dict(r["s"]) for r in rows]
|
||||
|
||||
async def _read_actuators(self, ids: List[Any]) -> List[Dict[str, Any]]:
|
||||
if not ids:
|
||||
return []
|
||||
rows = await self.db.read_all("""
|
||||
UNWIND $ids AS aid
|
||||
MATCH (a:actuator {id:aid}) RETURN a
|
||||
""", ids=ids)
|
||||
return [dict(r["a"]) for r in rows]
|
||||
|
||||
async def _read_neurons(self, ids: List[Any]) -> List[Dict[str, Any]]:
|
||||
if not ids:
|
||||
return []
|
||||
rows = await self.db.read_all("""
|
||||
UNWIND $ids AS nid
|
||||
MATCH (n:neuron {id:nid}) RETURN n
|
||||
""", ids=ids)
|
||||
return [dict(r["n"]) for r in rows]
|
||||
|
||||
# -------------- Convenience: delete / print ---------------
|
||||
|
||||
async def delete_Agent(self, agent_id: Any) -> None:
|
||||
a = await self._read_agent(agent_id)
|
||||
if not a:
|
||||
return
|
||||
cx = await self._read_cortex(a["cx_id"])
|
||||
if cx:
|
||||
await (await self.db.run_read("""
|
||||
MATCH (c:cortex {id:$cid})
|
||||
OPTIONAL MATCH (c)-[:HAS_NEURON]->(n:neuron)
|
||||
OPTIONAL MATCH (c)-[:HAS_SENSOR]->(s:sensor)
|
||||
OPTIONAL MATCH (c)-[:HAS_ACTUATOR]->(a:actuator)
|
||||
DETACH DELETE n, s, a, c
|
||||
""", cid=cx["id"])).consume()
|
||||
await (await self.db.run_read("MATCH (a:agent {id:$id}) DETACH DELETE a", id=agent_id)).consume()
|
||||
|
||||
async def print(self, agent_id: Any) -> None:
|
||||
a = await self._read_agent(agent_id)
|
||||
if not a:
|
||||
print("agent not found:", agent_id)
|
||||
return
|
||||
cx = await self._read_cortex(a["cx_id"])
|
||||
print("AGENT:", a)
|
||||
print("CORTEX:", cx)
|
||||
if not cx:
|
||||
return
|
||||
sensors = await self._read_sensors(cx.get("sensor_ids", []))
|
||||
res = await self.db.read_all("""
|
||||
UNWIND $ids AS nid
|
||||
MATCH (n:neuron {id:nid}) RETURN n
|
||||
""", ids=cx.get("neuron_ids", []))
|
||||
neurons = [dict(r["n"]) for r in res]
|
||||
actuators = await self._read_actuators(cx.get("actuator_ids", []))
|
||||
for s in sensors:
|
||||
print("SENSOR:", s)
|
||||
for n in neurons:
|
||||
print("NEURON:", n)
|
||||
for ac in actuators:
|
||||
print("ACTUATOR:", ac)
|
||||
36
mathema/car_racing_main.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from mathema.core.population_monitor import init_population
|
||||
from mathema.utils.logging_config import setup_logging
|
||||
|
||||
setup_logging()
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def run_car_test(
|
||||
pop_id: str = "car_pop",
|
||||
gens: int = 200
|
||||
):
|
||||
monitor = await init_population((
|
||||
pop_id,
|
||||
[{"morphology": "car_racing_features", "neural_afs": ["tanh"]}],
|
||||
"gt",
|
||||
"competition"
|
||||
))
|
||||
|
||||
for _ in range(gens):
|
||||
await monitor.gen_ended.wait()
|
||||
s = monitor.state
|
||||
best = await monitor._best_fitness_in_population(s.population_id)
|
||||
log.info(f"[car] gen={s.pop_gen} best_fitness={best:.6f} eval_acc={s.eval_acc}")
|
||||
|
||||
await monitor.stop("normal")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_car_test())
|
||||
|
||||
0
mathema/core/__init__.py
Normal file
BIN
mathema/core/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
mathema/core/__pycache__/db.cpython-312.pyc
Normal file
BIN
mathema/core/__pycache__/exoself.cpython-312.pyc
Normal file
BIN
mathema/core/__pycache__/morphology.cpython-312.pyc
Normal file
BIN
mathema/core/__pycache__/population_monitor.cpython-312.pyc
Normal file
66
mathema/core/db.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from neo4j import AsyncGraphDatabase
|
||||
|
||||
NEO4J_CONSTRAINTS = [
|
||||
"CREATE CONSTRAINT cortex_id IF NOT EXISTS FOR (n:cortex) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT sensor_id IF NOT EXISTS FOR (n:sensor) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT neuron_id IF NOT EXISTS FOR (n:neuron) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT actuator_id IF NOT EXISTS FOR (n:actuator) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE CONSTRAINT agent_id IF NOT EXISTS FOR (n:agent) REQUIRE n.id IS UNIQUE",
|
||||
"CREATE INDEX agent_id IF NOT EXISTS FOR (a:agent) ON (a.id)",
|
||||
"CREATE INDEX agent_population IF NOT EXISTS FOR (a:agent) ON (a.population_id)",
|
||||
"CREATE INDEX cortex_id IF NOT EXISTS FOR (cx:cortex) ON (cx.id)",
|
||||
"CREATE INDEX neuron_id IF NOT EXISTS FOR (n:neuron) ON (n.id)",
|
||||
"CREATE CONSTRAINT spec_id IF NOT EXISTS FOR (s:specie) REQUIRE s.id IS UNIQUE;"
|
||||
"CREATE CONSTRAINT pop_id IF NOT EXISTS FOR (p:population) REQUIRE p.id IS UNIQUE;"
|
||||
# constraint record has no id field, no need to make it unique
|
||||
]
|
||||
|
||||
|
||||
class Neo4jDB:
|
||||
def __init__(self, uri: str, user: str, password: str, database: str | None = None):
|
||||
self._driver = AsyncGraphDatabase.driver(uri, auth=(user, password))
|
||||
self._database = database
|
||||
|
||||
async def close(self):
|
||||
await self._driver.close()
|
||||
|
||||
"""
|
||||
async def run(self, cypher: str, **params):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
return await s.run(cypher, **params)
|
||||
"""
|
||||
|
||||
async def run_read(self, cypher: str, **params):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
return await s.run(cypher, **params)
|
||||
|
||||
async def read_single(self, cypher: str, **params):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
res = await s.run(cypher, **params)
|
||||
return await res.single()
|
||||
|
||||
async def read_all(self, cypher: str, **params):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
res = await s.run(cypher, **params)
|
||||
return [r async for r in res]
|
||||
|
||||
async def run_consume(self, cypher: str, **params):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
res = await s.run(cypher, **params)
|
||||
return await res.consume()
|
||||
|
||||
async def create_schema(self):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
for stmt in NEO4J_CONSTRAINTS:
|
||||
await s.run(stmt)
|
||||
|
||||
async def purge_all_nodes(self):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
await s.run("MATCH (n) DETACH DELETE n")
|
||||
|
||||
async def drop_schema(self):
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
res = await s.run("SHOW CONSTRAINTS")
|
||||
async for record in res:
|
||||
name = record["name"]
|
||||
await s.run(f"DROP CONSTRAINT {name} IF EXISTS")
|
||||
@@ -2,20 +2,28 @@ import asyncio
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import logging
|
||||
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
|
||||
from mathema.genotype.neo4j.genotype import load_genotype_snapshot, persist_neuron_backups
|
||||
from mathema.actors.actor import Actor
|
||||
from mathema.actors.cortex import Cortex
|
||||
from mathema.actors.sensor import Sensor
|
||||
from mathema.actors.neuron import Neuron
|
||||
from mathema.actors.actuator import Actuator
|
||||
from mathema.scape.scape import XorScape
|
||||
from mathema.scape.car_racing import CarRacingScape
|
||||
from mathema.envs.openai_car_racing import CarRacing
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Exoself(Actor):
|
||||
def __init__(self, genotype: Dict[str, Any], file_name: Optional[str] = None):
|
||||
super().__init__("Exoself")
|
||||
self.monitor = None
|
||||
self.agent_id = None
|
||||
self.g = genotype
|
||||
self.file_name = file_name
|
||||
|
||||
@@ -26,17 +34,61 @@ class Exoself(Actor):
|
||||
|
||||
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.MAX_ATTEMPTS = 10
|
||||
self.actuator_scape = None
|
||||
|
||||
self._perturbed: List[Neuron] = []
|
||||
|
||||
@classmethod
|
||||
async def start(cls, agent_id: str, monitor) -> "Exoself":
|
||||
try:
|
||||
g = await load_genotype_snapshot(agent_id)
|
||||
except Exception as e:
|
||||
|
||||
log.error(f"[Exoself {agent_id}] START FAILED: {e!r}")
|
||||
try:
|
||||
await monitor.cast(("terminated", agent_id, float("-inf"), 0, 0, 0.0))
|
||||
finally:
|
||||
|
||||
class _Dummy:
|
||||
async def cast(self, *_a, **_k): pass
|
||||
|
||||
async def stop(self): pass
|
||||
|
||||
return _Dummy()
|
||||
|
||||
self = cls(g)
|
||||
self.agent_id = agent_id
|
||||
self.monitor = monitor
|
||||
|
||||
async def _runner():
|
||||
fitness = float("-inf")
|
||||
evals = cycles = 0
|
||||
elapsed = 0.0
|
||||
try:
|
||||
fitness, evals, cycles, elapsed = await self.train_until_stop()
|
||||
except Exception as e:
|
||||
log.error(f"[Exoself {self.agent_id}] CRASH in train_until_stop(): {e!r}")
|
||||
fitness = float("-inf")
|
||||
evals = int(self.eval_acc)
|
||||
cycles = int(self.cycle_acc)
|
||||
elapsed = float(self.time_acc)
|
||||
finally:
|
||||
try:
|
||||
await monitor.cast(("terminated", self.agent_id, fitness, evals, cycles, elapsed))
|
||||
except Exception as e:
|
||||
log.error(f"[Exoself {self.agent_id}] FAILED to notify monitor: {e!r}")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
self._runner_task = loop.create_task(_runner(), name=f"Exoself-{self.agent_id}")
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def from_file(path: str) -> "Exoself":
|
||||
with open(path, "r") as f:
|
||||
@@ -44,7 +96,7 @@ class Exoself(Actor):
|
||||
return Exoself(g, file_name=path)
|
||||
|
||||
async def run(self):
|
||||
self._build_pid_map_and_spawn()
|
||||
self.build_pid_map_and_spawn()
|
||||
|
||||
self._link_cortex()
|
||||
|
||||
@@ -64,9 +116,9 @@ class Exoself(Actor):
|
||||
return
|
||||
|
||||
async def run_evaluation(self):
|
||||
print("build network and link...")
|
||||
self._build_pid_map_and_spawn()
|
||||
print("link cortex...")
|
||||
log.debug(f"exoself: build network and link...")
|
||||
self.build_pid_map_and_spawn()
|
||||
log.debug(f"exoself: link cortex...")
|
||||
self._link_cortex()
|
||||
|
||||
for a in self.sensor_actors + self.neuron_actors + self.actuator_actors:
|
||||
@@ -75,11 +127,9 @@ class Exoself(Actor):
|
||||
if self.actuator_scape:
|
||||
self.tasks.append(asyncio.create_task(self.actuator_scape.run()))
|
||||
|
||||
print("network actors are running...")
|
||||
|
||||
while True:
|
||||
msg = await self.inbox.get()
|
||||
print("message in exsoself: ", msg)
|
||||
log.debug("message in exsoself %r: ", msg)
|
||||
tag = msg[0]
|
||||
if tag == "evaluation_completed":
|
||||
_, fitness, cycles, elapsed = msg
|
||||
@@ -89,14 +139,8 @@ class Exoself(Actor):
|
||||
await self._terminate_all()
|
||||
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.
|
||||
"""
|
||||
def build_pid_map_and_spawn(self):
|
||||
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,
|
||||
@@ -105,7 +149,8 @@ class Exoself(Actor):
|
||||
actuator_pids=[]
|
||||
)
|
||||
|
||||
self.actuator_scape = XorScape()
|
||||
env = CarRacing(seed_value=5, render_mode=None)
|
||||
self.actuator_scape = CarRacingScape(env=env)
|
||||
|
||||
layers: Dict[int, List[Dict[str, Any]]] = defaultdict(list)
|
||||
for n in self.g["neurons"]:
|
||||
@@ -116,24 +161,35 @@ class Exoself(Actor):
|
||||
|
||||
for layer in ordered_layers:
|
||||
for n in layer:
|
||||
input_idps = [(iw["input_id"], iw["weights"]) for iw in n["input_weights"]]
|
||||
input_idps = [
|
||||
(iw["input_id"], iw["weights"], bool(iw.get("recurrent", False)))
|
||||
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
|
||||
output_pids=[],
|
||||
bias=n.get("bias")
|
||||
)
|
||||
id2neuron_actor[n["id"]] = neuron
|
||||
self.neuron_actors.append(neuron)
|
||||
|
||||
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
|
||||
out_map: Dict[Any, set] = {nid: set() for nid in id2neuron_actor.keys()}
|
||||
for layer in ordered_layers:
|
||||
for tgt in layer:
|
||||
tgt_pid = id2neuron_actor[tgt["id"]]
|
||||
for iw in tgt["input_weights"]:
|
||||
src_id = iw["input_id"]
|
||||
if src_id in id2neuron_actor:
|
||||
out_map[src_id].add(tgt_pid)
|
||||
for src_id, targets in out_map.items():
|
||||
id2neuron_actor[src_id].outputs = list(targets)
|
||||
|
||||
actuators = self._get_actuators_block()
|
||||
if not actuators:
|
||||
log.error(f"genotype does not include 'actuator' or 'actuators' section")
|
||||
raise ValueError("Genotype must include 'actuator' or 'actuators'.")
|
||||
|
||||
for a in actuators:
|
||||
@@ -149,26 +205,36 @@ class Exoself(Actor):
|
||||
)
|
||||
self.actuator_actors.append(actuator)
|
||||
|
||||
if ordered_layers:
|
||||
last_layer = ordered_layers[-1]
|
||||
out_targets = self.actuator_actors
|
||||
for n in last_layer:
|
||||
id2neuron_actor[n["id"]].outputs = out_targets
|
||||
for a in self.actuator_actors:
|
||||
for src_id in a.fanin_ids:
|
||||
assert src_id in id2neuron_actor, f"Actuator {a.aid}: fanin_id {src_id} ist kein Neuron"
|
||||
if src_id in id2neuron_actor:
|
||||
na = id2neuron_actor[src_id]
|
||||
if a not in na.outputs:
|
||||
na.outputs.append(a)
|
||||
|
||||
sensors = self._get_sensors_block()
|
||||
if not sensors:
|
||||
log.error(f"Genotype must include 'sensor' or '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]
|
||||
sensor_targets: Dict[Any, List[Neuron]] = defaultdict(list)
|
||||
for layer in ordered_layers:
|
||||
for tgt in layer:
|
||||
tgt_pid = id2neuron_actor[tgt["id"]]
|
||||
for iw in tgt["input_weights"]:
|
||||
src_id = iw["input_id"]
|
||||
if src_id != "bias":
|
||||
sensor_targets[src_id].append(tgt_pid)
|
||||
|
||||
for s in sensors:
|
||||
fanout_pids = sensor_targets.get(s["id"], [])
|
||||
sensor = Sensor(
|
||||
sid=s["id"],
|
||||
cx_pid=self.cx_actor,
|
||||
name=s["name"],
|
||||
vector_length=s["vector_length"],
|
||||
fanout_pids=first_layer_pids,
|
||||
fanout_pids=fanout_pids,
|
||||
scape=self.actuator_scape
|
||||
)
|
||||
self.sensor_actors.append(sensor)
|
||||
@@ -197,10 +263,9 @@ class Exoself(Actor):
|
||||
self.tasks.append(asyncio.create_task(self.cx_actor.run()))
|
||||
|
||||
async def train_until_stop(self):
|
||||
self._build_pid_map_and_spawn()
|
||||
self.build_pid_map_and_spawn()
|
||||
self._link_cortex()
|
||||
|
||||
# 2) Start tasks
|
||||
for a in self.sensor_actors + self.neuron_actors + self.actuator_actors:
|
||||
self.tasks.append(asyncio.create_task(a.run()))
|
||||
if self.actuator_scape:
|
||||
@@ -213,9 +278,7 @@ class Exoself(Actor):
|
||||
if tag == "evaluation_completed":
|
||||
_, fitness, cycles, elapsed = msg
|
||||
maybe_stats = await self._on_evaluation_completed(fitness, cycles, elapsed)
|
||||
# _on_evaluation_completed() ruft bei Stop bereits _backup_genotype() und _terminate_all()
|
||||
if isinstance(maybe_stats, dict):
|
||||
# Trainingsende – Daten aus self.* zurückgeben (wie im Buch: Fitness/Evals/Cycles/Time)
|
||||
return (
|
||||
float(self.highest_fitness),
|
||||
int(self.eval_acc),
|
||||
@@ -232,10 +295,28 @@ class Exoself(Actor):
|
||||
self.cycle_acc += int(cycles)
|
||||
self.time_acc += float(elapsed)
|
||||
|
||||
print(f"[Exoself] evaluation_completed: fitness={fitness:.6f} cycles={cycles} time={elapsed:.3f}s")
|
||||
log.info(f"[Exoself] evaluation_completed: fitness={fitness:.6f} cycles={cycles} time={elapsed:.3f}s")
|
||||
log.info(f"[Exoself] attempt {self.attempt}")
|
||||
|
||||
REL = 1e-6
|
||||
if fitness > self.highest_fitness * (1.0 + REL):
|
||||
REL = 1e-4
|
||||
ABS_EPS = 1e-2
|
||||
|
||||
valid_fitness = isinstance(fitness, (int, float)) and math.isfinite(fitness)
|
||||
if not valid_fitness:
|
||||
fitness = float("-inf")
|
||||
|
||||
# --- NEW: per-episode log to monitor (only if finite) ---
|
||||
if valid_fitness:
|
||||
try:
|
||||
# fitness is the episodic return from Cortex.fitness_acc
|
||||
await self.monitor.cast(("episode_done", str(self.agent_id), float(fitness), int(self.eval_acc)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
thresh = max(ABS_EPS, REL * abs(self.highest_fitness if math.isfinite(self.highest_fitness) else 0.0))
|
||||
improved = fitness > self.highest_fitness + thresh
|
||||
|
||||
if improved:
|
||||
self.highest_fitness = fitness
|
||||
self.attempt = 0
|
||||
for n in self.neuron_actors:
|
||||
@@ -246,8 +327,9 @@ class Exoself(Actor):
|
||||
await n.send(("weight_restore",))
|
||||
|
||||
if self.attempt >= self.MAX_ATTEMPTS:
|
||||
print(
|
||||
f"[Exoself] STOP. Best fitness={self.highest_fitness:.6f} evals={self.eval_acc} cycles={self.cycle_acc}")
|
||||
log.info(
|
||||
f"[Exoself] STOP. Best fitness={self.highest_fitness:.6f} "
|
||||
f"evals={self.eval_acc} cycles={self.cycle_acc}")
|
||||
await self._backup_genotype()
|
||||
await self._terminate_all()
|
||||
return {
|
||||
@@ -272,34 +354,31 @@ class Exoself(Actor):
|
||||
await n.send(("get_backup",))
|
||||
|
||||
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))
|
||||
backups.append((str(nid), idps))
|
||||
remaining -= 1
|
||||
|
||||
id2n = {n["id"]: n for n in self.g["neurons"]}
|
||||
bias_rows = []
|
||||
edge_rows = []
|
||||
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])
|
||||
for inp_id, weights in idps:
|
||||
if inp_id == "bias":
|
||||
b = float(weights[0])
|
||||
bias_rows.append({"nid": str(nid), "bias": b})
|
||||
else:
|
||||
input_id, weights = item
|
||||
new_iw.append({"input_id": input_id, "weights": list(weights)})
|
||||
id2n[nid]["input_weights"] = new_iw
|
||||
if bias_val is not None:
|
||||
id2n[nid].setdefault("input_weights", []).append({"input_id": "bias", "weights": [bias_val]})
|
||||
edge_rows.append({
|
||||
"from_id": str(inp_id),
|
||||
"to_id": str(nid),
|
||||
"weights": [float(x) for x in list(weights)],
|
||||
})
|
||||
|
||||
await persist_neuron_backups(bias_rows, edge_rows)
|
||||
|
||||
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}")
|
||||
log.debug("[Exoself] Hinweis: file_name gesetzt, aber Persistenz läuft über Genotyp-API (Neo4j).")
|
||||
|
||||
async def _terminate_all(self):
|
||||
for a in self.sensor_actors + self.neuron_actors + self.actuator_actors:
|
||||
@@ -1,27 +1,30 @@
|
||||
# morphology.py
|
||||
import time
|
||||
import uuid
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, List, Union
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MorphologyType = Union[str, Callable[[str], List[Dict[str, Any]]]]
|
||||
|
||||
|
||||
def generate_id() -> float:
|
||||
now = time.time()
|
||||
return 1.0 / now
|
||||
def generate_id() -> str:
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def get_InitSensor(morphology: MorphologyType) -> Dict[str, Any]:
|
||||
def get_InitSensor(morphology: MorphologyType):
|
||||
sensors = get_Sensors(morphology)
|
||||
if not sensors:
|
||||
log.error("Morphology has no sensors.")
|
||||
raise ValueError("Morphology has no sensors.")
|
||||
return sensors[0]
|
||||
return [sensors[0]]
|
||||
|
||||
|
||||
def get_InitActuator(morphology: MorphologyType) -> Dict[str, Any]:
|
||||
def get_InitActuator(morphology: MorphologyType):
|
||||
actuators = get_Actuators(morphology)
|
||||
if not actuators:
|
||||
log.error("Morphology has no actuators.")
|
||||
raise ValueError("Morphology has no actuators.")
|
||||
return actuators[0]
|
||||
return [actuators[0]]
|
||||
|
||||
|
||||
def get_Sensors(morphology: MorphologyType) -> List[Dict[str, Any]]:
|
||||
@@ -38,22 +41,24 @@ def _resolve_morphology(morphology: MorphologyType) -> Callable[[str], List[Dict
|
||||
if callable(morphology):
|
||||
return morphology
|
||||
|
||||
# 2) String -> Registry
|
||||
if isinstance(morphology, str):
|
||||
reg = {
|
||||
"xor_mimic": xor_mimic,
|
||||
"car_racing_features": car_racing_features
|
||||
}
|
||||
if morphology in reg:
|
||||
return reg[morphology]
|
||||
log.error(f"Unknown morphology name: {morphology}")
|
||||
raise ValueError(f"Unknown morphology name: {morphology}")
|
||||
|
||||
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
|
||||
|
||||
log.error("morphology must be a callable, a module with 'xor_mimic', or a registered string key")
|
||||
raise TypeError("morphology must be a callable, a module with 'xor_mimic', or a registered string key")
|
||||
|
||||
|
||||
@@ -61,20 +66,47 @@ def xor_mimic(kind: str) -> List[Dict[str, Any]]:
|
||||
if kind == "sensors":
|
||||
return [
|
||||
{
|
||||
"id": generate_id(),
|
||||
"name": "xor_GetInput",
|
||||
"vector_length": 2,
|
||||
"scape": {"private": "xor_sim"}
|
||||
"scape": "xor_sim"
|
||||
}
|
||||
]
|
||||
elif kind == "actuators":
|
||||
return [
|
||||
{
|
||||
"id": generate_id(),
|
||||
"name": "xor_SendOutput",
|
||||
"vector_length": 1,
|
||||
"scape": {"private": "xor_sim"}
|
||||
"scape": "xor_sim"
|
||||
}
|
||||
]
|
||||
else:
|
||||
log.error(f"xor_mimic: unsupported kind '{kind}', expected 'sensors' or 'actuators'")
|
||||
raise ValueError(f"xor_mimic: unsupported kind '{kind}', expected 'sensors' or 'actuators'")
|
||||
|
||||
|
||||
def car_racing_features(kind: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
car racing morphology
|
||||
"""
|
||||
LOOK_AHEAD = 10
|
||||
feature_len = LOOK_AHEAD + 6
|
||||
|
||||
if kind == "sensors":
|
||||
return [
|
||||
{
|
||||
"name": "car_GetFeatures",
|
||||
"vector_length": feature_len,
|
||||
"scape": "car_racing"
|
||||
}
|
||||
]
|
||||
elif kind == "actuators":
|
||||
return [
|
||||
{
|
||||
"name": "car_ApplyAction",
|
||||
"vector_length": 3,
|
||||
"scape": "car_racing"
|
||||
}
|
||||
]
|
||||
else:
|
||||
log.error(f"car_racing_features: unsupported kind '{kind}', expected 'sensors' or 'actuators'")
|
||||
raise ValueError("car_racing_features: kind must be 'sensors' or 'actuators'")
|
||||
172
mathema/core/polis.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from mathema.core.db import Neo4jDB
|
||||
from mathema.actors.actor import Actor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ===============================================================
|
||||
# Datentypen
|
||||
# ===============================================================
|
||||
|
||||
@dataclass
|
||||
class ScapeEntry:
|
||||
type: str
|
||||
actor: Actor # Actor-Instanz (Pid-Äquivalent)
|
||||
task: asyncio.Task # laufender run()-Task
|
||||
parameters: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolisState:
|
||||
active_mods: List[str] = field(default_factory=list)
|
||||
active_scapes: List[ScapeEntry] = field(default_factory=list)
|
||||
|
||||
|
||||
# ===============================================================
|
||||
# Polis-Klasse
|
||||
# ===============================================================
|
||||
|
||||
class Polis:
|
||||
"""
|
||||
Python-Asyncio Port von Erlangs polis.
|
||||
- start/stop, create/reset wie im Buch
|
||||
- Mods = Nebenmodule mit start/stop
|
||||
- Scapes = Actor-basierte Environments (z.B. XorScape)
|
||||
- Neo4j statt Mnesia
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mods: List[tuple[str, Callable[[], Any], Callable[[], Any]]] = None,
|
||||
public_scapes: List[tuple[str, dict, Callable[[], Actor]]] = None,
|
||||
neo4j_uri: str = "bolt://localhost:7687",
|
||||
neo4j_user: str = "neo4j",
|
||||
neo4j_pass: str = "mathema2",
|
||||
neo4j_db: Optional[str] = None,
|
||||
):
|
||||
self._mods = mods or [] # (name, async_start, async_stop)
|
||||
self._public_scapes = public_scapes or [] # (type, params, factory->Actor)
|
||||
self._state = PolisState()
|
||||
self._db = Neo4jDB(neo4j_uri, neo4j_user, neo4j_pass, neo4j_db)
|
||||
self._lock = asyncio.Lock()
|
||||
self._online_evt = asyncio.Event()
|
||||
self._closing = False
|
||||
self._scapes: Dict[str, ScapeEntry] = {} # type -> ScapeEntry
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# API
|
||||
# -----------------------------------------------------------
|
||||
|
||||
async def sync(self) -> None:
|
||||
"""Im Erlang-Code: make:all([load]). Hier als Platzhalter (Hot-Reload)."""
|
||||
return
|
||||
|
||||
async def create(self) -> None:
|
||||
"""Schema in Neo4j anlegen (Constraints/Indizes)."""
|
||||
await self._db.create_schema()
|
||||
|
||||
async def reset(self) -> None:
|
||||
"""Alle Daten löschen, Schema bleibt erhalten."""
|
||||
await self._db.purge_all_nodes()
|
||||
await self._db.create_schema()
|
||||
|
||||
async def start(self) -> None:
|
||||
async with self._lock:
|
||||
if self._online_evt.is_set():
|
||||
log.info("polis already online.")
|
||||
return
|
||||
|
||||
await self._db.create_schema()
|
||||
# await self._start_supmods(self._mods)
|
||||
active_scapes = await self._start_scapes(self._public_scapes)
|
||||
|
||||
self._state = PolisState(
|
||||
active_mods=[name for (name, _s, _t) in self._mods],
|
||||
active_scapes=active_scapes,
|
||||
)
|
||||
self._online_evt.set()
|
||||
log.info("******** Polis: ##MATHEMA## is now online.")
|
||||
|
||||
async def stop(self, reason: str = "normal") -> None:
|
||||
async with self._lock:
|
||||
if not self._online_evt.is_set():
|
||||
log.info("polis is offline")
|
||||
return
|
||||
|
||||
self._closing = True
|
||||
await self._stop_scapes(self._state.active_scapes)
|
||||
# await self._stop_supmods(self._mods)
|
||||
await self._db.close()
|
||||
self._online_evt.clear()
|
||||
log.info(f"******** Polis: ##MATHEMA## is now offline, terminated with reason:{reason}")
|
||||
|
||||
async def get_scape(self, scape_type: str) -> Optional[Actor]:
|
||||
"""Pid-Äquivalent (Actor-Instanz) für Scape-Typ zurückgeben."""
|
||||
entry = self._scapes.get(scape_type)
|
||||
return entry.actor if entry else None
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# intern: Mods
|
||||
# -----------------------------------------------------------
|
||||
|
||||
"""
|
||||
|
||||
async def _start_supmods(self, mods: List[tuple[str, Callable[[], Any], Callable[[], Any]]]) -> None:
|
||||
for name, start_fn, _stop_fn in mods:
|
||||
print(f"Starting mod: {name}")
|
||||
res = start_fn()
|
||||
if asyncio.iscoroutine(res):
|
||||
await res
|
||||
|
||||
async def _stop_supmods(self, mods: List[tuple[str, Callable[[], Any], Callable[[], Any]]]) -> None:
|
||||
for name, _start_fn, stop_fn in mods:
|
||||
print(f"Stopping mod: {name}")
|
||||
res = stop_fn()
|
||||
if asyncio.iscoroutine(res):
|
||||
await res
|
||||
|
||||
"""
|
||||
|
||||
# -----------------------------------------------------------
|
||||
# intern: Scapes
|
||||
# -----------------------------------------------------------
|
||||
|
||||
async def _start_scapes(
|
||||
self, scapes_cfg: List[tuple[str, dict, Callable[[], Actor]]]
|
||||
) -> List[ScapeEntry]:
|
||||
active: List[ScapeEntry] = []
|
||||
for scape_type, params, factory in scapes_cfg:
|
||||
actor: Actor = factory() # Actor-Instanz erzeugen
|
||||
task = asyncio.create_task(actor.run(), name=f"scape:{scape_type}")
|
||||
entry = ScapeEntry(type=scape_type, actor=actor, task=task, parameters=params or {})
|
||||
self._scapes[scape_type] = entry
|
||||
active.append(entry)
|
||||
log.debug(f"Scape started: {scape_type} -> task={task.get_name()}")
|
||||
return active
|
||||
|
||||
async def _stop_scapes(self, scapes: List[ScapeEntry]) -> None:
|
||||
for e in scapes:
|
||||
try:
|
||||
await e.actor.send(("terminate",))
|
||||
except Exception as ex:
|
||||
log.warning(f"Warn: terminate send failed for {e.type}: {ex}")
|
||||
|
||||
for e in scapes:
|
||||
try:
|
||||
await asyncio.wait_for(e.task, timeout=2.0)
|
||||
except asyncio.TimeoutError:
|
||||
e.task.cancel()
|
||||
try:
|
||||
await e.task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as ex:
|
||||
log.warning(f"Warn: scape {e.type} exit error: {ex}")
|
||||
|
||||
self._scapes.clear()
|
||||
658
mathema/core/population_monitor.py
Normal file
@@ -0,0 +1,658 @@
|
||||
"""
|
||||
module used to monitor a population of neural networks
|
||||
solving a shared or isolated scape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import uuid
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, List, Literal, Optional, Sequence, Tuple, Callable, Awaitable
|
||||
|
||||
from mathema.genotype.neo4j.genotype import (
|
||||
neo4j,
|
||||
construct_agent,
|
||||
clone_agent,
|
||||
delete_agent,
|
||||
update_fingerprint,
|
||||
)
|
||||
from mathema.genotype.neo4j.genotype_mutator import GenotypeMutator
|
||||
from mathema.utils import stats
|
||||
from mathema.core.exoself import Exoself
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
OpTag = Literal["continue", "pause", "done"]
|
||||
SelectionAlgorithm = Literal["competition", "top3"]
|
||||
|
||||
EFF: float = 0.05
|
||||
SURVIVAL_PERCENTAGE: float = 0.5
|
||||
SPECIE_SIZE_LIMIT: int = 10
|
||||
INIT_SPECIE_SIZE: int = 10
|
||||
|
||||
GENERATION_LIMIT: int = 1000
|
||||
EVALUATIONS_LIMIT: int = 100_000
|
||||
FITNESS_GOAL: float = 6000
|
||||
|
||||
INIT_POPULATION_ID: str = "test"
|
||||
INIT_OP_MODE: str = "gt"
|
||||
INIT_SELECTION_ALGO: SelectionAlgorithm = "competition"
|
||||
INIT_CONSTRAINTS: list[dict] = [
|
||||
{"morphology": "xor_mimic", "neural_afs": ["tanh"]},
|
||||
]
|
||||
|
||||
EXOSELF_START: Optional[Callable[[str, "PopulationMonitor"], Awaitable[Any]]] = None
|
||||
|
||||
|
||||
def _new_id() -> str:
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def _mean(xs: Sequence[float]) -> float:
|
||||
return sum(xs) / len(xs) if xs else 0.0
|
||||
|
||||
|
||||
def _std(xs: Sequence[float]) -> float:
|
||||
if not xs: return 0.0
|
||||
m = _mean(xs)
|
||||
return math.sqrt(sum((x - m) ** 2 for x in xs) / len(xs))
|
||||
|
||||
|
||||
async def _read_all(q: str, **params):
|
||||
return await neo4j.read_all(q, **params)
|
||||
|
||||
|
||||
async def _run(q: str, **params):
|
||||
return await neo4j.run_consume(q, **params)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorState:
|
||||
op_mode: str
|
||||
population_id: str
|
||||
selection_algorithm: SelectionAlgorithm
|
||||
op_tag: OpTag = "continue"
|
||||
|
||||
active: List[Tuple[str, Any]] = field(default_factory=list)
|
||||
agent_ids: List[str] = field(default_factory=list)
|
||||
|
||||
tot_agents: int = 0
|
||||
agents_left: int = 0
|
||||
|
||||
pop_gen: int = 0
|
||||
eval_acc: int = 0
|
||||
cycle_acc: int = 0
|
||||
time_acc: int = 0
|
||||
rows: List[dict] = field(default_factory=list)
|
||||
|
||||
|
||||
async def _population_aggregate(population_id: str) -> dict:
|
||||
rows = await _read_all("""
|
||||
MATCH (a:agent {population_id:$pid})
|
||||
RETURN collect(coalesce(toFloat(a.fitness),0.0)) AS fs
|
||||
""", pid=str(population_id))
|
||||
fs = [float(x) for x in (rows[0]["fs"] if rows else [])]
|
||||
|
||||
if not fs:
|
||||
return {"cum_fitness": 0.0, "avg": 0.0, "std": 0.0, "best": 0.0, "min": 0.0, "n": 0, "agents": 0}
|
||||
n = len(fs)
|
||||
s = sum(fs)
|
||||
m = s / n
|
||||
var = sum((x - m) ** 2 for x in fs) / n
|
||||
|
||||
return {
|
||||
"cum_fitness": s,
|
||||
"avg": m,
|
||||
"std": math.sqrt(var),
|
||||
"best": max(fs),
|
||||
"min": min(fs),
|
||||
"n": n,
|
||||
"agents": n
|
||||
}
|
||||
|
||||
|
||||
class PopulationMonitor:
|
||||
"""
|
||||
Orchestriert Generationen: spawn -> warten -> selektieren/mutieren -> next.
|
||||
Expects exoself.start(agent_id, monitor) and exoself will cast(("terminated", aid, fitness, eval_acc, cycle_acc, time_acc)).
|
||||
"""
|
||||
|
||||
def __init__(self, op_mode: str, population_id: str, selection_algorithm: SelectionAlgorithm):
|
||||
self.state = MonitorState(op_mode, population_id, selection_algorithm)
|
||||
self.inbox: asyncio.Queue = asyncio.Queue()
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._stopped_evt = asyncio.Event()
|
||||
self.mutator = GenotypeMutator(neo4j)
|
||||
self.gen_ended = asyncio.Event()
|
||||
self._exoself_start = EXOSELF_START or Exoself.start
|
||||
|
||||
# logging stuff
|
||||
self.run_id = None
|
||||
self._t0 = None
|
||||
self._best_so_far = float("-inf")
|
||||
self.train_time_sec = 30*60
|
||||
|
||||
# logging file handles
|
||||
self._episodes_f = None
|
||||
self._progress_f = None
|
||||
|
||||
@classmethod
|
||||
async def start(cls, op_mode: str, population_id: str,
|
||||
selection_algorithm: SelectionAlgorithm) -> "PopulationMonitor":
|
||||
self = cls(op_mode, population_id, selection_algorithm)
|
||||
self._task = asyncio.create_task(self._run(), name=f"PopulationMonitor-{population_id}")
|
||||
stats.register_atexit(population_id, lambda: list(self.state.rows))
|
||||
|
||||
# init logging
|
||||
loop = asyncio.get_running_loop()
|
||||
self._t0 = loop.time()
|
||||
self.run_id = f"{population_id}__{uuid.uuid4().hex[:8]}"
|
||||
os.makedirs(f"runs/{self.run_id}", exist_ok=True)
|
||||
|
||||
self._episodes_f = open(f"runs/{self.run_id}/episodes.csv", "w", buffering=1)
|
||||
self._episodes_f.write("t_sec,pop_gen,agent_id,eval_idx,episode_return\n")
|
||||
|
||||
self._progress_f = open(f"runs/{self.run_id}/progress.csv", "w", buffering=1)
|
||||
self._progress_f.write("t_sec,t_norm,pop_gen,best_gen,best_so_far,avg,std\n")
|
||||
|
||||
self._deadline_task = asyncio.create_task(self._stop_after_deadline(), name=f"Deadline-{self.run_id}")
|
||||
|
||||
await self._init_generation()
|
||||
|
||||
return self
|
||||
|
||||
async def _stop_after_deadline(self):
|
||||
await asyncio.sleep(self.train_time_sec)
|
||||
await self.inbox.put(("stop", "normal"))
|
||||
|
||||
async def cast(self, msg: tuple) -> None:
|
||||
await self.inbox.put(msg)
|
||||
|
||||
async def stop(self, mode: Literal["normal", "shutdown"] = "normal") -> None:
|
||||
await self.inbox.put(("stop", mode))
|
||||
await self._stopped_evt.wait()
|
||||
|
||||
async def _run(self) -> None:
|
||||
try:
|
||||
while True:
|
||||
msg = await self.inbox.get()
|
||||
tag = msg[0] if isinstance(msg, tuple) else None
|
||||
|
||||
if tag == "stop":
|
||||
await self._handle_stop(msg[1])
|
||||
break
|
||||
elif tag == "pause":
|
||||
if self.state.op_tag == "continue":
|
||||
self.state.op_tag = "pause"
|
||||
log.debug("Population Monitor will pause after this generation.")
|
||||
elif tag == "continue":
|
||||
if self.state.op_tag == "pause":
|
||||
self.state.op_tag = "continue"
|
||||
await self._init_generation()
|
||||
elif tag == "episode_done":
|
||||
await self._handle_episode_done(*msg[1:])
|
||||
elif tag == "terminated":
|
||||
await self._handle_agent_terminated(*msg[1:])
|
||||
else:
|
||||
|
||||
pass
|
||||
finally:
|
||||
self._stopped_evt.set()
|
||||
|
||||
async def _init_generation(self) -> None:
|
||||
s = self.state
|
||||
agent_ids = await self._extract_agent_ids(s.population_id)
|
||||
s.agent_ids = agent_ids
|
||||
s.tot_agents = len(agent_ids)
|
||||
s.agents_left = 0
|
||||
|
||||
active = []
|
||||
for aid in agent_ids:
|
||||
try:
|
||||
handle = await self._exoself_start(aid, self)
|
||||
active.append((aid, handle))
|
||||
s.agents_left += 1
|
||||
except Exception as e:
|
||||
log.error(f"[Monitor] FAILED to start agent {aid}: {e!r}")
|
||||
|
||||
await _run("MATCH (a:agent {id:$aid}) SET a.fitness = toFloat($f)", aid=str(aid), f=float("-inf"))
|
||||
|
||||
s.active = active
|
||||
s.tot_agents = s.agents_left
|
||||
log.info(f"*** Population monitor started: pop={s.population_id}, mode={s.op_mode}, "
|
||||
f"selection={s.selection_algorithm}, agents={s.tot_agents}")
|
||||
|
||||
async def _handle_stop(self, mode: str) -> None:
|
||||
s = self.state
|
||||
|
||||
for (_aid, h) in list(s.active):
|
||||
try:
|
||||
if hasattr(h, "cast"):
|
||||
await h.cast(("terminate",))
|
||||
elif hasattr(h, "stop"):
|
||||
await h.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for f in (self._episodes_f, self._progress_f):
|
||||
if f is not None:
|
||||
try:
|
||||
f.flush()
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._episodes_f = None
|
||||
self._progress_f = None
|
||||
log.info(f"*** Population_Monitor:{s.population_id} shutdown. op_tag={s.op_tag}, op_mode={s.op_mode}")
|
||||
|
||||
await neo4j.close()
|
||||
|
||||
async def _handle_episode_done(self, agent_id: str, ep_return: float, eval_idx: int) -> None:
|
||||
s = self.state
|
||||
t_sec = asyncio.get_running_loop().time() - (self._t0 or 0.0)
|
||||
if self._episodes_f is not None:
|
||||
self._episodes_f.write(f"{t_sec:.6f},{s.pop_gen+1},{agent_id},{eval_idx},{ep_return:.10f}\n")
|
||||
|
||||
async def _handle_agent_terminated(self, agent_id: str, fitness: float, agent_eval: int, agent_cycle: int,
|
||||
agent_time: int) -> None:
|
||||
log.info(f"agent terminated: , {agent_id}, {fitness}, {agent_eval}, {agent_cycle}, {agent_time}")
|
||||
s = self.state
|
||||
|
||||
s.eval_acc += int(agent_eval)
|
||||
s.cycle_acc += int(agent_cycle)
|
||||
s.time_acc += int(agent_time)
|
||||
s.agents_left -= 1
|
||||
|
||||
await _run("MATCH (a:agent {id:$aid}) SET a.fitness = toFloat($f)", aid=str(agent_id), f=float(fitness))
|
||||
|
||||
s.active = [(aid, h) for (aid, h) in s.active if aid != agent_id]
|
||||
|
||||
log.info(f"[Monitor] agent done: {agent_id} | agents_left={s.agents_left}/{s.tot_agents}")
|
||||
if 0 < s.agents_left <= 3:
|
||||
log.info("[Monitor] still active: %s", [str(aid) for (aid, _h) in s.active])
|
||||
|
||||
if s.agents_left <= 0:
|
||||
await self._generation_finished()
|
||||
|
||||
async def _generation_finished(self) -> None:
|
||||
s = self.state
|
||||
await self._mutate_population(s.population_id, SPECIE_SIZE_LIMIT, s.selection_algorithm)
|
||||
s.pop_gen += 1
|
||||
log.info(f"Population {s.population_id} generation {s.pop_gen} ended.\n")
|
||||
|
||||
pop_stats = await _population_aggregate(s.population_id)
|
||||
|
||||
# aggregate logging information
|
||||
t_sec = asyncio.get_running_loop().time() - (self._t0 or 0.0)
|
||||
t_norm = min(1.0, t_sec / self.train_time_sec)
|
||||
best_gen = float(pop_stats["best"])
|
||||
self._best_so_far = max(self._best_so_far, best_gen)
|
||||
|
||||
# write logs
|
||||
if self._progress_f is not None:
|
||||
self._progress_f.write(
|
||||
f"{t_sec:.6f},{t_norm:.6f},{s.pop_gen},{best_gen:.10f},{self._best_so_far:.10f},"
|
||||
f"{float(pop_stats['avg']):.10f},{float(pop_stats['std']):.10f}\n"
|
||||
)
|
||||
|
||||
s.rows.append({
|
||||
"gen": int(s.pop_gen),
|
||||
"t_sec": int(asyncio.get_running_loop().time()),
|
||||
"cum_fitness": float(pop_stats["cum_fitness"]),
|
||||
"best": float(pop_stats["best"]),
|
||||
"avg": float(pop_stats["avg"]),
|
||||
"std": float(pop_stats["std"]),
|
||||
"agents": int(pop_stats["agents"]),
|
||||
"eval_acc": int(s.eval_acc),
|
||||
"cycle_acc": float(s.cycle_acc),
|
||||
"time_acc": float(s.time_acc),
|
||||
})
|
||||
|
||||
self.gen_ended.set()
|
||||
self.gen_ended = asyncio.Event()
|
||||
|
||||
# check time
|
||||
now = asyncio.get_running_loop().time()
|
||||
elapsed = now - self._t0
|
||||
time_limit_reached = elapsed >= self.train_time_sec
|
||||
if time_limit_reached:
|
||||
log.info(
|
||||
f"[Monitor] time limit reached "
|
||||
f"({elapsed:.1f}s >= {self.train_time_sec}s), stopping run"
|
||||
)
|
||||
|
||||
best = await self._best_fitness_in_population(s.population_id)
|
||||
end_condition = (s.pop_gen >= GENERATION_LIMIT) or (s.eval_acc >= EVALUATIONS_LIMIT) or (best > FITNESS_GOAL) or time_limit_reached
|
||||
if s.pop_gen >= GENERATION_LIMIT:
|
||||
log.info(f"reached generation limit {GENERATION_LIMIT}, stopping")
|
||||
if s.eval_acc >= EVALUATIONS_LIMIT:
|
||||
log.info(f"reached evaluation limit {EVALUATIONS_LIMIT}, stopping")
|
||||
if best > FITNESS_GOAL:
|
||||
log.info(f"reached best fitness {best}, stopping")
|
||||
|
||||
if s.op_tag == "done" or end_condition:
|
||||
await self.inbox.put(("stop", "normal"))
|
||||
return
|
||||
|
||||
if s.op_tag == "pause":
|
||||
log.info("Population Monitor paused.")
|
||||
return
|
||||
|
||||
await self._init_generation()
|
||||
|
||||
async def _ensure_population_node(self, population_id: str) -> None:
|
||||
await _run("MERGE (:population {id:$pid})", pid=str(population_id))
|
||||
|
||||
async def _ensure_specie_node(self, specie_id: str, population_id: str, constraint_json: str) -> None:
|
||||
await _run("""
|
||||
MERGE (s:specie {id:$sid})
|
||||
SET s.population_id = $pid,
|
||||
s.constraint_json = $cjson
|
||||
""", sid=str(specie_id), pid=str(population_id), cjson=str(constraint_json))
|
||||
|
||||
async def _extract_agent_ids(self, population_id: str) -> List[str]:
|
||||
rows = await _read_all("MATCH (a:agent {population_id:$pid}) RETURN a.id AS id ORDER BY id",
|
||||
pid=str(population_id))
|
||||
return [str(r["id"]) for r in rows]
|
||||
|
||||
async def _extract_specie_ids(self, population_id: str) -> List[str]:
|
||||
rows = await _read_all("""
|
||||
MATCH (s:specie {population_id:$pid}) RETURN s.id AS id ORDER BY id
|
||||
""", pid=str(population_id))
|
||||
return [str(r["id"]) for r in rows]
|
||||
|
||||
async def _extract_specie_agent_ids(self, specie_id: str) -> List[str]:
|
||||
rows = await _read_all("""
|
||||
MATCH (:specie {id:$sid})-[:HAS]->(a:agent) RETURN a.id AS id
|
||||
""", sid=str(specie_id))
|
||||
return [str(r["id"]) for r in rows]
|
||||
|
||||
async def _mutate_population(self, population_id: str, keep_tot: int,
|
||||
selection_algorithm: SelectionAlgorithm) -> None:
|
||||
energy_cost = await self._calculate_energy_cost(population_id)
|
||||
specie_ids = await self._extract_specie_ids(population_id)
|
||||
for sid in specie_ids:
|
||||
await self._mutate_specie(sid, keep_tot, energy_cost, selection_algorithm)
|
||||
|
||||
async def _mutate_specie(self, specie_id: str, population_limit: int, neural_energy_cost: float,
|
||||
selection_algorithm: SelectionAlgorithm) -> None:
|
||||
agent_ids = await self._extract_specie_agent_ids(specie_id)
|
||||
|
||||
summaries = await self._construct_agent_summaries(agent_ids)
|
||||
summaries.sort(key=lambda t: t[0], reverse=True)
|
||||
|
||||
if not summaries:
|
||||
return
|
||||
|
||||
if selection_algorithm == "competition":
|
||||
tot_survivors = round(len(summaries) * SURVIVAL_PERCENTAGE)
|
||||
|
||||
weighted = sorted(
|
||||
[(fit / (max(tn, 1) ** EFF), (fit, tn, aid)) for (fit, tn, aid) in summaries],
|
||||
key=lambda x: x[0], reverse=True
|
||||
)
|
||||
valid = [val for (_score, val) in weighted][:tot_survivors]
|
||||
invalid = [x for x in summaries if x not in valid]
|
||||
top3 = valid[:3]
|
||||
top_agent_ids = [aid for (_f, _tn, aid) in top3]
|
||||
|
||||
for (_f, _tn, aid) in invalid:
|
||||
await delete_agent(aid)
|
||||
await _run("""
|
||||
MATCH (:specie {id:$sid})-[h:HAS]->(a:agent {id:$aid}) DELETE h
|
||||
""", sid=str(specie_id), aid=str(aid))
|
||||
|
||||
new_ids = await self._competition(valid, population_limit, neural_energy_cost, specie_id)
|
||||
|
||||
elif selection_algorithm == "top3":
|
||||
valid = summaries[:3]
|
||||
invalid = [x for x in summaries if x not in valid]
|
||||
top_agent_ids = [aid for (_f, _tn, aid) in valid]
|
||||
for (_f, _tn, aid) in invalid:
|
||||
await delete_agent(aid)
|
||||
await _run("MATCH (:specie {id:$sid})-[h:HAS]->(a:agent {id:$aid}) DELETE h",
|
||||
sid=str(specie_id), aid=str(aid))
|
||||
|
||||
offspring_needed = max(0, population_limit - len(top_agent_ids))
|
||||
new_ids = await self._top3(top_agent_ids, offspring_needed, specie_id)
|
||||
|
||||
else:
|
||||
log.error(f"Unknown selection algorithm: {selection_algorithm}")
|
||||
raise ValueError(f"Unknown selection algorithm: {selection_algorithm}")
|
||||
|
||||
f_list = [f for (f, _tn, _aid) in summaries]
|
||||
avg, std, maxf, minf = _mean(f_list), _std(f_list), max(f_list), min(f_list)
|
||||
|
||||
row = await _read_all("MATCH (s:specie {id:$sid}) RETURN toInteger(s.innovation_factor) AS inv",
|
||||
sid=str(specie_id))
|
||||
inv = int(row[0]["inv"]) if row and row[0]["inv"] is not None else 0
|
||||
u_inv = 0 if (maxf > inv) else (inv - 1)
|
||||
|
||||
await _run("""
|
||||
MATCH (s:specie {id:$sid})
|
||||
SET s.fitness_avg = toFloat($avg),
|
||||
s.fitness_std = toFloat($std),
|
||||
s.fitness_max = toFloat($maxf),
|
||||
s.fitness_min = toFloat($minf),
|
||||
s.innovation_factor = toInteger($inv),
|
||||
s.champion_ids = $champs
|
||||
""", sid=str(specie_id), avg=float(avg), std=float(std), maxf=float(maxf), minf=float(minf),
|
||||
inv=int(u_inv), champs=[str(x) for x in top_agent_ids])
|
||||
|
||||
for aid in new_ids:
|
||||
try:
|
||||
await update_fingerprint(aid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _competition(self, valid: List[Tuple[float, int, str]], population_limit: int,
|
||||
neural_energy_cost: float, specie_id: str) -> List[str]:
|
||||
alot, est = await self._calculate_alotments(valid, neural_energy_cost)
|
||||
normalizer = (est / population_limit) if population_limit > 0 else 1.0
|
||||
log.debug(f"Population size normalizer: {normalizer:.4f}")
|
||||
return await self._gather_survivors(alot, normalizer, specie_id)
|
||||
|
||||
async def _calculate_alotments(self, valid: List[Tuple[float, int, str]], neural_energy_cost: float
|
||||
) -> Tuple[List[Tuple[float, float, int, str]], float]:
|
||||
acc: List[Tuple[float, float, int, str]] = []
|
||||
new_pop_acc = 0.0
|
||||
for (fit, tn, aid) in valid:
|
||||
neural_alot = (fit / neural_energy_cost) if neural_energy_cost > 0 else 0.0
|
||||
mutant_alot = (neural_alot / max(tn, 1))
|
||||
new_pop_acc += mutant_alot
|
||||
acc.append((mutant_alot, fit, tn, aid))
|
||||
log.debug(f"NewPopAcc: {new_pop_acc:.4f}")
|
||||
return acc, new_pop_acc
|
||||
|
||||
async def _gather_survivors(self, alot: List[Tuple[float, float, int, str]], normalizer: float, specie_id: str) -> \
|
||||
List[str]:
|
||||
new_ids: List[str] = []
|
||||
for (ma, fit, tn, aid) in alot:
|
||||
count = int(round(ma / normalizer)) if normalizer > 0 else 0
|
||||
log.info(f"Agent {aid}: normalized allotment = {count}")
|
||||
if count >= 1:
|
||||
|
||||
await _run("""
|
||||
MATCH (s:specie {id:$sid}), (a:agent {id:$aid})
|
||||
MERGE (s)-[:HAS]->(a)
|
||||
""", sid=str(specie_id), aid=str(aid))
|
||||
new_ids.append(aid)
|
||||
|
||||
k = count - 1
|
||||
for _ in range(k):
|
||||
cid = await self._create_mutant_offspring(aid, specie_id)
|
||||
new_ids.append(cid)
|
||||
else:
|
||||
|
||||
await delete_agent(aid)
|
||||
await _run("MATCH (:specie {id:$sid})-[h:HAS]->(a:agent {id:$aid}) DELETE h",
|
||||
sid=str(specie_id), aid=str(aid))
|
||||
|
||||
log.info(f"New Population Size (specie={specie_id}): {len(new_ids)}")
|
||||
return new_ids
|
||||
|
||||
async def _create_mutant_offspring(self, parent_id: str, specie_id: str) -> str:
|
||||
clone_id = _new_id()
|
||||
|
||||
await clone_agent(parent_id, clone_id)
|
||||
|
||||
rows = await _read_all("MATCH (a:agent {id:$aid}) RETURN a.specie_id AS sid, a.population_id AS pid",
|
||||
aid=str(parent_id))
|
||||
sid = rows[0]["sid"] if rows else specie_id
|
||||
pid = rows[0]["pid"] if rows else None
|
||||
await _run("""
|
||||
MATCH (a:agent {id:$cid})
|
||||
SET a.specie_id = $sid,
|
||||
a.population_id = $pid,
|
||||
a.fitness = NULL
|
||||
""", cid=str(clone_id), sid=str(sid), pid=str(pid))
|
||||
|
||||
await _run("""
|
||||
MATCH (s:specie {id:$sid}), (a:agent {id:$cid})
|
||||
MERGE (s)-[:HAS]->(a)
|
||||
""", sid=str(specie_id), cid=str(clone_id))
|
||||
|
||||
await self._mutate_one_step(clone_id)
|
||||
return clone_id
|
||||
|
||||
async def _mutate_one_step(self, agent_id: str) -> None:
|
||||
|
||||
ops = [
|
||||
self.mutator.mutate_weights,
|
||||
self.mutator.add_bias,
|
||||
self.mutator.remove_bias,
|
||||
self.mutator.add_inlink,
|
||||
self.mutator.add_outlink,
|
||||
self.mutator.add_neuron,
|
||||
self.mutator.outsplice,
|
||||
self.mutator.add_actuator,
|
||||
]
|
||||
op = random.choice(ops)
|
||||
try:
|
||||
await op(agent_id)
|
||||
except Exception:
|
||||
|
||||
try:
|
||||
await self.mutator.mutate_weights(agent_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _top3(self, valid_ids: List[str], offspring_needed: int, specie_id: str) -> List[str]:
|
||||
new_ids = list(valid_ids)
|
||||
for _ in range(offspring_needed):
|
||||
parent = random.choice(valid_ids)
|
||||
cid = await self._create_mutant_offspring(parent, specie_id)
|
||||
new_ids.append(cid)
|
||||
|
||||
return new_ids
|
||||
|
||||
async def _construct_agent_summaries(self, agent_ids: Sequence[str]) -> List[Tuple[float, int, str]]:
|
||||
if not agent_ids:
|
||||
return []
|
||||
rows = await _read_all("""
|
||||
UNWIND $ids AS aid
|
||||
MATCH (a:agent {id:aid})-[:OWNS]->(cx:cortex)
|
||||
OPTIONAL MATCH (cx)-[:HAS]->(n:neuron)
|
||||
RETURN aid AS id,
|
||||
toFloat(a.fitness) AS f,
|
||||
count(n) AS k
|
||||
""", ids=[str(x) for x in agent_ids])
|
||||
out: List[Tuple[float, int, str]] = []
|
||||
for r in rows:
|
||||
f = float(r["f"]) if r["f"] is not None else 0.0
|
||||
k = int(r["k"])
|
||||
out.append((f, k, str(r["id"])))
|
||||
return out
|
||||
|
||||
async def _calculate_energy_cost(self, population_id: str) -> float:
|
||||
rows = await _read_all("""
|
||||
MATCH (a:agent {population_id:$pid})-[:OWNS]->(cx:cortex)
|
||||
OPTIONAL MATCH (cx)-[:HAS]->(n:neuron)
|
||||
RETURN sum(coalesce(toFloat(a.fitness),0.0)) AS totE,
|
||||
count(n) AS totN
|
||||
""", pid=str(population_id))
|
||||
if not rows:
|
||||
return 0.0
|
||||
totE = float(rows[0]["totE"] or 0.0)
|
||||
totN = int(rows[0]["totN"] or 0)
|
||||
return (totE / totN) if totN > 0 else 0.0
|
||||
|
||||
async def _best_fitness_in_population(self, population_id: str) -> float:
|
||||
rows = await _read_all("""
|
||||
MATCH (a:agent {population_id:$pid})
|
||||
RETURN max(toFloat(a.fitness)) AS best
|
||||
""", pid=str(population_id))
|
||||
return float(rows[0]["best"] or 0.0) if rows else 0.0
|
||||
|
||||
|
||||
async def init_population(params: Tuple[str, List[dict], str, SelectionAlgorithm]) -> PopulationMonitor:
|
||||
"""
|
||||
params = (Population_Id, Specie_Constraints, OpMode, Selection_Algorithm)
|
||||
- erzeugt Population/Spezies-Knoten
|
||||
- konstruiert Agents (über dein construct_agent)
|
||||
- verknüpft Spezies->Agent, setzt agent.population_id
|
||||
- startet den Monitor
|
||||
"""
|
||||
population_id, specie_constraints, op_mode, selection_algorithm = params
|
||||
|
||||
await delete_population(population_id)
|
||||
|
||||
await _run("MERGE (:population {id:$pid})", pid=str(population_id))
|
||||
|
||||
# TODO: this has to move to the genotype module
|
||||
for specon in specie_constraints:
|
||||
sid = _new_id()
|
||||
await _run("""
|
||||
MERGE (p:population {id:$pid})
|
||||
MERGE (s:specie {id:$sid})
|
||||
ON CREATE SET
|
||||
s.population_id = $pid,
|
||||
s.constraint_json = $cjson,
|
||||
s.innovation_factor = 0
|
||||
ON MATCH SET
|
||||
s.population_id = $pid,
|
||||
s.constraint_json = $cjson
|
||||
MERGE (p)-[:HAS]->(s)
|
||||
""", pid=str(population_id),
|
||||
sid=str(sid),
|
||||
cjson=json.dumps(specon, separators=(",", ":"), sort_keys=True))
|
||||
|
||||
for _ in range(INIT_SPECIE_SIZE):
|
||||
aid = _new_id()
|
||||
await construct_agent(sid, aid, specon)
|
||||
|
||||
# todo: this needs to move to the genotype
|
||||
await _run("""
|
||||
MATCH (a:agent {id:$aid}), (s:specie {id:$sid})
|
||||
SET a.population_id = $pid, a.specie_id = $sid, a.fitness = NULL
|
||||
MERGE (s)-[:HAS]->(a)
|
||||
""", aid=str(aid), sid=str(sid), pid=str(population_id))
|
||||
|
||||
monitor = await PopulationMonitor.start(op_mode, population_id, selection_algorithm)
|
||||
return monitor
|
||||
|
||||
|
||||
async def continue_(op_mode: str, selection_algorithm: SelectionAlgorithm,
|
||||
population_id: str = INIT_POPULATION_ID) -> PopulationMonitor:
|
||||
return await PopulationMonitor.start(op_mode, population_id, selection_algorithm)
|
||||
|
||||
|
||||
async def delete_population(population_id: str) -> None:
|
||||
await _run("""
|
||||
MATCH (p:population {id:$pid})
|
||||
OPTIONAL MATCH (s:specie {population_id:$pid})
|
||||
OPTIONAL MATCH (s)-[:HAS]->(a:agent)-[:OWNS]->(cx:cortex)
|
||||
OPTIONAL MATCH (cx)-[:HAS]->(n:neuron)
|
||||
OPTIONAL MATCH (cx)-[:HAS]->(sen:sensor)
|
||||
OPTIONAL MATCH (cx)-[:HAS]->(act:actuator)
|
||||
DETACH DELETE p, s, a, cx, n, sen, act
|
||||
""", pid=str(population_id))
|
||||
|
||||
|
||||
async def test() -> PopulationMonitor:
|
||||
return await init_population((INIT_POPULATION_ID, INIT_CONSTRAINTS, INIT_OP_MODE, INIT_SELECTION_ALGO))
|
||||
@@ -1,11 +1,10 @@
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
|
||||
import morphology
|
||||
from genotype import construct, save_genotype, print_genotype
|
||||
from exoself import Exoself
|
||||
from mathema.core import morphology
|
||||
from mathema.genotype.neo4j.genotype import construct, print_genotype
|
||||
from mathema.core.exoself import Exoself
|
||||
|
||||
|
||||
class Trainer:
|
||||
0
mathema/envs/__init__.py
Normal file
BIN
mathema/envs/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
mathema/envs/__pycache__/openai_car_racing.cpython-312.pyc
Normal file
BIN
mathema/envs/__pycache__/openai_car_racing_sac.cpython-312.pyc
Normal file
726
mathema/envs/openai_car_racing.py
Normal file
@@ -0,0 +1,726 @@
|
||||
import math
|
||||
import numpy as np
|
||||
import logging
|
||||
|
||||
import Box2D
|
||||
from Box2D import (b2FixtureDef, b2PolygonShape, b2ContactListener)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import gymnasium as gym
|
||||
from gymnasium import spaces
|
||||
from gymnasium.utils import seeding
|
||||
|
||||
GYM_AVAILABLE = True
|
||||
except Exception:
|
||||
GYM_AVAILABLE = False
|
||||
spaces = None
|
||||
seeding = None
|
||||
|
||||
from gymnasium.envs.box2d.car_dynamics import Car
|
||||
|
||||
import pygame
|
||||
|
||||
DEBUG_DRAWING = False
|
||||
LOOK_AHEAD = 10
|
||||
|
||||
STATE_W = 96
|
||||
STATE_H = 96
|
||||
VIDEO_W = 1200
|
||||
VIDEO_H = 800
|
||||
WINDOW_W = 1350
|
||||
WINDOW_H = 950
|
||||
|
||||
SCALE = 6.0
|
||||
TRACK_RAD = 900 / SCALE
|
||||
PLAYFIELD = 2000 / SCALE
|
||||
FPS = 60
|
||||
ZOOM = 2.7
|
||||
ZOOM_FOLLOW = True
|
||||
|
||||
TRACK_DETAIL_STEP = 21 / SCALE
|
||||
TRACK_TURN_RATE = 0.31
|
||||
TRACK_WIDTH = 40 / SCALE
|
||||
BORDER = 8 / SCALE
|
||||
BORDER_MIN_COUNT = 4
|
||||
|
||||
ROAD_COLOR = [0.4, 0.4, 0.4]
|
||||
|
||||
MAX_TIME_SEC = 90.0
|
||||
MAX_STEPS = int(FPS * MAX_TIME_SEC)
|
||||
NO_PROGRESS_SEC = 8.0
|
||||
NO_PROGRESS_STEPS = int(FPS * NO_PROGRESS_SEC)
|
||||
STALL_MIN_SPEED = 4.0
|
||||
STALL_SEC = 4.0
|
||||
STALL_STEPS = int(FPS * STALL_SEC)
|
||||
FUEL_LIMIT = 120.0
|
||||
|
||||
|
||||
def standardize_angle(theta: float) -> float:
|
||||
return np.remainder(theta + np.pi, 2 * np.pi) - np.pi
|
||||
|
||||
|
||||
def f2c(rgb_float):
|
||||
"""float [0..1] -> int [0..255] color tuple"""
|
||||
return tuple(max(0, min(255, int(255 * x))) for x in rgb_float)
|
||||
|
||||
|
||||
class MyState:
|
||||
def __init__(self):
|
||||
self.angle_deltas = None
|
||||
self.reward = None
|
||||
self.on_road = None
|
||||
self.laps = None
|
||||
|
||||
self.wheel_angle = None
|
||||
self.car_angle = None
|
||||
self.angular_vel = None
|
||||
self.true_speed = None
|
||||
self.off_center = None
|
||||
self.vel_angle = None
|
||||
|
||||
def as_array(self, n: int):
|
||||
return np.append(
|
||||
self.angle_deltas[:n],
|
||||
[
|
||||
self.wheel_angle,
|
||||
self.car_angle,
|
||||
self.angular_vel,
|
||||
self.true_speed,
|
||||
self.off_center,
|
||||
self.vel_angle,
|
||||
],
|
||||
).astype(np.float32)
|
||||
|
||||
def as_feature_vector(self, lookahead: int = LOOK_AHEAD):
|
||||
return self.as_array(lookahead)
|
||||
|
||||
|
||||
class FrictionDetector(b2ContactListener):
|
||||
def __init__(self, env):
|
||||
super().__init__()
|
||||
self.env = env
|
||||
|
||||
def BeginContact(self, contact):
|
||||
self._contact(contact, True)
|
||||
|
||||
def EndContact(self, contact):
|
||||
self._contact(contact, False)
|
||||
|
||||
def _contact(self, contact, begin):
|
||||
tile = None
|
||||
obj = None
|
||||
u1 = contact.fixtureA.body.userData
|
||||
u2 = contact.fixtureB.body.userData
|
||||
if u1 and "road_friction" in u1.__dict__:
|
||||
tile = u1
|
||||
obj = u2
|
||||
if u2 and "road_friction" in u2.__dict__:
|
||||
tile = u2
|
||||
obj = u1
|
||||
if not tile:
|
||||
return
|
||||
|
||||
tile.color[0] = ROAD_COLOR[0]
|
||||
tile.color[1] = ROAD_COLOR[1]
|
||||
tile.color[2] = ROAD_COLOR[2]
|
||||
if not obj or "tiles" not in obj.__dict__:
|
||||
return
|
||||
if begin:
|
||||
obj.tiles.add(tile)
|
||||
if tile.index_on_track == self.env.next_road_tile:
|
||||
self.env.reward += 1000.0 / len(self.env.track)
|
||||
self.env.tile_visited_count += 1
|
||||
self.env.next_road_tile += 1
|
||||
if self.env.next_road_tile >= len(self.env.road):
|
||||
self.env.next_road_tile = 0
|
||||
self.env.laps += 1
|
||||
else:
|
||||
if tile in obj.tiles:
|
||||
obj.tiles.remove(tile)
|
||||
self.env.on_road = len(obj.tiles) > 0
|
||||
|
||||
|
||||
class CarRacing:
|
||||
metadata = {
|
||||
"render_modes": ["human", "rgb_array", None],
|
||||
"render_fps": FPS,
|
||||
}
|
||||
|
||||
def __init__(self, seed_value: int = 5, render_mode: str | None = "human"):
|
||||
|
||||
self.offroad_frames = None
|
||||
if seeding is not None:
|
||||
self.np_random, _ = seeding.np_random(seed_value)
|
||||
else:
|
||||
self.np_random = np.random.RandomState(seed_value)
|
||||
|
||||
self.contactListener_keepref = FrictionDetector(self)
|
||||
self.world = Box2D.b2World((0, 0), contactListener=self.contactListener_keepref)
|
||||
|
||||
if GYM_AVAILABLE:
|
||||
self.action_space = spaces.Box(
|
||||
np.array([-1, 0, 0], dtype=np.float32),
|
||||
np.array([+1, +1, +1], dtype=np.float32),
|
||||
dtype=np.float32,
|
||||
)
|
||||
self.observation_space = spaces.Box(
|
||||
low=0, high=255, shape=(STATE_H, STATE_W, 3), dtype=np.uint8
|
||||
)
|
||||
|
||||
self.viewer = None
|
||||
self.road = None
|
||||
self.car = None
|
||||
self.reward = 0.0
|
||||
self.prev_reward = 0.0
|
||||
|
||||
self.laps = 0
|
||||
self.on_road = True
|
||||
self.ctrl_pts = None
|
||||
self.outward_vectors = None
|
||||
self.angles = None
|
||||
self.angle_deltas = None
|
||||
self.original_road_poly = None
|
||||
self.indices = None
|
||||
self.my_state = MyState()
|
||||
self.next_road_tile = 0
|
||||
|
||||
self.render_mode = render_mode
|
||||
self._pg = None
|
||||
|
||||
self.tile_visited_count = 0
|
||||
self.t = 0.0
|
||||
self.human_render = False
|
||||
|
||||
self._build_new_episode()
|
||||
|
||||
self.offroad_frames = 0
|
||||
self.offroad_grace_frames = int(0.7 * FPS)
|
||||
self.offroad_penalty_per_frame = 2.0
|
||||
|
||||
self.steps = 0
|
||||
self._last_progress_count = 0
|
||||
self._no_progress_steps = 0
|
||||
self._stall_steps = 0
|
||||
|
||||
class _PygameCtx:
|
||||
def __init__(self):
|
||||
self.initialized = False
|
||||
self.screen = None
|
||||
self.clock = None
|
||||
self.font = None
|
||||
self.rgb_surface = None
|
||||
|
||||
def _ensure_pygame(self):
|
||||
if self._pg is None:
|
||||
self._pg = self._PygameCtx()
|
||||
if not self._pg.initialized:
|
||||
if not pygame.get_init():
|
||||
pygame.init()
|
||||
flags = 0
|
||||
if self.render_mode == "human":
|
||||
self._pg.screen = pygame.display.set_mode((WINDOW_W, WINDOW_H))
|
||||
else:
|
||||
|
||||
self._pg.screen = pygame.Surface((WINDOW_W, WINDOW_H))
|
||||
self._pg.clock = pygame.time.Clock()
|
||||
try:
|
||||
pygame.font.init()
|
||||
self._pg.font = pygame.font.SysFont("Arial", 20)
|
||||
except Exception:
|
||||
self._pg.font = None
|
||||
self._pg.initialized = True
|
||||
|
||||
def _world_to_screen(self, x, y, zoom, angle, scroll_x, scroll_y):
|
||||
ca, sa = math.cos(angle), math.sin(angle)
|
||||
|
||||
rx = (x - scroll_x) * ca + (y - scroll_y) * sa
|
||||
ry = -(x - scroll_x) * sa + (y - scroll_y) * ca
|
||||
|
||||
sx = int(WINDOW_W / 2 + rx * zoom)
|
||||
sy = int(WINDOW_H / 4 + ry * zoom)
|
||||
return sx, sy
|
||||
|
||||
def get_feature_vector(self, lookahead: int = LOOK_AHEAD) -> list[float]:
|
||||
my_s: MyState = self.my_state
|
||||
vec = my_s.as_feature_vector(lookahead).tolist()
|
||||
return vec
|
||||
|
||||
def _draw_polygon_world(self, poly, color, zoom, angle, scroll_x, scroll_y):
|
||||
pts = [self._world_to_screen(px, py, zoom, angle, scroll_x, scroll_y) for (px, py) in poly]
|
||||
pygame.draw.polygon(self._pg.screen, f2c(color), pts)
|
||||
|
||||
def _draw_body(self, body, color=(0.7, 0.7, 0.7), zoom=1.0, angle=0.0, scroll_x=0.0, scroll_y=0.0):
|
||||
|
||||
col = f2c(color)
|
||||
for fixture in body.fixtures:
|
||||
shape = fixture.shape
|
||||
if isinstance(shape, b2PolygonShape):
|
||||
verts = [body.transform * v for v in shape.vertices]
|
||||
pts = [self._world_to_screen(v[0], v[1], zoom, angle, scroll_x, scroll_y) for v in verts]
|
||||
pygame.draw.polygon(self._pg.screen, col, pts, width=0)
|
||||
|
||||
def _destroy(self):
|
||||
if not self.road:
|
||||
return
|
||||
|
||||
for t in self.road:
|
||||
try:
|
||||
t.userData = None
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.world.DestroyBody(t)
|
||||
except Exception:
|
||||
pass
|
||||
self.road = []
|
||||
|
||||
if self.car is not None:
|
||||
try:
|
||||
self.car.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self.car = None
|
||||
|
||||
def _create_track(self):
|
||||
CHECKPOINTS = 12
|
||||
checkpoints = []
|
||||
for c in range(CHECKPOINTS):
|
||||
alpha = 2 * math.pi * c / CHECKPOINTS + self.np_random.uniform(0, 2 * math.pi * 1 / CHECKPOINTS)
|
||||
rad = self.np_random.uniform(TRACK_RAD / 3, TRACK_RAD)
|
||||
if c == 0:
|
||||
alpha = 0
|
||||
rad = 1.5 * TRACK_RAD
|
||||
if c == CHECKPOINTS - 1:
|
||||
alpha = 2 * math.pi * c / CHECKPOINTS
|
||||
self.start_alpha = 2 * math.pi * (-0.5) / CHECKPOINTS
|
||||
rad = 1.5 * TRACK_RAD
|
||||
checkpoints.append((alpha, rad * math.cos(alpha), rad * math.sin(alpha)))
|
||||
|
||||
self.road = []
|
||||
|
||||
x, y, beta = 1.5 * TRACK_RAD, 0, 0
|
||||
dest_i = 0
|
||||
laps = 0
|
||||
track = []
|
||||
no_freeze = 2500
|
||||
visited_other_side = False
|
||||
while True:
|
||||
alpha = math.atan2(y, x)
|
||||
if visited_other_side and alpha > 0:
|
||||
laps += 1
|
||||
visited_other_side = False
|
||||
if alpha < 0:
|
||||
visited_other_side = True
|
||||
alpha += 2 * math.pi
|
||||
while True:
|
||||
failed = True
|
||||
while True:
|
||||
dest_alpha, dest_x, dest_y = checkpoints[dest_i % len(checkpoints)]
|
||||
if alpha <= dest_alpha:
|
||||
failed = False
|
||||
break
|
||||
dest_i += 1
|
||||
if dest_i % len(checkpoints) == 0:
|
||||
break
|
||||
if not failed:
|
||||
break
|
||||
alpha -= 2 * math.pi
|
||||
continue
|
||||
r1x = math.cos(beta)
|
||||
r1y = math.sin(beta)
|
||||
p1x = -r1y
|
||||
p1y = r1x
|
||||
dest_dx = dest_x - x
|
||||
dest_dy = dest_y - y
|
||||
proj = r1x * dest_dx + r1y * dest_dy
|
||||
while beta - alpha > 1.5 * math.pi:
|
||||
beta -= 2 * math.pi
|
||||
while beta - alpha < -1.5 * math.pi:
|
||||
beta += 2 * math.pi
|
||||
prev_beta = beta
|
||||
proj *= SCALE
|
||||
if proj > 0.3:
|
||||
beta -= min(TRACK_TURN_RATE, abs(0.001 * proj))
|
||||
if proj < -0.3:
|
||||
beta += min(TRACK_TURN_RATE, abs(0.001 * proj))
|
||||
x += p1x * TRACK_DETAIL_STEP
|
||||
y += p1y * TRACK_DETAIL_STEP
|
||||
track.append((alpha, prev_beta * 0.5 + beta * 0.5, x, y))
|
||||
if laps > 4:
|
||||
break
|
||||
no_freeze -= 1
|
||||
if no_freeze == 0:
|
||||
break
|
||||
|
||||
i1, i2 = -1, -1
|
||||
i = len(track)
|
||||
while True:
|
||||
i -= 1
|
||||
if i == 0:
|
||||
return False
|
||||
pass_through_start = track[i][0] > self.start_alpha >= track[i - 1][0]
|
||||
if pass_through_start and i2 == -1:
|
||||
i2 = i
|
||||
elif pass_through_start and i1 == -1:
|
||||
i1 = i
|
||||
break
|
||||
print(f"Track generation: {i1}..{i2} -> {i2 - i1}-tiles track")
|
||||
assert i1 != -1 and i2 != -1
|
||||
|
||||
track = track[i1: i2 - 1]
|
||||
|
||||
first_beta = track[0][1]
|
||||
first_perp_x = math.cos(first_beta)
|
||||
first_perp_y = math.sin(first_beta)
|
||||
well_glued_together = np.sqrt(
|
||||
np.square(first_perp_x * (track[0][2] - track[-1][2]))
|
||||
+ np.square(first_perp_y * (track[0][3] - track[-1][3]))
|
||||
)
|
||||
if well_glued_together > TRACK_DETAIL_STEP:
|
||||
return False
|
||||
|
||||
border = [False] * len(track)
|
||||
for i in range(len(track)):
|
||||
good = True
|
||||
oneside = 0
|
||||
for neg in range(BORDER_MIN_COUNT):
|
||||
beta1 = track[i - neg - 0][1]
|
||||
beta2 = track[i - neg - 1][1]
|
||||
good &= abs(beta1 - beta2) > TRACK_TURN_RATE * 0.2
|
||||
oneside += np.sign(beta1 - beta2)
|
||||
good &= abs(oneside) == BORDER_MIN_COUNT
|
||||
border[i] = bool(good)
|
||||
for i in range(len(track)):
|
||||
for neg in range(BORDER_MIN_COUNT):
|
||||
border[i - neg] |= border[i]
|
||||
|
||||
self.road_poly = []
|
||||
for i in range(len(track)):
|
||||
alpha1, beta1, x1, y1 = track[i]
|
||||
alpha2, beta2, x2, y2 = track[i - 1]
|
||||
road1_l = (x1 - TRACK_WIDTH * math.cos(beta1), y1 - TRACK_WIDTH * math.sin(beta1))
|
||||
road1_r = (x1 + TRACK_WIDTH * math.cos(beta1), y1 + TRACK_WIDTH * math.sin(beta1))
|
||||
road2_l = (x2 - TRACK_WIDTH * math.cos(beta2), y2 - TRACK_WIDTH * math.sin(beta2))
|
||||
road2_r = (x2 + TRACK_WIDTH * math.cos(beta2), y2 + TRACK_WIDTH * math.sin(beta2))
|
||||
t = self.world.CreateStaticBody(
|
||||
fixtures=b2FixtureDef(shape=b2PolygonShape(vertices=[road1_l, road1_r, road2_r, road2_l]))
|
||||
)
|
||||
t.userData = t
|
||||
t.index_on_track = i
|
||||
c = 0.01 * (i % 3)
|
||||
t.color = [ROAD_COLOR[0] + c, ROAD_COLOR[1] + c, ROAD_COLOR[2] + c]
|
||||
t.road_visited = False
|
||||
t.road_friction = 1.0
|
||||
t.fixtures[0].sensor = True
|
||||
self.road_poly.append(([road1_l, road1_r, road2_r, road2_l], t.color))
|
||||
self.road.append(t)
|
||||
if border[i]:
|
||||
side = np.sign(beta2 - beta1)
|
||||
b1_l = (x1 + side * TRACK_WIDTH * math.cos(beta1), y1 + side * TRACK_WIDTH * math.sin(beta1))
|
||||
b1_r = (
|
||||
x1 + side * (TRACK_WIDTH + BORDER) * math.cos(beta1),
|
||||
y1 + side * (TRACK_WIDTH + BORDER) * math.sin(beta1),
|
||||
)
|
||||
b2_l = (x2 + side * TRACK_WIDTH * math.cos(beta2), y2 + side * TRACK_WIDTH * math.sin(beta2))
|
||||
b2_r = (
|
||||
x2 + side * (TRACK_WIDTH + BORDER) * math.cos(beta2),
|
||||
y2 + side * (TRACK_WIDTH + BORDER) * math.sin(beta2),
|
||||
)
|
||||
self.road_poly.append(([b1_l, b1_r, b2_r, b2_l], (1, 1, 1) if i % 2 == 0 else (1, 0, 0)))
|
||||
self.track = track
|
||||
|
||||
self.original_road_poly = [((list(poly)), list(color)) for (poly, color) in self.road_poly]
|
||||
self.ctrl_pts = np.array(list(map(lambda x: x[2:], self.track)))
|
||||
self.angles = np.array(list(map(lambda x: x[1], self.track)))
|
||||
self.outward_vectors = [np.array([np.cos(theta), np.sin(theta)]) for theta in self.angles]
|
||||
angle_deltas = self.angles - np.roll(self.angles, 1)
|
||||
self.angle_deltas = np.array(list(map(standardize_angle, angle_deltas)))
|
||||
self.indices = np.array(range(len(self.ctrl_pts)))
|
||||
return True
|
||||
|
||||
def _build_new_episode(self):
|
||||
|
||||
self._destroy()
|
||||
self.reward = 0.0
|
||||
self.prev_reward = 0.0
|
||||
self.tile_visited_count = 0
|
||||
self.t = 0.0
|
||||
self.road_poly = []
|
||||
self.human_render = False
|
||||
self.laps = 0
|
||||
self.on_road = True
|
||||
self.next_road_tile = 0
|
||||
|
||||
while True:
|
||||
success = self._create_track()
|
||||
if success:
|
||||
break
|
||||
print("retry to generate track (normal if there are not many of this messages)")
|
||||
|
||||
self.car = Car(self.world, *self.track[0][1:4])
|
||||
|
||||
self.car.tiles = set()
|
||||
|
||||
self.steps = 0
|
||||
self._last_progress_count = 0
|
||||
self._no_progress_steps = 0
|
||||
self._stall_steps = 0
|
||||
|
||||
def reset(self, *, seed: int | None = None, options: dict | None = None):
|
||||
if seed is not None:
|
||||
|
||||
if seeding is not None:
|
||||
self.np_random, _ = seeding.np_random(seed)
|
||||
else:
|
||||
self.np_random = np.random.RandomState(seed)
|
||||
self._build_new_episode()
|
||||
obs = self._get_observation()
|
||||
info = {}
|
||||
return obs, info
|
||||
|
||||
def fast_reset(self):
|
||||
|
||||
self.car = None
|
||||
self.laps = 0
|
||||
self.on_road = True
|
||||
self.next_road_tile = 0
|
||||
|
||||
self.reward = 0.0
|
||||
self.prev_reward = 0.0
|
||||
self.tile_visited_count = 0
|
||||
self.t = 0.0
|
||||
self.human_render = False
|
||||
for tile in self.road:
|
||||
tile.road_visited = False
|
||||
self.road_poly = [((list(poly)), list(color)) for (poly, color) in self.original_road_poly]
|
||||
try:
|
||||
self.car.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self.car = Car(self.world, *self.track[0][1:4])
|
||||
self.car.tiles = set()
|
||||
|
||||
self.steps = 0
|
||||
self._last_progress_count = 0
|
||||
self._no_progress_steps = 0
|
||||
self._stall_steps = 0
|
||||
|
||||
return self.step(np.array([0.0, 0.0, 0.0], dtype=np.float32))
|
||||
|
||||
def step(self, action):
|
||||
|
||||
if action is not None:
|
||||
self.car.steer(-float(action[0]))
|
||||
self.car.gas(float(action[1]))
|
||||
self.car.brake(float(action[2]))
|
||||
|
||||
self.car.step(1.0 / FPS)
|
||||
self.world.Step(1.0 / FPS, 6 * 30, 2 * 30)
|
||||
self.t += 1.0 / FPS
|
||||
|
||||
self.steps += 1
|
||||
|
||||
terminated = False
|
||||
truncated = False
|
||||
|
||||
if action is not None:
|
||||
|
||||
self.reward -= 5.0 / FPS
|
||||
|
||||
if self.tile_visited_count == len(self.track):
|
||||
terminated = True
|
||||
|
||||
x, y = self.car.hull.position
|
||||
if abs(x) > PLAYFIELD or abs(y) > PLAYFIELD:
|
||||
self.reward -= 100.0
|
||||
terminated = True
|
||||
|
||||
if not self.on_road:
|
||||
self.offroad_frames += 1
|
||||
self.reward -= self.offroad_penalty_per_frame / FPS
|
||||
if self.offroad_frames > self.offroad_grace_frames:
|
||||
self.reward -= 20.0
|
||||
terminated = True
|
||||
else:
|
||||
self.offroad_frames = 0
|
||||
|
||||
if self.tile_visited_count > self._last_progress_count:
|
||||
self._last_progress_count = self.tile_visited_count
|
||||
self._no_progress_steps = 0
|
||||
else:
|
||||
self._no_progress_steps += 1
|
||||
if self._no_progress_steps >= NO_PROGRESS_STEPS:
|
||||
truncated = True
|
||||
|
||||
step_reward = self.reward - self.prev_reward
|
||||
self.prev_reward = self.reward
|
||||
else:
|
||||
step_reward = 0.0
|
||||
|
||||
v1 = self.outward_vectors[self.next_road_tile - 2]
|
||||
v2 = np.array(self.car.hull.position) - self.ctrl_pts[self.next_road_tile - 1]
|
||||
off_center = float(np.dot(v1, v2))
|
||||
angular_vel = float(self.car.hull.angularVelocity)
|
||||
vel = self.car.hull.linearVelocity
|
||||
true_speed = float(np.linalg.norm(vel))
|
||||
car_angle = float(self.car.hull.angle - self.angles[self.next_road_tile])
|
||||
wheel_angle = float(self.car.wheels[0].joint.angle)
|
||||
if true_speed < 0.2:
|
||||
vel_angle = 0.0
|
||||
else:
|
||||
vel_angle = float(math.atan2(vel[1], vel[0]) - (self.angles[self.next_road_tile] + np.pi / 2))
|
||||
|
||||
wheel_angle = standardize_angle(wheel_angle)
|
||||
car_angle = standardize_angle(car_angle)
|
||||
vel_angle = standardize_angle(vel_angle)
|
||||
|
||||
tip = np.array((self.car.wheels[0].position + self.car.wheels[1].position) / 2)
|
||||
p1 = self.ctrl_pts[self.next_road_tile - 1]
|
||||
p2 = self.ctrl_pts[self.next_road_tile - 2]
|
||||
u = (p1 - p2) / TRACK_DETAIL_STEP
|
||||
v = (tip - p2) / TRACK_DETAIL_STEP
|
||||
interp = float(np.dot(v, u))
|
||||
interp_angle_deltas = np.interp(self.indices + interp, self.indices, self.angle_deltas)
|
||||
|
||||
self.my_state.angle_deltas = np.roll(interp_angle_deltas, -self.next_road_tile)
|
||||
self.my_state.reward = self.reward
|
||||
self.my_state.on_road = self.on_road
|
||||
self.my_state.laps = self.laps
|
||||
self.my_state.true_speed = true_speed
|
||||
self.my_state.off_center = off_center
|
||||
self.my_state.wheel_angle = wheel_angle
|
||||
self.my_state.car_angle = car_angle
|
||||
self.my_state.angular_vel = angular_vel
|
||||
self.my_state.vel_angle = vel_angle
|
||||
|
||||
self.my_state.angle_deltas *= 2.3
|
||||
self.my_state.true_speed /= 100.0
|
||||
self.my_state.off_center /= TRACK_WIDTH
|
||||
self.my_state.wheel_angle *= 2.1
|
||||
self.my_state.car_angle *= 1.5
|
||||
self.my_state.vel_angle *= 1.5
|
||||
self.my_state.angular_vel /= 3.74
|
||||
|
||||
obs = self._get_observation()
|
||||
info = {"features": self.my_state}
|
||||
return obs, step_reward, terminated, truncated, info
|
||||
|
||||
def _get_observation(self):
|
||||
|
||||
return None
|
||||
|
||||
def render(self):
|
||||
self._ensure_pygame()
|
||||
|
||||
if self.render_mode == "human":
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
self.close()
|
||||
return None
|
||||
|
||||
zoom = 0.1 * SCALE * max(1 - self.t, 0) + ZOOM * SCALE * min(self.t, 1)
|
||||
scroll_x = self.car.hull.position[0]
|
||||
scroll_y = self.car.hull.position[1]
|
||||
angle = -self.car.hull.angle
|
||||
vel = self.car.hull.linearVelocity
|
||||
if np.linalg.norm(vel) > 0.5:
|
||||
angle = math.atan2(vel[0], vel[1])
|
||||
|
||||
self._pg.screen.fill((102, 230, 102))
|
||||
|
||||
k = PLAYFIELD / 20.0
|
||||
grid_color = (110, 240, 110)
|
||||
for x in range(-20, 20, 2):
|
||||
for y in range(-20, 20, 2):
|
||||
x0, y0 = k * x + 0, k * y + 0
|
||||
x1, y1 = k * x + k, k * y + k
|
||||
p0 = self._world_to_screen(x0, y0, zoom, angle, scroll_x, scroll_y)
|
||||
p1 = self._world_to_screen(x1, y0, zoom, angle, scroll_x, scroll_y)
|
||||
p2 = self._world_to_screen(x1, y1, zoom, angle, scroll_x, scroll_y)
|
||||
p3 = self._world_to_screen(x0, y1, zoom, angle, scroll_x, scroll_y)
|
||||
pygame.draw.polygon(self._pg.screen, grid_color, [p0, p1, p2, p3])
|
||||
|
||||
for poly, color in self.road_poly:
|
||||
self._draw_polygon_world(poly, color, zoom, angle, scroll_x, scroll_y)
|
||||
|
||||
car_col = (0.25, 0.25, 0.25)
|
||||
self._draw_body(self.car.hull, car_col, zoom, angle, scroll_x, scroll_y)
|
||||
for w in self.car.wheels:
|
||||
self._draw_body(w, (0.15, 0.15, 0.15), zoom, angle, scroll_x, scroll_y)
|
||||
|
||||
if self._pg.font is not None:
|
||||
txt = f"reward={self.reward:0.1f} laps={self.laps}"
|
||||
surf = self._pg.font.render(txt, True, (255, 255, 255))
|
||||
self._pg.screen.blit(surf, (10, 10))
|
||||
|
||||
if self.render_mode == "human":
|
||||
pygame.display.flip()
|
||||
self._pg.clock.tick(FPS)
|
||||
return None
|
||||
else:
|
||||
|
||||
arr = pygame.surfarray.array3d(self._pg.screen)
|
||||
arr = np.transpose(arr, (1, 0, 2))
|
||||
return arr
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self._pg and self._pg.initialized:
|
||||
pygame.display.quit()
|
||||
pygame.quit()
|
||||
except Exception:
|
||||
pass
|
||||
self._pg = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pygame
|
||||
|
||||
pygame.init()
|
||||
env = CarRacing(render_mode="human")
|
||||
|
||||
action = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
running = True
|
||||
|
||||
|
||||
def handle_keys(a):
|
||||
keys = pygame.key.get_pressed()
|
||||
steer = 0.0
|
||||
if keys[pygame.K_LEFT]:
|
||||
steer -= 1.0
|
||||
if keys[pygame.K_RIGHT]:
|
||||
steer += 1.0
|
||||
gas = 1.0 if keys[pygame.K_UP] else 0.0
|
||||
brake = 0.5 if keys[pygame.K_DOWN] else 0.0
|
||||
a[0], a[1], a[2] = steer, gas, brake
|
||||
|
||||
|
||||
env.reset()
|
||||
try:
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
|
||||
env.fast_reset()
|
||||
|
||||
handle_keys(action)
|
||||
obs, r, terminated, truncated, info = env.step(action)
|
||||
|
||||
if int(env.t * FPS) % 200 == 0:
|
||||
ms: MyState = info.get("features")
|
||||
if ms is not None:
|
||||
print(
|
||||
f"speed={ms.true_speed:5.2f} off_center={ms.off_center:+.2f} car_ang={ms.car_angle:+.2f} "
|
||||
f"reward={r:+.2f}"
|
||||
f"reward={ms.reward:+.2f}"
|
||||
)
|
||||
|
||||
env.render()
|
||||
if terminated or truncated:
|
||||
env.fast_reset()
|
||||
|
||||
finally:
|
||||
env.close()
|
||||
790
mathema/envs/openai_car_racing_sac.py
Normal file
@@ -0,0 +1,790 @@
|
||||
# TODO: this will be one env for both systems
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
import logging
|
||||
|
||||
import Box2D
|
||||
from Box2D import (b2FixtureDef, b2PolygonShape, b2ContactListener)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Optional gym import for spaces only; code runs without strict gym registration
|
||||
try:
|
||||
import gymnasium as gym
|
||||
from gymnasium import spaces
|
||||
from gymnasium.utils import seeding
|
||||
|
||||
GYM_AVAILABLE = True
|
||||
except Exception:
|
||||
GYM_AVAILABLE = False
|
||||
spaces = None
|
||||
seeding = None
|
||||
|
||||
# Car dynamics from classic gym (Box2D)
|
||||
from gymnasium.envs.box2d.car_dynamics import Car
|
||||
|
||||
# --- pygame renderer ---
|
||||
import pygame
|
||||
|
||||
DEBUG_DRAWING = False
|
||||
LOOK_AHEAD = 10
|
||||
|
||||
STATE_W = 96
|
||||
STATE_H = 96
|
||||
VIDEO_W = 1200
|
||||
VIDEO_H = 800
|
||||
WINDOW_W = 1350
|
||||
WINDOW_H = 950
|
||||
|
||||
SCALE = 6.0
|
||||
TRACK_RAD = 900 / SCALE
|
||||
PLAYFIELD = 2000 / SCALE
|
||||
FPS = 60
|
||||
ZOOM = 2.7
|
||||
ZOOM_FOLLOW = True
|
||||
|
||||
TRACK_DETAIL_STEP = 21 / SCALE
|
||||
TRACK_TURN_RATE = 0.31
|
||||
TRACK_WIDTH = 40 / SCALE
|
||||
BORDER = 8 / SCALE
|
||||
BORDER_MIN_COUNT = 4
|
||||
|
||||
ROAD_COLOR = [0.4, 0.4, 0.4]
|
||||
|
||||
# limits & timeouts
|
||||
MAX_TIME_SEC = 90.0
|
||||
MAX_STEPS = int(FPS * MAX_TIME_SEC)
|
||||
NO_PROGRESS_SEC = 8.0
|
||||
NO_PROGRESS_STEPS = int(FPS * NO_PROGRESS_SEC)
|
||||
STALL_MIN_SPEED = 4.0
|
||||
STALL_SEC = 4.0
|
||||
STALL_STEPS = int(FPS * STALL_SEC)
|
||||
FUEL_LIMIT = 120.0
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Utilities
|
||||
# ----------------------------
|
||||
def standardize_angle(theta: float) -> float:
|
||||
return np.remainder(theta + np.pi, 2 * np.pi) - np.pi
|
||||
|
||||
|
||||
def f2c(rgb_float):
|
||||
"""float [0..1] -> int [0..255] color tuple"""
|
||||
return tuple(max(0, min(255, int(255 * x))) for x in rgb_float)
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# MyState: feature container
|
||||
# ----------------------------
|
||||
class MyState:
|
||||
def __init__(self):
|
||||
self.angle_deltas = None
|
||||
self.reward = None
|
||||
self.on_road = None
|
||||
self.laps = None
|
||||
|
||||
self.wheel_angle = None
|
||||
self.car_angle = None
|
||||
self.angular_vel = None
|
||||
self.true_speed = None
|
||||
self.off_center = None
|
||||
self.vel_angle = None
|
||||
|
||||
def as_array(self, n: int):
|
||||
return np.append(
|
||||
self.angle_deltas[:n],
|
||||
[
|
||||
self.wheel_angle,
|
||||
self.car_angle,
|
||||
self.angular_vel,
|
||||
self.true_speed,
|
||||
self.off_center,
|
||||
self.vel_angle,
|
||||
],
|
||||
).astype(np.float32)
|
||||
|
||||
def as_feature_vector(self, lookahead: int = LOOK_AHEAD):
|
||||
return self.as_array(lookahead)
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Contact listener: counts tiles, progress & reward
|
||||
# ----------------------------
|
||||
class FrictionDetector(b2ContactListener):
|
||||
def __init__(self, env):
|
||||
super().__init__()
|
||||
self.env = env
|
||||
|
||||
def BeginContact(self, contact):
|
||||
self._contact(contact, True)
|
||||
|
||||
def EndContact(self, contact):
|
||||
self._contact(contact, False)
|
||||
|
||||
def _contact(self, contact, begin):
|
||||
tile = None
|
||||
obj = None
|
||||
u1 = contact.fixtureA.body.userData
|
||||
u2 = contact.fixtureB.body.userData
|
||||
if u1 and "road_friction" in u1.__dict__:
|
||||
tile = u1
|
||||
obj = u2
|
||||
if u2 and "road_friction" in u2.__dict__:
|
||||
tile = u2
|
||||
obj = u1
|
||||
if not tile:
|
||||
return
|
||||
|
||||
tile.color[0] = ROAD_COLOR[0]
|
||||
tile.color[1] = ROAD_COLOR[1]
|
||||
tile.color[2] = ROAD_COLOR[2]
|
||||
if not obj or "tiles" not in obj.__dict__:
|
||||
return
|
||||
if begin:
|
||||
obj.tiles.add(tile)
|
||||
if tile.index_on_track == self.env.next_road_tile:
|
||||
self.env.reward += 1000.0 / len(self.env.track)
|
||||
self.env.tile_visited_count += 1
|
||||
self.env.next_road_tile += 1
|
||||
if self.env.next_road_tile >= len(self.env.road):
|
||||
self.env.next_road_tile = 0
|
||||
self.env.laps += 1
|
||||
else:
|
||||
if tile in obj.tiles:
|
||||
obj.tiles.remove(tile)
|
||||
self.env.on_road = len(obj.tiles) > 0
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# CarRacing with pygame rendering and Gym(nasium) 0.26+ compatible step/reset
|
||||
# ----------------------------
|
||||
class CarRacing(gym.Env):
|
||||
metadata = {
|
||||
"render_modes": ["human", "rgb_array", None],
|
||||
"render_fps": FPS,
|
||||
}
|
||||
|
||||
def __init__(self, seed_value: int = 5, render_mode: str | None = "human"):
|
||||
# RNG
|
||||
self.offroad_frames = None
|
||||
if seeding is not None:
|
||||
self.np_random, _ = seeding.np_random(seed_value)
|
||||
else:
|
||||
self.np_random = np.random.RandomState(seed_value)
|
||||
|
||||
# Physics world
|
||||
self.contactListener_keepref = FrictionDetector(self)
|
||||
self.world = Box2D.b2World((0, 0), contactListener=self.contactListener_keepref)
|
||||
|
||||
# Gym-style spaces (optional)
|
||||
if GYM_AVAILABLE:
|
||||
self.action_space = spaces.Box(
|
||||
np.array([-1, 0, 0], dtype=np.float32),
|
||||
np.array([+1, +1, +1], dtype=np.float32),
|
||||
dtype=np.float32,
|
||||
)
|
||||
# Feature-Vektor-Länge = LOOK_AHEAD + 6 (wheel, car, angular_vel, true_speed, off_center, vel_angle)
|
||||
feat_len = LOOK_AHEAD + 6
|
||||
self.observation_space = spaces.Box(
|
||||
low=-np.inf, high=np.inf, shape=(feat_len,), dtype=np.float32
|
||||
)
|
||||
|
||||
# State
|
||||
self.viewer = None # unused (pyglet placeholder)
|
||||
self.road = None
|
||||
self.car = None
|
||||
self.reward = 0.0
|
||||
self.prev_reward = 0.0
|
||||
|
||||
self.laps = 0
|
||||
self.on_road = True
|
||||
self.ctrl_pts = None
|
||||
self.outward_vectors = None
|
||||
self.angles = None
|
||||
self.angle_deltas = None
|
||||
self.original_road_poly = None
|
||||
self.indices = None
|
||||
self.my_state = MyState()
|
||||
self.next_road_tile = 0
|
||||
|
||||
# Rendering
|
||||
self.render_mode = render_mode
|
||||
self._pg = None # pygame objects container
|
||||
|
||||
# Episode control
|
||||
self.tile_visited_count = 0
|
||||
self.t = 0.0
|
||||
self.human_render = False
|
||||
|
||||
# Build initial track + car
|
||||
self._build_new_episode()
|
||||
|
||||
self.offroad_frames = 0
|
||||
self.offroad_grace_frames = int(0.2 * FPS)
|
||||
self.offroad_penalty_per_frame = 2.0
|
||||
|
||||
self.steps = 0
|
||||
self._last_progress_count = 0
|
||||
self._no_progress_steps = 0
|
||||
self._stall_steps = 0
|
||||
|
||||
# ------------------------
|
||||
# Helpers: pygame
|
||||
# ------------------------
|
||||
class _PygameCtx:
|
||||
def __init__(self):
|
||||
self.initialized = False
|
||||
self.screen = None
|
||||
self.clock = None
|
||||
self.font = None
|
||||
self.rgb_surface = None # offscreen for rgb_array
|
||||
|
||||
def _ensure_pygame(self):
|
||||
if self._pg is None:
|
||||
self._pg = self._PygameCtx()
|
||||
if not self._pg.initialized:
|
||||
if not pygame.get_init():
|
||||
pygame.init()
|
||||
flags = 0
|
||||
if self.render_mode == "human":
|
||||
self._pg.screen = pygame.display.set_mode((WINDOW_W, WINDOW_H))
|
||||
else:
|
||||
# offscreen surface; we can still blit/draw onto it
|
||||
self._pg.screen = pygame.Surface((WINDOW_W, WINDOW_H))
|
||||
self._pg.clock = pygame.time.Clock()
|
||||
try:
|
||||
pygame.font.init()
|
||||
self._pg.font = pygame.font.SysFont("Arial", 20)
|
||||
except Exception:
|
||||
self._pg.font = None
|
||||
self._pg.initialized = True
|
||||
|
||||
def _world_to_screen(self, x, y, zoom, angle, scroll_x, scroll_y):
|
||||
ca, sa = math.cos(angle), math.sin(angle)
|
||||
# rotate around (scroll_x, scroll_y)
|
||||
rx = (x - scroll_x) * ca + (y - scroll_y) * sa
|
||||
ry = -(x - scroll_x) * sa + (y - scroll_y) * ca
|
||||
# scale & translate (match original camera placement)
|
||||
sx = int(WINDOW_W / 2 + rx * zoom)
|
||||
sy = int(WINDOW_H / 4 + ry * zoom)
|
||||
return sx, sy
|
||||
|
||||
def get_feature_vector(self, lookahead: int = LOOK_AHEAD) -> list[float]:
|
||||
my_s: MyState = self.my_state
|
||||
vec = my_s.as_feature_vector(lookahead).tolist()
|
||||
return vec
|
||||
|
||||
def _draw_polygon_world(self, poly, color, zoom, angle, scroll_x, scroll_y):
|
||||
pts = [self._world_to_screen(px, py, zoom, angle, scroll_x, scroll_y) for (px, py) in poly]
|
||||
pygame.draw.polygon(self._pg.screen, f2c(color), pts)
|
||||
|
||||
def _draw_body(self, body, color=(0.7, 0.7, 0.7), zoom=1.0, angle=0.0, scroll_x=0.0, scroll_y=0.0):
|
||||
# Draw each fixture polygon
|
||||
col = f2c(color)
|
||||
for fixture in body.fixtures:
|
||||
shape = fixture.shape
|
||||
if isinstance(shape, b2PolygonShape):
|
||||
verts = [body.transform * v for v in shape.vertices]
|
||||
pts = [self._world_to_screen(v[0], v[1], zoom, angle, scroll_x, scroll_y) for v in verts]
|
||||
pygame.draw.polygon(self._pg.screen, col, pts, width=0)
|
||||
|
||||
# ------------------------
|
||||
# Track & episode setup
|
||||
# ------------------------
|
||||
def _destroy(self):
|
||||
if not self.road:
|
||||
return
|
||||
# userData lösen, dann Bodies zerstören
|
||||
for t in self.road:
|
||||
try:
|
||||
t.userData = None
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.world.DestroyBody(t)
|
||||
except Exception:
|
||||
pass
|
||||
self.road = []
|
||||
|
||||
if self.car is not None:
|
||||
try:
|
||||
self.car.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self.car = None
|
||||
|
||||
def _create_track(self):
|
||||
CHECKPOINTS = 12
|
||||
checkpoints = []
|
||||
for c in range(CHECKPOINTS):
|
||||
alpha = 2 * math.pi * c / CHECKPOINTS + self.np_random.uniform(0, 2 * math.pi * 1 / CHECKPOINTS)
|
||||
rad = self.np_random.uniform(TRACK_RAD / 3, TRACK_RAD)
|
||||
if c == 0:
|
||||
alpha = 0
|
||||
rad = 1.5 * TRACK_RAD
|
||||
if c == CHECKPOINTS - 1:
|
||||
alpha = 2 * math.pi * c / CHECKPOINTS
|
||||
self.start_alpha = 2 * math.pi * (-0.5) / CHECKPOINTS
|
||||
rad = 1.5 * TRACK_RAD
|
||||
checkpoints.append((alpha, rad * math.cos(alpha), rad * math.sin(alpha)))
|
||||
|
||||
self.road = []
|
||||
|
||||
x, y, beta = 1.5 * TRACK_RAD, 0, 0
|
||||
dest_i = 0
|
||||
laps = 0
|
||||
track = []
|
||||
no_freeze = 2500
|
||||
visited_other_side = False
|
||||
while True:
|
||||
alpha = math.atan2(y, x)
|
||||
if visited_other_side and alpha > 0:
|
||||
laps += 1
|
||||
visited_other_side = False
|
||||
if alpha < 0:
|
||||
visited_other_side = True
|
||||
alpha += 2 * math.pi
|
||||
while True:
|
||||
failed = True
|
||||
while True:
|
||||
dest_alpha, dest_x, dest_y = checkpoints[dest_i % len(checkpoints)]
|
||||
if alpha <= dest_alpha:
|
||||
failed = False
|
||||
break
|
||||
dest_i += 1
|
||||
if dest_i % len(checkpoints) == 0:
|
||||
break
|
||||
if not failed:
|
||||
break
|
||||
alpha -= 2 * math.pi
|
||||
continue
|
||||
r1x = math.cos(beta)
|
||||
r1y = math.sin(beta)
|
||||
p1x = -r1y
|
||||
p1y = r1x
|
||||
dest_dx = dest_x - x
|
||||
dest_dy = dest_y - y
|
||||
proj = r1x * dest_dx + r1y * dest_dy
|
||||
while beta - alpha > 1.5 * math.pi:
|
||||
beta -= 2 * math.pi
|
||||
while beta - alpha < -1.5 * math.pi:
|
||||
beta += 2 * math.pi
|
||||
prev_beta = beta
|
||||
proj *= SCALE
|
||||
if proj > 0.3:
|
||||
beta -= min(TRACK_TURN_RATE, abs(0.001 * proj))
|
||||
if proj < -0.3:
|
||||
beta += min(TRACK_TURN_RATE, abs(0.001 * proj))
|
||||
x += p1x * TRACK_DETAIL_STEP
|
||||
y += p1y * TRACK_DETAIL_STEP
|
||||
track.append((alpha, prev_beta * 0.5 + beta * 0.5, x, y))
|
||||
if laps > 4:
|
||||
break
|
||||
no_freeze -= 1
|
||||
if no_freeze == 0:
|
||||
break
|
||||
|
||||
i1, i2 = -1, -1
|
||||
i = len(track)
|
||||
while True:
|
||||
i -= 1
|
||||
if i == 0:
|
||||
return False
|
||||
pass_through_start = track[i][0] > self.start_alpha >= track[i - 1][0]
|
||||
if pass_through_start and i2 == -1:
|
||||
i2 = i
|
||||
elif pass_through_start and i1 == -1:
|
||||
i1 = i
|
||||
break
|
||||
print(f"Track generation: {i1}..{i2} -> {i2 - i1}-tiles track")
|
||||
assert i1 != -1 and i2 != -1
|
||||
|
||||
track = track[i1: i2 - 1]
|
||||
|
||||
first_beta = track[0][1]
|
||||
first_perp_x = math.cos(first_beta)
|
||||
first_perp_y = math.sin(first_beta)
|
||||
well_glued_together = np.sqrt(
|
||||
np.square(first_perp_x * (track[0][2] - track[-1][2]))
|
||||
+ np.square(first_perp_y * (track[0][3] - track[-1][3]))
|
||||
)
|
||||
if well_glued_together > TRACK_DETAIL_STEP:
|
||||
return False
|
||||
|
||||
border = [False] * len(track)
|
||||
for i in range(len(track)):
|
||||
good = True
|
||||
oneside = 0
|
||||
for neg in range(BORDER_MIN_COUNT):
|
||||
beta1 = track[i - neg - 0][1]
|
||||
beta2 = track[i - neg - 1][1]
|
||||
good &= abs(beta1 - beta2) > TRACK_TURN_RATE * 0.2
|
||||
oneside += np.sign(beta1 - beta2)
|
||||
good &= abs(oneside) == BORDER_MIN_COUNT
|
||||
border[i] = bool(good)
|
||||
for i in range(len(track)):
|
||||
for neg in range(BORDER_MIN_COUNT):
|
||||
border[i - neg] |= border[i]
|
||||
|
||||
self.road_poly = []
|
||||
for i in range(len(track)):
|
||||
alpha1, beta1, x1, y1 = track[i]
|
||||
alpha2, beta2, x2, y2 = track[i - 1]
|
||||
road1_l = (x1 - TRACK_WIDTH * math.cos(beta1), y1 - TRACK_WIDTH * math.sin(beta1))
|
||||
road1_r = (x1 + TRACK_WIDTH * math.cos(beta1), y1 + TRACK_WIDTH * math.sin(beta1))
|
||||
road2_l = (x2 - TRACK_WIDTH * math.cos(beta2), y2 - TRACK_WIDTH * math.sin(beta2))
|
||||
road2_r = (x2 + TRACK_WIDTH * math.cos(beta2), y2 + TRACK_WIDTH * math.sin(beta2))
|
||||
t = self.world.CreateStaticBody(
|
||||
fixtures=b2FixtureDef(shape=b2PolygonShape(vertices=[road1_l, road1_r, road2_r, road2_l]))
|
||||
)
|
||||
t.userData = t
|
||||
t.index_on_track = i
|
||||
c = 0.01 * (i % 3)
|
||||
t.color = [ROAD_COLOR[0] + c, ROAD_COLOR[1] + c, ROAD_COLOR[2] + c]
|
||||
t.road_visited = False
|
||||
t.road_friction = 1.0
|
||||
t.fixtures[0].sensor = True
|
||||
self.road_poly.append(([road1_l, road1_r, road2_r, road2_l], t.color))
|
||||
self.road.append(t)
|
||||
if border[i]:
|
||||
side = np.sign(beta2 - beta1)
|
||||
b1_l = (x1 + side * TRACK_WIDTH * math.cos(beta1), y1 + side * TRACK_WIDTH * math.sin(beta1))
|
||||
b1_r = (
|
||||
x1 + side * (TRACK_WIDTH + BORDER) * math.cos(beta1),
|
||||
y1 + side * (TRACK_WIDTH + BORDER) * math.sin(beta1),
|
||||
)
|
||||
b2_l = (x2 + side * TRACK_WIDTH * math.cos(beta2), y2 + side * TRACK_WIDTH * math.sin(beta2))
|
||||
b2_r = (
|
||||
x2 + side * (TRACK_WIDTH + BORDER) * math.cos(beta2),
|
||||
y2 + side * (TRACK_WIDTH + BORDER) * math.sin(beta2),
|
||||
)
|
||||
self.road_poly.append(([b1_l, b1_r, b2_r, b2_l], (1, 1, 1) if i % 2 == 0 else (1, 0, 0)))
|
||||
self.track = track
|
||||
|
||||
self.original_road_poly = [((list(poly)), list(color)) for (poly, color) in self.road_poly]
|
||||
self.ctrl_pts = np.array(list(map(lambda x: x[2:], self.track)))
|
||||
self.angles = np.array(list(map(lambda x: x[1], self.track)))
|
||||
self.outward_vectors = [np.array([np.cos(theta), np.sin(theta)]) for theta in self.angles]
|
||||
angle_deltas = self.angles - np.roll(self.angles, 1)
|
||||
self.angle_deltas = np.array(list(map(standardize_angle, angle_deltas)))
|
||||
self.indices = np.array(range(len(self.ctrl_pts)))
|
||||
return True
|
||||
|
||||
def _build_new_episode(self):
|
||||
# build track (may retry)
|
||||
self._destroy()
|
||||
self.reward = 0.0
|
||||
self.prev_reward = 0.0
|
||||
self.tile_visited_count = 0
|
||||
self.t = 0.0
|
||||
self.road_poly = []
|
||||
self.human_render = False
|
||||
self.laps = 0
|
||||
self.on_road = True
|
||||
self.next_road_tile = 0
|
||||
|
||||
while True:
|
||||
success = self._create_track()
|
||||
if success:
|
||||
break
|
||||
print("retry to generate track (normal if there are not many of this messages)")
|
||||
|
||||
self.car = Car(self.world, *self.track[0][1:4])
|
||||
|
||||
# attach tiles set to car for contact tracking
|
||||
self.car.tiles = set()
|
||||
|
||||
self.steps = 0
|
||||
self._last_progress_count = 0
|
||||
self._no_progress_steps = 0
|
||||
self._stall_steps = 0
|
||||
|
||||
# ------------------------
|
||||
# Public API (Gym 0.26+/Gymnasium style)
|
||||
# ------------------------
|
||||
def _update_features(self):
|
||||
v1 = self.outward_vectors[self.next_road_tile - 2]
|
||||
v2 = np.array(self.car.hull.position) - self.ctrl_pts[self.next_road_tile - 1]
|
||||
off_center = float(np.dot(v1, v2))
|
||||
angular_vel = float(self.car.hull.angularVelocity)
|
||||
vel = self.car.hull.linearVelocity
|
||||
true_speed = float(np.linalg.norm(vel))
|
||||
car_angle = float(self.car.hull.angle - self.angles[self.next_road_tile])
|
||||
wheel_angle = float(self.car.wheels[0].joint.angle)
|
||||
if true_speed < 0.2:
|
||||
vel_angle = 0.0
|
||||
else:
|
||||
vel_angle = float(math.atan2(vel[1], vel[0]) - (self.angles[self.next_road_tile] + np.pi / 2))
|
||||
|
||||
wheel_angle = standardize_angle(wheel_angle)
|
||||
car_angle = standardize_angle(car_angle)
|
||||
vel_angle = standardize_angle(vel_angle)
|
||||
|
||||
tip = np.array((self.car.wheels[0].position + self.car.wheels[1].position) / 2)
|
||||
p1 = self.ctrl_pts[self.next_road_tile - 1]
|
||||
p2 = self.ctrl_pts[self.next_road_tile - 2]
|
||||
u = (p1 - p2) / TRACK_DETAIL_STEP
|
||||
v = (tip - p2) / TRACK_DETAIL_STEP
|
||||
interp = float(np.dot(v, u))
|
||||
interp_angle_deltas = np.interp(self.indices + interp, self.indices, self.angle_deltas)
|
||||
|
||||
self.my_state.angle_deltas = np.roll(interp_angle_deltas, -self.next_road_tile)
|
||||
self.my_state.reward = self.reward
|
||||
self.my_state.on_road = self.on_road
|
||||
self.my_state.laps = self.laps
|
||||
self.my_state.true_speed = true_speed
|
||||
self.my_state.off_center = off_center
|
||||
self.my_state.wheel_angle = wheel_angle
|
||||
self.my_state.car_angle = car_angle
|
||||
self.my_state.angular_vel = angular_vel
|
||||
self.my_state.vel_angle = vel_angle
|
||||
|
||||
# Normalization
|
||||
self.my_state.angle_deltas *= 2.3
|
||||
self.my_state.true_speed /= 100.0
|
||||
self.my_state.off_center /= TRACK_WIDTH
|
||||
self.my_state.wheel_angle *= 2.1
|
||||
self.my_state.car_angle *= 1.5
|
||||
self.my_state.vel_angle *= 1.5
|
||||
self.my_state.angular_vel /= 3.74
|
||||
|
||||
def reset(self, *, seed: int | None = None, options: dict | None = None):
|
||||
if seed is not None:
|
||||
if seeding is not None:
|
||||
self.np_random, _ = seeding.np_random(seed)
|
||||
else:
|
||||
self.np_random = np.random.RandomState(seed)
|
||||
self._build_new_episode()
|
||||
# Wichtig: initiale Features befüllen
|
||||
self._update_features()
|
||||
obs = self.my_state.as_feature_vector(LOOK_AHEAD).astype(np.float32)
|
||||
info = {}
|
||||
return obs, info
|
||||
|
||||
def fast_reset(self):
|
||||
# keep the same track, respawn car
|
||||
self.car = None
|
||||
self.laps = 0
|
||||
self.on_road = True
|
||||
self.next_road_tile = 0
|
||||
|
||||
self.reward = 0.0
|
||||
self.prev_reward = 0.0
|
||||
self.tile_visited_count = 0
|
||||
self.t = 0.0
|
||||
self.human_render = False
|
||||
for tile in self.road:
|
||||
tile.road_visited = False
|
||||
self.road_poly = [((list(poly)), list(color)) for (poly, color) in self.original_road_poly]
|
||||
try:
|
||||
self.car.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self.car = Car(self.world, *self.track[0][1:4])
|
||||
self.car.tiles = set()
|
||||
|
||||
self.steps = 0
|
||||
self._last_progress_count = 0
|
||||
self._no_progress_steps = 0
|
||||
self._stall_steps = 0
|
||||
|
||||
return self.step(np.array([0.0, 0.0, 0.0], dtype=np.float32))
|
||||
|
||||
def step(self, action):
|
||||
# log.info("got action: {}".format(action))
|
||||
# Expect action: [steer (-1..1), gas (0..1), brake (0..1)]
|
||||
if action is not None:
|
||||
# TODO: this was changed! minus in steer was removed
|
||||
self.car.steer(float(action[0]))
|
||||
self.car.gas(float(action[1]))
|
||||
self.car.brake(float(action[2]))
|
||||
|
||||
self.car.step(1.0 / FPS)
|
||||
self.world.Step(1.0 / FPS, 6 * 30, 2 * 30)
|
||||
self.t += 1.0 / FPS
|
||||
|
||||
self.steps += 1
|
||||
|
||||
terminated = False
|
||||
truncated = False
|
||||
|
||||
if action is not None:
|
||||
# (1) ALLE Reward-Änderungen zuerst einarbeiten
|
||||
self.reward -= 5.0 / FPS # Zeitstrafe
|
||||
|
||||
# Ziel erreicht?
|
||||
if self.tile_visited_count == len(self.track):
|
||||
terminated = True
|
||||
|
||||
# Out-of-bounds: Strafe IN reward addieren (nicht step_reward überschreiben)
|
||||
x, y = self.car.hull.position
|
||||
if abs(x) > PLAYFIELD or abs(y) > PLAYFIELD:
|
||||
self.reward -= 100.0
|
||||
terminated = True
|
||||
|
||||
# Offroad: kontinuierliche Strafe + Grace-Fenster; bei Timeout Zusatzstrafe
|
||||
if not self.on_road:
|
||||
self.offroad_frames += 1
|
||||
self.reward -= self.offroad_penalty_per_frame / FPS
|
||||
if self.offroad_frames > self.offroad_grace_frames:
|
||||
self.reward -= 20.0
|
||||
terminated = True
|
||||
else:
|
||||
self.offroad_frames = 0
|
||||
|
||||
if self.tile_visited_count > self._last_progress_count:
|
||||
self._last_progress_count = self.tile_visited_count
|
||||
self._no_progress_steps = 0
|
||||
else:
|
||||
self._no_progress_steps += 1
|
||||
if self._no_progress_steps >= NO_PROGRESS_STEPS:
|
||||
truncated = True
|
||||
|
||||
# (2) JETZT genau einmal das Delta bilden
|
||||
step_reward = self.reward - self.prev_reward
|
||||
self.prev_reward = self.reward
|
||||
else:
|
||||
step_reward = 0.0
|
||||
|
||||
# --- Feature computation (unverändert) ---
|
||||
self._update_features()
|
||||
|
||||
obs = self.my_state.as_feature_vector(LOOK_AHEAD).astype(np.float32)
|
||||
info = {} # features nicht mehr nötig
|
||||
return obs, step_reward, terminated, truncated, info
|
||||
|
||||
def _get_observation(self):
|
||||
# This env is feature-first; return None unless user asks for rgb_array via render()
|
||||
return None
|
||||
|
||||
# ------------------------
|
||||
# Rendering (pygame)
|
||||
# ------------------------
|
||||
def render(self):
|
||||
self._ensure_pygame()
|
||||
|
||||
# Handle window events only in human mode
|
||||
if self.render_mode == "human":
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
self.close()
|
||||
return None
|
||||
|
||||
# Camera math (match original)
|
||||
zoom = 0.1 * SCALE * max(1 - self.t, 0) + ZOOM * SCALE * min(self.t, 1)
|
||||
scroll_x = self.car.hull.position[0]
|
||||
scroll_y = self.car.hull.position[1]
|
||||
angle = -self.car.hull.angle
|
||||
vel = self.car.hull.linearVelocity
|
||||
if np.linalg.norm(vel) > 0.5:
|
||||
angle = math.atan2(vel[0], vel[1])
|
||||
|
||||
# Draw grass background
|
||||
self._pg.screen.fill((102, 230, 102))
|
||||
# simple grid for texture
|
||||
k = PLAYFIELD / 20.0
|
||||
grid_color = (110, 240, 110)
|
||||
for x in range(-20, 20, 2):
|
||||
for y in range(-20, 20, 2):
|
||||
x0, y0 = k * x + 0, k * y + 0
|
||||
x1, y1 = k * x + k, k * y + k
|
||||
p0 = self._world_to_screen(x0, y0, zoom, angle, scroll_x, scroll_y)
|
||||
p1 = self._world_to_screen(x1, y0, zoom, angle, scroll_x, scroll_y)
|
||||
p2 = self._world_to_screen(x1, y1, zoom, angle, scroll_x, scroll_y)
|
||||
p3 = self._world_to_screen(x0, y1, zoom, angle, scroll_x, scroll_y)
|
||||
pygame.draw.polygon(self._pg.screen, grid_color, [p0, p1, p2, p3])
|
||||
|
||||
# Road polygons
|
||||
for poly, color in self.road_poly:
|
||||
self._draw_polygon_world(poly, color, zoom, angle, scroll_x, scroll_y)
|
||||
|
||||
# Draw car hull + wheels (approx)
|
||||
car_col = (0.25, 0.25, 0.25)
|
||||
self._draw_body(self.car.hull, car_col, zoom, angle, scroll_x, scroll_y)
|
||||
for w in self.car.wheels:
|
||||
self._draw_body(w, (0.15, 0.15, 0.15), zoom, angle, scroll_x, scroll_y)
|
||||
|
||||
# Indicators (speed, wheel, gyro)
|
||||
if self._pg.font is not None:
|
||||
# simple HUD text
|
||||
txt = f"reward={self.reward:0.1f} laps={self.laps}"
|
||||
surf = self._pg.font.render(txt, True, (255, 255, 255))
|
||||
self._pg.screen.blit(surf, (10, 10))
|
||||
|
||||
# Output
|
||||
if self.render_mode == "human":
|
||||
pygame.display.flip()
|
||||
self._pg.clock.tick(FPS)
|
||||
return None
|
||||
else:
|
||||
# Offscreen: return RGB array like gym does
|
||||
arr = pygame.surfarray.array3d(self._pg.screen) # (W,H,3)
|
||||
arr = np.transpose(arr, (1, 0, 2)) # -> (H,W,3)
|
||||
return arr
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self._pg and self._pg.initialized:
|
||||
pygame.display.quit()
|
||||
pygame.quit()
|
||||
except Exception:
|
||||
pass
|
||||
self._pg = None
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Keyboard demo (pygame)
|
||||
# ----------------------------
|
||||
if __name__ == "__main__":
|
||||
import pygame
|
||||
|
||||
pygame.init()
|
||||
env = CarRacing(render_mode="human")
|
||||
|
||||
action = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
running = True
|
||||
|
||||
|
||||
def handle_keys(a):
|
||||
keys = pygame.key.get_pressed()
|
||||
steer = 0.0
|
||||
if keys[pygame.K_LEFT]:
|
||||
steer -= 1.0
|
||||
if keys[pygame.K_RIGHT]:
|
||||
steer += 1.0
|
||||
gas = 1.0 if keys[pygame.K_UP] else 0.0
|
||||
brake = 0.5 if keys[pygame.K_DOWN] else 0.0
|
||||
a[0], a[1], a[2] = steer, gas, brake
|
||||
|
||||
|
||||
# initial reset
|
||||
env.reset()
|
||||
try:
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
|
||||
env.fast_reset()
|
||||
|
||||
handle_keys(action)
|
||||
obs, r, terminated, truncated, info = env.step(action)
|
||||
|
||||
# print every ~200 frames
|
||||
if int(env.t * FPS) % 200 == 0:
|
||||
ms: MyState = info.get("features")
|
||||
if ms is not None:
|
||||
print(
|
||||
f"speed={ms.true_speed:5.2f} off_center={ms.off_center:+.2f} car_ang={ms.car_angle:+.2f} "
|
||||
f"reward={r:+.2f}"
|
||||
f"reward={ms.reward:+.2f}"
|
||||
)
|
||||
|
||||
env.render()
|
||||
if terminated or truncated:
|
||||
env.fast_reset()
|
||||
|
||||
finally:
|
||||
env.close()
|
||||
48
mathema/eval_main.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from mathema.core.population_monitor import init_population
|
||||
from mathema.utils.logging_config import setup_logging
|
||||
|
||||
setup_logging()
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
N_RUNS = 10
|
||||
|
||||
|
||||
async def run_single_car_experiment(run_idx: int):
|
||||
pop_id = f"car_pop_run{run_idx:02d}"
|
||||
|
||||
log.info(f"=== START RUN {run_idx + 1}/{N_RUNS} ({pop_id}) ===")
|
||||
|
||||
monitor = await init_population((
|
||||
pop_id,
|
||||
[{"morphology": "car_racing_features", "neural_afs": ["tanh"]}],
|
||||
"gt",
|
||||
"competition",
|
||||
))
|
||||
|
||||
# 👉 warten, bis der Monitor sich selbst beendet
|
||||
await monitor._stopped_evt.wait()
|
||||
|
||||
# optional: letzte Stats loggen
|
||||
s = monitor.state
|
||||
best = await monitor._best_fitness_in_population(s.population_id)
|
||||
log.info(
|
||||
f"=== END RUN {run_idx + 1}/{N_RUNS} "
|
||||
f"gens={s.pop_gen} best_fitness={best:.6f} evals={s.eval_acc} ==="
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
load_dotenv()
|
||||
|
||||
for i in range(N_RUNS):
|
||||
await run_single_car_experiment(i)
|
||||
|
||||
log.info("=== ALL RUNS FINISHED ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
21
mathema/exoself_test.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import asyncio
|
||||
|
||||
from mathema.core.exoself import Exoself
|
||||
from mathema.genotype.neo4j.genotype import load_genotype_snapshot
|
||||
|
||||
|
||||
async def main():
|
||||
print("i am here!")
|
||||
snapshot = await load_genotype_snapshot("08bf4d92d8c0438295399f8f2a8fef1a")
|
||||
|
||||
print("gathered snapshot")
|
||||
print(snapshot)
|
||||
|
||||
print("------- build exoself ---------")
|
||||
exo = Exoself(snapshot)
|
||||
|
||||
print("-------- building processes ---------")
|
||||
exo.build_pid_map_and_spawn()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
0
mathema/genotype/__init__.py
Normal file
BIN
mathema/genotype/__pycache__/__init__.cpython-312.pyc
Normal file
0
mathema/genotype/neo4j/__init__.py
Normal file
BIN
mathema/genotype/neo4j/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
mathema/genotype/neo4j/__pycache__/genotype.cpython-312.pyc
Normal file
1097
mathema/genotype/neo4j/genotype.py
Normal file
3
mathema/genotype/neo4j/genotype_connector.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
read and write api for exoself
|
||||
"""
|
||||
1176
mathema/genotype/neo4j/genotype_mutator.py
Normal file
683909
mathema/logs/mathema.log
Normal file
17
mathema/main.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import asyncio
|
||||
|
||||
from mathema.genotype.neo4j.genotype import test_add_neuron
|
||||
|
||||
|
||||
async def main():
|
||||
# polis = Polis()
|
||||
# await polis.create()
|
||||
# await polis.start()
|
||||
|
||||
# await polis.stop()
|
||||
# await test_mut_operators()
|
||||
await test_add_neuron()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -1,108 +0,0 @@
|
||||
# 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}
|
||||
"""
|
||||
|
||||
self.bias = 0.0
|
||||
for (inp_id, weights) in input_idps:
|
||||
if inp_id == "bias":
|
||||
self.bias = float(weights[0])
|
||||
else:
|
||||
self.order.append(inp_id)
|
||||
self.inputs[inp_id] = {"weights": list(weights), "got": False, "val": None}
|
||||
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
print(f"Neuron {self.nid}: perturbing {len([w for i in self.order for w in self.inputs[i]['weights']])} weights")
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
49
mathema/population_monitor_test.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# tests/test_population_monitor_integration_lite.py
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
import mathema.core.population_monitor as pm
|
||||
|
||||
|
||||
class FakeExoself:
|
||||
def __init__(self, agent_id, monitor):
|
||||
self.agent_id = agent_id
|
||||
self.monitor = monitor
|
||||
self._task = asyncio.create_task(self._run())
|
||||
|
||||
async def _run(self):
|
||||
try:
|
||||
await asyncio.sleep(0.01)
|
||||
seed = sum(ord(c) for c in str(self.agent_id)) % 1000
|
||||
random.seed(seed)
|
||||
fitness = 0.5 + random.random()
|
||||
await self.monitor.cast(("terminated", self.agent_id, float(fitness), 4, 4, 10))
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def cast(self, msg):
|
||||
if msg and msg[0] == "terminate":
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
|
||||
|
||||
async def fake_exoself_start(agent_id, monitor):
|
||||
return FakeExoself(agent_id, monitor)
|
||||
|
||||
|
||||
pm.EXOSELF_START = fake_exoself_start # 👉 sauberer DI-Hook
|
||||
|
||||
|
||||
async def main():
|
||||
monitor = await pm.init_population(("pop_iso", pm.INIT_CONSTRAINTS, "gt", "competition"))
|
||||
|
||||
G = 3
|
||||
for _ in range(G):
|
||||
await monitor.gen_ended.wait()
|
||||
|
||||
# await monitor.gen_ended.wait() # gezielt auf Generationsende warten
|
||||
await monitor.stop("normal")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
86
mathema/replay.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import numpy as np
|
||||
import pygame
|
||||
|
||||
from mathema.genotype.neo4j.genotype import load_genotype_snapshot, neo4j
|
||||
from viz_replay import build_policy_from_snapshot
|
||||
from mathema.envs.openai_car_racing import CarRacing
|
||||
|
||||
|
||||
async def _best_agent_in_population(population_id: str) -> str:
|
||||
rows = await neo4j.read_all("""
|
||||
MATCH (a:agent {population_id:$pid})
|
||||
WHERE a.fitness IS NOT NULL
|
||||
RETURN a.id AS id, toFloat(a.fitness) AS f
|
||||
ORDER BY f DESC
|
||||
LIMIT 1
|
||||
""", pid=str(population_id))
|
||||
print(rows)
|
||||
if not rows:
|
||||
raise RuntimeError(f"no agents found with fitness in '{population_id}'")
|
||||
return str(rows[0]["id"])
|
||||
|
||||
|
||||
def _post_process_action(y: np.ndarray) -> np.ndarray:
|
||||
y0 = float(y[0]) if y.size >= 1 else 0.0
|
||||
y1 = float(y[1]) if y.size >= 2 else 0.0
|
||||
y2 = float(y[2]) if y.size >= 3 else 0.0
|
||||
|
||||
steer = max(-1.0, min(1.0, y0))
|
||||
gas = max(0.0, min(1.0, 0.5 * (y1 + 1.0)))
|
||||
brake = max(0.0, min(1.0, 0.5 * (y2 + 1.0)))
|
||||
|
||||
return np.array([steer, gas, brake], dtype=np.float32)
|
||||
|
||||
|
||||
async def replay_best(population_id: str, seed: int = 5, lookahead: int = 10):
|
||||
aid = await _best_agent_in_population(population_id)
|
||||
snap = await load_genotype_snapshot(aid)
|
||||
policy, I = build_policy_from_snapshot(snap)
|
||||
|
||||
env = CarRacing(seed_value=seed, render_mode="human")
|
||||
_, _ = env.reset()
|
||||
policy.reset_state()
|
||||
|
||||
_ = env.step(np.array([0.0, 0.0, 0.0], dtype=np.float32))
|
||||
|
||||
frame = 0
|
||||
|
||||
try:
|
||||
while True:
|
||||
feats = np.array(env.get_feature_vector(lookahead), dtype=np.float32)
|
||||
|
||||
if feats.shape[0] < I:
|
||||
x = np.zeros((I,), dtype=np.float32)
|
||||
x[:feats.shape[0]] = feats
|
||||
else:
|
||||
x = feats[:I]
|
||||
|
||||
y = policy.step(x)
|
||||
act = _post_process_action(y)
|
||||
_, r, terminated, truncated, _ = env.step(act)
|
||||
|
||||
if frame % 2 == 0:
|
||||
env.render()
|
||||
|
||||
frame += 1
|
||||
|
||||
if not pygame.display.get_init() or pygame.display.get_surface() is None:
|
||||
break
|
||||
|
||||
if terminated:
|
||||
env.tile_visited_count = 0
|
||||
env.prev_reward = env.reward
|
||||
|
||||
continue
|
||||
|
||||
if truncated:
|
||||
env._no_progress_steps = 0
|
||||
continue
|
||||
finally:
|
||||
env.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(replay_best(population_id="car_pop", seed=1, lookahead=10))
|
||||
1252
mathema/runs/car_pop_run00__5aebb0d3/episodes.csv
Normal file
6
mathema/runs/car_pop_run00__5aebb0d3/progress.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
t_sec,t_norm,pop_gen,best_gen,best_so_far,avg,std
|
||||
149.598299,0.083110,1,31.1378419453,31.1378419453,8.3156990881,10.4468610324
|
||||
218.266750,0.121259,2,107.0880445795,107.0880445795,13.8780192503,32.4749862952
|
||||
388.852873,0.216029,3,709.3696555218,709.3696555218,90.7517477204,210.0076645724
|
||||
474.285299,0.263492,4,708.0363221884,709.3696555218,89.0388449848,209.4723157637
|
||||
570.957412,0.317199,5,829.9438196555,829.9438196555,261.6982674772,318.9569529186
|
||||
|
0
mathema/scape/__init__.py
Normal file
BIN
mathema/scape/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
mathema/scape/__pycache__/car_racing.cpython-312.pyc
Normal file
BIN
mathema/scape/__pycache__/scape.cpython-312.pyc
Normal file
47
mathema/scape/car_racing.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import numpy as np
|
||||
import logging
|
||||
from mathema.actors.actor import Actor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CarRacingScape(Actor):
|
||||
def __init__(self, env, name: str = "CarRacingScape"):
|
||||
super().__init__(name)
|
||||
self.env = env
|
||||
self._stepped = False
|
||||
|
||||
def _get_features(self) -> list[float]:
|
||||
if not self._stepped:
|
||||
_, _, term, trunc, _ = self.env.step(np.array([0.0, 0.0, 0.0], dtype=np.float32))
|
||||
self._stepped = True
|
||||
return self.env.get_feature_vector()
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
msg = await self.inbox.get()
|
||||
tag = msg[0]
|
||||
|
||||
if tag == "sense":
|
||||
_, sid, sensor_pid = msg
|
||||
vec = self._get_features()
|
||||
await sensor_pid.send(("percept", vec))
|
||||
|
||||
elif tag == "action":
|
||||
_, action, actuator_pid = msg
|
||||
_, step_reward, terminated, truncated, _ = self.env.step(np.asarray(action, dtype=np.float32))
|
||||
self._stepped = True
|
||||
|
||||
halt_flag = 1 if (terminated or truncated) else 0
|
||||
await actuator_pid.send(("result", float(step_reward), halt_flag))
|
||||
|
||||
if halt_flag == 1:
|
||||
self.env.fast_reset()
|
||||
self._stepped = False
|
||||
|
||||
elif tag == "terminate":
|
||||
try:
|
||||
self.env.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from actor import Actor
|
||||
from mathema.actors.actor import Actor
|
||||
import math
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XorScape(Actor):
|
||||
@@ -21,11 +23,11 @@ class XorScape(Actor):
|
||||
tag = msg[0]
|
||||
|
||||
if tag == "sense":
|
||||
print("SCAPE: got sensed by sensor...")
|
||||
log.debug("SCAPE: got sensed by sensor...")
|
||||
_, sid, from_pid = msg
|
||||
self.last_actuator = from_pid
|
||||
inputs, correct = self.data[self.index]
|
||||
print("SENSOR input /correct: ", inputs, correct)
|
||||
log.debug("SENSOR input /correct: ", inputs, correct)
|
||||
await from_pid.send(("percept", inputs))
|
||||
|
||||
elif tag == "action":
|
||||
@@ -34,7 +36,7 @@ class XorScape(Actor):
|
||||
step_error = sum((o - c) ** 2 for o, c in zip(output, correct))
|
||||
step_rmse = math.sqrt(step_error)
|
||||
|
||||
print(f"XOR PATTERN: Input={self.data[self.index][0]} → Network={output} → Expected={correct}")
|
||||
log.debug(f"XOR PATTERN: Input={self.data[self.index][0]} → Network={output} → Expected={correct}")
|
||||
|
||||
self.index += 1
|
||||
|
||||
145
mathema/smoke_test_car.py
Normal file
@@ -0,0 +1,145 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
from mathema.actors.actor import Actor
|
||||
from mathema.actors.sensor import Sensor
|
||||
from mathema.actors.actuator import Actuator
|
||||
from mathema.scape.car_racing import CarRacingScape
|
||||
from mathema.envs.openai_car_racing import CarRacing
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("smoke")
|
||||
|
||||
FEATURE_LEN = 10 + 6
|
||||
|
||||
|
||||
class DummyCortex(Actor):
|
||||
"""Minimaler Cortex: zählt Fitness und Episoden. Erwartet Actuator->('sync', aid, fitness, halt_flag)."""
|
||||
|
||||
def __init__(self, stop_after_episodes: int = 3):
|
||||
super().__init__("DummyCortex")
|
||||
self.total_fitness = 0.0
|
||||
self.episodes = 0
|
||||
self.stop_after = int(stop_after_episodes)
|
||||
self.sensors = []
|
||||
self.neurons = []
|
||||
self.actuators = []
|
||||
|
||||
async def run(self):
|
||||
log.info("[Cortex] started. stop_after=%d", self.stop_after)
|
||||
try:
|
||||
while True:
|
||||
msg = await self.inbox.get()
|
||||
tag = msg[0]
|
||||
if tag == "sync":
|
||||
_, aid, fitness_delta, halt_flag = msg
|
||||
self.total_fitness += float(fitness_delta)
|
||||
if halt_flag == 1:
|
||||
self.episodes += 1
|
||||
log.info("[Cortex] EPISODE done: %d cum_fitness=%.3f",
|
||||
self.episodes, self.total_fitness)
|
||||
if self.episodes >= self.stop_after:
|
||||
log.info("[Cortex] stopping smoke test...")
|
||||
|
||||
for a in (self.sensors + self.neurons + self.actuators):
|
||||
try:
|
||||
await a.send(("terminate",))
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
elif tag == "reactivate":
|
||||
|
||||
pass
|
||||
elif tag == "terminate":
|
||||
return
|
||||
finally:
|
||||
log.info("[Cortex] terminated.")
|
||||
|
||||
|
||||
class RelayNeuron(Actor):
|
||||
"""
|
||||
Minimal-Neuron: nimmt Sensor-Features entgegen ("forward", sid, vec)
|
||||
und sendet eine 3-dimensionale Aktor-Action ("forward", nid, [steer,gas,brake]) weiter.
|
||||
"""
|
||||
|
||||
def __init__(self, nid: str, out_actuator: Actuator):
|
||||
super().__init__(f"RelayNeuron-{nid}")
|
||||
self.nid = nid
|
||||
self.out = out_actuator
|
||||
|
||||
async def run(self):
|
||||
try:
|
||||
while True:
|
||||
msg = await self.inbox.get()
|
||||
tag = msg[0]
|
||||
if tag == "forward":
|
||||
_, _sid, features = msg
|
||||
|
||||
action_vec = [0.0, 0.2, -1.0]
|
||||
await self.out.send(("forward", self.nid, action_vec))
|
||||
elif tag == "terminate":
|
||||
return
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
async def main():
|
||||
env = CarRacing(seed_value=5, render_mode=None)
|
||||
scape = CarRacingScape(env)
|
||||
|
||||
cx = DummyCortex(stop_after_episodes=3)
|
||||
|
||||
actuator = Actuator(
|
||||
aid="A1",
|
||||
cx_pid=cx,
|
||||
name="car_ApplyAction",
|
||||
fanin_ids=["N1"],
|
||||
expect_count=1,
|
||||
scape=scape
|
||||
)
|
||||
|
||||
neuron = RelayNeuron("N1", actuator)
|
||||
|
||||
sensor = Sensor(
|
||||
sid="S1",
|
||||
cx_pid=cx,
|
||||
name="car_GetFeatures",
|
||||
vector_length=FEATURE_LEN,
|
||||
fanout_pids=[neuron],
|
||||
scape=scape
|
||||
)
|
||||
|
||||
cx.sensors = [sensor]
|
||||
cx.neurons = [neuron]
|
||||
cx.actuators = [actuator]
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(scape.run(), name="CarScape"),
|
||||
asyncio.create_task(cx.run(), name="Cortex"),
|
||||
asyncio.create_task(sensor.run(), name="Sensor"),
|
||||
asyncio.create_task(neuron.run(), name="Neuron"),
|
||||
asyncio.create_task(actuator.run(), name="Actuator"),
|
||||
]
|
||||
|
||||
steps = 0
|
||||
try:
|
||||
while not tasks[1].done():
|
||||
await sensor.send(("sync",))
|
||||
steps += 1
|
||||
|
||||
await asyncio.sleep(0.0)
|
||||
log.info("[SMOKE] finished after %d steps. ✅", steps)
|
||||
finally:
|
||||
|
||||
try:
|
||||
await scape.send(("terminate",))
|
||||
except Exception:
|
||||
pass
|
||||
for t in tasks:
|
||||
if not t.done():
|
||||
t.cancel()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
19
mathema/stats/car_pop.jsonl
Normal file
@@ -0,0 +1,19 @@
|
||||
{"gen":1,"ts":429466,"cum_fitness":406.1130192502508,"best":213.04574468084934,"avg":40.611301925025074,"std":81.34536458570878,"agents":10,"eval_acc":229,"cycle_acc":112776.0,"time_acc":504.0}
|
||||
{"gen":2,"ts":429516,"cum_fitness":385.63829787233476,"best":194.25435663626644,"avg":38.56382978723347,"std":77.13033019288423,"agents":10,"eval_acc":436,"cycle_acc":187892.0,"time_acc":794.0}
|
||||
{"gen":3,"ts":429604,"cum_fitness":828.4902735562338,"best":310.83920972644523,"avg":75.31729759602125,"std":124.90189226798081,"agents":11,"eval_acc":714,"cycle_acc":291121.0,"time_acc":1305.0}
|
||||
{"gen":4,"ts":429682,"cum_fitness":671.7009118541074,"best":309.0392097264455,"avg":74.63343465045638,"std":126.25620416926591,"agents":9,"eval_acc":976,"cycle_acc":410377.0,"time_acc":1694.0}
|
||||
{"gen":5,"ts":429724,"cum_fitness":893.6996453900745,"best":309.57254305977864,"avg":99.29996059889716,"std":140.72493489483733,"agents":9,"eval_acc":1157,"cycle_acc":487735.0,"time_acc":1904.0}
|
||||
{"gen":6,"ts":429770,"cum_fitness":1133.7375886524387,"best":539.5438196554759,"avg":113.37375886524387,"std":184.31148311830162,"agents":10,"eval_acc":1329,"cycle_acc":574588.0,"time_acc":2210.0}
|
||||
{"gen":7,"ts":429816,"cum_fitness":1744.1695035460898,"best":836.2938196555104,"avg":158.56086395873544,"std":243.32059311203213,"agents":11,"eval_acc":1503,"cycle_acc":670749.0,"time_acc":2500.0}
|
||||
{"gen":8,"ts":429885,"cum_fitness":2040.9778115501408,"best":875.7271529888461,"avg":226.7753123944601,"std":258.41233668126716,"agents":9,"eval_acc":1734,"cycle_acc":788501.0,"time_acc":2949.0}
|
||||
{"gen":9,"ts":429978,"cum_fitness":2287.8132725430496,"best":793.935764944267,"avg":254.20147472700552,"std":319.4685367363856,"agents":9,"eval_acc":1932,"cycle_acc":924645.0,"time_acc":3269.0}
|
||||
{"gen":10,"ts":430023,"cum_fitness":2287.209321175259,"best":868.7271529888463,"avg":254.1343690194732,"std":333.23731351153145,"agents":9,"eval_acc":2075,"cycle_acc":1016506.0,"time_acc":3434.0}
|
||||
{"gen":11,"ts":430064,"cum_fitness":2325.6576494427395,"best":868.9604863221797,"avg":232.56576494427395,"std":327.1874379309061,"agents":10,"eval_acc":2219,"cycle_acc":1111684.0,"time_acc":3659.0}
|
||||
{"gen":12,"ts":430106,"cum_fitness":2326.053140830775,"best":883.6604863221783,"avg":232.60531408307753,"std":334.03967991212033,"agents":10,"eval_acc":2414,"cycle_acc":1202188.0,"time_acc":3894.0}
|
||||
{"gen":13,"ts":430164,"cum_fitness":2811.455116514665,"best":893.8104863221778,"avg":281.1455116514665,"std":374.3884505195095,"agents":10,"eval_acc":2613,"cycle_acc":1310617.0,"time_acc":4269.0}
|
||||
{"gen":14,"ts":430214,"cum_fitness":3671.2328267476837,"best":894.4771529888444,"avg":407.9147585275204,"std":398.5607736543426,"agents":9,"eval_acc":2777,"cycle_acc":1407899.0,"time_acc":4537.0}
|
||||
{"gen":15,"ts":430283,"cum_fitness":3473.175278622045,"best":896.1271529888445,"avg":385.90836429133833,"std":431.5960755241998,"agents":9,"eval_acc":2945,"cycle_acc":1535928.0,"time_acc":4954.0}
|
||||
{"gen":16,"ts":430360,"cum_fitness":3461.7943768996524,"best":896.2271529888443,"avg":314.70857971815025,"std":416.6829267465861,"agents":11,"eval_acc":3130,"cycle_acc":1675309.0,"time_acc":5326.0}
|
||||
{"gen":17,"ts":430416,"cum_fitness":5278.096251266399,"best":898.7938196555115,"avg":439.8413542721999,"std":439.94310781767507,"agents":12,"eval_acc":3309,"cycle_acc":1797053.0,"time_acc":5697.0}
|
||||
{"gen":18,"ts":430538,"cum_fitness":5295.679584599731,"best":899.1938196555117,"avg":441.3066320499776,"std":441.3905509458581,"agents":12,"eval_acc":3611,"cycle_acc":1982624.0,"time_acc":6595.0}
|
||||
{"gen":19,"ts":430661,"cum_fitness":5304.096251266397,"best":900.5771529888449,"avg":442.0080209388664,"std":442.0866607933945,"agents":12,"eval_acc":3858,"cycle_acc":2183705.0,"time_acc":7363.0}
|
||||
BIN
mathema/stats/car_pop_agents.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
mathema/stats/car_pop_avg.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
mathema/stats/car_pop_best.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
mathema/stats/car_pop_cum_fitness.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
mathema/stats/car_pop_cycle_acc.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
mathema/stats/car_pop_eval_acc.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
5
mathema/stats/car_pop_run00.jsonl
Normal file
@@ -0,0 +1,5 @@
|
||||
{"ts":1765629719,"gen":1,"t_sec":357578,"cum_fitness":83.15699088145817,"best":31.13784194528847,"avg":8.315699088145816,"std":10.446861032430222,"agents":10,"eval_acc":273,"cycle_acc":146133.0,"time_acc":843.0}
|
||||
{"ts":1765629719,"gen":2,"t_sec":357647,"cum_fitness":138.78019250253263,"best":107.08804457953383,"avg":13.878019250253264,"std":32.4749862952049,"agents":10,"eval_acc":453,"cycle_acc":231231.0,"time_acc":1253.0}
|
||||
{"ts":1765629719,"gen":3,"t_sec":357818,"cum_fitness":907.5174772036397,"best":709.3696555217725,"avg":90.75174772036397,"std":210.0076645723655,"agents":10,"eval_acc":749,"cycle_acc":389454.0,"time_acc":2210.0}
|
||||
{"ts":1765629719,"gen":4,"t_sec":357903,"cum_fitness":890.3884498480174,"best":708.0363221884387,"avg":89.03884498480174,"std":209.47231576372903,"agents":10,"eval_acc":921,"cycle_acc":493606.0,"time_acc":2788.0}
|
||||
{"ts":1765629719,"gen":5,"t_sec":358000,"cum_fitness":2616.982674771991,"best":829.9438196555033,"avg":261.69826747719907,"std":318.9569529186433,"agents":10,"eval_acc":1135,"cycle_acc":607753.0,"time_acc":3398.0}
|
||||
BIN
mathema/stats/car_pop_run00_agents.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
mathema/stats/car_pop_run00_avg.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
mathema/stats/car_pop_run00_best.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
mathema/stats/car_pop_run00_cum_fitness.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
mathema/stats/car_pop_run00_cycle_acc.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
mathema/stats/car_pop_run00_eval_acc.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
mathema/stats/car_pop_run00_std.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
mathema/stats/car_pop_run00_t_sec.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
mathema/stats/car_pop_run00_time_acc.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
mathema/stats/car_pop_std.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
mathema/stats/car_pop_time_acc.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
45
mathema/stats/xor_pop.jsonl
Normal file
@@ -0,0 +1,45 @@
|
||||
{"gen":1,"ts":321948,"cum_fitness":3.535490412154955,"best":0.707098082430991,"avg":0.35354904121549546,"std":0.35354904121549546,"agents":10,"eval_acc":945,"cycle_acc":3780.0,"time_acc":0.0}
|
||||
{"gen":2,"ts":321949,"cum_fitness":3.5354927525295303,"best":0.7071004228055664,"avg":0.35354927525295304,"std":0.35354927525357277,"agents":10,"eval_acc":1501,"cycle_acc":6004.0,"time_acc":0.0}
|
||||
{"gen":3,"ts":321949,"cum_fitness":29.581795443265467,"best":29.581795443265467,"avg":3.6977244304081833,"std":9.783259259708018,"agents":8,"eval_acc":2169,"cycle_acc":8676.0,"time_acc":0.0}
|
||||
{"gen":4,"ts":321950,"cum_fitness":8.856365418978141,"best":6.735068112430009,"avg":0.8051241289980129,"std":1.9004936297986483,"agents":11,"eval_acc":3099,"cycle_acc":12396.0,"time_acc":0.0}
|
||||
{"gen":5,"ts":321950,"cum_fitness":13.627023608376208,"best":10.091529771445954,"avg":1.1355853006980172,"std":2.7212783264525835,"agents":12,"eval_acc":4248,"cycle_acc":16992.0,"time_acc":0.0}
|
||||
{"gen":6,"ts":321951,"cum_fitness":4.2498434127003515,"best":0.7143496834727742,"avg":0.3541536177250293,"std":0.35415877239773463,"agents":12,"eval_acc":5336,"cycle_acc":21344.0,"time_acc":0.0}
|
||||
{"gen":7,"ts":321951,"cum_fitness":4.5021926658698606,"best":0.9666988224881597,"avg":0.3751827221558217,"std":0.3813687035616592,"agents":12,"eval_acc":6296,"cycle_acc":25184.0,"time_acc":0.0}
|
||||
{"gen":8,"ts":321952,"cum_fitness":4.249843526854475,"best":0.7143496834727742,"avg":0.3541536272378729,"std":0.3541587818779628,"agents":12,"eval_acc":7060,"cycle_acc":28240.0,"time_acc":0.0}
|
||||
{"gen":9,"ts":321953,"cum_fitness":4.2563711757342135,"best":0.7143496834727742,"avg":0.3546975979778511,"std":0.3547050614807084,"agents":12,"eval_acc":7818,"cycle_acc":31272.0,"time_acc":0.0}
|
||||
{"gen":10,"ts":321954,"cum_fitness":4.256374349810292,"best":0.7143496834727742,"avg":0.354697862484191,"std":0.35470532426949464,"agents":12,"eval_acc":8693,"cycle_acc":34772.0,"time_acc":0.0}
|
||||
{"gen":11,"ts":321954,"cum_fitness":4.256374829169983,"best":0.7143496834727742,"avg":0.35469790243083194,"std":0.3547053639569165,"agents":12,"eval_acc":9591,"cycle_acc":38364.0,"time_acc":0.0}
|
||||
{"gen":12,"ts":321955,"cum_fitness":4.2563743097290745,"best":0.7143496834727742,"avg":0.3546978591440895,"std":0.3547053209510696,"agents":12,"eval_acc":10435,"cycle_acc":41740.0,"time_acc":0.0}
|
||||
{"gen":13,"ts":321955,"cum_fitness":4.2563743097290745,"best":0.7143496834727742,"avg":0.3546978591440895,"std":0.3547053209510696,"agents":12,"eval_acc":11309,"cycle_acc":45236.0,"time_acc":0.0}
|
||||
{"gen":14,"ts":321956,"cum_fitness":4.2563743097290745,"best":0.7143496834727742,"avg":0.3546978591440895,"std":0.3547053209510696,"agents":12,"eval_acc":12086,"cycle_acc":48344.0,"time_acc":0.0}
|
||||
{"gen":15,"ts":321956,"cum_fitness":9.553563169632652,"best":6.010811062944182,"avg":0.8685057426938774,"std":1.6608537151826483,"agents":11,"eval_acc":13036,"cycle_acc":52144.0,"time_acc":0.0}
|
||||
{"gen":16,"ts":321957,"cum_fitness":12.924619966862355,"best":9.381867967221527,"avg":1.0770516639051964,"std":2.5266780480098343,"agents":12,"eval_acc":13983,"cycle_acc":55932.0,"time_acc":0.0}
|
||||
{"gen":17,"ts":321958,"cum_fitness":13.727800067791875,"best":10.185044594698395,"avg":1.1439833389826564,"std":2.7468318741100606,"agents":12,"eval_acc":14954,"cycle_acc":59816.0,"time_acc":0.0}
|
||||
{"gen":18,"ts":321958,"cum_fitness":91.18998836460624,"best":65.95994946269528,"avg":9.118998836460623,"std":19.619481446836627,"agents":10,"eval_acc":16167,"cycle_acc":64668.0,"time_acc":0.0}
|
||||
{"gen":19,"ts":321959,"cum_fitness":90.0204406554163,"best":37.448365587326506,"avg":10.002271183935143,"std":13.636650609486347,"agents":9,"eval_acc":17016,"cycle_acc":68064.0,"time_acc":0.0}
|
||||
{"gen":20,"ts":321959,"cum_fitness":74.49198487354458,"best":30.40442306135378,"avg":8.27688720817162,"std":10.494831537063169,"agents":9,"eval_acc":17839,"cycle_acc":71356.0,"time_acc":0.0}
|
||||
{"gen":21,"ts":321960,"cum_fitness":80.70295993186457,"best":46.691543683144985,"avg":8.070295993186457,"std":14.146068126892585,"agents":10,"eval_acc":18627,"cycle_acc":74508.0,"time_acc":0.0}
|
||||
{"gen":22,"ts":321961,"cum_fitness":17.09561480298055,"best":10.185044594698395,"avg":1.8995127558867277,"std":3.450697192453181,"agents":9,"eval_acc":19628,"cycle_acc":78512.0,"time_acc":0.0}
|
||||
{"gen":23,"ts":321962,"cum_fitness":16.09247312003576,"best":10.185044594698395,"avg":1.7880525688928621,"std":3.493782688590394,"agents":9,"eval_acc":20543,"cycle_acc":82172.0,"time_acc":0.0}
|
||||
{"gen":24,"ts":321963,"cum_fitness":16.09247312003576,"best":10.185044594698395,"avg":1.7880525688928621,"std":3.493782688590394,"agents":9,"eval_acc":21338,"cycle_acc":85352.0,"time_acc":0.0}
|
||||
{"gen":25,"ts":321963,"cum_fitness":19.678147930304753,"best":10.185044594698395,"avg":2.1864608811449724,"std":3.471541578158789,"agents":9,"eval_acc":22280,"cycle_acc":89120.0,"time_acc":0.0}
|
||||
{"gen":26,"ts":321964,"cum_fitness":54.45134956146205,"best":38.3588764414263,"avg":6.0501499512735615,"std":11.928478608078674,"agents":9,"eval_acc":23169,"cycle_acc":92676.0,"time_acc":0.0}
|
||||
{"gen":27,"ts":321964,"cum_fitness":16.09247312003576,"best":10.185044594698395,"avg":1.7880525688928621,"std":3.493782688590394,"agents":9,"eval_acc":24074,"cycle_acc":96296.0,"time_acc":0.0}
|
||||
{"gen":28,"ts":321965,"cum_fitness":22.574146861817873,"best":10.185044594698395,"avg":2.257414686181787,"std":3.601190483978599,"agents":10,"eval_acc":25061,"cycle_acc":100244.0,"time_acc":0.0}
|
||||
{"gen":29,"ts":321965,"cum_fitness":30.36345014794506,"best":14.270977027909302,"avg":3.036345014794506,"std":5.000997100783054,"agents":10,"eval_acc":26258,"cycle_acc":105032.0,"time_acc":0.0}
|
||||
{"gen":30,"ts":321966,"cum_fitness":28.898113108697004,"best":12.805639988661246,"avg":3.2109014565218894,"std":4.828483396882332,"agents":9,"eval_acc":27117,"cycle_acc":108468.0,"time_acc":0.0}
|
||||
{"gen":31,"ts":321966,"cum_fitness":34.70951927056421,"best":12.805639988661246,"avg":3.470951927056421,"std":4.646661484076346,"agents":10,"eval_acc":28082,"cycle_acc":112328.0,"time_acc":0.0}
|
||||
{"gen":32,"ts":321967,"cum_fitness":42.1733282727597,"best":15.54737562468631,"avg":4.217332827275969,"std":5.5986512947356095,"agents":10,"eval_acc":28894,"cycle_acc":115576.0,"time_acc":0.0}
|
||||
{"gen":33,"ts":321967,"cum_fitness":40.09475832177063,"best":13.46880567369724,"avg":4.0094758321770625,"std":5.198430512503286,"agents":10,"eval_acc":29751,"cycle_acc":119004.0,"time_acc":0.0}
|
||||
{"gen":34,"ts":321968,"cum_fitness":55.10813373274723,"best":18.841892708849972,"avg":6.123125970305248,"std":7.257208686081356,"agents":9,"eval_acc":30902,"cycle_acc":123608.0,"time_acc":0.0}
|
||||
{"gen":35,"ts":321969,"cum_fitness":42.17766903843793,"best":15.547716922437663,"avg":4.217766903843793,"std":5.598841221465249,"agents":10,"eval_acc":31802,"cycle_acc":127208.0,"time_acc":0.0}
|
||||
{"gen":36,"ts":321970,"cum_fitness":47.32475411472398,"best":15.547716908753264,"avg":4.732475411472398,"std":5.421208869609718,"agents":10,"eval_acc":32595,"cycle_acc":130380.0,"time_acc":0.0}
|
||||
{"gen":37,"ts":321970,"cum_fitness":77.75220612360023,"best":45.90557988681607,"avg":7.775220612360023,"std":13.340276246021885,"agents":10,"eval_acc":33459,"cycle_acc":133836.0,"time_acc":0.0}
|
||||
{"gen":38,"ts":321971,"cum_fitness":96.42953649904732,"best":64.58291026226316,"avg":9.642953649904731,"std":18.75615829845497,"agents":10,"eval_acc":34390,"cycle_acc":137560.0,"time_acc":0.0}
|
||||
{"gen":39,"ts":321971,"cum_fitness":58.00238635155899,"best":21.858327395227583,"avg":6.444709594617666,"std":7.0718675121605665,"agents":9,"eval_acc":35288,"cycle_acc":141152.0,"time_acc":0.0}
|
||||
{"gen":40,"ts":321972,"cum_fitness":85.27003380363533,"best":55.124247641710866,"avg":8.527003380363533,"std":16.171417394577574,"agents":10,"eval_acc":36020,"cycle_acc":144080.0,"time_acc":0.0}
|
||||
{"gen":41,"ts":321972,"cum_fitness":101.3393550677986,"best":71.19356890587414,"avg":9.212668642527147,"std":20.087266316539765,"agents":11,"eval_acc":37192,"cycle_acc":148768.0,"time_acc":0.0}
|
||||
{"gen":42,"ts":321973,"cum_fitness":30.145786161924455,"best":10.533479528037628,"avg":3.0145786161924457,"std":4.611786547392002,"agents":10,"eval_acc":38259,"cycle_acc":153036.0,"time_acc":0.0}
|
||||
{"gen":43,"ts":321973,"cum_fitness":35.8846428651949,"best":10.533479528037628,"avg":3.58846428651949,"std":4.557700196628995,"agents":10,"eval_acc":39190,"cycle_acc":156760.0,"time_acc":0.0}
|
||||
{"gen":44,"ts":321974,"cum_fitness":117.94828167231363,"best":82.06363880711874,"avg":11.794828167231362,"std":23.832243221106737,"agents":10,"eval_acc":40117,"cycle_acc":160468.0,"time_acc":0.0}
|
||||
{"gen":45,"ts":321975,"cum_fitness":193.04603572221515,"best":173.43372908832833,"avg":19.304603572221517,"std":51.52202402626367,"agents":10,"eval_acc":41255,"cycle_acc":165020.0,"time_acc":0.0}
|
||||
BIN
mathema/stats/xor_pop_agents.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
mathema/stats/xor_pop_avg.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
mathema/stats/xor_pop_best.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
mathema/stats/xor_pop_cum_fitness.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
mathema/stats/xor_pop_cycle_acc.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
mathema/stats/xor_pop_eval_acc.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
mathema/stats/xor_pop_std.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
mathema/stats/xor_pop_time_acc.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
142
mathema/test_recurrence.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import asyncio
|
||||
from mathema.actors.neuron import Neuron
|
||||
from mathema.actors.actor import Actor
|
||||
|
||||
|
||||
class Collector(Actor):
|
||||
def __init__(self, name="Collector"):
|
||||
super().__init__(name)
|
||||
self.events = []
|
||||
self._stop = asyncio.Event()
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
msg = await self.inbox.get()
|
||||
tag = msg[0]
|
||||
if tag == "forward":
|
||||
_, from_id, vec = msg
|
||||
self.events.append((from_id, vec))
|
||||
elif tag == "terminate":
|
||||
self._stop.set()
|
||||
return
|
||||
|
||||
|
||||
async def start(actor):
|
||||
return asyncio.create_task(actor.run())
|
||||
|
||||
|
||||
async def test_feedforward():
|
||||
col = Collector("COL-ff")
|
||||
N = Neuron(nid="N", cx_pid=None, af_name="tanh",
|
||||
input_idps=[("S", [1.0], False), ("bias", [0.0], False)],
|
||||
output_pids=[col], bias=None)
|
||||
|
||||
tN = await start(N)
|
||||
tC = await start(col)
|
||||
|
||||
await N.send(("cycle_start",))
|
||||
|
||||
await N.send(("forward", "S", [1.0]))
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
print("FF events:", col.events)
|
||||
|
||||
await col.send(("terminate",))
|
||||
await N.send(("terminate",))
|
||||
await asyncio.gather(tN, tC)
|
||||
|
||||
|
||||
async def test_lateral_nonrecurrent():
|
||||
col = Collector("COL-lat")
|
||||
N1 = Neuron("N1", None, "tanh",
|
||||
[("S", [1.0], False), ("bias", [0.0], False)], [], None)
|
||||
N2 = Neuron("N2", None, "tanh",
|
||||
[("S", [1.0], False), ("N1", [1.0], False), ("bias", [0.0], False)], [col], None)
|
||||
|
||||
N1.outputs = [N2]
|
||||
|
||||
t1 = await start(N1)
|
||||
t2 = await start(N2)
|
||||
tC = await start(col)
|
||||
|
||||
await N1.send(("cycle_start",))
|
||||
await N2.send(("cycle_start",))
|
||||
|
||||
await N1.send(("forward", "S", [1.0]))
|
||||
await N2.send(("forward", "S", [1.0]))
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
print("LAT events:", col.events)
|
||||
|
||||
await col.send(("terminate",))
|
||||
await N1.send(("terminate",))
|
||||
await N2.send(("terminate",))
|
||||
await asyncio.gather(t1, t2, tC)
|
||||
|
||||
|
||||
async def test_recurrent_edge():
|
||||
col = Collector("COL-rec")
|
||||
N1 = Neuron("N1", None, "tanh",
|
||||
[("S", [1.0], False), ("bias", [0.0], False)], [], None)
|
||||
N2 = Neuron("N2", None, "tanh",
|
||||
[("S", [1.0], False), ("N1", [1.0], True), ("bias", [0.0], False)], [col], None)
|
||||
|
||||
N1.outputs = [N2]
|
||||
|
||||
t1 = await start(N1)
|
||||
t2 = await start(N2)
|
||||
tC = await start(col)
|
||||
|
||||
await N1.send(("cycle_start",))
|
||||
await N2.send(("cycle_start",))
|
||||
|
||||
await N1.send(("forward", "S", [1.0]))
|
||||
await N2.send(("forward", "S", [1.0]))
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
await N1.send(("cycle_start",))
|
||||
await N2.send(("cycle_start",))
|
||||
await N1.send(("forward", "S", [1.0]))
|
||||
await N2.send(("forward", "S", [1.0]))
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
print("REC events:", col.events)
|
||||
|
||||
await col.send(("terminate",))
|
||||
await N1.send(("terminate",))
|
||||
await N2.send(("terminate",))
|
||||
await asyncio.gather(t1, t2, tC)
|
||||
|
||||
|
||||
async def test_self_loop():
|
||||
col = Collector("COL-self")
|
||||
N = Neuron("N", None, "tanh",
|
||||
[("S", [1.0], False), ("N", [1.0], True), ("bias", [0.0], False)], [], None)
|
||||
|
||||
N.outputs = [N, col]
|
||||
|
||||
tN = await start(N)
|
||||
tC = await start(col)
|
||||
|
||||
await N.send(("cycle_start",))
|
||||
await N.send(("forward", "S", [1.0]))
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
await N.send(("cycle_start",))
|
||||
await N.send(("forward", "S", [1.0]))
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
print("SELF events:", col.events)
|
||||
|
||||
await col.send(("terminate",))
|
||||
await N.send(("terminate",))
|
||||
await asyncio.gather(tN, tC)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_feedforward())
|
||||
asyncio.run(test_lateral_nonrecurrent())
|
||||
asyncio.run(test_recurrent_edge())
|
||||
asyncio.run(test_self_loop())
|
||||