last changes

This commit is contained in:
2026-02-21 10:58:05 +01:00
parent 841bc7c805
commit 0902732f60
136 changed files with 10387 additions and 2015 deletions

View File

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

View File

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

View File

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

View File

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

View File

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