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; action_cost quotes a finite, non-negative upper bound before mutation; step applies one action and returns (observation, actual_cost); action_space lists the actions currently legal to take. The actual cost must not exceed the quote. This reservation contract lets interact() enforce one budget semantics across arbitrary environments without executing an action first and discovering afterward that it was unaffordable.

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:
action_cost(action)[source]

Return the exact cost of a supported action without mutating the world.

Parameters:

action (dict[str, Any])

Return type:

float

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.

level must be finite and in the open interval (0, 1); the returned bounds are finite and ordered by postcondition, so a caller never receives a reversed or NaN “credible” interval.

Parameters:
Return type:

tuple[float, float]

class InteractionLog(seed, budget, policy, trace, total_cost, n_actions, n_attempts, max_steps, stop_reason)[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)

  • n_attempts (int)

  • max_steps (int)

  • stop_reason (str)

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, max_steps=1_000)[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