Source code for quantum_mind.embodied.brain

"""The main brain: a supervisor that keeps a state machine over the robot's sub-agents and may consult a local language model.

Structure
---------
* **Sub-agents** (:class:`MotorSubAgent`, :class:`SensorSubAgent`, :class:`PerceptionSubAgent`, :class:`Heartbeat`) each watch
  one sub-system and report a :class:`Status` with a health level (``OK``, ``WARN``, ``FAULT``, ``HEALING``, ``FAILED``). The
  motor sub-agent wraps :class:`~quantum_mind.embodied.agent.MotorAgent`, which does its own local self-healing.
* **State machine** (:class:`StateMachine`): the robot is in one :class:`Mode` (``BOOT``, ``CALIBRATING``, ``NOMINAL``,
  ``DEGRADED``, ``HOLD``, ``SAFE_STOP``). Only the transitions in :data:`TRANSITIONS` exist; anything else is refused and
  logged.
* **Brain** (:class:`RuleBrain` or :class:`LocalLLMBrain`): when a sub-system reports something other than OK, the brain picks
  one *action* from the set allowed in the current mode. A local LLM can be the brain, but it only proposes: its reply must be
  JSON with an allowed action, and hard rules run first and cannot be overridden (any ``FAILED`` sub-agent means ``SAFE_STOP``;
  ``SAFE_STOP`` is left only by a human ``reset``). An unparsable or disallowed reply falls back to the rule brain and is
  counted.
* **Supervisor** (:class:`Supervisor`): runs the sub-agents every step, fires state-machine events, asks the brain, applies the
  action to the command, and keeps an incident log.

The LLM is optional and runs locally (:func:`ollama_generate` talks to an Ollama server on this machine; any ``str -> str``
callable works). No language model ships with the library and none was run during development: the LLM path is tested with
scripted replies only.
"""
from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from enum import Enum


[docs] class Health(Enum): """Health level a sub-agent reports, from fine (``OK``) to out of service (``FAILED``).""" OK = 0 WARN = 1 HEALING = 2 FAULT = 3 FAILED = 4
[docs] class Mode(Enum): """Operating mode of the robot, the states of the supervisor's state machine.""" BOOT = 'boot' CALIBRATING = 'calibrating' NOMINAL = 'nominal' DEGRADED = 'degraded' HOLD = 'hold' SAFE_STOP = 'safe_stop'
#: (mode, event) -> next mode. Any pair not listed is an illegal transition. TRANSITIONS = { (Mode.BOOT, 'start'): Mode.CALIBRATING, (Mode.CALIBRATING, 'calibrated'): Mode.NOMINAL, (Mode.NOMINAL, 'fault'): Mode.DEGRADED, (Mode.DEGRADED, 'recovered'): Mode.NOMINAL, (Mode.NOMINAL, 'ambiguity'): Mode.HOLD, (Mode.DEGRADED, 'ambiguity'): Mode.HOLD, (Mode.HOLD, 'clarified'): Mode.NOMINAL, (Mode.HOLD, 'fault'): Mode.DEGRADED, (Mode.BOOT, 'failure'): Mode.SAFE_STOP, (Mode.CALIBRATING, 'failure'): Mode.SAFE_STOP, (Mode.NOMINAL, 'failure'): Mode.SAFE_STOP, (Mode.DEGRADED, 'failure'): Mode.SAFE_STOP, (Mode.HOLD, 'failure'): Mode.SAFE_STOP, (Mode.SAFE_STOP, 'reset'): Mode.BOOT, } #: Actions the brain may choose in each mode. ALLOWED = { Mode.BOOT: {'continue'}, Mode.CALIBRATING: {'continue'}, Mode.NOMINAL: {'continue', 'slow_down', 'safe_stop'}, Mode.DEGRADED: {'continue', 'slow_down', 'hold', 'safe_stop'}, Mode.HOLD: {'hold', 'resume', 'safe_stop'}, Mode.SAFE_STOP: {'stay'}, }
[docs] @dataclass class Status: name: str health: Health message: str = '' data: dict = field(default_factory=dict)
[docs] @dataclass class Decision: action: str reason: str = '' source: str = 'rules'
[docs] class StateMachine: def __init__(self): self.mode, self.log, self.refused = Mode.BOOT, [], [] def fire(self, event, t=0.0): nxt = TRANSITIONS.get((self.mode, event)) if nxt is None: self.refused.append((round(t, 2), self.mode.value, event)) return False self.log.append((round(t, 2), self.mode.value, event, nxt.value)) self.mode = nxt return True
# ------------------------------------------------------------------------------------------------------ sub-agents
[docs] class MotorSubAgent: """Reports the motor agent's view: healing a fault, failed (safe stop) or fine.""" name = 'motor' def update(self, ctx): a = ctx['agent'] plant = sorted(a.flags & {'actuator_degraded', 'friction_increase', 'external_push'}) if a.safe_stop: return Status(self.name, Health.FAILED, 'actuator unrecoverable', {'flags': sorted(a.flags)}) if plant or not a.enc_ok: kind = a.label if plant and a.label else (a.final_diagnosis or 'fault') return Status(self.name, Health.HEALING if a.heal else Health.FAULT, 'diagnosed %s' % kind, {'flags': sorted(a.flags)}) return Status(self.name, Health.OK, 'nominal')
[docs] class SensorSubAgent: """Encoder, MEMS and quantum accelerometer health.""" name = 'sensors' def __init__(self, quantum_timeout=3.0, bias_limit=0.15): self.quantum_timeout, self.bias_limit, self.last_q = quantum_timeout, bias_limit, 0.0 def update(self, ctx): a, t = ctx['agent'], ctx['t'] if ctx.get('a_q') is not None: self.last_q = t if not a.enc_ok: return Status(self.name, Health.FAULT, 'encoder unusable', {'encoder': False}) bias = abs(a.est.x[2]) * a.s if bias > self.bias_limit: return Status(self.name, Health.WARN, 'MEMS bias %.2f m/s^2 is large' % bias, {'bias': float(bias)}) if a.quantum and t - self.last_q > self.quantum_timeout: return Status(self.name, Health.WARN, 'no quantum accelerometer sample for %.1f s' % (t - self.last_q)) return Status(self.name, Health.OK, 'nominal')
[docs] class PerceptionSubAgent: """Grounding confidence: WARN while the instruction is ambiguous (the gate would ask).""" name = 'perception' def update(self, ctx): if ctx.get('ambiguous'): return Status(self.name, Health.WARN, 'instruction ambiguous: asking the person', {}) return Status(self.name, Health.OK, 'nominal')
[docs] class Heartbeat: """Watchdog: FAILED if no telemetry arrived within ``timeout`` seconds.""" name = 'heartbeat' def __init__(self, timeout=0.5): self.timeout, self.last = timeout, 0.0 def update(self, ctx): if ctx.get('fresh', True): self.last = ctx['t'] if ctx['t'] - self.last > self.timeout: return Status(self.name, Health.FAILED, 'telemetry lost for %.2f s' % (ctx['t'] - self.last)) return Status(self.name, Health.OK, 'alive')
# ------------------------------------------------------------------------------------------------------ brains
[docs] class RuleBrain: """Deterministic policy; also the fallback when the LLM misbehaves.""" source = 'rules' def decide(self, mode, statuses, history=()): if mode == Mode.SAFE_STOP: return Decision('stay', 'safe stop is left only by a human reset') worst = max((s.health.value for s in statuses), default=0) if mode == Mode.HOLD: waiting = any(s.name == 'perception' and s.health != Health.OK for s in statuses) return Decision('hold', 'waiting for the clarification') if waiting else Decision('resume', 'the instruction is clear again') if worst >= Health.FAULT.value: return Decision('hold', 'a sub-system fault is not being healed') if worst == Health.HEALING.value: return Decision('slow_down', 'a fault is being healed; move at reduced command') return Decision('continue', 'all sub-systems nominal' if worst == 0 else 'warnings only')
def _extract_json(text): m = re.search(r'\{.*\}', text, re.S) if not m: raise ValueError('no JSON object in the reply') return json.loads(m.group(0))
[docs] class LocalLLMBrain: """Ask a local language model to choose the action. ``generate(prompt) -> str`` is any local text generator. The prompt carries the mode, every status and the allowed actions; the reply must contain ``{"action": ..., "reason": ...}``. A reply that is not JSON, or names an action that is not allowed in this mode, is discarded and the rule brain decides (counted in ``stats``). """ source = 'llm' def __init__(self, generate, fallback=None): self.generate, self.fallback = generate, fallback or RuleBrain() self.stats = dict(calls=0, accepted=0, invalid_json=0, disallowed=0, errors=0) self.last_prompt = self.last_reply = None def prompt(self, mode, statuses, history): body = dict(mode=mode.value, allowed_actions=sorted(ALLOWED[mode]), sub_systems=[dict(name=s.name, health=s.health.name, message=s.message) for s in statuses], recent_events=list(history)[-5:]) return ('You supervise a robot motor. Reply with one JSON object {"action": <one of allowed_actions>, ' '"reason": <short text>} and nothing else.\n' + json.dumps(body)) def decide(self, mode, statuses, history=()): self.stats['calls'] += 1 self.last_prompt = self.prompt(mode, statuses, history) try: self.last_reply = self.generate(self.last_prompt) out = _extract_json(self.last_reply) except Exception: self.stats['errors' if self.last_reply is None else 'invalid_json'] += 1 return self.fallback.decide(mode, statuses, history) action = str(out.get('action', '')) if action not in ALLOWED[mode]: self.stats['disallowed'] += 1 return self.fallback.decide(mode, statuses, history) self.stats['accepted'] += 1 return Decision(action, str(out.get('reason', '')), 'llm')
[docs] def ollama_generate(model='llama3.2', host='http://localhost:11434', timeout=30.0): """A ``generate`` function for a local Ollama server (needs Ollama running on this machine; not exercised in the test suite).""" import urllib.request def generate(prompt): payload = json.dumps(dict(model=model, prompt=prompt, stream=False, format='json')).encode() req = urllib.request.Request(host + '/api/generate', payload, {'Content-Type': 'application/json'}) with urllib.request.urlopen(req, timeout=timeout) as r: # noqa: S310 - local server return json.loads(r.read())['response'] return generate
# ------------------------------------------------------------------------------------------------------ supervisor
[docs] class Supervisor: """The main brain's loop: sub-agent statuses -> state-machine events -> brain decision -> command limits.""" def __init__(self, subagents=None, brain=None, calibrate_steps=20, recover_steps=40, dt=0.05, slow_factor=0.5): self.subs = subagents or [MotorSubAgent(), SensorSubAgent(), Heartbeat()] self.brain, self.sm = brain or RuleBrain(), StateMachine() self.cal_steps, self.rec_steps, self.dt, self.slow = calibrate_steps, recover_steps, dt, slow_factor self.reset() def reset(self): self.sm = StateMachine() self.k, self.ok_run, self.action, self.statuses = 0, 0, 'continue', [] self.incidents, self.decisions, self._last_key = [], [], None self.sm.fire('start', 0.0) @property def mode(self): return self.sm.mode
[docs] def human_reset(self): """The only way out of SAFE_STOP (an operator has inspected the machine).""" if self.sm.fire('reset', self.k * self.dt): self.reset()
[docs] def observe(self, ctx): """Run one supervision step with the telemetry context; returns the current mode.""" self.k += 1 t = ctx['t'] self.statuses = [s.update(ctx) for s in self.subs] worst = max(s.health.value for s in self.statuses) if self.mode == Mode.SAFE_STOP: self.action = 'stay' return self.mode if worst >= Health.FAILED.value: # hard rule: not negotiable, the LLM is not asked self.sm.fire('failure', t) self.action = 'safe_stop' self._note(t, 'safe_stop', 'hard rule: ' + '; '.join(s.message for s in self.statuses if s.health == Health.FAILED), 'rules') return self.mode perception_warn = any(s.name == 'perception' and s.health != Health.OK for s in self.statuses) fault = any(s.health in (Health.HEALING, Health.FAULT) for s in self.statuses) self.ok_run = self.ok_run + 1 if worst == 0 else 0 if self.mode == Mode.CALIBRATING and self.k >= self.cal_steps and not fault: self.sm.fire('calibrated', t) elif self.mode == Mode.NOMINAL and fault: self.sm.fire('fault', t) elif self.mode == Mode.HOLD and not perception_warn: self.sm.fire('clarified', t) if fault: self.sm.fire('fault', t) elif self.mode == Mode.DEGRADED and self.ok_run >= self.rec_steps: self.sm.fire('recovered', t) if perception_warn and self.mode in (Mode.NOMINAL, Mode.DEGRADED): self.sm.fire('ambiguity', t) key = (self.mode, tuple((s.name, s.health) for s in self.statuses)) if worst > 0 or self.mode == Mode.HOLD: # consult the brain only when something is not OK if key != self._last_key: d = self.brain.decide(self.mode, self.statuses, [e[1:] for e in self.incidents]) self._note(t, d.action, d.reason, d.source) self.action = d.action if d.action == 'safe_stop': self.sm.fire('failure', t) elif d.action == 'resume' and self.mode == Mode.HOLD: self.sm.fire('clarified', t) else: self.action = 'continue' self._last_key = key return self.mode
def _note(self, t, action, reason, source): self.decisions.append((round(t, 2), action, source)) self.incidents.append((round(t, 2), action, '[%s] %s' % (source, reason)))
[docs] def limit(self, u): """Apply the current action to a motor command.""" if self.mode in (Mode.SAFE_STOP, Mode.HOLD) or self.action in ('hold', 'safe_stop', 'stay'): return 0.0 if self.action == 'slow_down': return float(u) * self.slow return float(u)
def report(self): lines = ['mode: %s' % self.mode.value] lines += [' %-10s %-8s %s' % (s.name, s.health.name, s.message) for s in self.statuses] lines += ['incident at %.2fs: %s (%s)' % i for i in self.incidents[-5:]] return '\n'.join(lines)
[docs] def run_supervised_episode(act, rng, faults=(), brain=None, steps=100, switch_at=30, tol=0.0625, heal=True, dropped_telemetry_at=None): """Two-leg move under the main brain. Returns the final error, the mode trace and the supervisor. ``dropped_telemetry_at`` (seconds) stops telemetry from arriving from then on, which the heartbeat must catch.""" from .agent import MotorAgent from .state import SensedMotor sm = SensedMotor(faults=faults) agent = MotorAgent(heal=heal) sup = Supervisor(brain=brain) a, b = rng.uniform(-0.9, 0.9, 2) z0 = sm.reset(rng.uniform(-1, 1), rng) agent.reset(z0) th, om, trace = z0, 0.0, [] for k in range(steps): tgt = a if k < switch_at else b u = sup.limit(agent.control(float(act(tgt, th, om)))) meas = sm.step(u) fresh = dropped_telemetry_at is None or sm.t < dropped_telemetry_at if fresh: th, om = agent.step(meas, u) sup.observe(dict(t=sm.t, agent=agent, a_q=meas['a_q'], fresh=fresh)) trace.append(sup.mode.value) err = abs(sm.m.theta - b) return dict(error=err, success=err < tol, trace=trace, supervisor=sup)