Exhaustive search¶
An exhaustive search scores every length-H sequence of a declared, finite action set and returns the best one. It also returns a CompletenessCertificate saying every sequence was scored. EFESelector samples a continuous range and reports CORROBORATED. A search decides over its declared set and reports PROVED. The names here are imported from cpomdp.enumeration.
FiniteActionSet
dataclass
¶
A finite, declared, versioned set of actions to enumerate over.
A distinct type from the continuous grid's (lo, hi, n_candidates) config, so the
enumerated (PROVED) and sampled (CORROBORATED) families cannot be mixed up
at a call site. Construction-time only (not a pytree).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
actions
|
ArrayLike
|
the declared actions, shape |
required |
version
|
str
|
a non-empty tag, so an action added later shows up in the diff. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/cpomdp/enumeration.py
Two searches, one interface¶
The two searches take the same model, set and horizon, and answer the same evaluate. Swapping one for the other changes the memory a plan needs and no line that reads the result. EnumeratedEFESearch holds every policy and every score, and is the one to reach for first. ChunkedEFESearch scores in blocks at a footprint its block sets. Use it past the front-loaded search's budget, which is 2^21 policies unless set.
In plain terms
Give the search a menu of moves and a number of steps. It writes down every way of chaining that many moves from the menu, scores each chain by expected free energy from the current belief, and hands back the lowest. Nothing is sampled. Every chain on the menu is tried, so "this is the best plan on the menu" is a decided fact rather than a likely one, and the certificate records the count that makes it so.
The number of chains is the menu's size raised to the number of steps, so it grows fast. The front-loaded search keeps every chain and every score in memory at once, which is quick and lets you look at all the scores afterwards. Past a budget it refuses at construction instead of running the machine out of memory, and its error says how big the set was and what to use instead. The chunked search works through the chains a block at a time and keeps only the best so far. It returns the same plan, bit for bit, and never holds the whole set.
One convention to know. A plan's cost is reported per plan, as cost_per_plan.
What an agent step costs depends on how often the agent plans, so only the
selectors below report cost_per_cycle.
ExhaustiveEFESearch ¶
Bases: ABC
An EFE search that scores every length-H sequence of a declared action set.
The kind the drivers take.
EnumeratedEFESearch holds every policy
and every score at once, and
ChunkedEFESearch scores the set a chunk
at a time. Both earn the same PROVED certificate. Code that reads only what
this class and SearchResult declare runs on
either.
It is public as a type, for annotations and isinstance. Subclassing it outside
cpomdp is not supported: a subclass records its set and horizon through the private
_enumerate, and the PROVED it inherits holds only for a search that does.
n_policies
property
¶
The enumerated policy count |A|^H, attributable work (RFC-001).
model
property
¶
The model every policy is scored under, on a graph the backend's own.
On a coupled graph it is the interface model, which leaves out the couplings
the score reads off backend.
backend
property
¶
The graph backend the policies are scored on, or None on a flat model.
cost_per_plan
abstractmethod
property
¶
Step evaluations one evaluate runs (RFC-001).
evaluate
abstractmethod
¶
Score every policy from belief, and return the argmin with its evidence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
belief
|
Belief
|
the belief to plan from. |
required |
preference
|
Preference
|
the target and precision the EFE scores against. |
required |
Source code in src/cpomdp/enumeration.py
policies_at ¶
The policies indices name, decoded from the index rather than looked up.
The way to turn a result's best_index back into a plan, on either search.
An index outside 0..|A|^H - 1 names no policy. Under jit or vmap it
decodes to a NaN policy, the one a result with no winner carries.
Raises:
| Type | Description |
|---|---|
ValueError
|
If a concrete index lies outside the enumeration, including the -1 a result carries when no policy scored finitely. Decoded, it would wrap to a real policy. |
Source code in src/cpomdp/enumeration.py
SearchResult
dataclass
¶
SearchResult(
best_policy: Float64[Array, "H p"],
best_g: Float64[Array, ""],
best_index: Int[Array, ""],
certificate: CompletenessCertificate,
)
The outcome of one exhaustive search: the best plan and the evidence for it.
ChunkedEFESearch returns one. The
front-loaded search returns
EnumeratedSearchResult, which adds
the score vector. Fields are read by name. A result does not unpack or index, so
no position can name one field on one search and another on the other.
A JAX pytree whose certificate is static data, so a jitted or vmapped
evaluate returns the whole result.
Attributes:
| Name | Type | Description |
|---|---|---|
best_policy |
Float64[Array, 'H p']
|
the argmin sequence, shape |
best_g |
Float64[Array, '']
|
the argmin's summed EFE, G. |
best_index |
Int[Array, '']
|
the argmin's position in the enumeration. |
certificate |
CompletenessCertificate
|
the completeness certificate (ADR-030). |
When no policy scores finitely there is no argmin. best_policy and best_g
are then NaN and best_index is -1. The certificate still holds, since it
certifies that every policy was scored.
EnumeratedEFESearch ¶
EnumeratedEFESearch(
model: LinearGaussianModel,
action_set: FiniteActionSet,
*,
horizon: int,
max_policies: int | None = _DEFAULT_MAX_POLICIES,
)
Exhaustive EFE search over A^H for a declared finite action set (ADR-031).
Front-loads (ADR-002) the full |A|^H enumeration of varying sequences at
construction, and checks its completeness there. evaluate scores them all and
returns the argmin, its score and index, the certificate and the full G vector.
Because the set is finite and fully enumerated the search decides its universal,
warrant PROVED, distinct from EFESelector's CORROBORATED grid.
ChunkedEFESearch runs the same enumeration
in blocks. The two take the same model, set and horizon, answer the same
evaluate and drive the same selectors. Swapping one for the other changes the
memory a plan needs and no line that reads the result.
Scoring is a strategy (ADR-034): the default constructor scores on a flat
LinearGaussianModel (policy_efe); over_backend scores on an FFG backend
(policy_efe_ffg, aimed at a node's block) so the same exhaustive search runs the
coupled-tree crossover model a flat model cannot express. The enumeration,
the certificate, and the cost are shared — only the per-policy scoring differs.
Unlike EFESelector this supports p >= 1 and varying sequences: it can
express a detour-then-exploit policy the constant-action family cannot.
Memory grows with the set. While evaluate runs, every policy, its score and a
belief per policy are resident at once, at least 8 · |A|^H · (H·p + 1 + n + n²)
bytes before XLA's intermediates. A set past max_policies is refused at
construction with that floor in the message. ChunkedEFESearch holds one block
at a time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
LinearGaussianModel
|
the flat model to score on. It needs a control matrix. |
required |
action_set
|
FiniteActionSet
|
the declared actions, enumerated as every length-H sequence. |
required |
horizon
|
int
|
H, the sequence length. |
required |
max_policies
|
int | None
|
the most policies the search will hold, |
_DEFAULT_MAX_POLICIES
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
PolicyBudgetError
|
If |
ValueError
|
If the model has no control matrix, if the action set's dimension
or the horizon does not fit, or if |
Source code in src/cpomdp/enumeration.py
policies
property
¶
The enumerated A^H policies, shape (|A|^H, H, p) (front-loaded).
A result's best_index goes through policies_at, which refuses the -1
of a result with no winner. Used to index this array, -1 picks the last
policy.
cost_per_plan
property
¶
Step evaluations one evaluate runs, |A|^H * H (RFC-001).
n_policies
property
¶
The enumerated policy count |A|^H, attributable work (RFC-001).
model
property
¶
The model every policy is scored under, on a graph the backend's own.
On a coupled graph it is the interface model, which leaves out the couplings
the score reads off backend.
backend
property
¶
The graph backend the policies are scored on, or None on a flat model.
over_backend
classmethod
¶
over_backend(
backend: GraphBackend,
action_set: FiniteActionSet,
*,
info_node: SupportsIndex
| Sequence[SupportsIndex]
| None = None,
horizon: int,
max_policies: int | None = _DEFAULT_MAX_POLICIES,
) -> EnumeratedEFESearch
Exhaustive search that scores on an FFG backend via policy_efe_ffg.
The FFG counterpart of the flat constructor: it enumerates the same A^H set
with the same completeness certificate, but scores each policy on a coupling
graph with the epistemic aimed at info_node's state, or the whole joint
state when it is None. This runs the coupled-tree model the flat
constructor cannot express. The policy budget is the flat constructor's.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
InadmissiblePartitionError
|
If the backend's carry partition severs an EFE-load-bearing edge. |
PolicyBudgetError
|
If |
ValueError
|
If the backend has no control matrix, if |
Source code in src/cpomdp/enumeration.py
evaluate ¶
Score every enumerated policy, and return the argmin with its evidence.
One vmap of the scorer over the front-loaded |A|^H policies, then a
NaN-safe argmin (a NaN-scoring policy must not silently win). The scorer
holds the flat model or the FFG backend as a closure constant, so the vmap
maps only over the policies. It is compiled once per search, so a plan after
the first runs the compiled program.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
belief
|
Belief
|
the belief to plan from. |
required |
preference
|
Preference
|
the target and precision the EFE scores against. |
required |
Returns:
| Type | Description |
|---|---|
EnumeratedSearchResult
|
The argmin, its score, its index, the certificate and the score vector. |
Source code in src/cpomdp/enumeration.py
policies_at ¶
The policies indices name, decoded from the index rather than looked up.
The way to turn a result's best_index back into a plan, on either search.
An index outside 0..|A|^H - 1 names no policy. Under jit or vmap it
decodes to a NaN policy, the one a result with no winner carries.
Raises:
| Type | Description |
|---|---|
ValueError
|
If a concrete index lies outside the enumeration, including the -1 a result carries when no policy scored finitely. Decoded, it would wrap to a real policy. |
Source code in src/cpomdp/enumeration.py
EnumeratedSearchResult
dataclass
¶
EnumeratedSearchResult(
best_policy: Float64[Array, "H p"],
best_g: Float64[Array, ""],
best_index: Int[Array, ""],
certificate: CompletenessCertificate,
g: Float64[Array, N],
)
Bases: SearchResult
The outcome of one front-loaded exhaustive search, every policy's score included.
A SearchResult plus the score vector, which
only the front-loaded path holds. Code that reads the shared fields runs on either
search.
Attributes:
| Name | Type | Description |
|---|---|---|
g |
Float64[Array, N]
|
the summed EFE of every enumerated policy, in enumeration order, shape
|
ChunkedEFESearch ¶
ChunkedEFESearch(
model: LinearGaussianModel,
action_set: FiniteActionSet,
*,
horizon: int,
chunk: int = _DEFAULT_CHUNK,
)
Exhaustive EFE search over A^H in blocks, at O(chunk) residency.
The same enumeration as
EnumeratedEFESearch and the same
warrant, run so that neither the policy set nor the score vector is ever
materialised. That is what puts sets past the front-loaded path's memory ceiling in
reach, and the reason to have it is a set declared too large to hold, never a
result that came out inconvenient.
Three things make the two paths interchangeable rather than merely similar. Blocks
are visited in increasing index order and the argmin updates on a strict <, so
ties resolve to the globally lowest index exactly as jnp.argmin does over the
whole vector. Both paths decode an index to its policy with one function. And
the trailing block is padded, with its padded lanes masked out of the argmin
and out of visited, so completeness is a count of real work rather than of
array slots.
It takes no policy budget. Its footprint is set by chunk, and its time grows
with cost_per_plan.
evaluate composes with jit, vmap and lax.scan. Inside one, the
loop's count exists only once the program runs. The certificate then states the
expected count, and a callback checks the loop's count in the run, so a miscount
fails the run instead of certifying it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
LinearGaussianModel
|
the flat model to score on. It needs a control matrix. |
required |
action_set
|
FiniteActionSet
|
the declared actions, enumerated as every length-H sequence. |
required |
horizon
|
int
|
H, the sequence length. |
required |
chunk
|
int
|
the most policies one block scores, |
_DEFAULT_CHUNK
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If the model has no control matrix, if the action set's dimension
or the horizon does not fit, if |
Source code in src/cpomdp/enumeration.py
chunk
property
¶
The block size actually used, clamped to |A|^H.
The answer does not depend on it. The residency does, and so does the wasted arithmetic in the padded tail.
cost_per_plan
property
¶
Step evaluations one evaluate runs, |A|^H * H (RFC-001).
n_policies
property
¶
The enumerated policy count |A|^H, attributable work (RFC-001).
model
property
¶
The model every policy is scored under, on a graph the backend's own.
On a coupled graph it is the interface model, which leaves out the couplings
the score reads off backend.
backend
property
¶
The graph backend the policies are scored on, or None on a flat model.
over_backend
classmethod
¶
over_backend(
backend: GraphBackend,
action_set: FiniteActionSet,
*,
info_node: SupportsIndex
| Sequence[SupportsIndex]
| None = None,
horizon: int,
chunk: int = _DEFAULT_CHUNK,
) -> ChunkedEFESearch
Chunked search that scores on an FFG backend via policy_efe_ffg.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
InadmissiblePartitionError
|
If the backend's carry partition severs an EFE-load-bearing edge. |
ValueError
|
If the backend has no control matrix, if |
Source code in src/cpomdp/enumeration.py
evaluate ¶
Score every policy in blocks, and return the argmin with its evidence.
One lax.scan over ceil(|A|^H / chunk) blocks. Each block decodes its own
indices, scores them under one vmap, and folds the result into running
scalars, so nothing of size |A|^H is ever alive. The loop is compiled once
per search, so a plan after the first runs the compiled program.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
belief
|
Belief
|
the belief to plan from. |
required |
preference
|
Preference
|
the target and precision the EFE scores against. |
required |
Returns:
| Type | Description |
|---|---|
SearchResult
|
The argmin, its score, its index, and the certificate the loop's own count |
SearchResult
|
earns. |
Source code in src/cpomdp/enumeration.py
policies_at ¶
The policies indices name, decoded from the index rather than looked up.
The way to turn a result's best_index back into a plan, on either search.
An index outside 0..|A|^H - 1 names no policy. Under jit or vmap it
decodes to a NaN policy, the one a result with no winner carries.
Raises:
| Type | Description |
|---|---|
ValueError
|
If a concrete index lies outside the enumeration, including the -1 a result carries when no policy scored finitely. Decoded, it would wrap to a real policy. |
Source code in src/cpomdp/enumeration.py
PolicyBudgetError ¶
Bases: ValueError
A front-loaded search asked to hold more policies than its budget allows.
EnumeratedEFESearch holds every policy and every score at once, so its
footprint grows with |A|^H. The count is exact before anything is allocated,
and an oversized set is refused there rather than exhausting the host. The message
gives the count, the step evaluations one plan costs and a floor on the memory. It
names ChunkedEFESearch, which runs the
same enumeration at a footprint its block sets and drives the same selectors.
IncompleteEnumerationError ¶
Bases: Exception
Fewer than |A|^H policies enumerated: a sample, not a decision.
Driving an agent¶
A search returns a whole plan. The two selectors below turn one into an action per step for Agent(selector=...), and each takes any ExhaustiveEFESearch. RecedingHorizonSelector plans every step and applies the first action. OpenLoopSelector plans once and applies the whole sequence. Their cost_per_cycle differs by a factor of H for that reason.
RecedingHorizonSelector ¶
Re-plans every step and applies the first action of the best sequence.
An ActionSelector over an exhaustive search. Every select re-runs the
full A^H search on the current belief, so the belief drives every choice. It
costs a whole plan per step, cost_per_cycle = |A|^H * H. A measurement has to
declare which mode it ran, so this and OpenLoopSelector stay separate (ADR-031).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
search
|
ExhaustiveEFESearch
|
the search to plan with. Either shipped search gives the same actions. |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/cpomdp/enumeration.py
replan_interval
property
¶
Agent steps between two plans. 1: this driver plans every step.
backend
property
¶
The graph backend the search scores on, or None on a flat model.
certificate
property
¶
The last plan's completeness certificate (ADR-030), None before one.
The chunked search earns its certificate by running, so a driver has one only once it has planned.
cost_per_cycle
property
¶
Step evaluations per agent step, one plan spread over its interval.
select ¶
Re-plan on belief and return the first action of the best sequence.
OpenLoopSelector ¶
Plans once, then applies the whole best sequence without looking at beliefs.
An ActionSelector over an exhaustive search. It plans on the belief of the
first select and returns the plan's actions in order, ignoring the interim
beliefs. It re-plans only when the committed sequence is spent, or after reset.
Ignoring the interim beliefs is what makes it open-loop. One plan covers H steps, so
cost_per_cycle = |A|^H. A measurement has to declare which mode it ran, so this
and RecedingHorizonSelector stay separate (ADR-031).
Stateful: it holds the committed plan and a step counter. The Agent drives it
eagerly, one select per step, so the mutation is safe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
search
|
ExhaustiveEFESearch
|
the search to plan with. Either shipped search gives the same actions. |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/cpomdp/enumeration.py
replan_interval
property
¶
Agent steps between two plans, H. It commits to the whole sequence.
backend
property
¶
The graph backend the search scores on, or None on a flat model.
certificate
property
¶
The last plan's completeness certificate (ADR-030), None before one.
The chunked search earns its certificate by running, so a driver has one only once it has planned.
cost_per_cycle
property
¶
Step evaluations per agent step, one plan spread over its interval.
select ¶
Return the next committed action, re-planning on belief when spent.
Source code in src/cpomdp/enumeration.py
reset ¶
Drop the committed plan and its certificate, so the next select plans.