mixle.task.environment module

Environment protocol + belief-driven interaction loop (roadmap M1) – the generic act-observe-update spine that on-the-fly simulators (M2), inversion (M3), the language<->belief bridge (M5), and environments-as-selection-pressure (L1) build on.

interact() drives ANY object satisfying the Environment protocol against a streaming BELIEF over its latents (mixle.inference.streaming), picking actions by EIG (reusing mixle.task.probe_policy.myopic_eig_policy() unchanged), a belief-driven greedy heuristic, or a caller-supplied callable – then hands back a replayable InteractionLog (mixle.task.replay).

ExplorationWorld becomes the first environment: ExplorationEnvironment below is a THIN wrapper – it holds episode config and adapts reset/step/action_space onto the world; ExplorationWorld itself is untouched, so existing callers (run_episode, probe_policy) keep working exactly as before.

Belief math (see notes/designs/M1.md for the full writeup): a cell’s underlying latent (“geology”) is fixed for the episode; each accepted survey observation is one more noisy read of it. GaussianStreamingBelief folds those reads one at a time through mixle.inference.streaming.StreamingEstimator with a harmonic(1.0) schedule – which is exactly the textbook incremental-mean/-variance recursion (rho_t = 1/t), so the running GaussianDistribution is the exact batch MLE over all reads so far, not merely an approximation. Streaming’s own nobs bookkeeping is a decayed effective count (it does not grow with t under a stationary rho schedule), so credible intervals track their own read count separately for the standard-error term.

class Environment(*args, **kwargs)[source]

Bases: Protocol

Generic act-observe world.

reset starts (or restarts) an episode from a seed and returns an initial observation; step applies one action and returns (observation, cost); action_space lists the actions currently legal to take. Costs are returned per step (not tracked internally) so interact() can enforce ONE budget semantics uniformly across arbitrary environments.

class ExplorationEnvironment(n_cells, n_targets, budget)[source]

Bases: object

Thin Environment wrapper over ExplorationWorld.

Holds the episode config (cell/target/budget counts); reset(seed) builds a fresh ExplorationWorld and keeps it as self.world (so a caller – or the "eig" policy below, which reads ExplorationWorld internals exactly the way myopic_eig_policy() already does – can still get at the raw world). ExplorationWorld’s own public API is unmodified; this class only adapts it.

Parameters:
class GaussianStreamingBelief(prior_mu=0.0, prior_sigma2=4.0, min_covar=0.05, belief_pseudo_count=0.05)[source]

Bases: object

Per-cell streaming posterior over a scalar continuous latent (ExplorationWorld’s per-cell “geology” value), folded in one accepted survey observation at a time via mixle.inference.streaming.StreamingEstimator – the generic online sufficient- statistic machinery M0’s condition() is built to consume once a fitted model exists. One independent GaussianDistribution per cell; an unsurveyed cell reports the shared prior.

Parameters:
update(obs)[source]

Fold one accepted survey observation’s prospectivity read into that cell’s belief. Drill/rejected/other observations carry no continuous read and are not folded in here – a drill resolves ground truth directly, it needs no posterior (v1 scope).

Parameters:

obs (dict[str, Any])

Return type:

None

credible_interval(cell, level=0.9)[source]

A level-credible interval for the cell’s latent: the running Gaussian’s own mean, and a standard error of the mean built from a prior/sample-variance blend (see belief_pseudo_count) over this belief’s own read count – not the raw per-cell sample variance alone, which is degenerate (zero, before min_covar clamps it) at a single read and undercovers badly until several reads accumulate.

Parameters:
Return type:

tuple[float, float]

class InteractionLog(seed, budget, policy, trace, total_cost, n_actions)[source]

Bases: object

One episode’s action/observation/cost trace, replayable via mixle.task.replay.

Each recorded "act" step bundles POLICY DECISION + env.step + belief update as one unit (rather than recording the chosen action alone and replaying it against a bare env.step) because a world-peeking policy like "eig" (myopic_eig_policy() reads ExplorationWorld’s own RNG-backed prospectivity() while DECIDING) consumes the same environment randomness the eventual observation depends on – replaying only the action list would silently desync that RNG stream and stop reproducing bit-for-bit. Bundling the policy call into the replayed unit keeps the two draws in the same relative order both times.

Parameters:
  • seed (int | None)

  • budget (float)

  • policy (str)

  • trace (ExecutionTrace)

  • total_cost (float)

  • n_actions (int)

is_deterministic(env, belief_model)[source]

Replay this log against a fresh env/belief_model pair (same policy name, same seed) and confirm every recorded step reproduces exactly – the M1 replay receipt. Only named policies ("eig", "greedy") can be reconstructed for replay; a log built from an arbitrary callable policy cannot (the callable itself is not serialized).

Parameters:
  • env (Environment)

  • belief_model (Any)

Return type:

bool

interact(env, belief_model, *, policy='eig', budget, seed=None)[source]

Drive the act-observe-update loop.

Resets env, then repeatedly: pick an action over env.action_space() (EIG / belief- greedy / a caller callable), execute it via env.step, fold the observation into belief_model.update(obs), until the summed action cost would exceed budget or the policy/environment stops (action_space() empty, policy returns None, or the environment refuses the action). Every reset/act is recorded as a TraceStep (see InteractionLog for why policy decision + step are bundled into one "act" unit) so the returned InteractionLog replays deterministically via mixle.task.replay.

Parameters:
Return type:

InteractionLog