last changes
9
.gitignore
vendored
@@ -1,2 +1,9 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.idea/
|
||||
.venv/
|
||||
|
||||
mathema/runs/
|
||||
|
||||
sac/tb_*/
|
||||
sac/td*/
|
||||
neo4j_db/
|
||||
BIN
Archiv.zip
Normal file
107
README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Neuroevolution
|
||||
|
||||
`mathema` ist ein experimentelles Neuroevolutions-Framework in Python, das agentenbasierte Architekturen mit evolutiven Mechanismen kombiniert und sich dabei
|
||||
architektonisch am DXNN-System von Gene Sher orientiert.
|
||||
Das Projekt ist modular aufgebaut und erlaubt die Evaluation evolvierender Agenten in verschiedenen Scapes (einer angepassten CarRacing-Umgebung).
|
||||
|
||||
Der Fokus liegt auf:
|
||||
- evolutionären Lernprozessen
|
||||
- populationsbasierten Trainingsläufen
|
||||
- reproduzierbarer Evaluation
|
||||
- klarer Trennung von Genotyp, Phänotyp und Umgebung
|
||||
|
||||
---
|
||||
|
||||
## Projektüberblick
|
||||
|
||||
Das Framework besteht aus mehreren logisch getrennten Komponenten:
|
||||
|
||||
- **core/**
|
||||
Zentrale Evolutionslogik (Agenten, Genotypen, Mutationen, Selektion, Populationen)
|
||||
|
||||
- **scape/**
|
||||
Umgebungen, in denen Agenten agieren (z. B. CarRacing)
|
||||
|
||||
- **eval / main-Skripte**
|
||||
Training, Evaluation, Vergleich mehrerer Runs
|
||||
|
||||
- **utils/**
|
||||
Hilfsfunktionen (Logging, Konfiguration, Seed-Handling, I/O)
|
||||
|
||||
- **archive/**
|
||||
Ältere oder experimentelle Implementationen (nicht aktiv genutzt)
|
||||
|
||||
---
|
||||
|
||||
## Verzeichnisstruktur
|
||||
mathema/
|
||||
├── core/ # Evolutionskern (Agent, Genotyp, Population, Mutation)
|
||||
├── scape/ # Umgebungen / Aufgabenräume
|
||||
├── utils/ # Hilfsfunktionen
|
||||
├── archive/ # Alte / experimentelle Module
|
||||
├── eval_main.py # Evaluations- & Benchmark-Skript
|
||||
├── car_racing_main.py # Einstiegspunkt für CarRacing-Experimente
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Zentrale Konzepte
|
||||
|
||||
### Agent
|
||||
Ein Agent repräsentiert eine evolvierbare Einheit, die:
|
||||
- einen Genotyp besitzt
|
||||
- daraus einen Phänotyp (z. B. neuronales Netz) ableitet
|
||||
- in einer Scape Aktionen ausführt
|
||||
|
||||
### Genotyp / Phänotyp
|
||||
- Der Genotyp beschreibt die Struktur (z. B. Neuronen, Kanten, Parameter)
|
||||
- Der Phänotyp ist die ausführbare Repräsentation (z. B. Policy / Controller)
|
||||
|
||||
### Neuroevolution
|
||||
- Populationen mehrerer Agenten
|
||||
- Mutation (Topologie & Parameter)
|
||||
- Selektion auf Basis von Fitness
|
||||
- optional populationsbasierte Strategien
|
||||
|
||||
### Scapes
|
||||
Eine Scape definiert:
|
||||
- Zustandsraum
|
||||
- Aktionsraum
|
||||
- Reward-/Fitness-Berechnung
|
||||
- Episodenlogik
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Virtuelle Umgebung und Neo4j DB
|
||||
|
||||
```bash
|
||||
docker compose up -d # starte neo4j db
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
Requirements installieren:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
### CarRacing-Experiment starten
|
||||
|
||||
```bash
|
||||
python mathema/car_racing_main.py
|
||||
```
|
||||
startet eine Evolutionsrunde mit Agenten in der CarRacing-Umgebung.
|
||||
|
||||
### Mehrere Runs (Thesis-Tests)
|
||||
```bash
|
||||
python mathema/eval_main.py
|
||||
````
|
||||
Führt mehrere unabhängige Läufe aus und aggregiert Fitness-Statistiken.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import asyncio
|
||||
|
||||
|
||||
class Actor:
|
||||
"""
|
||||
actor base class.
|
||||
"""
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.inbox = asyncio.Queue()
|
||||
|
||||
@@ -7,6 +7,33 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Actuator(Actor):
|
||||
"""
|
||||
Actuator actor responsible for collecting outputs from upstream neurons
|
||||
(fanin), assembling them into an action/output vector, interacting with
|
||||
a scape (environment), and synchronizing the result back to the cortex.
|
||||
|
||||
Conceptually, an Actuator represents the *output layer* of a cortex/agent:
|
||||
- It waits for `forward` messages from all expected fanin sources.
|
||||
- Once all signals are received, they are concatenated in the order
|
||||
defined by `fanin_ids` into a flat output vector.
|
||||
- Depending on `aname`, the output is:
|
||||
* used for debugging/testing ("pts"),
|
||||
* sent directly as an action to a scape ("xor_SendOutput"),
|
||||
* mapped to a car control action and sent to a CarRacing scape
|
||||
("car_ApplyAction"),
|
||||
* or ignored with a default fitness.
|
||||
- After the interaction, the actuator reports the resulting fitness
|
||||
and halt flag back to the cortex via a `"sync"` message.
|
||||
|
||||
Inbox message protocol:
|
||||
- ("forward", from_id, vec):
|
||||
`from_id` is the ID of the sending fanin neuron,
|
||||
`vec` is its output vector.
|
||||
- ("result", fitness, halt_flag):
|
||||
Response from the scape after an action was applied.
|
||||
- ("terminate",):
|
||||
Terminates the actor.
|
||||
"""
|
||||
def __init__(self, aid, cx_pid, name, fanin_ids, expect_count, scape=None):
|
||||
super().__init__(f"Actuator-{aid}")
|
||||
self.aid = aid
|
||||
|
||||
@@ -6,6 +6,41 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Cortex(Actor):
|
||||
"""
|
||||
Cortex actor coordinating a network of Sensors, Neurons, and Actuators.
|
||||
|
||||
The Cortex is responsible for driving the network forward in discrete
|
||||
computation cycles, collecting fitness feedback from all actuators, and
|
||||
reporting evaluation results to an Exoself (supervisor) actor.
|
||||
|
||||
High-level behavior:
|
||||
- At the start of an episode, the cortex triggers a new cycle:
|
||||
1) It tells all neurons to prepare recurrent state for the new cycle
|
||||
via ("cycle_start",).
|
||||
2) It optionally triggers neurons via ("tick",) (scheduler hook).
|
||||
3) It tells all sensors to produce outputs via ("sync",).
|
||||
- Actuators eventually send back ("sync", aid, fitness, halt_flag).
|
||||
- Once all actuators have synchronized for the current cycle, the cortex
|
||||
either:
|
||||
* ends the evaluation if any actuator requested a halt (halt_flag > 0),
|
||||
and reports ("evaluation_completed", total_fitness, cycles, elapsed)
|
||||
to the exoself, or
|
||||
* starts the next cycle.
|
||||
|
||||
Message protocol (inbox):
|
||||
- ("register_actuators", aids):
|
||||
Provide/replace the set of actuator IDs that must sync each cycle.
|
||||
Used when actuators are created dynamically or not known at init.
|
||||
- ("sync", aid, fitness, halt_flag):
|
||||
Fitness feedback from an actuator for the current cycle.
|
||||
The cortex accumulates fitness and checks halt conditions.
|
||||
- ("reactivate",):
|
||||
Restart a new evaluation episode (reset counters and kick sensors).
|
||||
- ("terminate",):
|
||||
Terminate the cortex and cascade termination to sensors/neurons/actuators.
|
||||
- ("backup_from_neuron", nid, idps...):
|
||||
Forward neuron backup data upstream to the exoself.
|
||||
"""
|
||||
def __init__(self, cid, exoself_pid, sensor_pids, neuron_pids, actuator_pids):
|
||||
super().__init__(f"Cortex-{cid}")
|
||||
self.cid = cid
|
||||
|
||||
@@ -12,6 +12,24 @@ def tanh(x): return math.tanh(x)
|
||||
|
||||
|
||||
class Neuron(Actor):
|
||||
"""
|
||||
Neuron actor implementing a weighted-sum neuron with an activation function
|
||||
and optional recurrent inputs.
|
||||
|
||||
The Neuron receives input vectors from upstream neurons, accumulates them
|
||||
according to its weight configuration, applies an activation function,
|
||||
and forwards the resulting output to downstream actors.
|
||||
|
||||
It supports:
|
||||
- feed-forward and recurrent connections
|
||||
- bias handling
|
||||
- asynchronous message-based execution
|
||||
- weight backup, restoration, and stochastic perturbation (mutation)
|
||||
- cycle-based updates for recurrent networks
|
||||
|
||||
This actor is designed to be used inside a cortex/agent actor network,
|
||||
where synchronization and evolution are coordinated externally.
|
||||
"""
|
||||
def __init__(self, nid, cx_pid, af_name, input_idps, output_pids, bias: Optional[float] = None):
|
||||
super().__init__(f"Neuron-{nid}")
|
||||
self.nid = nid
|
||||
|
||||
@@ -6,6 +6,42 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Sensor(Actor):
|
||||
"""
|
||||
Sensor actor that produces an input vector for the network and forwards it
|
||||
to downstream actors (fanout).
|
||||
|
||||
A Sensor is an *input node* in the actor-based neural architecture. It does
|
||||
not compute from other neurons; instead, it generates observations either
|
||||
from:
|
||||
- a local source (e.g., random numbers), or
|
||||
- an external scape/environment actor.
|
||||
|
||||
When the cortex triggers a sensor with a ("sync",) message, the sensor:
|
||||
1) calls `_sense()` to obtain a vector,
|
||||
2) broadcasts that vector to all downstream targets in `fanout` via
|
||||
("forward", sid, vec).
|
||||
|
||||
Supported sensor types (controlled by `sname`):
|
||||
- "rng":
|
||||
Produces `vl` random floats in [0, 1).
|
||||
- "xor_GetInput" (requires `scape`):
|
||||
Requests an input vector from the scape and expects a ("percept", vec)
|
||||
reply on its own inbox.
|
||||
- "car_GetFeatures" (requires `scape`):
|
||||
Requests a feature vector from the scape and normalizes it:
|
||||
* clamps values to [-1, 1],
|
||||
* pads with zeros or truncates to exactly `vl` elements.
|
||||
- default:
|
||||
Returns a zero vector of length `vl`.
|
||||
|
||||
Inbox message protocol:
|
||||
- ("sync",):
|
||||
Trigger sensing and forwarding to fanout.
|
||||
- ("percept", vec):
|
||||
Scape reply to a previous ("sense", sid, self) request (handled inside `_sense()`).
|
||||
- ("terminate",):
|
||||
Stop the actor.
|
||||
"""
|
||||
def __init__(self, sid, cx_pid, name, vector_length, fanout_pids, scape=None):
|
||||
super().__init__(f"Sensor-{sid}")
|
||||
self.sid = sid
|
||||
|
||||
@@ -12,7 +12,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def run_car_test(
|
||||
pop_id: str = "car_pop",
|
||||
pop_id: str = "car_pop_transaction_test23",
|
||||
gens: int = 200
|
||||
):
|
||||
monitor = await init_population((
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from neo4j import AsyncGraphDatabase
|
||||
from typing import LiteralString, cast
|
||||
|
||||
from neo4j import AsyncGraphDatabase, Query
|
||||
|
||||
NEO4J_CONSTRAINTS = [
|
||||
"CREATE CONSTRAINT cortex_id IF NOT EXISTS FOR (n:cortex) REQUIRE n.id IS UNIQUE",
|
||||
@@ -30,37 +32,129 @@ class Neo4jDB:
|
||||
return await s.run(cypher, **params)
|
||||
"""
|
||||
|
||||
async def run_read(self, cypher: str, **params):
|
||||
async def execute_write(self, work):
|
||||
"""
|
||||
|
||||
Method: execute_write
|
||||
|
||||
Description:
|
||||
This method is used to execute a write operation on the database using the provided work.
|
||||
|
||||
Parameters:
|
||||
self: The current instance of the class.
|
||||
work: The work to be executed as a write operation on the database.
|
||||
|
||||
"""
|
||||
async with self._driver.session(database=self._database) as session:
|
||||
return await session.execute_write(work)
|
||||
|
||||
async def run_read(self, cypher: LiteralString | Query, **params):
|
||||
"""
|
||||
|
||||
Method: run_read
|
||||
|
||||
Description:
|
||||
This method allows running a read operation with the provided cypher query and parameters using the underlying
|
||||
driver session. It returns the result of the read operation.
|
||||
|
||||
Parameters:
|
||||
- cypher: str or Query object representing the cypher query to be executed.
|
||||
- **params: Additional keyword arguments that represent parameters to be passed to the cypher query.
|
||||
|
||||
Returns:
|
||||
Result of the read operation based on the provided cypher query and parameters.
|
||||
|
||||
"""
|
||||
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 def read_single(self, cypher: LiteralString | Query, **params):
|
||||
"""
|
||||
Reads a single record from the database using the provided Cypher query and parameters.
|
||||
|
||||
:param cypher: The Cypher query to execute.
|
||||
:param params: Additional parameters to be passed to the query.
|
||||
|
||||
:return: A single record retrieved from the database based on the given Cypher query and parameters.
|
||||
"""
|
||||
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 def read_all(self, cypher: LiteralString | Query, **params):
|
||||
"""
|
||||
Reads all records from the database based on the given cypher query and parameters.
|
||||
|
||||
Parameters:
|
||||
cypher (str): The Cypher query to execute.
|
||||
**params: Additional parameters to pass to the Cypher query.
|
||||
|
||||
Return Type:
|
||||
list: A list of retrieved records from the database.
|
||||
"""
|
||||
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 def run_consume(self, cypher: LiteralString | Query, **params):
|
||||
"""
|
||||
|
||||
Run a Cypher query and consume the result.
|
||||
|
||||
Parameters:
|
||||
cypher : Union[LiteralString, Query]
|
||||
The Cypher query to be executed.
|
||||
**params : Any
|
||||
Keyword arguments for query parameters.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Creates database schema by running Neo4j constraints queries.
|
||||
|
||||
Parameters:
|
||||
- self: instance of the class
|
||||
- database: name of the database to be used for creating schema
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
for stmt in NEO4J_CONSTRAINTS:
|
||||
await s.run(stmt)
|
||||
await s.run(cast(LiteralString, stmt))
|
||||
|
||||
async def purge_all_nodes(self):
|
||||
"""
|
||||
Purge all nodes in the database.
|
||||
|
||||
Method parameters:
|
||||
- None
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
async with self._driver.session(database=self._database) as s:
|
||||
await s.run("MATCH (n) DETACH DELETE n")
|
||||
|
||||
async def drop_schema(self):
|
||||
"""
|
||||
Drop the current schema by dropping all constraints in the database.
|
||||
|
||||
Parameters:
|
||||
None
|
||||
|
||||
Return Type:
|
||||
None
|
||||
"""
|
||||
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")
|
||||
await s.run(cast(LiteralString, f"DROP CONSTRAINT {name} IF EXISTS"))
|
||||
|
||||
@@ -12,7 +12,6 @@ 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
|
||||
|
||||
@@ -20,6 +19,24 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Exoself(Actor):
|
||||
"""
|
||||
Exoself actor coordinating genotype-driven agent evaluation and learning.
|
||||
|
||||
The Exoself represents the *outer control loop* of an agent in the mathema
|
||||
framework. It is responsible for:
|
||||
- loading a genotype snapshot from persistent storage (Neo4j),
|
||||
- constructing the executable phenotype (Sensors, Neurons, Actuators,
|
||||
Cortex, and Scape),
|
||||
- running repeated evaluation episodes,
|
||||
- applying evolutionary weight perturbations,
|
||||
- tracking and reporting fitness statistics,
|
||||
- persisting improved parameters back to the genotype store.
|
||||
|
||||
Conceptually, Exoself corresponds to the “body/executive self” around a
|
||||
cortex:
|
||||
- the Cortex handles step-by-step execution and fitness accumulation,
|
||||
- the Exoself handles episode-level control, learning, and persistence.
|
||||
"""
|
||||
def __init__(self, genotype: Dict[str, Any], file_name: Optional[str] = None):
|
||||
super().__init__("Exoself")
|
||||
self.monitor = None
|
||||
@@ -46,7 +63,14 @@ class Exoself(Actor):
|
||||
self._perturbed: List[Neuron] = []
|
||||
|
||||
@classmethod
|
||||
async def start(cls, agent_id: str, monitor) -> "Exoself":
|
||||
async def start(cls, agent_id: str, monitor):
|
||||
"""
|
||||
|
||||
Method start takes agent_id and monitor as parameters and is a class method. It initializes some attributes of
|
||||
the class and creates a task to run the _runner coroutine. If an exception is caught during execution, a placeholder
|
||||
_Dummy class is returned.
|
||||
|
||||
"""
|
||||
try:
|
||||
g = await load_genotype_snapshot(agent_id)
|
||||
except Exception as e:
|
||||
@@ -73,8 +97,8 @@ class Exoself(Actor):
|
||||
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}")
|
||||
except Exception as err:
|
||||
log.error(f"[Exoself {self.agent_id}] CRASH in train_until_stop(): {err!r}")
|
||||
fitness = float("-inf")
|
||||
evals = int(self.eval_acc)
|
||||
cycles = int(self.cycle_acc)
|
||||
@@ -82,8 +106,8 @@ class Exoself(Actor):
|
||||
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}")
|
||||
except Exception as err:
|
||||
log.error(f"[Exoself {self.agent_id}] FAILED to notify monitor: {err!r}")
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
self._runner_task = loop.create_task(_runner(), name=f"Exoself-{self.agent_id}")
|
||||
@@ -96,6 +120,10 @@ class Exoself(Actor):
|
||||
return Exoself(g, file_name=path)
|
||||
|
||||
async def run(self):
|
||||
"""
|
||||
run loop of the exoself. Builds the network (mapping from genotype to phenotype=
|
||||
and waits for messages of the cortex.
|
||||
"""
|
||||
self.build_pid_map_and_spawn()
|
||||
|
||||
self._link_cortex()
|
||||
@@ -116,6 +144,20 @@ class Exoself(Actor):
|
||||
return
|
||||
|
||||
async def run_evaluation(self):
|
||||
"""
|
||||
|
||||
Description:
|
||||
Method to run evaluation of exoself by building network and linking the components,
|
||||
spawning PID map, linking cortex, and running sensor, neuron, actuator actors.
|
||||
It processes messages from the inbox and terminates upon specific tags.
|
||||
|
||||
Parameters:
|
||||
None
|
||||
|
||||
Return:
|
||||
Tuple containing evaluation results in the format (fitness: float, flag: int, cycles: int, elapsed: float)
|
||||
|
||||
"""
|
||||
log.debug(f"exoself: build network and link...")
|
||||
self.build_pid_map_and_spawn()
|
||||
log.debug(f"exoself: link cortex...")
|
||||
@@ -140,6 +182,17 @@ class Exoself(Actor):
|
||||
return float("-inf"), 0, 0, 0.0
|
||||
|
||||
def build_pid_map_and_spawn(self):
|
||||
"""
|
||||
|
||||
Builds the PID map for the Cortex actor and spawns Neuron, Actuator, and Sensor actors.
|
||||
|
||||
Parameters:
|
||||
- self: reference to the class instance
|
||||
|
||||
Returns:
|
||||
- None
|
||||
|
||||
"""
|
||||
cx = self.g["cortex"]
|
||||
self.cx_actor = Cortex(
|
||||
cid=cx["id"],
|
||||
@@ -254,6 +307,7 @@ class Exoself(Actor):
|
||||
return []
|
||||
|
||||
def _link_cortex(self):
|
||||
|
||||
self.cx_actor.sensors = [a for a in self.sensor_actors if a]
|
||||
self.cx_actor.neurons = [a for a in self.neuron_actors if a]
|
||||
self.cx_actor.actuators = [a for a in self.actuator_actors if a]
|
||||
@@ -263,6 +317,23 @@ class Exoself(Actor):
|
||||
self.tasks.append(asyncio.create_task(self.cx_actor.run()))
|
||||
|
||||
async def train_until_stop(self):
|
||||
"""
|
||||
|
||||
train_until_stop method runs the training until the stop condition is met. It builds the PID map and spawns
|
||||
necessary components, including sensor actors, neuron actors, and actuator actors. If an actuator scape is present,
|
||||
it runs the actuator scape as well.
|
||||
|
||||
The method continuously waits for incoming messages from the inbox and processes them based on the message tag.
|
||||
If the tag is "evaluation_completed," it calls the _on_evaluation_completed method with the received fitness, cycles,
|
||||
and elapsed time. If the _on_evaluation_completed method returns a dictionary, the method returns a tuple containing
|
||||
the highest fitness, evaluation accuracy, cycle accuracy, and time accuracy.
|
||||
|
||||
If the message tag is "terminate," the method calls the _terminate_all method to stop the training process and
|
||||
returns a tuple with negative infinity for fitness and zeros for other metrics.
|
||||
|
||||
This method does not return any value explicitly during normal training execution.
|
||||
|
||||
"""
|
||||
self.build_pid_map_and_spawn()
|
||||
self._link_cortex()
|
||||
|
||||
@@ -291,6 +362,30 @@ class Exoself(Actor):
|
||||
return float("-inf"), 0, 0, 0.0
|
||||
|
||||
async def _on_evaluation_completed(self, fitness: float, cycles: int, elapsed: float):
|
||||
"""
|
||||
This method _on_evaluation_completed is an asynchronous function that handles the completion
|
||||
of an evaluation process.
|
||||
|
||||
Parameters:
|
||||
- fitness: a float representing the fitness value obtained from the evaluation process.
|
||||
- cycles: an integer indicating the number of cycles involved in the evaluation.
|
||||
- elapsed: a float representing the elapsed time for the evaluation process.
|
||||
|
||||
This method updates internal counters and logs information about the evaluation process. It also performs
|
||||
actions based on the evaluation results, such as updating the highest fitness value, backing up weights,
|
||||
or restoring weights of neuron actors.
|
||||
|
||||
If the number of attempts reaches the maximum allowed attempts, it stops the evaluation process,
|
||||
backs up the genotype, terminates all actors, and returns a dictionary containing information
|
||||
about the best fitness value, evaluation count, cycle count, and accumulated time.
|
||||
|
||||
Finally, it calculates the perturbation probability, selects a subset of neuron actors for weight perturbation,
|
||||
sends perturbation commands to selected neuron actors, and reactivates the cx_actor.
|
||||
|
||||
Note: This method does not have a return statement for successful execution. If an error occurs during the
|
||||
episode_done message sending, it will ignore the exception. No additional
|
||||
errors or exceptions are caught or handled in this method.
|
||||
"""
|
||||
self.eval_acc += 1
|
||||
self.cycle_acc += int(cycles)
|
||||
self.time_acc += float(elapsed)
|
||||
|
||||
@@ -12,6 +12,16 @@ def generate_id() -> str:
|
||||
|
||||
|
||||
def get_InitSensor(morphology: MorphologyType):
|
||||
"""
|
||||
Return the initial sensor configuration for a given morphology.
|
||||
|
||||
This helper selects a minimal starting set of sensors used to bootstrap a
|
||||
new agent's morphology. It resolves the full sensor list for the provided
|
||||
morphology and returns a list containing only the first sensor entry.
|
||||
|
||||
Raises:
|
||||
ValueError: If the resolved morphology provides no sensors.
|
||||
"""
|
||||
sensors = get_Sensors(morphology)
|
||||
if not sensors:
|
||||
log.error("Morphology has no sensors.")
|
||||
@@ -20,6 +30,17 @@ def get_InitSensor(morphology: MorphologyType):
|
||||
|
||||
|
||||
def get_InitActuator(morphology: MorphologyType):
|
||||
"""
|
||||
Return the initial actuator configuration for a given morphology.
|
||||
|
||||
This helper selects a minimal starting set of actuators used to bootstrap
|
||||
a new agent's morphology. It resolves the full actuator list for the
|
||||
provided morphology and returns a list containing only the first actuator
|
||||
entry.
|
||||
|
||||
Raises:
|
||||
ValueError: If the resolved morphology provides no actuators.
|
||||
"""
|
||||
actuators = get_Actuators(morphology)
|
||||
if not actuators:
|
||||
log.error("Morphology has no actuators.")
|
||||
@@ -28,22 +49,74 @@ def get_InitActuator(morphology: MorphologyType):
|
||||
|
||||
|
||||
def get_Sensors(morphology: MorphologyType) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Resolve and return the list of sensor specifications for a morphology.
|
||||
|
||||
The morphology may be provided as:
|
||||
- a callable that accepts a kind string ("sensors" or "actuators"),
|
||||
- a registered string key mapping to a known morphology implementation,
|
||||
- or a module-like object exposing a callable 'xor_mimic' function.
|
||||
|
||||
Args:
|
||||
morphology: Morphology selector (callable, string key, or module-like).
|
||||
|
||||
Returns:
|
||||
A list of sensor specification dictionaries, each describing a sensor
|
||||
actor (e.g., name, vector length, and associated scape).
|
||||
"""
|
||||
fn = _resolve_morphology(morphology)
|
||||
return fn("sensors")
|
||||
|
||||
|
||||
def get_Actuators(morphology: MorphologyType) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Resolve and return the list of actuator specifications for a morphology.
|
||||
|
||||
The morphology may be provided as:
|
||||
- a callable that accepts a kind string ("sensors" or "actuators"),
|
||||
- a registered string key mapping to a known morphology implementation,
|
||||
- or a module-like object exposing a callable 'xor_mimic' function.
|
||||
|
||||
Args:
|
||||
morphology: Morphology selector (callable, string key, or module-like).
|
||||
|
||||
Returns:
|
||||
A list of actuator specification dictionaries, each describing an
|
||||
actuator actor (e.g., name, vector length, and associated scape).
|
||||
"""
|
||||
fn = _resolve_morphology(morphology)
|
||||
return fn("actuators")
|
||||
|
||||
|
||||
def _resolve_morphology(morphology: MorphologyType) -> Callable[[str], List[Dict[str, Any]]]:
|
||||
"""
|
||||
Resolve a morphology selector into a callable that can produce sensor or actuator specs.
|
||||
|
||||
This function normalizes different morphology representations into a
|
||||
single callable interface: fn(kind) -> List[Dict[str, Any]]. Supported
|
||||
inputs are:
|
||||
|
||||
- A callable: returned as-is.
|
||||
- A string key: looked up in a registry of known morphologies.
|
||||
- A module-like object: if it exposes a callable attribute 'xor_mimic',
|
||||
that callable is used.
|
||||
|
||||
Args:
|
||||
morphology: Morphology selector (callable, string key, or module-like).
|
||||
|
||||
Returns:
|
||||
A callable that accepts "sensors" or "actuators" and returns the
|
||||
corresponding specification list.
|
||||
|
||||
Raises:
|
||||
ValueError: If a string key is provided but not registered.
|
||||
TypeError: If morphology cannot be resolved to a valid callable.
|
||||
"""
|
||||
if callable(morphology):
|
||||
return morphology
|
||||
|
||||
if isinstance(morphology, str):
|
||||
reg = {
|
||||
"xor_mimic": xor_mimic,
|
||||
"car_racing_features": car_racing_features
|
||||
}
|
||||
if morphology in reg:
|
||||
@@ -62,31 +135,23 @@ def _resolve_morphology(morphology: MorphologyType) -> Callable[[str], List[Dict
|
||||
raise TypeError("morphology must be a callable, a module with 'xor_mimic', or a registered string key")
|
||||
|
||||
|
||||
def xor_mimic(kind: str) -> List[Dict[str, Any]]:
|
||||
if kind == "sensors":
|
||||
return [
|
||||
{
|
||||
"name": "xor_GetInput",
|
||||
"vector_length": 2,
|
||||
"scape": "xor_sim"
|
||||
}
|
||||
]
|
||||
elif kind == "actuators":
|
||||
return [
|
||||
{
|
||||
"name": "xor_SendOutput",
|
||||
"vector_length": 1,
|
||||
"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
|
||||
Provide a feature-based CarRacing morphology specification.
|
||||
|
||||
This morphology exposes:
|
||||
- One sensor ("car_GetFeatures") producing a fixed-length feature vector
|
||||
derived from a look-ahead horizon plus additional scalar features.
|
||||
- One actuator ("car_ApplyAction") consuming a 3-element action vector.
|
||||
|
||||
Args:
|
||||
kind: Either "sensors" or "actuators".
|
||||
|
||||
Returns:
|
||||
A list containing a single specification dictionary for the requested kind.
|
||||
|
||||
Raises:
|
||||
ValueError: If kind is not "sensors" or "actuators".
|
||||
"""
|
||||
LOOK_AHEAD = 10
|
||||
feature_len = LOOK_AHEAD + 6
|
||||
|
||||
@@ -21,31 +21,17 @@ from mathema.genotype.neo4j.genotype import (
|
||||
delete_agent,
|
||||
update_fingerprint,
|
||||
)
|
||||
from mathema.genotype.neo4j.genotype_mutator import GenotypeMutator
|
||||
from mathema.genotype.neo4j.genotype_mutator_tx import GenotypeMutator
|
||||
from mathema.utils import stats
|
||||
from mathema.core.exoself import Exoself
|
||||
from mathema.settings import (EFF, SURVIVAL_PERCENTAGE, SPECIE_SIZE_LIMIT,
|
||||
INIT_SPECIE_SIZE, GENERATION_LIMIT, EVALUATIONS_LIMIT,
|
||||
FITNESS_GOAL, INIT_POPULATION_ID)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -92,6 +78,23 @@ class MonitorState:
|
||||
|
||||
|
||||
async def _population_aggregate(population_id: str) -> dict:
|
||||
"""
|
||||
Retrieve fitness values for agents in a population and calculate aggregate statistics.
|
||||
|
||||
Parameters:
|
||||
population_id (str): The unique identifier of the population.
|
||||
|
||||
Returns:
|
||||
Dictionary: A dictionary containing the following aggregate statistics:
|
||||
- "cum_fitness": Sum of all fitness values.
|
||||
- "avg": Average fitness value.
|
||||
- "std": Standard deviation of fitness values.
|
||||
- "best": Maximum fitness value.
|
||||
- "min": Minimum fitness value.
|
||||
- "n": Number of fitness values.
|
||||
- "agents": Number of agents in the population.
|
||||
|
||||
"""
|
||||
rows = await _read_all("""
|
||||
MATCH (a:agent {population_id:$pid})
|
||||
RETURN collect(coalesce(toFloat(a.fitness),0.0)) AS fs
|
||||
@@ -116,12 +119,170 @@ async def _population_aggregate(population_id: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
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)).
|
||||
async def _best_fitness_in_population(population_id: str) -> float:
|
||||
"""
|
||||
|
||||
Get the best fitness score in a given population based on the maximum fitness value of all agents.
|
||||
|
||||
Parameters:
|
||||
- population_id (str): The ID of the population to search for.
|
||||
|
||||
Returns:
|
||||
- float: The best fitness value found in the population, or 0.0 if no fitness values are found.
|
||||
|
||||
"""
|
||||
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 _calculate_energy_cost(population_id: str) -> float:
|
||||
"""
|
||||
Calculate the energy cost based on the fitness of agents and the number of neurons
|
||||
in the cortex associated with the given population ID.
|
||||
|
||||
Parameters:
|
||||
population_id (str): The ID of the population for which the energy cost needs to be calculated.
|
||||
|
||||
Returns:
|
||||
float: The calculated energy cost.
|
||||
"""
|
||||
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 _construct_agent_summaries(agent_ids: Sequence[str]):
|
||||
"""
|
||||
Constructs summaries for the given list of agent IDs.
|
||||
|
||||
Parameters:
|
||||
agent_ids (Sequence[str]): A list of agent IDs for which summaries are to be constructed.
|
||||
|
||||
Return Type:
|
||||
List[Tuple[float, int, str]]: A list of tuples where each tuple contains the agent's fitness as a float,
|
||||
the count of neurons as an integer, and the agent ID as a string.
|
||||
"""
|
||||
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_alotments(valid: List[Tuple[float, int, str]],
|
||||
neural_energy_cost: float
|
||||
):
|
||||
"""
|
||||
Calculate allotments based on fitness values and neural energy cost.
|
||||
|
||||
Parameters:
|
||||
valid (List[Tuple[float, int, str]]): A list of tuples containing fitness, total neurons, and agent ID.
|
||||
neural_energy_cost (float): The energy cost for neural activity.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Tuple[float, float, int, str]], float]: A tuple containing a list of tuples with allotments,
|
||||
fitness values, total neurons, and agent IDs, and the total allotments for the new population.
|
||||
"""
|
||||
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 _extract_specie_agent_ids(specie_id: str) -> List[str]:
|
||||
"""
|
||||
Extracts the IDs of agents associated with a given specie.
|
||||
|
||||
:param specie_id: The ID of the specie for which to retrieve agent IDs
|
||||
:type specie_id: str
|
||||
:return: List of agent IDs associated with the specie
|
||||
:rtype: 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 _extract_specie_ids(population_id: str) -> List[str]:
|
||||
"""
|
||||
Extract specie IDs from Neo4j database for a given population ID.
|
||||
|
||||
:param population_id: str - The population ID for which to extract specie IDs.
|
||||
:return: List[str] - A list of specie IDs associated with the given population ID.
|
||||
|
||||
"""
|
||||
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 _ensure_specie_node(specie_id: str, population_id: str, constraint_json: str):
|
||||
"""
|
||||
Ensure that the specie node exists. Used to keep the agent database coherent.
|
||||
|
||||
Parameters:
|
||||
specie_id (str): The ID of the specie to create.
|
||||
constraint_json (str): The JSON string of the constraints for the specie.
|
||||
|
||||
Returns:
|
||||
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(population_id: str) -> List[str]:
|
||||
"""
|
||||
Extracts the agent IDs associated with a given population ID.
|
||||
|
||||
Parameters:
|
||||
population_id (str): The ID of the population to extract agent IDs for.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of agent IDs as strings extracted from the database based on the provided population ID.
|
||||
"""
|
||||
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 _ensure_population_node(population_id: str) -> None:
|
||||
await _run("MERGE (:population {id:$pid})", pid=str(population_id))
|
||||
|
||||
|
||||
class PopulationMonitor:
|
||||
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()
|
||||
@@ -136,6 +297,7 @@ class PopulationMonitor:
|
||||
self._t0 = None
|
||||
self._best_so_far = float("-inf")
|
||||
self.train_time_sec = 30*60
|
||||
self._deadline_task = None
|
||||
|
||||
# logging file handles
|
||||
self._episodes_f = None
|
||||
@@ -144,6 +306,29 @@ class PopulationMonitor:
|
||||
@classmethod
|
||||
async def start(cls, op_mode: str, population_id: str,
|
||||
selection_algorithm: SelectionAlgorithm) -> "PopulationMonitor":
|
||||
"""
|
||||
Create and start a new population monitor instance.
|
||||
|
||||
This class method initializes a PopulationMonitor for the given
|
||||
population and selection algorithm, starts its asynchronous message
|
||||
processing loop, and prepares all logging and bookkeeping required for a
|
||||
training run.
|
||||
|
||||
Specifically, it:
|
||||
1. Instantiates the monitor with the specified operation mode, population
|
||||
identifier, and selection algorithm.
|
||||
2. Launches the internal actor-style run loop as an asyncio task.
|
||||
3. Registers an atexit hook to collect generation-level statistics for
|
||||
post-run analysis.
|
||||
4. Initializes timing, assigns a unique run identifier, and creates the
|
||||
corresponding output directory.
|
||||
5. Opens and initializes episode-level and progress-level CSV log files.
|
||||
6. Starts a deadline task to enforce the configured training time limit.
|
||||
7. Initializes and activates the first generation of the population.
|
||||
|
||||
The method returns the fully initialized and running PopulationMonitor
|
||||
instance, which can then be controlled via its public interface.
|
||||
"""
|
||||
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))
|
||||
@@ -173,11 +358,48 @@ class PopulationMonitor:
|
||||
async def cast(self, msg: tuple) -> None:
|
||||
await self.inbox.put(msg)
|
||||
|
||||
async def stop(self, mode: Literal["normal", "shutdown"] = "normal") -> None:
|
||||
async def stop(self, mode: Literal["normal", "shutdown"] = "normal"):
|
||||
"""
|
||||
Request termination of the population monitor and await shutdown.
|
||||
|
||||
This method sends a stop command to the monitor's internal message loop
|
||||
and blocks until the monitor has completed its graceful shutdown
|
||||
procedure. The optional mode parameter indicates the reason for stopping
|
||||
(e.g. normal completion versus external shutdown) and is forwarded to
|
||||
the internal stop handler.
|
||||
|
||||
It provides a synchronous-style interface for external components to
|
||||
ensure that all agents are terminated, logs are flushed, and final
|
||||
results are persisted before control returns to the caller.
|
||||
"""
|
||||
await self.inbox.put(("stop", mode))
|
||||
await self._stopped_evt.wait()
|
||||
|
||||
async def _run(self) -> None:
|
||||
async def _run(self):
|
||||
"""
|
||||
Main asynchronous message-processing loop of the population monitor.
|
||||
|
||||
This method continuously consumes messages from the internal inbox and
|
||||
dispatches them to the appropriate handlers, coordinating the lifecycle
|
||||
of an evolutionary run. It implements an actor-style control loop with
|
||||
the following responsibilities:
|
||||
|
||||
1. Reacts to control messages:
|
||||
- "stop": triggers graceful shutdown and finalization of the run.
|
||||
- "pause": requests a pause after the current generation completes.
|
||||
- "continue": resumes execution and initializes a new generation
|
||||
after a pause.
|
||||
2. Handles evaluation-related events:
|
||||
- "episode_done": logs completion of a single evaluation episode.
|
||||
- "terminated": processes termination of an agent and updates
|
||||
generation-level state.
|
||||
3. Maintains correct sequencing of generations and respects the current
|
||||
operational mode (continue, pause, done).
|
||||
|
||||
The loop runs until a stop message is received or the task is cancelled.
|
||||
On exit, it signals completion via an internal stopped-event to allow
|
||||
other components to await full shutdown.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
msg = await self.inbox.get()
|
||||
@@ -199,14 +421,24 @@ class PopulationMonitor:
|
||||
elif tag == "terminated":
|
||||
await self._handle_agent_terminated(*msg[1:])
|
||||
else:
|
||||
|
||||
pass
|
||||
finally:
|
||||
self._stopped_evt.set()
|
||||
|
||||
async def _init_generation(self) -> None:
|
||||
"""
|
||||
Initialize the agent generation process by extracting agent ids, setting initial values for the state object,
|
||||
and starting agents asynchronously. If any errors occur during the agent initialization process,
|
||||
the corresponding agent's fitness is set to negative infinity.
|
||||
|
||||
Parameters:
|
||||
- self: The reference to the current object instance.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
s = self.state
|
||||
agent_ids = await self._extract_agent_ids(s.population_id)
|
||||
agent_ids = await _extract_agent_ids(s.population_id)
|
||||
s.agent_ids = agent_ids
|
||||
s.tot_agents = len(agent_ids)
|
||||
s.agents_left = 0
|
||||
@@ -227,7 +459,26 @@ class PopulationMonitor:
|
||||
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:
|
||||
async def _handle_stop(self, mode: str):
|
||||
"""
|
||||
Gracefully stop and finalize the population monitoring run.
|
||||
|
||||
This method is invoked when the monitoring loop receives a stop signal.
|
||||
It performs an orderly shutdown of the ongoing evolutionary run by:
|
||||
|
||||
1. Terminating all currently active agent handlers or actors.
|
||||
2. Flushing and closing episode-level and progress-level log files.
|
||||
3. Cancelling any active deadline or time-limit task.
|
||||
4. Finalizing the global best-so-far fitness value based on the current
|
||||
population state.
|
||||
5. Writing a run summary to disk, including identifiers, training
|
||||
duration, generation count, accumulated evaluation statistics, and
|
||||
final best-so-far fitness.
|
||||
6. Clearing internal file handles and logging the shutdown event.
|
||||
|
||||
The method ensures that partial results are safely persisted and that
|
||||
all asynchronous resources are released before the run terminates.
|
||||
"""
|
||||
s = self.state
|
||||
|
||||
for (_aid, h) in list(s.active):
|
||||
@@ -246,20 +497,76 @@ class PopulationMonitor:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if getattr(self, "_deadline_task", None):
|
||||
self._deadline_task.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
best = await _best_fitness_in_population(self.state.population_id)
|
||||
self._best_so_far = max(self._best_so_far, float(best))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
summary = {
|
||||
"run_id": self.run_id,
|
||||
"population_id": self.state.population_id,
|
||||
"train_time_sec": self.train_time_sec,
|
||||
"gens": self.state.pop_gen,
|
||||
"eval_acc": self.state.eval_acc,
|
||||
"best_so_far": self._best_so_far,
|
||||
"op_tag": self.state.op_tag,
|
||||
}
|
||||
with open(f"runs/{self.run_id}/summary.json", "w") as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
|
||||
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):
|
||||
"""
|
||||
Handle completion of a single evaluation episode for an agent.
|
||||
|
||||
async def _handle_episode_done(self, agent_id: str, ep_return: float, eval_idx: int) -> None:
|
||||
This method is called whenever an agent finishes one evaluation episode
|
||||
within the current generation. It records episode-level information for
|
||||
later analysis and monitoring but does not alter population-level state
|
||||
or control flow.
|
||||
|
||||
Specifically, it:
|
||||
1. Computes the elapsed wall-clock time since the start of the run.
|
||||
2. Logs the episode result (time, generation index, agent identifier,
|
||||
evaluation index, and episode return) to the episode-level log file,
|
||||
if enabled.
|
||||
"""
|
||||
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")
|
||||
self._episodes_f.write(f"{t_sec:.6f},{s.pop_gen},{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:
|
||||
agent_time: int):
|
||||
"""
|
||||
Handle termination of a single agent evaluation.
|
||||
|
||||
This method is invoked when an agent finishes its evaluation within the
|
||||
current generation. It performs the following actions:
|
||||
|
||||
1. Accumulates per-agent evaluation statistics (evaluation count, cycle
|
||||
count, and execution time) into generation-level counters.
|
||||
2. Decrements the number of remaining active agents in the generation.
|
||||
3. Persists the agent's final fitness value to the underlying genotype
|
||||
storage.
|
||||
4. Removes the terminated agent from the list of currently active agents.
|
||||
5. Logs progress information, including how many agents are still active.
|
||||
6. Triggers generation finalization once all agents in the generation
|
||||
have completed their evaluations.
|
||||
|
||||
The method is fully asynchronous and is typically called by the actor
|
||||
supervision or monitoring component when an agent signals termination.
|
||||
"""
|
||||
log.info(f"agent terminated: , {agent_id}, {fitness}, {agent_eval}, {agent_cycle}, {agent_time}")
|
||||
s = self.state
|
||||
|
||||
@@ -280,6 +587,31 @@ class PopulationMonitor:
|
||||
await self._generation_finished()
|
||||
|
||||
async def _generation_finished(self) -> None:
|
||||
"""
|
||||
Handle the end of a population generation.
|
||||
|
||||
This method is called once all agents of the current generation have
|
||||
completed their evaluations. It performs the following steps:
|
||||
|
||||
1. Mutates and selects the next generation according to the configured
|
||||
selection algorithm and species size limit.
|
||||
2. Increments the generation counter and logs aggregated population
|
||||
statistics (best, average, standard deviation of fitness).
|
||||
3. Updates time-based metrics, including elapsed wall-clock time,
|
||||
normalized training time, and the global best-so-far fitness.
|
||||
4. Writes a progress entry to the progress log (CSV-style) and appends
|
||||
a detailed statistics record to the in-memory history.
|
||||
5. Signals the end of the generation via an asyncio.Event to unblock
|
||||
dependent tasks.
|
||||
6. Checks termination conditions, including generation limit,
|
||||
evaluation limit, fitness goal, and wall-clock time limit.
|
||||
7. Depending on the current operation tag and termination conditions,
|
||||
either stops the run, pauses execution, or initializes the next
|
||||
generation.
|
||||
|
||||
The method is fully asynchronous and intended to be called from the
|
||||
population monitoring loop that coordinates evolutionary training.
|
||||
"""
|
||||
s = self.state
|
||||
await self._mutate_population(s.population_id, SPECIE_SIZE_LIMIT, s.selection_algorithm)
|
||||
s.pop_gen += 1
|
||||
@@ -326,7 +658,7 @@ class PopulationMonitor:
|
||||
f"({elapsed:.1f}s >= {self.train_time_sec}s), stopping run"
|
||||
)
|
||||
|
||||
best = await self._best_fitness_in_population(s.population_id)
|
||||
best = await _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")
|
||||
@@ -345,45 +677,63 @@ class PopulationMonitor:
|
||||
|
||||
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)
|
||||
selection_algorithm: SelectionAlgorithm):
|
||||
"""
|
||||
Apply evolutionary mutation and selection to an entire population.
|
||||
|
||||
This method orchestrates the mutation step at the population level at the
|
||||
end of a generation. It first computes shared contextual information,
|
||||
such as the current energy cost of the population, and then iterates over
|
||||
all species belonging to the population.
|
||||
|
||||
For each species, it delegates the actual selection and mutation process
|
||||
to the species-level mutation routine, ensuring that the total number of
|
||||
individuals retained respects the configured population size limit and
|
||||
the chosen selection algorithm.
|
||||
|
||||
The method is asynchronous and intended to be invoked as part of the
|
||||
generation finalization phase of the evolutionary loop.
|
||||
"""
|
||||
energy_cost = await _calculate_energy_cost(population_id)
|
||||
specie_ids = await _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)
|
||||
selection_algorithm: SelectionAlgorithm):
|
||||
"""
|
||||
Mutate and repopulate a single species according to the chosen selection strategy.
|
||||
|
||||
summaries = await self._construct_agent_summaries(agent_ids)
|
||||
This method performs the end-of-generation evolutionary step for one
|
||||
species ("specie") within a population. It:
|
||||
|
||||
1. Loads all agents belonging to the species and constructs fitness
|
||||
summaries, sorting agents by fitness in descending order.
|
||||
2. Selects survivors and removes non-survivors from both the genotype
|
||||
store and the species membership relationship.
|
||||
3. Creates new offspring agents to refill the species up to the target
|
||||
population limit, using one of two selection algorithms:
|
||||
- "competition": keeps a fraction of the best agents (SURVIVAL_PERCENTAGE),
|
||||
ranks them by an efficiency-weighted score (fitness adjusted by
|
||||
network size/complexity), deletes the rest, and produces offspring
|
||||
via the competition routine.
|
||||
- "top3": keeps only the top 3 agents, deletes all others, and produces
|
||||
the required number of offspring from these champions.
|
||||
4. Computes aggregate fitness statistics for the species (mean, standard
|
||||
deviation, min, max) and updates the species node with these values as
|
||||
well as the list of champion agent IDs.
|
||||
5. Updates the species' innovation factor based on whether the current
|
||||
best fitness exceeds the stored innovation threshold.
|
||||
6. Refreshes fingerprints for newly created agents to keep derived
|
||||
metadata consistent.
|
||||
|
||||
The method is asynchronous and is intended to be called from the
|
||||
population-level mutation step after a generation has completed.
|
||||
"""
|
||||
agent_ids = await _extract_specie_agent_ids(specie_id)
|
||||
|
||||
summaries = await _construct_agent_summaries(agent_ids)
|
||||
summaries.sort(key=lambda t: t[0], reverse=True)
|
||||
|
||||
if not summaries:
|
||||
@@ -452,31 +802,63 @@ class PopulationMonitor:
|
||||
|
||||
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)
|
||||
"""
|
||||
Perform competition-based reproduction for a species.
|
||||
|
||||
This method implements the competition selection strategy for a single
|
||||
species. Given a set of valid (surviving) agents, it:
|
||||
|
||||
1. Computes reproduction allotments for each agent based on fitness and
|
||||
neural energy cost, yielding a total estimated population size.
|
||||
2. Derives a normalization factor to scale these allotments so that the
|
||||
resulting number of survivors and offspring respects the configured
|
||||
population size limit.
|
||||
3. Delegates to the survivor-gathering routine to retain parent agents
|
||||
and generate the appropriate number of mutant offspring.
|
||||
|
||||
The method returns a list of agent identifiers representing the new
|
||||
species population after competition-based selection and mutation. It is
|
||||
asynchronous and intended to be used during the species-level mutation
|
||||
phase of the evolutionary cycle.
|
||||
"""
|
||||
alot, est = await _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]:
|
||||
"""
|
||||
Collect survivors and generate offspring for a species based on normalized allotments.
|
||||
|
||||
This method takes a list of agents with precomputed allotment scores and
|
||||
determines how many times each agent should survive or reproduce into
|
||||
the next generation. For each agent, it:
|
||||
|
||||
1. Computes a reproduction count by normalizing the agent's allotment
|
||||
value against a global normalizer.
|
||||
2. Enforces a hard safety constraint to ensure that each listed agent
|
||||
contributes at least one survivor to the next generation.
|
||||
3. Ensures that the surviving agent remains linked to the species.
|
||||
4. Creates the required number of mutant offspring clones for the agent
|
||||
to satisfy its allotted reproduction count.
|
||||
5. Removes agents that would otherwise receive zero allotment from the
|
||||
species and deletes their genotype representation.
|
||||
|
||||
The method returns a list of agent identifiers representing the newly
|
||||
formed species population, including both surviving parents and newly
|
||||
created offspring. It is asynchronous and intended to be used as part of
|
||||
the species-level reproduction process.
|
||||
"""
|
||||
new_ids: List[str] = []
|
||||
for (ma, fit, tn, aid) in alot:
|
||||
count = int(round(ma / normalizer)) if normalizer > 0 else 0
|
||||
# hard safety: keep at least one survivor
|
||||
if count <= 0:
|
||||
count = 1
|
||||
log.info(f"Agent {aid}: normalized allotment = {count}")
|
||||
# TODO: this is redundant!
|
||||
if count >= 1:
|
||||
|
||||
await _run("""
|
||||
MATCH (s:specie {id:$sid}), (a:agent {id:$aid})
|
||||
MERGE (s)-[:HAS]->(a)
|
||||
@@ -497,6 +879,26 @@ class PopulationMonitor:
|
||||
return new_ids
|
||||
|
||||
async def _create_mutant_offspring(self, parent_id: str, specie_id: str) -> str:
|
||||
"""
|
||||
Create a mutated offspring from a parent agent.
|
||||
|
||||
This method clones an existing agent to create a new offspring and
|
||||
integrates it into the evolutionary population. It performs the
|
||||
following steps:
|
||||
|
||||
1. Generates a new unique agent identifier and clones the parent agent's
|
||||
genotype into the offspring.
|
||||
2. Inherits and sets species and population identifiers for the cloned
|
||||
agent and clears its fitness value to mark it as unevaluated.
|
||||
3. Establishes the species membership relationship between the offspring
|
||||
agent and the corresponding species.
|
||||
4. Applies a single mutation step to the offspring's genotype to
|
||||
introduce variation.
|
||||
|
||||
The method returns the identifier of the newly created mutant offspring.
|
||||
It is asynchronous and intended to be used during the reproduction phase
|
||||
of species-level evolution.
|
||||
"""
|
||||
clone_id = _new_id()
|
||||
|
||||
await clone_agent(parent_id, clone_id)
|
||||
@@ -520,17 +922,32 @@ class PopulationMonitor:
|
||||
await self._mutate_one_step(clone_id)
|
||||
return clone_id
|
||||
|
||||
async def _mutate_one_step(self, agent_id: str) -> None:
|
||||
async def _mutate_one_step(self, agent_id: str):
|
||||
"""
|
||||
Apply a single random mutation step to an agent.
|
||||
|
||||
This method selects one mutation operator at random from the available
|
||||
set of genotype mutation operations (e.g. weight mutation, bias
|
||||
insertion/removal, structural link or neuron changes) and applies it to
|
||||
the specified agent.
|
||||
|
||||
If the selected mutation operation fails, the method falls back to a
|
||||
simple weight-mutation step as a safety measure. Any further failure is
|
||||
silently ignored to ensure that the evolutionary process can continue
|
||||
without interrupting the run.
|
||||
|
||||
The method is asynchronous and intended to introduce variation during
|
||||
offspring creation.
|
||||
"""
|
||||
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,
|
||||
self.mutator.mutate_weights_tx,
|
||||
self.mutator.add_bias_tx,
|
||||
self.mutator.remove_bias_tx,
|
||||
self.mutator.add_inlink_tx,
|
||||
self.mutator.add_outlink_tx,
|
||||
self.mutator.add_neuron_tx,
|
||||
self.mutator.outsplice_tx,
|
||||
self.mutator.add_actuator_tx,
|
||||
]
|
||||
op = random.choice(ops)
|
||||
try:
|
||||
@@ -538,65 +955,57 @@ class PopulationMonitor:
|
||||
except Exception:
|
||||
|
||||
try:
|
||||
await self.mutator.mutate_weights(agent_id)
|
||||
await self.mutator.mutate_weights_tx(agent_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _top3(self, valid_ids: List[str], offspring_needed: int, specie_id: str) -> List[str]:
|
||||
"""
|
||||
Generate offspring using the top-3 selection strategy.
|
||||
|
||||
This method implements the "top3" reproduction strategy for a species.
|
||||
It retains the three best-performing agents unchanged and fills the
|
||||
remaining population slots by repeatedly selecting one of these top
|
||||
agents at random and creating a mutated offspring from it.
|
||||
|
||||
The method returns a list of agent identifiers representing the new
|
||||
species population, consisting of the original top agents and their
|
||||
offspring. It is asynchronous and intended to be used during the
|
||||
species-level mutation phase of the evolutionary cycle.
|
||||
"""
|
||||
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
|
||||
Initialize a new evolutionary population and start its monitor.
|
||||
|
||||
This function creates a fresh population in the genotype store based on
|
||||
the provided species constraints and launches a PopulationMonitor to
|
||||
manage its evolutionary process. It performs the following steps:
|
||||
|
||||
1. Deletes any existing population with the given identifier to ensure a
|
||||
clean initialization.
|
||||
2. Creates a new population node in the datastore.
|
||||
3. For each specified species constraint:
|
||||
- Creates a new species node associated with the population.
|
||||
- Stores the species constraint configuration and initializes its
|
||||
innovation factor.
|
||||
- Creates an initial set of agents for the species, constructing their
|
||||
genotypes according to the given constraints.
|
||||
- Assigns population and species identifiers to each agent, clears
|
||||
fitness values, and establishes species membership relationships.
|
||||
4. Starts a PopulationMonitor for the initialized population, using the
|
||||
specified operation mode and selection algorithm.
|
||||
|
||||
The function returns the running PopulationMonitor instance, which
|
||||
coordinates evaluation, mutation, and logging for the newly created
|
||||
population.
|
||||
"""
|
||||
population_id, specie_constraints, op_mode, selection_algorithm = params
|
||||
|
||||
@@ -639,10 +1048,39 @@ async def init_population(params: Tuple[str, List[dict], str, SelectionAlgorithm
|
||||
|
||||
async def continue_(op_mode: str, selection_algorithm: SelectionAlgorithm,
|
||||
population_id: str = INIT_POPULATION_ID) -> PopulationMonitor:
|
||||
"""
|
||||
Resume or start a population monitor for an existing population.
|
||||
|
||||
This convenience function starts a PopulationMonitor for the specified
|
||||
population using the given operation mode and selection algorithm,
|
||||
without reinitializing or modifying the underlying population structure.
|
||||
|
||||
It is intended to continue an existing evolutionary run or to attach a
|
||||
new monitor to a pre-existing population state. The function returns the
|
||||
running PopulationMonitor instance, which immediately begins coordinating
|
||||
evaluation and evolution according to the provided parameters.
|
||||
"""
|
||||
return await PopulationMonitor.start(op_mode, population_id, selection_algorithm)
|
||||
|
||||
|
||||
async def delete_population(population_id: str) -> None:
|
||||
async def delete_population(population_id: str):
|
||||
"""
|
||||
Delete a population and all associated evolutionary entities.
|
||||
|
||||
This function removes a population and all data linked to it from the
|
||||
genotype store. It performs a cascading deletion that includes:
|
||||
|
||||
1. The population node itself.
|
||||
2. All species belonging to the population.
|
||||
3. All agents within those species.
|
||||
4. All cortical structures owned by the agents, including neurons,
|
||||
sensors, and actuators.
|
||||
|
||||
All relationships are detached prior to deletion to ensure referential
|
||||
integrity. The operation is destructive and intended to be used during
|
||||
population reinitialization or cleanup before starting a new evolutionary
|
||||
run.
|
||||
"""
|
||||
await _run("""
|
||||
MATCH (p:population {id:$pid})
|
||||
OPTIONAL MATCH (s:specie {population_id:$pid})
|
||||
@@ -652,7 +1090,3 @@ async def delete_population(population_id: str) -> None:
|
||||
OPTIONAL MATCH (cx)-[:HAS]->(act:actuator)
|
||||
DETACH DELETE p, s, a, cx, n, sen, act
|
||||
""", pid=str(population_id))
|
||||
|
||||
|
||||
async def test() -> PopulationMonitor:
|
||||
return await init_population((INIT_POPULATION_ID, INIT_CONSTRAINTS, INIT_OP_MODE, INIT_SELECTION_ALGO))
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
|
||||
from mathema.core import morphology
|
||||
from mathema.genotype.neo4j.genotype import construct, print_genotype
|
||||
from mathema.core.exoself import Exoself
|
||||
|
||||
|
||||
class Trainer:
|
||||
def __init__(
|
||||
self,
|
||||
morphology_spec=morphology,
|
||||
hidden_layer_densities: List[int] = None,
|
||||
*,
|
||||
max_attempts: int = 5,
|
||||
eval_limit: float = float("inf"),
|
||||
fitness_target: float = float("inf"),
|
||||
experimental_file: Optional[str] = "experimental.json",
|
||||
best_file: Optional[str] = "best.json",
|
||||
exoself_steps_per_eval: int = 0,
|
||||
):
|
||||
self.morphology_spec = morphology_spec
|
||||
self.hds = hidden_layer_densities or []
|
||||
self.max_attempts = max_attempts
|
||||
self.eval_limit = eval_limit
|
||||
self.fitness_target = fitness_target
|
||||
self.experimental_file = experimental_file
|
||||
self.best_file = best_file
|
||||
self.exoself_steps_per_eval = exoself_steps_per_eval
|
||||
|
||||
self.best_fitness = float("-inf")
|
||||
self.best_genotype: Optional[Dict[str, Any]] = None
|
||||
|
||||
self.eval_acc = 0
|
||||
self.cycle_acc = 0
|
||||
self.time_acc = 0.0
|
||||
|
||||
async def _run_one_attempt(self) -> Tuple[float, int, int, float]:
|
||||
print("constructing genotype...")
|
||||
geno = construct(
|
||||
self.morphology_spec,
|
||||
self.hds,
|
||||
file_name=self.experimental_file, # <-- schreibt Startnetz nach experimental.json
|
||||
add_bias=True
|
||||
)
|
||||
fitness, evals, cycles, elapsed = await self._evaluate_with_exoself(geno)
|
||||
return fitness, evals, cycles, elapsed
|
||||
|
||||
async def _evaluate_with_exoself(self, genotype: Dict[str, Any]) -> Tuple[float, int, int, float]:
|
||||
print("creating exoself...")
|
||||
ex = Exoself(genotype, file_name=self.experimental_file)
|
||||
best_fitness, evals, cycles, elapsed = await ex.train_until_stop()
|
||||
return best_fitness, evals, cycles, elapsed
|
||||
|
||||
async def go(self):
|
||||
attempt = 1
|
||||
while True:
|
||||
print(".........")
|
||||
print("current attempt: ", attempt)
|
||||
print(".........")
|
||||
|
||||
if attempt > self.max_attempts or self.eval_acc >= self.eval_limit or self.best_fitness >= self.fitness_target:
|
||||
# Abschlussausgabe wie im Buch
|
||||
if self.best_file and os.path.exists(self.best_file):
|
||||
print_genotype(self.best_file)
|
||||
print(
|
||||
f" Morphology: {getattr(self.morphology_spec, '__name__', str(self.morphology_spec))} | "
|
||||
f"Best Fitness: {self.best_fitness} | EvalAcc: {self.eval_acc}"
|
||||
)
|
||||
return {
|
||||
"best_fitness": self.best_fitness,
|
||||
"eval_acc": self.eval_acc,
|
||||
"cycle_acc": self.cycle_acc,
|
||||
"time_acc": self.time_acc,
|
||||
"best_file": self.best_file,
|
||||
}
|
||||
|
||||
print("RUN ONE ATTEMPT!")
|
||||
fitness, evals, cycles, elapsed = await self._run_one_attempt()
|
||||
|
||||
print("update akkus...")
|
||||
|
||||
self.eval_acc += evals
|
||||
self.cycle_acc += cycles
|
||||
self.time_acc += elapsed
|
||||
|
||||
# Besser als bisher?
|
||||
if fitness > self.best_fitness:
|
||||
self.best_fitness = fitness
|
||||
if self.best_file and self.experimental_file and os.path.exists(self.experimental_file):
|
||||
os.replace(self.experimental_file, self.best_file)
|
||||
attempt = 1
|
||||
else:
|
||||
attempt += 1
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
trainer = Trainer(
|
||||
morphology_spec=morphology,
|
||||
hidden_layer_densities=[2],
|
||||
max_attempts=200,
|
||||
eval_limit=float("inf"),
|
||||
fitness_target=99.9,
|
||||
experimental_file="experimental.json",
|
||||
best_file="best.json",
|
||||
exoself_steps_per_eval=0,
|
||||
)
|
||||
|
||||
asyncio.run(trainer.go())
|
||||
@@ -1,6 +1,7 @@
|
||||
import math
|
||||
import numpy as np
|
||||
import logging
|
||||
import pygame
|
||||
|
||||
import Box2D
|
||||
from Box2D import (b2FixtureDef, b2PolygonShape, b2ContactListener)
|
||||
@@ -20,8 +21,6 @@ except Exception:
|
||||
|
||||
from gymnasium.envs.box2d.car_dynamics import Car
|
||||
|
||||
import pygame
|
||||
|
||||
DEBUG_DRAWING = False
|
||||
LOOK_AHEAD = 10
|
||||
|
||||
@@ -98,9 +97,9 @@ class MyState:
|
||||
|
||||
|
||||
class FrictionDetector(b2ContactListener):
|
||||
def __init__(self, env):
|
||||
def __init__(self, car_env):
|
||||
super().__init__()
|
||||
self.env = env
|
||||
self.env = car_env
|
||||
|
||||
def BeginContact(self, contact):
|
||||
self._contact(contact, True)
|
||||
@@ -142,6 +141,17 @@ class FrictionDetector(b2ContactListener):
|
||||
self.env.on_road = len(obj.tiles) > 0
|
||||
|
||||
|
||||
def _world_to_screen(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
|
||||
|
||||
|
||||
class CarRacing:
|
||||
metadata = {
|
||||
"render_modes": ["human", "rgb_array", None],
|
||||
@@ -150,6 +160,7 @@ class CarRacing:
|
||||
|
||||
def __init__(self, seed_value: int = 5, render_mode: str | None = "human"):
|
||||
|
||||
self.road_poly = None
|
||||
self.offroad_frames = None
|
||||
if seeding is not None:
|
||||
self.np_random, _ = seeding.np_random(seed_value)
|
||||
@@ -216,9 +227,9 @@ class CarRacing:
|
||||
if self._pg is None:
|
||||
self._pg = self._PygameCtx()
|
||||
if not self._pg.initialized:
|
||||
import pygame
|
||||
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:
|
||||
@@ -232,23 +243,13 @@ class CarRacing:
|
||||
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]
|
||||
pts = [_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):
|
||||
@@ -258,7 +259,7 @@ class CarRacing:
|
||||
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]
|
||||
pts = [_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):
|
||||
@@ -432,8 +433,8 @@ class CarRacing:
|
||||
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.ctrl_pts = np.array(list(map(lambda x_coord: x_coord[2:], self.track)))
|
||||
self.angles = np.array(list(map(lambda x_coord: x_coord[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)))
|
||||
@@ -468,7 +469,7 @@ class CarRacing:
|
||||
self._no_progress_steps = 0
|
||||
self._stall_steps = 0
|
||||
|
||||
def reset(self, *, seed: int | None = None, options: dict | None = None):
|
||||
def reset(self, *, seed: int | None = None):
|
||||
if seed is not None:
|
||||
|
||||
if seeding is not None:
|
||||
@@ -476,8 +477,9 @@ class CarRacing:
|
||||
else:
|
||||
self.np_random = np.random.RandomState(seed)
|
||||
self._build_new_episode()
|
||||
obs = self._get_observation()
|
||||
info = {}
|
||||
obs, _, _, _, info = self.step(
|
||||
np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
)
|
||||
return obs, info
|
||||
|
||||
def fast_reset(self):
|
||||
@@ -509,12 +511,12 @@ class CarRacing:
|
||||
|
||||
return self.step(np.array([0.0, 0.0, 0.0], dtype=np.float32))
|
||||
|
||||
def step(self, action):
|
||||
def step(self, env_action):
|
||||
|
||||
if action is not None:
|
||||
self.car.steer(-float(action[0]))
|
||||
self.car.gas(float(action[1]))
|
||||
self.car.brake(float(action[2]))
|
||||
if env_action is not None:
|
||||
self.car.steer(-float(env_action[0]))
|
||||
self.car.gas(float(env_action[1]))
|
||||
self.car.brake(float(env_action[2]))
|
||||
|
||||
self.car.step(1.0 / FPS)
|
||||
self.world.Step(1.0 / FPS, 6 * 30, 2 * 30)
|
||||
@@ -522,27 +524,27 @@ class CarRacing:
|
||||
|
||||
self.steps += 1
|
||||
|
||||
terminated = False
|
||||
truncated = False
|
||||
env_terminated = False
|
||||
env_truncated = False
|
||||
|
||||
if action is not None:
|
||||
if env_action is not None:
|
||||
|
||||
self.reward -= 5.0 / FPS
|
||||
|
||||
if self.tile_visited_count == len(self.track):
|
||||
terminated = True
|
||||
env_terminated = True
|
||||
|
||||
x, y = self.car.hull.position
|
||||
if abs(x) > PLAYFIELD or abs(y) > PLAYFIELD:
|
||||
self.reward -= 100.0
|
||||
terminated = True
|
||||
env_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
|
||||
env_terminated = True
|
||||
else:
|
||||
self.offroad_frames = 0
|
||||
|
||||
@@ -552,7 +554,7 @@ class CarRacing:
|
||||
else:
|
||||
self._no_progress_steps += 1
|
||||
if self._no_progress_steps >= NO_PROGRESS_STEPS:
|
||||
truncated = True
|
||||
env_truncated = True
|
||||
|
||||
step_reward = self.reward - self.prev_reward
|
||||
self.prev_reward = self.reward
|
||||
@@ -605,11 +607,10 @@ class CarRacing:
|
||||
|
||||
obs = self._get_observation()
|
||||
info = {"features": self.my_state}
|
||||
return obs, step_reward, terminated, truncated, info
|
||||
return obs, step_reward, env_terminated, env_truncated, info
|
||||
|
||||
def _get_observation(self):
|
||||
|
||||
return None
|
||||
return np.array(self.get_feature_vector(), dtype=np.float32)
|
||||
|
||||
def render(self):
|
||||
self._ensure_pygame()
|
||||
@@ -636,10 +637,10 @@ class CarRacing:
|
||||
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)
|
||||
p0 = _world_to_screen(x0, y0, zoom, angle, scroll_x, scroll_y)
|
||||
p1 = _world_to_screen(x1, y0, zoom, angle, scroll_x, scroll_y)
|
||||
p2 = _world_to_screen(x1, y1, zoom, angle, scroll_x, scroll_y)
|
||||
p3 = _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:
|
||||
|
||||
@@ -3,12 +3,13 @@ import logging
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from mathema.core.population_monitor import init_population
|
||||
from mathema.genotype.neo4j.genotype import neo4j
|
||||
from mathema.utils.logging_config import setup_logging
|
||||
|
||||
setup_logging()
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
N_RUNS = 10
|
||||
N_RUNS = 20
|
||||
|
||||
|
||||
async def run_single_car_experiment(run_idx: int):
|
||||
@@ -23,12 +24,32 @@ async def run_single_car_experiment(run_idx: int):
|
||||
"competition",
|
||||
))
|
||||
|
||||
# 👉 warten, bis der Monitor sich selbst beendet
|
||||
await monitor._stopped_evt.wait()
|
||||
try:
|
||||
# ⏱️ max. 35 Minuten warten (30 min Training + Puffer)
|
||||
await asyncio.wait_for(
|
||||
monitor._stopped_evt.wait(),
|
||||
timeout=35 * 60
|
||||
)
|
||||
|
||||
# optional: letzte Stats loggen
|
||||
except asyncio.TimeoutError:
|
||||
log.error(
|
||||
f"[RUN {run_idx:02d}] TIMEOUT after 35min – forcing shutdown"
|
||||
)
|
||||
|
||||
try:
|
||||
await monitor.stop("shutdown")
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
f"[RUN {run_idx:02d}] failed to stop monitor cleanly: {e}"
|
||||
)
|
||||
|
||||
# --- Post-run logging ---
|
||||
s = monitor.state
|
||||
best = await monitor._best_fitness_in_population(s.population_id)
|
||||
try:
|
||||
best = await monitor._best_fitness_in_population(s.population_id)
|
||||
except Exception:
|
||||
best = float("nan")
|
||||
|
||||
log.info(
|
||||
f"=== END RUN {run_idx + 1}/{N_RUNS} "
|
||||
f"gens={s.pop_gen} best_fitness={best:.6f} evals={s.eval_acc} ==="
|
||||
@@ -37,11 +58,15 @@ async def run_single_car_experiment(run_idx: int):
|
||||
|
||||
async def main():
|
||||
load_dotenv()
|
||||
|
||||
for i in range(N_RUNS):
|
||||
await run_single_car_experiment(i)
|
||||
|
||||
log.info("=== ALL RUNS FINISHED ===")
|
||||
try:
|
||||
for i in range(N_RUNS):
|
||||
await run_single_car_experiment(i)
|
||||
log.info("=== ALL RUNS FINISHED ===")
|
||||
finally:
|
||||
try:
|
||||
await neo4j.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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())
|
||||
@@ -10,7 +10,6 @@ import uuid
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from mathema.core.db import Neo4jDB
|
||||
from mathema.genotype.neo4j.genotype_mutator import GenotypeMutator
|
||||
|
||||
neo4j = Neo4jDB(
|
||||
user="neo4j",
|
||||
@@ -25,6 +24,17 @@ def now_unique():
|
||||
|
||||
|
||||
async def construct_agent(specie_id: any, agent_id: any, spec_con: Dict[str, Any]):
|
||||
"""
|
||||
|
||||
Constructs an agent with the provided parameters and stores it in the database.
|
||||
|
||||
:param specie_id: Identifier of the species the agent belongs to.
|
||||
:param agent_id: Identifier of the agent being constructed.
|
||||
:param spec_con: Dictionary containing specifications for the agent creation.
|
||||
|
||||
:return: Dictionary representing the constructed agent.
|
||||
|
||||
"""
|
||||
random.seed(time.time())
|
||||
generation = 0
|
||||
cx_id = await construct_cortex(agent_id, generation, spec_con)
|
||||
@@ -89,7 +99,20 @@ async def construct_agent(specie_id: any, agent_id: any, spec_con: Dict[str, Any
|
||||
return agent
|
||||
|
||||
|
||||
async def construct_cortex(agent_id, generation: any, spec_con: Dict[str, Any]):
|
||||
async def construct_cortex(_, generation: any, spec_con: Dict[str, Any]):
|
||||
"""
|
||||
|
||||
Async method to construct a cortex with given generation and specification configuration.
|
||||
|
||||
Parameters:
|
||||
_ : Ignored parameter
|
||||
generation: any - The generation of the cortex
|
||||
spec_con: Dict[str, Any] - The specification configuration for the cortex
|
||||
|
||||
Returns:
|
||||
str - The unique identifier of the constructed cortex
|
||||
|
||||
"""
|
||||
from importlib import import_module
|
||||
|
||||
morphology_mod = import_module("mathema.core.morphology")
|
||||
@@ -122,8 +145,9 @@ async def construct_cortex(agent_id, generation: any, spec_con: Dict[str, Any]):
|
||||
})
|
||||
|
||||
neuron_ids, neurons = await construct_initial_neurolayer(cx_uid, generation, spec_con, sensors, actuators)
|
||||
sensor_ids = [s["id"] for s in sensors]
|
||||
actuator_ids = [a["id"] for a in actuators]
|
||||
|
||||
# sensor_ids = [s["id"] for s in sensors]
|
||||
# actuator_ids = [a["id"] for a in actuators]
|
||||
|
||||
await write_cortex({"id": str(cx_uid)})
|
||||
await write_neurons(neurons)
|
||||
@@ -136,6 +160,24 @@ async def construct_cortex(agent_id, generation: any, spec_con: Dict[str, Any]):
|
||||
|
||||
|
||||
async def construct_initial_neurolayer(cx_id, generation, spec_con, sensors, actuators):
|
||||
"""
|
||||
Async function to construct initial neural layer.
|
||||
The initial neuro layer is constructed as follows:
|
||||
choose a random sensor with free capacity and connect it to
|
||||
a given neuron. Choose a random actuator with free capacity and
|
||||
connect the neuron to it.
|
||||
|
||||
Params:
|
||||
- cx_id (str): The ID of the context.
|
||||
- generation (int): The generation of the layer.
|
||||
- spec_con (dict): The specification of the layer.
|
||||
- sensors (list): List of sensors.
|
||||
- actuators (list): List of actuators.
|
||||
|
||||
Returns:
|
||||
- neuron_ids (list): List of neuron IDs.
|
||||
- neurons (list): List of constructed neurons.
|
||||
"""
|
||||
neuron_ids = []
|
||||
neurons = []
|
||||
for actuator in actuators:
|
||||
@@ -168,7 +210,14 @@ async def construct_initial_neurolayer(cx_id, generation, spec_con, sensors, act
|
||||
|
||||
async def link_units(sensors, neurons, actuators, cortex):
|
||||
"""
|
||||
link all units of the network with correct weights
|
||||
Link units in the brain model with sensors, neurons, actuators, and the cortex.
|
||||
|
||||
Parameters:
|
||||
- sensors (List[dict]): List of sensor data dictionaries.
|
||||
- neurons (List[dict]): List of neuron data dictionaries.
|
||||
- actuators (List[dict]): List of actuator data dictionaries.
|
||||
- cortex (dict): Cortex data dictionary.
|
||||
|
||||
"""
|
||||
s2n_rows = []
|
||||
for n in neurons:
|
||||
@@ -235,13 +284,26 @@ async def link_units(sensors, neurons, actuators, cortex):
|
||||
MERGE (cx)-[:HAS]->(a)
|
||||
""", rows=[{"id": str(a["id"])} for a in actuators], cx_id=str(cortex["id"]))
|
||||
|
||||
pass
|
||||
|
||||
|
||||
async def construct_neuron(cx_id, generation, spec_con, n_id, input_specs, output_specs, output_ids, layer_index):
|
||||
"""
|
||||
async def construct_neuron(_, generation, spec_con, n_id, input_specs, output_specs, output_ids, layer_index):
|
||||
"""
|
||||
|
||||
This method constructs a neuron object with the given parameters.
|
||||
|
||||
Parameters:
|
||||
- _: placeholder for the global activation function shared between neurons
|
||||
- generation: the current generation of the neuron
|
||||
- spec_con: dictionary containing configuration specifications
|
||||
- n_id: unique identifier for the neuron
|
||||
- input_specs: list of input specifications for the neuron
|
||||
- output_specs: list of output specifications for the neuron
|
||||
- output_ids: list of unique identifiers for the neuron's outputs
|
||||
- layer_index: index of the layer where the neuron is located
|
||||
|
||||
Returns:
|
||||
- neuron: dictionary representing the constructed neuron with the provided parameters
|
||||
|
||||
"""
|
||||
bias = None
|
||||
|
||||
neuron = {
|
||||
@@ -283,7 +345,14 @@ async def write_actuators(actuators):
|
||||
|
||||
async def write_neuron(neuron):
|
||||
"""
|
||||
write neuron to database
|
||||
|
||||
Write data of a neuron to the Neo4j database.
|
||||
|
||||
:param neuron: Dictionary representing the neuron data to be written.
|
||||
:type neuron: dict
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
await neo4j.run_consume("""
|
||||
MERGE (n:neuron {id: $id})
|
||||
@@ -302,7 +371,10 @@ async def write_neuron(neuron):
|
||||
|
||||
async def write_sensor(sensor):
|
||||
"""
|
||||
write sensor to database
|
||||
Method to write sensor data to Neo4j.
|
||||
|
||||
:param sensor: dictionary containing sensor data
|
||||
:type sensor: dict
|
||||
"""
|
||||
await neo4j.run_consume("""
|
||||
MERGE (s:sensor {id: $id})
|
||||
@@ -321,7 +393,16 @@ async def write_sensor(sensor):
|
||||
|
||||
async def write_actuator(actuator):
|
||||
"""
|
||||
write actuator to database
|
||||
Write actuator node in the graph database.
|
||||
|
||||
Parameters:
|
||||
- actuator (dict): A dictionary containing information about the actuator.
|
||||
It should have the following keys:
|
||||
- id (str): The unique identifier of the actuator.
|
||||
- name (str): The name of the actuator.
|
||||
- scape (str): The scape of the actuator.
|
||||
- vector_length (int): The length of the vector.
|
||||
- generation (int): The generation information of the actuator.
|
||||
"""
|
||||
await neo4j.run_consume("""
|
||||
MERGE (a:actuator {id: $id})
|
||||
@@ -340,9 +421,16 @@ async def write_actuator(actuator):
|
||||
|
||||
async def compute_pattern(cx_id: str):
|
||||
"""
|
||||
Liefert (pattern_ids, pattern_counts):
|
||||
- pattern_ids: [{ "layer_index": L, "neuron_ids": [..] }, ...] (IDs als Strings, stabil sortiert)
|
||||
- pattern: [{ "layer_index": L, "count": K }, ...]
|
||||
|
||||
Compute pattern for a given cortex ID.
|
||||
|
||||
Arguments:
|
||||
- cx_id (str): The ID of the cortex for which to compute the pattern.
|
||||
|
||||
Returns:
|
||||
- pattern_ids (list): A list of dictionaries containing the layer index and neuron IDs.
|
||||
- pattern (list): A list of dictionaries containing the layer index and the count of neuron IDs in that layer.
|
||||
|
||||
"""
|
||||
rows = await neo4j.read_all("""
|
||||
MATCH (cx:cortex {id:$cx_id})-[:HAS]->(n:neuron)
|
||||
@@ -362,8 +450,18 @@ async def compute_pattern(cx_id: str):
|
||||
|
||||
async def compute_generalized_io(cx_id: str):
|
||||
"""
|
||||
Liefert zwei Listen (generalized_sensors, generalized_actuators) ohne IDs/Cortex-Bezug.
|
||||
Je Element: nur (name, scape, vector_length). Stabil sortiert.
|
||||
|
||||
Async method to compute the generalized input and output for a given cortex ID.
|
||||
|
||||
Parameters:
|
||||
cx_id (str): ID of the cortex.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Dict[str, Union[str, int]]], List[Dict[str, Union[str, int]]]]:
|
||||
A tuple containing a list of dictionaries representing the normalized sensor data
|
||||
and a list of dictionaries representing the normalized actuator data. Each dictionary
|
||||
contains keys 'name' (str), 'scape' (str), and 'vector_length' (int).
|
||||
|
||||
"""
|
||||
s_rows = await neo4j.read_all("""
|
||||
MATCH (cx:cortex {id:$cx_id})-[:HAS]->(s:sensor)
|
||||
@@ -384,7 +482,7 @@ async def compute_generalized_io(cx_id: str):
|
||||
return normalize(s_rows), normalize(a_rows)
|
||||
|
||||
|
||||
async def compute_generalized_evo_hist(agent_id: str):
|
||||
async def compute_generalized_evo_hist(_):
|
||||
"""
|
||||
place holder until mutator works
|
||||
"""
|
||||
@@ -392,6 +490,15 @@ async def compute_generalized_evo_hist(agent_id: str):
|
||||
|
||||
|
||||
async def update_fingerprint(agent_id: str):
|
||||
"""
|
||||
Update fingerprint data for a given agent in the Neo4j database.
|
||||
|
||||
Parameters:
|
||||
agent_id (str): The unique identifier for the agent.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
rows = await neo4j.read_all("""
|
||||
MATCH (a:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
RETURN cx.id AS cx_id
|
||||
@@ -439,15 +546,21 @@ async def update_fingerprint(agent_id: str):
|
||||
)
|
||||
|
||||
|
||||
async def speciate(self, agent_id):
|
||||
pass
|
||||
|
||||
|
||||
async def clone_agent(agent_id: Any, clone_agent_id: Any) -> Any:
|
||||
"""
|
||||
Klont den kompletten Genotyp (Cortex, Sensoren, Neuronen, Aktuatoren + Kanten)
|
||||
eines Agents unter neuer Agent-ID `clone_agent_id`.
|
||||
Gibt die Clone-Agent-ID zurück.
|
||||
|
||||
Async method to clone an existing agent in Neo4j database.
|
||||
|
||||
Parameters:
|
||||
- agent_id: Identifier of the existing agent to be cloned.
|
||||
- clone_agent_id: Identifier of the cloned agent.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
|
||||
This method clones the specified agent along with its associated data in the Neo4j database.
|
||||
If the specified agent is not found, a ValueError will be raised.
|
||||
|
||||
"""
|
||||
aid = str(agent_id)
|
||||
cid = str(clone_agent_id)
|
||||
@@ -476,19 +589,21 @@ async def clone_agent(agent_id: Any, clone_agent_id: Any) -> Any:
|
||||
pattern_json = arow.get("pattern_json") or "[]"
|
||||
pattern_layers = arow.get("pattern_layers") or []
|
||||
pattern_counts = arow.get("pattern_counts") or []
|
||||
evo_hist = arow.get("evo_hist") or []
|
||||
evo_hist = [json.dumps({"op": "clone_from", "parent": aid}, separators=(",", ":"))]
|
||||
population_id = arow.get("population_id")
|
||||
fitness = arow.get("fitness")
|
||||
src_cx = str(arow["cxid"])
|
||||
|
||||
srows = await neo4j.read_all("""
|
||||
MATCH (:cortex {id:$cx})-[:HAS]->(s:sensor)
|
||||
RETURN s.id AS id, s.name AS name, s.scape AS scape, toInteger(s.vector_length) AS vl, toInteger(s.generation) AS gen
|
||||
RETURN s.id AS id, s.name AS name, s.scape AS scape, toInteger(s.vector_length) AS vl,
|
||||
toInteger(s.generation) AS gen
|
||||
""", cx=src_cx)
|
||||
|
||||
arows2 = await neo4j.read_all("""
|
||||
MATCH (:cortex {id:$cx})-[:HAS]->(a:actuator)
|
||||
RETURN a.id AS id, a.name AS name, a.scape AS scape, toInteger(a.vector_length) AS vl, toInteger(a.generation) AS gen
|
||||
RETURN a.id AS id, a.name AS name, a.scape AS scape, toInteger(a.vector_length) AS vl,
|
||||
toInteger(a.generation) AS gen
|
||||
""", cx=src_cx)
|
||||
|
||||
nrows = await neo4j.read_all("""
|
||||
@@ -510,6 +625,11 @@ async def clone_agent(agent_id: Any, clone_agent_id: Any) -> Any:
|
||||
RETURN n.id AS nid, a.id AS aid, r.weights AS weights
|
||||
""", cx=src_cx)
|
||||
|
||||
n2n = await neo4j.read_all("""
|
||||
MATCH (:cortex {id:$cx})-[:HAS]->(src:neuron)-[r:FORWARD]->(dst:neuron)<-[:HAS]-(:cortex {id:$cx})
|
||||
RETURN src.id AS sid, dst.id AS did, r.weights AS weights, coalesce(r.recurrent,false) AS recurrent
|
||||
""", cx=src_cx)
|
||||
|
||||
def new_id() -> str:
|
||||
return uuid.uuid4().hex
|
||||
|
||||
@@ -625,6 +745,20 @@ async def clone_agent(agent_id: Any, clone_agent_id: Any) -> Any:
|
||||
"weights": [float(x) for x in (r.get("weights") or [])],
|
||||
} for r in n2a])
|
||||
|
||||
if n2n:
|
||||
await neo4j.run_consume("""
|
||||
UNWIND $rows AS row
|
||||
MATCH (src:neuron {id: row.from_id}), (dst:neuron {id: row.to_id})
|
||||
MERGE (src)-[r:FORWARD]->(dst)
|
||||
SET r.weights = [x IN row.weights | toFloat(x)],
|
||||
r.recurrent = row.recurrent
|
||||
""", rows=[{
|
||||
"from_id": id_map[str(r["sid"])],
|
||||
"to_id": id_map[str(r["did"])],
|
||||
"weights": [float(x) for x in (r.get("weights") or [])],
|
||||
"recurrent": bool(r.get("recurrent", False)),
|
||||
} for r in n2n])
|
||||
|
||||
await neo4j.run_consume("""
|
||||
MERGE (a:agent {id:$aid})
|
||||
SET a.generation = toInteger($generation),
|
||||
@@ -661,6 +795,14 @@ async def clone_agent(agent_id: Any, clone_agent_id: Any) -> Any:
|
||||
|
||||
|
||||
async def _get_cortex_id_of_agent(agent_id: str) -> str:
|
||||
"""
|
||||
|
||||
This method retrieves the cortex ID of a given agent ID.
|
||||
|
||||
:param agent_id: A string representing the ID of the agent.
|
||||
:return: A string representing the cortex ID of the agent.
|
||||
|
||||
"""
|
||||
rows = await neo4j.read_all(
|
||||
"""
|
||||
MATCH (a:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
@@ -674,6 +816,15 @@ async def _get_cortex_id_of_agent(agent_id: str) -> str:
|
||||
|
||||
|
||||
async def _list_ids_under_cortex(cx_id: str):
|
||||
"""
|
||||
List all the IDs under the given cortex ID.
|
||||
|
||||
:param cx_id: The ID of the cortex.
|
||||
:type cx_id: str
|
||||
|
||||
:returns: A tuple containing three lists, each list is a collection of IDs for sensors, neurons, and actuators under the given cortex ID respectively.
|
||||
:rtype: tuple
|
||||
"""
|
||||
s_rows = await neo4j.read_all(
|
||||
"MATCH (cx:cortex {id:$cx})-[:HAS]->(s:sensor) RETURN s.id AS id ORDER BY id",
|
||||
cx=str(cx_id),
|
||||
@@ -690,6 +841,12 @@ async def _list_ids_under_cortex(cx_id: str):
|
||||
|
||||
|
||||
async def _count_n2a(aid: str) -> int:
|
||||
"""
|
||||
Count the number of FORWARD relationships from a neuron to an actuator with the specified ID.
|
||||
|
||||
:param aid: The ID of the actuator.
|
||||
:return: The count of FORWARD relationships (int).
|
||||
"""
|
||||
rows = await neo4j.read_all("""
|
||||
MATCH (:neuron)-[r:FORWARD]->(a:actuator {id:$aid})
|
||||
RETURN count(r) AS k
|
||||
@@ -698,6 +855,15 @@ async def _count_n2a(aid: str) -> int:
|
||||
|
||||
|
||||
async def _get_one_n2a_link(aid: str):
|
||||
"""
|
||||
|
||||
Async method to get one Neuron to Actuator link based on the provided Actuator ID.
|
||||
|
||||
:param aid: str - The ID of the actuator for which the neuron to actuator link is to be retrieved.
|
||||
|
||||
:return: The neuron ID associated with the actuator ID provided. Returns None if no link is found.
|
||||
|
||||
"""
|
||||
rows = await neo4j.read_all(
|
||||
"""
|
||||
MATCH (n:neuron)-[:FORWARD]->(a:actuator {id:$aid})
|
||||
@@ -710,153 +876,6 @@ async def _get_one_n2a_link(aid: str):
|
||||
return rows[0]["nid"] if rows else None
|
||||
|
||||
|
||||
async def test():
|
||||
specie_id = "test"
|
||||
agent_id = "test"
|
||||
clone_agent_id = "test_clone"
|
||||
spec_con = {"morphology": "xor_mimic", "neural_afs": ["tanh", "cos", "gauss", "abs"]}
|
||||
|
||||
await construct_agent(specie_id, agent_id, spec_con)
|
||||
await clone_agent(agent_id, clone_agent_id)
|
||||
|
||||
await neo4j.close()
|
||||
|
||||
|
||||
async def test_mut_operators():
|
||||
specie_id = "test"
|
||||
agent_id = "test"
|
||||
clone_agent_id = "test_clone"
|
||||
spec_con = {"morphology": "xor_mimic", "neural_afs": ["tanh", "cos", "gauss", "abs"]}
|
||||
|
||||
genotype_mutator = GenotypeMutator(neo4j)
|
||||
|
||||
print("[TEST] construct_agent")
|
||||
await construct_agent(specie_id, agent_id, spec_con)
|
||||
|
||||
cx_id = await _get_cortex_id_of_agent(agent_id)
|
||||
sensors, neurons, actuators = await _list_ids_under_cortex(cx_id)
|
||||
print(f"[TEST] cortex={cx_id} | S={len(sensors)} N={len(neurons)} A={len(actuators)}")
|
||||
|
||||
print("[TEST] link neuron->neuron (self-loop)")
|
||||
n0 = neurons[0]
|
||||
await genotype_mutator.link_from_element_to_element(agent_id, n0, n0)
|
||||
rows = await neo4j.read_all(
|
||||
"""
|
||||
MATCH (:neuron {id:$nid})-[r:FORWARD]->(:neuron {id:$nid})
|
||||
RETURN count(r) AS k
|
||||
""",
|
||||
nid=str(n0),
|
||||
)
|
||||
assert int(rows[0]["k"]) == 1, "Expected self-loop to exist"
|
||||
|
||||
print("[TEST] link sensor->neuron (first non-duplicate)")
|
||||
linked_ok = False
|
||||
for sid in sensors:
|
||||
for nid in neurons[::-1]:
|
||||
try:
|
||||
await genotype_mutator.link_from_element_to_element(agent_id, sid, nid)
|
||||
linked_ok = True
|
||||
chosen_s, chosen_n = sid, nid
|
||||
break
|
||||
except ValueError as e:
|
||||
|
||||
if "already exists" in str(e):
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
if linked_ok:
|
||||
break
|
||||
print(
|
||||
f"[TEST] S->N linked: {linked_ok} ({chosen_s} -> {chosen_n})" if linked_ok else "[TEST] no new S->N link possible")
|
||||
|
||||
print("[TEST] link neuron->actuator (expect full, then free one slot)")
|
||||
a0 = actuators[0]
|
||||
n_try = neurons[-1]
|
||||
try:
|
||||
await genotype_mutator.link_from_element_to_element(agent_id, n_try, a0)
|
||||
|
||||
print("[TEST] N->A linked without freeing capacity (actuator had space)")
|
||||
except ValueError as e:
|
||||
if "fully connected" in str(e):
|
||||
print("[TEST] actuator full as expected; cutting one existing N->A to free capacity")
|
||||
victim_n = await _get_one_n2a_link(a0)
|
||||
assert victim_n is not None, "No existing N->A to cut, unexpected"
|
||||
await genotype_mutator.cut_link(victim_n, a0)
|
||||
|
||||
k_after = await _count_n2a(a0)
|
||||
rows_vl = await neo4j.read_all(
|
||||
"MATCH (a:actuator {id:$aid}) RETURN toInteger(a.vector_length) AS vl",
|
||||
aid=str(a0),
|
||||
)
|
||||
vl = int(rows_vl[0]["vl"])
|
||||
assert k_after < vl, "Capacity not freed as expected"
|
||||
|
||||
await genotype_mutator.link_from_element_to_element(agent_id, n_try, a0)
|
||||
print(f"[TEST] N->A linked after freeing capacity ({n_try} -> {a0})")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("[TEST] mutate weights")
|
||||
n_id = await genotype_mutator.mutate_weights(agent_id)
|
||||
print(f"[TEST] mutated weights: {n_id}")
|
||||
|
||||
print("[TEST] get activation function from neuron")
|
||||
print(await genotype_mutator.get_spec_neural_afs(agent_id))
|
||||
print("------------------------------------------")
|
||||
|
||||
print("[TEST] cut neuron->neuron (self-loop)")
|
||||
await genotype_mutator.cut_link(n0, n0)
|
||||
rows = await neo4j.read_all(
|
||||
"""
|
||||
MATCH (:neuron {id:$nid})-[r:FORWARD]->(:neuron {id:$nid})
|
||||
RETURN count(r) AS k
|
||||
""",
|
||||
nid=str(n0),
|
||||
)
|
||||
assert int(rows[0]["k"]) == 0, "Expected self-loop to be cut"
|
||||
|
||||
await update_fingerprint(agent_id)
|
||||
|
||||
print("[TEST] clone_agent")
|
||||
await clone_agent(agent_id, clone_agent_id)
|
||||
|
||||
await neo4j.close()
|
||||
print("[TEST] done.")
|
||||
|
||||
|
||||
async def test_add_neuron():
|
||||
specie_id = "test"
|
||||
agent_id = "test"
|
||||
clone_agent_id = "test_clone"
|
||||
spec_con = {"morphology": "xor_mimic", "neural_afs": ["tanh", "cos", "gauss", "abs"]}
|
||||
|
||||
genotype_mutator = GenotypeMutator(neo4j)
|
||||
|
||||
print("[TEST] construct_agent")
|
||||
await construct_agent(specie_id, agent_id, spec_con)
|
||||
|
||||
print("cloning agent")
|
||||
await clone_agent(agent_id, clone_agent_id)
|
||||
|
||||
print("mutating cloned agent: adding neuron")
|
||||
await genotype_mutator.outsplice(clone_agent_id)
|
||||
|
||||
print("mutating weights")
|
||||
await genotype_mutator.mutate_weights(clone_agent_id)
|
||||
|
||||
print("add bias")
|
||||
await genotype_mutator.add_bias(clone_agent_id)
|
||||
|
||||
print("add sensor")
|
||||
await genotype_mutator.add_sensor(clone_agent_id)
|
||||
|
||||
await neo4j.close()
|
||||
|
||||
|
||||
async def create_test():
|
||||
pass
|
||||
|
||||
|
||||
def generate_ids(vector_length):
|
||||
return [uuid.uuid4().hex for _ in range(vector_length)]
|
||||
|
||||
@@ -876,8 +895,13 @@ def create_input_weights(input_specs):
|
||||
|
||||
async def delete_agent(agent_id: Any):
|
||||
"""
|
||||
Löscht einen Agent und seinen gesamten Genotyp (Cortex, Neuronen, Sensoren, Aktuatoren + Kanten)
|
||||
anhand der Agent-ID.
|
||||
Delete agent from the graph database along with related cortex, neurons, sensors, and actuators.
|
||||
|
||||
Parameters:
|
||||
agent_id (Any): The unique identifier of the agent to be deleted.
|
||||
|
||||
Return Type:
|
||||
None
|
||||
"""
|
||||
aid = str(agent_id)
|
||||
|
||||
@@ -901,18 +925,24 @@ def _generate_neuron_af(afs: List[Any]):
|
||||
return random.choice(afs)
|
||||
|
||||
|
||||
def _generalize_evo_hist(evo_hist):
|
||||
def _generalize_evo_hist(_):
|
||||
pass
|
||||
|
||||
|
||||
async def print_agent(agent_id: Any):
|
||||
"""
|
||||
Gibt den kompletten Genotyp eines Agenten formatiert aus:
|
||||
- Agent-Props
|
||||
- Cortex
|
||||
- Sensoren
|
||||
- Neuronen
|
||||
- Aktuatoren
|
||||
|
||||
Async method print_agent to print details of an agent based on the provided agent_id.
|
||||
|
||||
Parameters:
|
||||
- agent_id: Any - the identifier of the agent to print details for
|
||||
|
||||
The method executes various queries to retrieve information about the agent, its cortex, sensors, neurons,
|
||||
actuators, as well as the connections between sensors/neurons and neurons/actuators.
|
||||
It then prints the retrieved data in a formatted manner.
|
||||
|
||||
This method does not return a value, it directly prints the information to the console.
|
||||
|
||||
"""
|
||||
aid = str(agent_id)
|
||||
|
||||
@@ -987,17 +1017,15 @@ async def print_agent(agent_id: Any):
|
||||
|
||||
async def load_genotype_snapshot(agent_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Liefert eine JSON-ähnliche Struktur für das Exoself:
|
||||
{
|
||||
"cortex": {"id": ...},
|
||||
"sensors": [{id, name, scape, vector_length}],
|
||||
"actuators": [{id, name, scape, vector_length, fanin_ids}],
|
||||
"neurons": [{
|
||||
"id", "activation_function", "layer_index",
|
||||
"bias": float|None,
|
||||
"input_weights": [{ "input_id": str, "weights": [float,...] }]
|
||||
}]
|
||||
}
|
||||
|
||||
Method to load genotype snapshot for a given agent.
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): The identifier of the agent.
|
||||
|
||||
Returns:
|
||||
- Dict[str, Any]: A dictionary containing the genotype snapshot information for the agent.
|
||||
|
||||
"""
|
||||
rows = await neo4j.read_all("""
|
||||
MATCH (a:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
@@ -1078,9 +1106,19 @@ async def persist_neuron_backups(
|
||||
edge_rows: List[Dict[str, Any]],
|
||||
) -> None:
|
||||
"""
|
||||
Schreibt die von Exoself/Neuronen gelieferten Backups:
|
||||
- bias_rows: [{ "nid": str, "bias": float }]
|
||||
- edge_rows: [{ "from_id": str, "to_id": str, "weights": [float,...] }]
|
||||
|
||||
This method persists neuron backups to a Neo4j database by updating the b
|
||||
ias and edge weights of neurons based on the provided input data.
|
||||
|
||||
Parameters:
|
||||
- bias_rows: A list of dictionaries where each dictionary represents a
|
||||
neuron's ID and its corresponding bias value.
|
||||
- edge_rows: A list of dictionaries where each dictionary represents the source neuron ID,
|
||||
destination neuron ID, and the weights of the edges between them.
|
||||
|
||||
Return Type:
|
||||
None
|
||||
|
||||
"""
|
||||
if bias_rows:
|
||||
await neo4j.run_consume("""
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
read and write api for exoself
|
||||
"""
|
||||
@@ -56,7 +56,20 @@ class GenotypeMutator:
|
||||
self.neo4j = neo4j
|
||||
|
||||
async def _pick_random_neuron_id(self, agent_id: str) -> str:
|
||||
"""Wählt zufällig ein Neuron unter dem Cortex des Agents aus."""
|
||||
"""
|
||||
|
||||
This method picks a random neuron ID related to a specific agent from the Neo4j database.
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): The ID of the agent for which a random neuron ID will be selected.
|
||||
|
||||
Returns:
|
||||
- str: A randomly selected neuron ID related to the specified agent.
|
||||
|
||||
Raises:
|
||||
- ValueError: If no neurons are found for the specified agent ID.
|
||||
|
||||
"""
|
||||
rows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (a:agent {id:$aid})-[:OWNS]->(cx:cortex)-[:HAS]->(n:neuron)
|
||||
@@ -69,6 +82,16 @@ class GenotypeMutator:
|
||||
return random.choice([r["nid"] for r in rows])
|
||||
|
||||
async def _append_evo(self, agent_id: str, entry: dict):
|
||||
"""
|
||||
Append a new entry to the evolution history of a specific agent.
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): The unique identifier of the agent.
|
||||
- entry (dict): The entry to be appended to the evolution history.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
s = json.dumps(entry, separators=(",", ":"))
|
||||
await self.neo4j.run_consume(
|
||||
"""
|
||||
@@ -80,6 +103,17 @@ class GenotypeMutator:
|
||||
)
|
||||
|
||||
async def _get_cortex_id_of_neuron(self, nid: str) -> str:
|
||||
"""
|
||||
|
||||
This method retrieves the cortex ID of a neuron based on the provided neuron ID.
|
||||
|
||||
Parameters:
|
||||
- nid (str): The ID of the neuron to query for cortex ID.
|
||||
|
||||
Returns:
|
||||
- str: The cortex ID associated with the provided neuron ID.
|
||||
|
||||
"""
|
||||
rows = await self.neo4j.read_all(
|
||||
"MATCH (cx:cortex)-[:HAS]->(n:neuron {id:$nid}) RETURN cx.id AS cxid",
|
||||
nid=str(nid),
|
||||
@@ -89,7 +123,17 @@ class GenotypeMutator:
|
||||
return rows[0]["cxid"]
|
||||
|
||||
async def get_spec_neural_afs(self, agent_id: str) -> list[str]:
|
||||
"""Holt die Liste erlaubter Aktivierungsfunktionen aus a.spec_con_json.neural_afs."""
|
||||
"""
|
||||
|
||||
async def get_spec_neural_afs(self, agent_id: str) -> list[str]:
|
||||
Retrieve the list of specified neural activation functions for a given agent.
|
||||
|
||||
Parameters:
|
||||
agent_id (str): The unique identifier of the agent.
|
||||
|
||||
Returns:
|
||||
list[str]: A list of neural activation functions specified for the agent, as strings.
|
||||
"""
|
||||
rows = await self.neo4j.read_all(
|
||||
"MATCH (a:agent {id:$aid}) RETURN a.spec_con_json AS scj",
|
||||
aid=str(agent_id),
|
||||
@@ -105,6 +149,20 @@ class GenotypeMutator:
|
||||
return list({str(x) for x in afs})
|
||||
|
||||
async def _get_elem_type(self, elem_id: str):
|
||||
"""
|
||||
|
||||
Retrieve the type of element based on the given element ID.
|
||||
|
||||
Parameters:
|
||||
- elem_id (str): The unique identifier of the element
|
||||
|
||||
Returns:
|
||||
- str: The type of the element ('neuron', 'sensor', 'actuator', or 'unknown')
|
||||
|
||||
Raises:
|
||||
- ValueError: If the element with the specified ID is not found or unlabeled
|
||||
|
||||
"""
|
||||
rows = await self.neo4j.read_all("""
|
||||
MATCH (e {id:$id}) RETURN
|
||||
CASE
|
||||
@@ -120,7 +178,15 @@ class GenotypeMutator:
|
||||
return rows[0]["t"]
|
||||
|
||||
async def _get_layer_index_or_none(self, elem_id: str):
|
||||
"""
|
||||
Get the layer index of the neuron with the specified element ID.
|
||||
|
||||
Parameters:
|
||||
- elem_id: str, the ID of the neuron element.
|
||||
|
||||
Returns:
|
||||
- int or None, the layer index of the neuron if found, otherwise None.
|
||||
"""
|
||||
rows = await self.neo4j.read_all("""
|
||||
MATCH (n:neuron {id:$id}) RETURN toInteger(n.layer_index) AS li
|
||||
""", id=str(elem_id))
|
||||
@@ -130,6 +196,18 @@ class GenotypeMutator:
|
||||
return int(rows[0]["li"])
|
||||
|
||||
async def _get_agent_generation(self, agent_id: str):
|
||||
"""
|
||||
This method fetches the generation value of a given agent ID from the Neo4j database.
|
||||
|
||||
Parameters:
|
||||
- agent_id: a string representing the ID of the agent whose generation value needs to be fetched.
|
||||
|
||||
Returns:
|
||||
- An integer value representing the generation number of the specified agent.
|
||||
|
||||
Raises:
|
||||
- ValueError: If the specified agent ID is not found in the database.
|
||||
"""
|
||||
rows = await self.neo4j.read_all("""
|
||||
MATCH (a:agent {id:$aid}) RETURN toInteger(a.generation) AS g
|
||||
""", aid=str(agent_id))
|
||||
@@ -139,6 +217,19 @@ class GenotypeMutator:
|
||||
return int(rows[0]["g"])
|
||||
|
||||
async def link_from_element_to_element(self, agent_id: Any, from_id: Any, to_id: Any):
|
||||
"""
|
||||
Async method to create a link between two elements specified by their ids.
|
||||
|
||||
:param agent_id: The id of the agent performing the link operation
|
||||
:param from_id: The id of the element to link from
|
||||
:param to_id: The id of the element to link to
|
||||
|
||||
:return: The result of the link operation
|
||||
|
||||
Raises:
|
||||
ValueError: If the link type is not supported
|
||||
|
||||
"""
|
||||
ft = await self._get_elem_type(str(from_id))
|
||||
tt = await self._get_elem_type(str(to_id))
|
||||
if ft == "neuron" and tt == "neuron":
|
||||
@@ -150,6 +241,13 @@ class GenotypeMutator:
|
||||
raise ValueError(f"Unsupported link {ft} -> {tt}")
|
||||
|
||||
async def _link_neuron_to_neuron(self, agent_id: Any, from_nid: Any, to_nid: Any):
|
||||
"""
|
||||
Link one neuron to another neuron within the agent's neural network.
|
||||
|
||||
:param agent_id: The ID of the agent.
|
||||
:param from_nid: The ID of the neuron to link from.
|
||||
:param to_nid: The ID of the neuron to link to.
|
||||
"""
|
||||
from_li = await self._get_layer_index_or_none(str(from_nid))
|
||||
to_li = await self._get_layer_index_or_none(str(to_nid))
|
||||
if from_li is None or to_li is None:
|
||||
@@ -178,6 +276,18 @@ class GenotypeMutator:
|
||||
""", from_id=str(from_nid), to_id=str(to_nid), g=int(gen))
|
||||
|
||||
async def _link_sensor_to_neuron(self, agent_id: Any, from_sid: Any, to_nid: Any):
|
||||
"""
|
||||
Async method to link a sensor to a neuron in the system.
|
||||
|
||||
Args:
|
||||
agent_id (Any): The ID of the agent to which the sensor and neuron belong.
|
||||
from_sid (Any): The ID of the source sensor to link.
|
||||
to_nid (Any): The ID of the target neuron to link the sensor to.
|
||||
|
||||
Raises:
|
||||
ValueError: If the source sensor is not found or if the link between the sensor and neuron already exists.
|
||||
|
||||
"""
|
||||
srows = await self.neo4j.read_all("""
|
||||
MATCH (s:sensor {id:$sid}) RETURN toInteger(s.vector_length) AS vl
|
||||
""", sid=str(from_sid))
|
||||
@@ -206,6 +316,19 @@ class GenotypeMutator:
|
||||
""", nid=str(to_nid), g=int(gen))
|
||||
|
||||
async def _link_neuron_to_actuator(self, agent_id: Any, from_nid: Any, to_aid: Any):
|
||||
"""
|
||||
Links a neuron to an actuator in the system.
|
||||
|
||||
Parameters:
|
||||
- agent_id: Represents the ID of the agent involved in the linking process.
|
||||
- from_nid: Represents the ID of the neuron from which the link originates.
|
||||
- to_aid: Represents the ID of the actuator to which the link is directed.
|
||||
|
||||
Raises:
|
||||
- ValueError: If the actuator specified by `to_aid` is not found or is already fully connected, or if the link between neuron `from_nid` and actuator `to_aid` already exists.
|
||||
|
||||
This method establishes a link between the specified neuron and actuator, setting the weights to an empty list and updating the generation of the neuron accordingly.
|
||||
"""
|
||||
rows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (a:actuator {id:$aid})
|
||||
@@ -258,6 +381,24 @@ class GenotypeMutator:
|
||||
""", from_id=str(from_nid), to_id=str(to_aid))
|
||||
|
||||
async def cut_link(self, from_id: Any, to_id: Any):
|
||||
"""
|
||||
Async method that cuts a link between two elements based on their types.
|
||||
|
||||
Parameters:
|
||||
from_id (Any): The ID of the element where the link originates.
|
||||
to_id (Any): The ID of the element where the link terminates.
|
||||
|
||||
Returns:
|
||||
The result of cutting the link between the elements.
|
||||
The result depends on the types of the elements:
|
||||
- If both elements are neurons, the link between them is cut using '_cut_n2n'.
|
||||
- If the from element is a sensor and the to element is a neuron, the link is cut using '_cut_s2n'.
|
||||
- If the from element is a neuron and the to element is an actuator, the link is cut using '_cut_n2a'.
|
||||
|
||||
Raises:
|
||||
ValueError: If the cut between the specified element types is not supported.
|
||||
|
||||
"""
|
||||
ft = await self._get_elem_type(str(from_id))
|
||||
tt = await self._get_elem_type(str(to_id))
|
||||
if ft == "neuron" and tt == "neuron":
|
||||
@@ -269,7 +410,18 @@ class GenotypeMutator:
|
||||
raise ValueError(f"Unsupported cut {ft} -> {tt}")
|
||||
|
||||
async def mutate_weights(self, agent_id: str):
|
||||
"""
|
||||
Mutates the weights of a neuron associated with a specified agent.
|
||||
|
||||
Parameters:
|
||||
- agent_id: str - The identifier of the agent for which weights are to be mutated.
|
||||
|
||||
Raises:
|
||||
- ValueError: If the agent specified by agent_id is not found or has no cortex, if there are no neurons under the cortex or if the neuron has no inputs and no bias.
|
||||
|
||||
Returns:
|
||||
- The identifier of the mutated neuron.
|
||||
"""
|
||||
rows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (a:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
@@ -362,9 +514,13 @@ class GenotypeMutator:
|
||||
|
||||
async def add_bias(self, agent_id: str) -> str:
|
||||
"""
|
||||
Buch-Semantik: Wähle zufälliges Neuron. Wenn Bias schon existiert -> Fehler.
|
||||
Sonst Bias hinzufügen (in unserem Schema: n.bias setzen), Generation updaten,
|
||||
EvoHist ergänzen.
|
||||
Add bias to a neuron.
|
||||
|
||||
Parameters:
|
||||
agent_id (str): The ID of the agent requesting to add bias.
|
||||
|
||||
Returns:
|
||||
str: The ID of the neuron to which bias has been added.
|
||||
"""
|
||||
nid = await self._pick_random_neuron_id(agent_id)
|
||||
|
||||
@@ -396,9 +552,27 @@ class GenotypeMutator:
|
||||
|
||||
async def remove_bias(self, agent_id: str) -> str:
|
||||
"""
|
||||
Buch-Semantik: Wähle zufälliges Neuron. Wenn kein Bias vorhanden -> Fehler.
|
||||
Sonst Bias entfernen (in Neo4j: Property auf NULL -> wird gelöscht), Generation updaten,
|
||||
EvoHist ergänzen.
|
||||
async def remove_bias(self, agent_id: str) -> str:
|
||||
'''
|
||||
Remove bias of a randomly selected neuron belonging to the specified agent.
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): The ID of the agent to remove bias from.
|
||||
|
||||
Returns:
|
||||
- str: The ID of the neuron that had its bias removed.
|
||||
|
||||
Raises:
|
||||
- ValueError: If the selected neuron does not have a bias set.
|
||||
|
||||
The method first picks a random neuron ID with '_pick_random_neuron_id' method.
|
||||
Then it reads the bias value of the neuron from Neo4j database.
|
||||
If the bias is None, it raises a ValueError.
|
||||
It retrieves the generation of the agent with '_get_agent_generation' method.
|
||||
Updates the neuron in the database by setting the bias to None and updating its generation.
|
||||
Appends the action of 'remove_bias' to the agent's evolution history.
|
||||
Finally, returns the ID of the neuron that had its bias removed.
|
||||
'''
|
||||
"""
|
||||
nid = await self._pick_random_neuron_id(agent_id)
|
||||
|
||||
@@ -429,12 +603,22 @@ class GenotypeMutator:
|
||||
|
||||
async def mutate_af(self, agent_id: str) -> tuple[str, str, str]:
|
||||
"""
|
||||
Wählt ein zufälliges Neuron und ersetzt seine Aktivierungsfunktion
|
||||
durch eine andere aus der Spec (neural_afs), ungleich der aktuellen.
|
||||
Fallback: 'tanh', wenn keine Alternative verfügbar.
|
||||
Rückgabe: (nid, old_af, new_af)
|
||||
"""
|
||||
Mutates the activation function of a neuron belonging to a given agent.
|
||||
|
||||
:param agent_id: The unique identifier of the agent to whom the neuron belongs.
|
||||
:type agent_id: str
|
||||
:return: A tuple containing the neuron ID, old activation function, and new activation function.
|
||||
:rtype: tuple[str, str, str]
|
||||
|
||||
This method picks a random neuron ID for the specified agent and retrieves its current activation function.
|
||||
It then determines the allowed activation functions for the agent and selects a new activation function different from the current one.
|
||||
If there are no alternative activation functions available, it defaults to using "tanh".
|
||||
|
||||
If the new activation function is the same as the old one, it returns without making any changes.
|
||||
Otherwise, it updates the neuron's activation function and generation in the database.
|
||||
Finally, it logs the mutation operation in the evolutionary history of the agent.
|
||||
|
||||
"""
|
||||
nid = await self._pick_random_neuron_id(agent_id)
|
||||
|
||||
row = await self.neo4j.read_all(
|
||||
@@ -473,15 +657,18 @@ class GenotypeMutator:
|
||||
|
||||
async def add_outlink(self, agent_id: str) -> tuple[str, str]:
|
||||
"""
|
||||
Buch-Äquivalent zu add_outlink/1:
|
||||
- Zufälliges Neuron A wählen.
|
||||
- Kandidaten-Ziele = (alle Neuronen außer bereits per A→* verlinkte) ∪ (alle Aktuatoren mit freiem Slot, die A noch nicht verlinkt).
|
||||
- Zufälliges Ziel B wählen.
|
||||
- Link A→B herstellen (N→N mit |weights|=1 und ggf. recurrent, N→A ohne Gewichte mit Kapazitätsprüfung).
|
||||
- Evo-Historie: {add_outlink, A, B}.
|
||||
Rückgabe: (A, B)
|
||||
"""
|
||||
Add an outbound link from a neuron identified by the given agent_id.
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): The ID of the agent requesting the update.
|
||||
|
||||
Returns:
|
||||
A tuple of two strings representing the IDs of the source and target neurons.
|
||||
|
||||
Raises:
|
||||
ValueError: If there are no available target neurons or actuators to link to.
|
||||
|
||||
"""
|
||||
a_nid = await self._pick_random_neuron_id(agent_id)
|
||||
cx_id = await self._get_cortex_id_of_neuron(a_nid)
|
||||
|
||||
@@ -523,14 +710,18 @@ class GenotypeMutator:
|
||||
|
||||
async def add_inlink(self, agent_id: str) -> tuple[str, str]:
|
||||
"""
|
||||
Wählt ein zufälliges Ziel-Neuron B und verlinkt eine neue Quelle A
|
||||
(Sensor ODER Neuron) auf B, sofern noch nicht verbunden.
|
||||
- S→N: Gewichte-Liste Länge = sensor.vl (Projekt: ±2π; Buch: ±π/2)
|
||||
- N→N: Skalar-Gewicht (±π/2), recurrent wenn li(B) ≤ li(A)
|
||||
EvoHist: {add_inlink, A, B}
|
||||
Rückgabe: (A_id, B_id)
|
||||
"""
|
||||
|
||||
Add inlink method adds a new connection between a sensor or neuron to a specified neuron in the Cortex.
|
||||
|
||||
Parameters:
|
||||
- agent_id: str - The ID of the agent requesting the connection.
|
||||
|
||||
Returns:
|
||||
- tuple[str, str] - A Tuple containing the ID of the source (sensor or neuron) and the destination neuron ID connected.
|
||||
|
||||
Note: This method may raise a ValueError if the specified neuron is already connected to all available sensors/neurons.
|
||||
|
||||
"""
|
||||
b_nid = await self._pick_random_neuron_id(agent_id)
|
||||
cx_id = await self._get_cortex_id_of_neuron(b_nid)
|
||||
|
||||
@@ -609,15 +800,17 @@ class GenotypeMutator:
|
||||
|
||||
async def add_sensorlink(self, agent_id: str) -> tuple[str, str]:
|
||||
"""
|
||||
Wählt einen zufälligen Sensor S und verbindet ihn mit einem Neuron N,
|
||||
das S noch nicht ansteuert (S -> N).
|
||||
- Gewichtsvektor-Länge = sensor.vector_length
|
||||
- Gewichte ~ U(-π/2, +π/2) (Buch)
|
||||
- Neuron bekommt Generation des Agents
|
||||
- EvoHist: {add_sensorlink, S, N}
|
||||
Rückgabe: (S_id, N_id)
|
||||
"""
|
||||
add_sensorlink method adds a connection between a sensor and a neuron for a given agent.
|
||||
|
||||
Args:
|
||||
agent_id (str): The ID of the agent to which the sensor and neuron connection will be added.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: A tuple containing the ID of the sensor and the ID of the neuron connected.
|
||||
|
||||
Raises:
|
||||
ValueError: If no sensors are found for the specified agent or if the sensor is already connected to all neurons.
|
||||
"""
|
||||
s_rows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (ag:agent {id:$aid})-[:OWNS]->(cx:cortex)-[:HAS]->(s:sensor)
|
||||
@@ -670,12 +863,15 @@ class GenotypeMutator:
|
||||
|
||||
async def add_actuatorlink(self, agent_id: str) -> tuple[str, str]:
|
||||
"""
|
||||
Wählt einen zufälligen Aktuator A mit freier Kapazität (k < A.vl),
|
||||
wählt ein Neuron N, das noch nicht auf A verlinkt ist,
|
||||
erzeugt N->A (keine Gewichte), und loggt EvoHist.
|
||||
Rückgabe: (N_id, A_id)
|
||||
"""
|
||||
Async method to add an actuator link for a given agent.
|
||||
|
||||
Parameters:
|
||||
- agent_id: str - Identifier of the agent to add the actuator link to
|
||||
|
||||
Returns:
|
||||
- Tuple containing two strings: n_id and a_id, which represent the identifiers of the neuron and actuator linked, respectively
|
||||
|
||||
"""
|
||||
arows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (ag:agent {id:$aid})-[:OWNS]->(cx:cortex)-[:HAS]->(a:actuator)
|
||||
@@ -715,15 +911,16 @@ class GenotypeMutator:
|
||||
|
||||
async def add_neuron(self, agent_id: str) -> tuple[str, str, str]:
|
||||
"""
|
||||
Buch-Operator add_neuron:
|
||||
- wähle random Target-Layer aus Agent.pattern
|
||||
- erzeuge neuen Neuron-Knoten (ohne Eingänge/Ausgänge, bias=None)
|
||||
- wähle From ∈ (Sensor ∪ Neuron_alt), To ∈ (Neuron_alt ∪ Actuator_mit_Platz)
|
||||
- verlinke From->NewN und NewN->To
|
||||
- logge EvoHist und aktualisiere Fingerprint
|
||||
Rückgabe: (from_id, new_nid, to_id)
|
||||
"""
|
||||
Add a new neuron to the specified agent in the neural network.
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): The ID of the agent to add the neuron to.
|
||||
|
||||
Returns:
|
||||
- tuple[str, str, str]: A tuple containing the IDs of the elements involved in the neuron addition process.
|
||||
The tuple includes the ID of the element the neuron is created from, the ID of the newly created neuron,
|
||||
and the ID of the element the neuron is connected to.
|
||||
"""
|
||||
arows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (a:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
@@ -821,37 +1018,18 @@ class GenotypeMutator:
|
||||
await self._append_evo(agent_id,
|
||||
{"op": "add_neuron", "from": str(from_id), "new": str(new_nid), "to": str(to_id)})
|
||||
|
||||
""""
|
||||
if hasattr(self, "update_fingerprint_fn"):
|
||||
|
||||
await self.update_fingerprint_fn(agent_id)
|
||||
else:
|
||||
|
||||
try:
|
||||
from genotype import update_fingerprint
|
||||
await update_fingerprint(agent_id)
|
||||
except Exception:
|
||||
pass
|
||||
"""
|
||||
|
||||
return from_id, new_nid, to_id
|
||||
|
||||
async def outsplice(self, agent_id: str) -> tuple[str, str, str]:
|
||||
"""
|
||||
Buch-Operator: outsplice
|
||||
- wähle A (Neuron)
|
||||
- wähle B aus A.output, aber nur feedforward:
|
||||
* B:neuron mit layer(B) > layer(A) ODER
|
||||
* B:actuator
|
||||
- erzeuge neue Schicht zwischen A und B:
|
||||
* wir nutzen integer-layers → insert layer A.layer+1:
|
||||
SHIFT: alle Neuronen mit layer_index >= A.layer+1 um +1 erhöhen
|
||||
* K in layer = A.layer+1 anlegen
|
||||
- cut A->B, link A->K und K->B
|
||||
- Generationen setzen, evo_hist anhängen, Fingerprint aktualisieren
|
||||
Rückgabe: (A_id, K_id, B_id)
|
||||
"""
|
||||
Asynchronous method to perform outsplice operation for a given agent ID.
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): ID of the agent for which outsplice operation needs to be performed.
|
||||
|
||||
Returns:
|
||||
- tuple containing IDs of three elements involved in the outsplice operation: a_id, k_id, b_id.
|
||||
"""
|
||||
arows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (ag:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
@@ -969,29 +1147,24 @@ class GenotypeMutator:
|
||||
append=[s],
|
||||
)
|
||||
|
||||
"""
|
||||
try:
|
||||
from neo4j_genotype import update_fingerprint
|
||||
await update_fingerprint(agent_id)
|
||||
except Exception:
|
||||
pass
|
||||
"""
|
||||
|
||||
return a_id, k_id, b_id
|
||||
|
||||
async def add_sensor(self, agent_id: str) -> tuple[str, str]:
|
||||
"""
|
||||
Fügt einen neuen Sensor der Morphologie hinzu und verbindet ihn auf
|
||||
ein zufälliges Neuron (S -> N).
|
||||
- Sensor-Template kommt aus morphology.get_Sensors(morph_name)
|
||||
- Already-used werden per (name, scape, vl) gegen den Cortex gefiltert
|
||||
- Link S->N mit Gewichtsvektor-Länge = sensor.vector_length
|
||||
(Buch-Semantik: Gewichte ~ U(-π/2, +π/2))
|
||||
- Ziel-Neuron erhält generation des Agents
|
||||
- EvoHist: {add_sensor, S, N}
|
||||
Rückgabe: (S_id, N_id)
|
||||
"""
|
||||
Async method add_sensor: add_sensor(agent_id: str) -> tuple[str, str]
|
||||
Description:
|
||||
This method adds a new sensor to the specified agent's cortex in the Neo4j database.
|
||||
|
||||
Parameters:
|
||||
agent_id (str): The ID of the agent for which the sensor is being added.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified agent is not found, or if the sensor cannot be added for various reasons.
|
||||
|
||||
Returns:
|
||||
tuple[str, str]: A tuple containing the ID of the added sensor and the ID of the connected neuron.
|
||||
|
||||
"""
|
||||
arows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (ag:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
@@ -1079,14 +1252,19 @@ class GenotypeMutator:
|
||||
|
||||
async def add_actuator(self, agent_id: str) -> tuple[str, str]:
|
||||
"""
|
||||
Fügt einen neuen Aktuator der Morphologie hinzu und verbindet ihn von
|
||||
einem zufälligen Neuron (N -> A).
|
||||
- Aktuator-Template kommt aus morphology.get_Actuators(morph_name)
|
||||
- Already-used werden per (name, scape, vl) gegen Cortex gefiltert
|
||||
- EvoHist wird als JSON-String appended
|
||||
Rückgabe: (neuron_id, actuator_id)
|
||||
"""
|
||||
Add actuator to an agent with the specified agent_id
|
||||
|
||||
Parameters:
|
||||
- agent_id (str): The ID of the agent to which the actuator will be added
|
||||
|
||||
Returns:
|
||||
- tuple[str, str]: A tuple containing the IDs of the neuron and actuator that have been connected
|
||||
|
||||
Raises:
|
||||
- ValueError: If the agent with the specified agent_id is not found, cannot read morphology from spec_con_json,
|
||||
NN already uses all available actuators for this morphology, cortex has no neurons to connect from
|
||||
|
||||
"""
|
||||
arows = await self.neo4j.read_all(
|
||||
"""
|
||||
MATCH (ag:agent {id:$aid})-[:OWNS]->(cx:cortex)
|
||||
@@ -1165,12 +1343,4 @@ class GenotypeMutator:
|
||||
append=[s],
|
||||
)
|
||||
|
||||
"""
|
||||
try:
|
||||
from neo4j_genotype import update_fingerprint
|
||||
await update_fingerprint(agent_id)
|
||||
except Exception:
|
||||
pass
|
||||
"""
|
||||
|
||||
return n_id, a_id
|
||||
|
||||
1539
mathema/genotype/neo4j/genotype_mutator_tx.py
Normal file
@@ -1,17 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
from mathema.genotype.neo4j.genotype import test_add_neuron
|
||||
|
||||
|
||||
async def main():
|
||||
# polis = Polis()
|
||||
# await polis.create()
|
||||
# await polis.start()
|
||||
|
||||
# await polis.stop()
|
||||
# await test_mut_operators()
|
||||
await test_add_neuron()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -1,49 +0,0 @@
|
||||
# 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())
|
||||
@@ -1,3 +1,18 @@
|
||||
"""
|
||||
Replay utility for visualizing the best evolved CarRacing agent.
|
||||
|
||||
This module loads the best-performing agent from a given population stored
|
||||
in Neo4j, reconstructs its policy from a genotype snapshot, and replays the
|
||||
agent in a human-rendered CarRacing environment using pygame.
|
||||
|
||||
High-level workflow:
|
||||
1. Query Neo4j for the agent with the highest recorded fitness in a population.
|
||||
2. Load the agent’s genotype snapshot.
|
||||
3. Build an executable policy from the snapshot.
|
||||
4. Run the policy in the CarRacing environment, step by step.
|
||||
5. Render the environment in real time and automatically handle episode resets.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pygame
|
||||
|
||||
|
||||
@@ -6,6 +6,29 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CarRacingScape(Actor):
|
||||
"""
|
||||
Scape (environment) actor wrapping a CarRacing-like Gymnasium environment.
|
||||
|
||||
This actor provides an asynchronous message interface for sensors and
|
||||
actuators in the actor-based cortex architecture:
|
||||
|
||||
- Sensors request observations/features via ("sense", sid, sensor_pid).
|
||||
The scape replies to the given sensor actor with ("percept", vec).
|
||||
|
||||
- Actuators apply actions via ("action", action, actuator_pid).
|
||||
The scape performs an env.step(action) and replies with
|
||||
("result", step_reward, halt_flag) where halt_flag is 1 if the episode
|
||||
terminated or was truncated.
|
||||
|
||||
In addition, the scape automatically resets the environment when an episode
|
||||
ends (halt_flag == 1) using env.fast_reset().
|
||||
|
||||
Notes about `_stepped`:
|
||||
- Some environments do not provide a meaningful feature vector immediately
|
||||
after reset until at least one `step()` was executed.
|
||||
- `_get_features()` ensures that the environment has been stepped once
|
||||
(with a zero action) before calling `env.get_feature_vector()`.
|
||||
"""
|
||||
def __init__(self, env, name: str = "CarRacingScape"):
|
||||
super().__init__(name)
|
||||
self.env = env
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
this is a test scape for validation.
|
||||
"""
|
||||
from mathema.actors.actor import Actor
|
||||
import math
|
||||
import logging
|
||||
|
||||
17
mathema/settings.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
global parameters and settings for the
|
||||
neuroevolutionary system.
|
||||
"""
|
||||
|
||||
# default if not otherwise specified
|
||||
INIT_POPULATION_ID: str = "test"
|
||||
|
||||
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
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
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())
|
||||
13
mathema/stats/car_pop_transaction_test.jsonl
Normal file
@@ -0,0 +1,13 @@
|
||||
{"ts":1765639613,"gen":1,"t_sec":367007,"cum_fitness":227.40101317121585,"best":197.72553191488373,"avg":25.266779241246205,"std":61.67146734447497,"agents":9,"eval_acc":277,"cycle_acc":159367.0,"time_acc":903.0}
|
||||
{"ts":1765639613,"gen":2,"t_sec":367081,"cum_fitness":317.6296859169127,"best":150.03019250252797,"avg":28.87542599244661,"std":56.67916151134183,"agents":11,"eval_acc":479,"cycle_acc":243635.0,"time_acc":1371.0}
|
||||
{"ts":1765639613,"gen":3,"t_sec":367169,"cum_fitness":613.0014690982745,"best":209.76859169199517,"avg":61.30014690982745,"std":87.56502883628107,"agents":10,"eval_acc":695,"cycle_acc":347287.0,"time_acc":2082.0}
|
||||
{"ts":1765639613,"gen":4,"t_sec":367256,"cum_fitness":614.7368794326097,"best":200.78581560283587,"avg":61.47368794326097,"std":78.19851334990376,"agents":10,"eval_acc":890,"cycle_acc":436408.0,"time_acc":2519.0}
|
||||
{"ts":1765639613,"gen":5,"t_sec":367331,"cum_fitness":771.1370820668635,"best":203.260536980748,"avg":77.11370820668635,"std":94.68909264303997,"agents":10,"eval_acc":1060,"cycle_acc":526977.0,"time_acc":2971.0}
|
||||
{"ts":1765639613,"gen":6,"t_sec":367442,"cum_fitness":1005.2231003039412,"best":240.36803444782174,"avg":100.52231003039412,"std":101.71769730692468,"agents":10,"eval_acc":1266,"cycle_acc":632455.0,"time_acc":3474.0}
|
||||
{"ts":1765639613,"gen":7,"t_sec":367520,"cum_fitness":1264.2317629179447,"best":382.49260385006573,"avg":114.9301602652677,"std":133.8757324454927,"agents":11,"eval_acc":1450,"cycle_acc":741490.0,"time_acc":3852.0}
|
||||
{"ts":1765639613,"gen":8,"t_sec":367650,"cum_fitness":2647.7638804457683,"best":883.2604863221765,"avg":240.70580731325165,"std":313.94121501548653,"agents":11,"eval_acc":1673,"cycle_acc":854553.0,"time_acc":4653.0}
|
||||
{"ts":1765639613,"gen":9,"t_sec":367771,"cum_fitness":3294.015704153962,"best":886.3604863221761,"avg":299.45597310490564,"std":357.0894275322936,"agents":11,"eval_acc":1892,"cycle_acc":1003231.0,"time_acc":5336.0}
|
||||
{"ts":1765639613,"gen":10,"t_sec":367912,"cum_fitness":3991.0866261397696,"best":894.5938196555113,"avg":399.10866261397695,"std":399.7360856267592,"agents":10,"eval_acc":2111,"cycle_acc":1171051.0,"time_acc":6085.0}
|
||||
{"ts":1765639613,"gen":11,"t_sec":367987,"cum_fitness":4355.802431610881,"best":898.543819655511,"avg":435.5802431610881,"std":435.8926029922781,"agents":10,"eval_acc":2262,"cycle_acc":1286687.0,"time_acc":6569.0}
|
||||
{"ts":1765639613,"gen":12,"t_sec":368146,"cum_fitness":4405.635764944219,"best":893.5438196555116,"avg":440.5635764944219,"std":440.64533248937005,"agents":10,"eval_acc":2482,"cycle_acc":1442441.0,"time_acc":7519.0}
|
||||
{"ts":1765639613,"gen":13,"t_sec":368243,"cum_fitness":4407.452431610885,"best":894.2271529888436,"avg":440.74524316108847,"std":440.8278024255607,"agents":10,"eval_acc":2662,"cycle_acc":1556196.0,"time_acc":8058.0}
|
||||
BIN
mathema/stats/car_pop_transaction_test_agents.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
mathema/stats/car_pop_transaction_test_avg.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
mathema/stats/car_pop_transaction_test_best.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
mathema/stats/car_pop_transaction_test_cum_fitness.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
mathema/stats/car_pop_transaction_test_cycle_acc.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
mathema/stats/car_pop_transaction_test_eval_acc.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
mathema/stats/car_pop_transaction_test_std.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
mathema/stats/car_pop_transaction_test_t_sec.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
mathema/stats/car_pop_transaction_test_time_acc.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
@@ -1,142 +0,0 @@
|
||||
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())
|
||||
@@ -1,34 +0,0 @@
|
||||
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())
|
||||