Action selection & goals¶
What the agent wants, and how it picks an action. StateGoal and ObservationGoal are the goals you hand the Agent (the continuous-state answer to pymdp's C); Preference and EFESelector are the machinery underneath expected-free-energy selection.
StateGoal
dataclass
¶
A state-space objective: reach a target state (the LQR / fixed-sensor regime).
The complete spec for the state-tracking path - the target plus the LQR cost
weights it implies. precision is LQR's state weight Q; effort is its
action weight R, left None here because the action dimension p isn't known
until the Agent pairs this with a model (the Agent fills the identity). The
Agent dispatches a StateGoal to an LQRSelector. Not a pytree - construction-
time only; the Agent extracts a Preference for the selector.
Source code in src/cpomdp/selection.py
ObservationGoal
dataclass
¶
ObservationGoal(
target,
*,
action_bounds,
precision=None,
n_candidates=21,
horizon=1,
info_node=None,
)
An observation-space objective: prefer to observe a target (the EFE regime).
The complete spec for the information-seeking path - the preferred observation,
how sharply it is preferred (precision), and the action-search config the
EFESelector front-loads: action_bounds is the action box, n_candidates
its resolution, horizon its lookahead depth. info_node optionally aims the
epistemic term at a single latent node (info gain about that node's marginal, the
factored-EFE regime on a branching backend, issue #26); None = whole-state info
gain (the default, unchanged behaviour). The Agent dispatches an ObservationGoal to
an EFESelector. Not a pytree - construction-time only; the Agent extracts a
Preference.
Source code in src/cpomdp/selection.py
Preference
dataclass
¶
What the agent wants: a goal and how sharply it is preferred.
Single-mode for v0.3 — one Gaussian preference. The disjunctive mixture
case (visit one of several goals) is RFC-002, deferred; this type is the seam
that a mixture Preference plugs into.
precision is unused by LQRSelector (it is baked into
the controller's Riccati solve at construction); it is carried here for the EFE
pragmatic term added in Phase 1A.
Source code in src/cpomdp/selection.py
EFESelector ¶
EFESelector(
model: LinearGaussianModel,
*,
n_candidates: int,
action_bounds: tuple[float, float],
horizon: int = 1,
)
EFE action selection over a front-loaded candidate grid, horizon-aware.
At horizon = 1 (default) it minimises one-step G over the grid. At
horizon > 1 it scores constant-action policies (each grid action held for H
steps) via policy_efe and returns the first action of the best one
(receding-horizon). Per-cycle cost is a single attributable number,
cost_per_cycle = n_candidates * horizon.
Honest caveat: horizon selects the best constant action, not the best
sequence. A genuinely sequential epistemic policy — move to sense, then exploit —
needs a varying sequence the constant-action family cannot express, so at H > 1 the
selector can still look myopic-ish on such tasks. True varying-sequence search is
EnumeratedEfeSearch in cpomdp.enumeration, which enumerates every length-H
sequence of a declared finite action set (ADR-031).
Source code in src/cpomdp/selection.py
n_candidates
property
¶
The per-cycle EFE-evaluation count — attributable work (RFC-001).
warrant
property
¶
CORROBORATED — the grid samples a continuum, it does not decide it.
The counterpart to EnumeratedEfeSearch.warrant (PROVED): the two search
families print in different vocabulary so a sampled result is never read as a
decided one (standing rule 6; ADR-031).
select ¶
The grid action minimising G over the horizon (the per-cycle work).
At horizon = 1 one vmap of the one-step kernel + argmin. At
horizon > 1 one vmap of policy_efe over the constant-action policies
+ argmin, returning the first (= constant) action of the best policy.
Source code in src/cpomdp/selection.py
ActionSelector is the protocol every selector satisfies. LQRSelector is the fixed-sensor path, where expected free energy provably reduces to LQR (ADR-003). FfgEfeSelector is EFESelector's peer for a branching backend.
ActionSelector ¶
Bases: Protocol
Chooses an action from a belief and a preference.
The abstraction wall for action selection: LQRSelector is
the fixed-sensor case (EFE collapses to LQR, ADR-003);
EFESelector is the state-dependent one.
Agent depends only on this, never on a concrete selector.
select ¶
LQRSelector
dataclass
¶
Adapts an LQRController to the ActionSelector interface.
A thin wrapper: it owns no control logic, it just forwards to the front-loaded controller, unpacking the belief's mean and the preference's goal. EFE collapses to LQR under a fixed sensor (ADR-003), so for that regime this is the action selector.
select ¶
Forward to the controller: action(belief.mean, preference.goal).
No control logic of its own — the Riccati solve was front-loaded into the
controller at construction. This just unpacks the belief and preference
into the controller's (mean, goal) signature.
Source code in src/cpomdp/selection.py
FfgEfeSelector ¶
FfgEfeSelector(
backend: EfeBackend,
*,
info_block: Sequence[int],
n_candidates: int,
action_bounds: tuple[float, float],
)
EFE action selection over a branching FFG backend (issue #26).
The FFG counterpart of EFESelector: instead of
_efe_step on a flat model it vmaps _ffg_efe_step over the candidate
action grid, reading μ⁺/Σ⁺
from the backend's predicted_belief (structural couplings folded in) and aiming
the epistemic term at info_block — a joint-state block (a node's block via
backend.block for info_node, or the whole state). One-step
(horizon = 1); the H-step FFG rollout is a later seam. Conforms to
ActionSelector.
Source code in src/cpomdp/selection.py
n_candidates
property
¶
The per-cycle EFE-evaluation count — attributable work (RFC-001).
select ¶
The grid action minimising G (the per-cycle work).
For each candidate: predict the joint (predicted_belief → μ⁺, Σ⁺),
score with _ffg_efe_step, argmin over the grid. R is re-read at
μ⁺ per candidate — constant under a fixed sensor, action-dependent under
R(x) (the dual effect). This is the RFC-001 hot path; keep it lean.