Files
neuroevolution/mathema/actors/actuator.py
2026-02-21 10:58:05 +01:00

104 lines
4.2 KiB
Python

import asyncio
import logging
from mathema.actors.actor import Actor
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
self.cx_pid = cx_pid
self.aname = name
self.fanin_ids = fanin_ids
self.expect = expect_count
self.received = {}
self.scape = scape
self.scape_inbox = asyncio.Queue()
async def run(self):
while True:
msg = await self.inbox.get()
tag = msg[0]
if tag == "forward":
_, from_id, vec = msg
self.received[from_id] = vec
if len(self.received) == self.expect:
log.debug("ACTUATOR: collected all signals...")
output = []
for fid in self.fanin_ids:
output.extend(self.received[fid])
if self.aname == "pts":
print(f"Actuator output: {output}")
fitness, halt_flag = 1.0, 0
elif self.aname == "xor_SendOutput" and self.scape:
log.debug("ACTUATOR: sending action to scape...")
await self.scape.send(("action", output, self))
while True:
resp = await self.inbox.get()
if resp[0] == "result":
log.debug("ACTUATOR: got scape response: %s", resp)
fitness, halt_flag = resp[1], resp[2]
break
elif self.aname == "car_ApplyAction" and self.scape:
y0 = float(output[0]) if len(output) > 0 else 0.0
y1 = float(output[1]) if len(output) > 1 else 0.0
y2 = float(output[2]) if len(output) > 2 else 0.0
steer = max(-1.0, min(1.0, y0))
gas = max(0.0, min(1.0, 0.5 * (y1 + 1.0)))
brake = max(0.0, min(1.0, 0.5 * (y2 + 1.0)))
action = [steer, gas, brake]
log.debug("ACTUATOR: sending action to car scape: %s", action)
await self.scape.send(("action", action, self))
while True:
resp = await self.inbox.get()
if resp[0] == "result":
log.debug("ACTUATOR: got scape response: %s", resp)
fitness, halt_flag = resp[1], resp[2]
break
else:
fitness, halt_flag = 0.0, 0
await self.cx_pid.send(("sync", self.aid, fitness, halt_flag))
log.debug("ACTUATOR: sent sync message to cortex.")
self.received.clear()
elif tag == "terminate":
return