last workig state.

This commit is contained in:
2025-12-13 14:12:35 +01:00
parent 1761de8acb
commit 841bc7c805
227 changed files with 694550 additions and 251 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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":

View File

@@ -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
View 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)

View File

@@ -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

View File

@@ -1,6 +1,5 @@
# genotype.py
import json
import math
import random
import time
from typing import Dict, List, Tuple, Any, Optional

View 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)

View 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
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

66
mathema/core/db.py Normal file
View 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")

View File

@@ -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:

View File

@@ -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
View 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()

View 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))

View File

@@ -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
View File

Binary file not shown.

View 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()

View 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
View 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
View 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())

View File

Binary file not shown.

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
"""
read and write api for exoself
"""

File diff suppressed because it is too large Load Diff

683909
mathema/logs/mathema.log Normal file

File diff suppressed because it is too large Load Diff

17
mathema/main.py Normal file
View 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())

View File

@@ -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)

View 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
View 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))

File diff suppressed because it is too large Load Diff

View 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
1 t_sec t_norm pop_gen best_gen best_so_far avg std
2 149.598299 0.083110 1 31.1378419453 31.1378419453 8.3156990881 10.4468610324
3 218.266750 0.121259 2 107.0880445795 107.0880445795 13.8780192503 32.4749862952
4 388.852873 0.216029 3 709.3696555218 709.3696555218 90.7517477204 210.0076645724
5 474.285299 0.263492 4 708.0363221884 709.3696555218 89.0388449848 209.4723157637
6 570.957412 0.317199 5 829.9438196555 829.9438196555 261.6982674772 318.9569529186

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View File

@@ -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
View 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())

View 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

142
mathema/test_recurrence.py Normal file
View 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())

View File

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,97 @@
import json
import logging
import logging.config
import os
from pathlib import Path
def _as_bool(val: str | None, default: bool = False) -> bool:
if val is None:
return default
return val.strip().lower() in ("1", "true", "yes", "on")
def _level_from_env() -> int:
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
return getattr(logging, level_name, logging.INFO)
def setup_logging() -> None:
level = _level_from_env()
to_file = _as_bool(os.getenv("LOG_TO_FILE"), True)
log_file = os.getenv("LOG_FILE", "logs/mathema.log")
as_json = _as_bool(os.getenv("LOG_JSON"), False)
if to_file:
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
if as_json:
fmt = '%(message)s'
else:
fmt = ("%(asctime)s | %(levelname)-8s | %(name)s | "
"task=%(taskName)s | %(message)s")
console_handler = {
"class": "logging.StreamHandler",
"level": level,
"formatter": "json" if as_json else "default",
}
file_handler = {
"class": "logging.FileHandler",
"level": level,
"filename": log_file,
"encoding": "utf-8",
"formatter": "json" if as_json else "default",
}
handlers = {"console": console_handler}
if to_file:
handlers["file"] = file_handler
formatters = {
"default": {
"format": fmt,
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"json": {
"()": "logging.Formatter",
"format": "%(message)s",
},
}
config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": formatters,
"handlers": handlers,
"root": {
"level": level,
"handlers": list(handlers.keys()),
},
"loggers": {
"mathema": {"level": level, "propagate": True},
"asyncio": {"level": "WARNING"},
"gymnasium": {"level": "WARNING"},
"pygame": {"level": "WARNING"},
"neo4j": {"level": "WARNING"},
},
}
logging.config.dictConfig(config)
if as_json:
class JsonFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
payload = {
"ts": record.created,
"level": record.levelname,
"logger": record.name,
"task": getattr(record, "taskName", None),
"msg": record.getMessage(),
}
record.msg = json.dumps(payload, ensure_ascii=False)
return True
for h in logging.getLogger().handlers:
h.addFilter(JsonFilter())

93
mathema/utils/stats.py Normal file
View File

@@ -0,0 +1,93 @@
from __future__ import annotations
import atexit, json, os, time, math
from typing import Dict, List, Callable, Iterable, Any, Optional
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
_FLUSHED: set[str] = set()
def ensure_dir() -> None:
os.makedirs("stats", exist_ok=True)
def save_series(population_id: str, rows: Iterable[Dict[str, Any]]) -> None:
ensure_dir()
pid = str(population_id)
rows = list(rows)
if not rows:
return
jsonl_path = os.path.join("stats", f"{pid}.jsonl")
tmp_path = jsonl_path + ".tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
for r in rows:
if "ts" not in r:
rr = {"ts": int(time.time())} | r
else:
rr = r
f.write(json.dumps(rr, separators=(",", ":"), ensure_ascii=False) + "\n")
os.replace(tmp_path, jsonl_path)
_plot_from_jsonl(jsonl_path, pid)
def register_atexit(population_id: str, rows_provider: Callable[[], Iterable[Dict[str, Any]]]) -> None:
pid = str(population_id)
def _flush_once() -> None:
if pid in _FLUSHED:
return
try:
save_series(pid, rows_provider())
finally:
_FLUSHED.add(pid)
atexit.register(_flush_once)
def _plot_from_jsonl(src_path: str, pid: str) -> None:
gens: List[int] = []
series: dict[str, list[float]] = {}
with open(src_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
g = int(obj.get("gen", 0))
gens.append(g)
for k, v in obj.items():
if k in ("gen", "ts"):
continue
try:
series.setdefault(k, []).append(float(v))
except Exception:
pass
if not gens or not series:
return
for metric, ys in series.items():
if len(ys) != len(gens):
L = min(len(ys), len(gens))
ys = ys[:L]
x = gens[:L]
else:
x = gens
plt.figure()
plt.title(f"{metric} over generations")
plt.xlabel("generation")
plt.ylabel(metric)
plt.plot(x, ys, marker="o")
out_png = os.path.join("stats", f"{pid}_{metric}.png")
plt.tight_layout()
plt.savefig(out_png, dpi=120)
plt.close()

120
mathema/viz_replay.py Normal file
View File

@@ -0,0 +1,120 @@
import numpy as np
from typing import Dict, Any, List, Tuple
from mathema.genotype.neo4j.genotype import load_genotype_snapshot, neo4j
from mathema.envs.openai_car_racing import CarRacing
def af_tanh(x): return np.tanh(x)
def af_cos(x): return np.cos(x)
def af_gauss(x): return np.exp(-np.square(x))
def af_abs(x): return np.abs(x)
AF_MAP = {
"tanh": af_tanh,
"cos": af_cos,
"gauss": af_gauss,
"abs": af_abs,
}
class DXNNPolicy:
def __init__(self, W_in, W_ff, W_rec, b_h, af_idx, af_funcs, out_index):
self.W_in, self.W_ff, self.W_rec = W_in, W_ff, W_rec
self.b_h = b_h
self.af_idx = np.array(af_idx, dtype=np.int32)
self.af_funcs = af_funcs
self.out_index = list(out_index)
H = W_ff.shape[0]
self.h = np.zeros((H,), dtype=np.float32)
self.h_prev = np.zeros_like(self.h)
def reset_state(self):
self.h.fill(0.0)
self.h_prev.fill(0.0)
def step(self, x_t: np.ndarray) -> np.ndarray:
z = self.b_h + self.W_in @ x_t + self.W_ff @ self.h + self.W_rec @ self.h_prev
h_new = np.empty_like(self.h)
for j, afi in enumerate(self.af_idx):
h_new[j] = self.af_funcs[afi](z[j])
y = np.array([h_new[j] for j in self.out_index], dtype=np.float32)
self.h_prev, self.h = self.h, h_new
return y
def _build_sensor_index(sensors: List[Dict[str, Any]]) -> Tuple[Dict[str, Tuple[int, int]], int]:
offset = 0
idx = {}
for s in sensors:
L = int(s["vector_length"])
idx[str(s["id"])] = (offset, L)
offset += L
return idx, offset
def build_policy_from_snapshot(snap: Dict[str, Any]) -> Tuple[DXNNPolicy, int]:
sensors = snap["sensors"]
neurons = snap["neurons"]
actuators = snap["actuators"]
s_idx, I = _build_sensor_index(sensors)
H = len(neurons)
nid2ix = {n["id"]: i for i, n in enumerate(neurons)}
W_in = np.zeros((H, I), dtype=np.float32)
W_ff = np.zeros((H, H), dtype=np.float32)
W_rec = np.zeros((H, H), dtype=np.float32)
b_h = np.zeros((H,), dtype=np.float32)
af_names = []
for j, n in enumerate(neurons):
b = n.get("bias")
b_h[j] = (float(b) if b is not None else 0.0)
af_names.append((n.get("activation_function") or "tanh").lower())
for inp in n.get("input_weights", []):
src = inp["input_id"]
ws = [float(x) for x in (inp.get("weights") or [])]
if src in s_idx:
off, L = s_idx[src]
if len(ws) != L:
if len(ws) > L:
ws = ws[:L]
else:
ws = ws + [0.0] * (L - len(ws))
W_in[j, off:off + L] += np.asarray(ws, dtype=np.float32)
elif src in nid2ix:
i = nid2ix[src]
w = float(ws[0]) if ws else 0.0
if bool(inp.get("recurrent", False)):
W_rec[j, i] += w
else:
W_ff[j, i] += w
else:
pass
af_funcs = [af_tanh, af_cos, af_gauss, af_abs]
af_name2idx = {"tanh": 0, "cos": 1, "gauss": 2, "abs": 3}
af_idx = [af_name2idx.get(nm, 0) for nm in af_names]
out_index = []
for a in actuators:
for nid in a.get("fanin_ids", []):
if nid in nid2ix:
out_index.append(nid2ix[nid])
policy = DXNNPolicy(W_in, W_ff, W_rec, b_h, af_idx, af_funcs, out_index)
return policy, I

34
mathema/xor_main.py Normal file
View File

@@ -0,0 +1,34 @@
import asyncio
from dotenv import load_dotenv
from mathema.core.population_monitor import init_population, continue_
from mathema.utils.logging_config import setup_logging
setup_logging()
import logging
log = logging.getLogger(__name__)
async def run_xor_test(
pop_id: str = "xor_pop",
gens: int = 1000,
):
monitor = await init_population((
pop_id,
[{"morphology": "xor_mimic", "neural_afs": ["tanh"]}],
"gt",
"competition",
))
for _ in range(gens):
await monitor.gen_ended.wait()
s = monitor.state
await monitor._best_fitness_in_population(s.population_id)
await monitor.stop("normal")
if __name__ == "__main__":
asyncio.run(run_xor_test())