mixle.epistemic.loop module

One epistemic-loop step: OBSERVE -> UPDATE -> ABDUCE (on surprise) -> PREDICT -> DISCRIMINATE -> ACT.

step() is a pure function the caller drives from their own loop (interactive, scripted, or agentic – this module doesn’t prescribe which). It is the integration point: everything in discrepancy, portfolio, and likelihood is a building block; this is where they compose. There is deliberately no multi-step run_until(...) driver and no persistence beyond one EpistemicStep’s own fields here – the program plan’s “episode”/investigation-trace concept (§4.1) is training-data machinery, out of scope for this plan (see notes/epistemic-loop-integration-workplan.md §6).

ACT’s expected-information-gain scoring does not call mixle.doe.active.expected_information_gain_nmc() directly: that function’s nested-Monte-Carlo estimator is written against a continuous numpy parameter space (prior_sampler(rng, n) -> (n, k) array), while a HypothesisPortfolio is a discrete weighted set of arbitrary typed hypothesis payloads. Forcing the portfolio through that interface would mean either requiring every hypothesis payload to be a numpy vector (defeating the point of a typed portfolio) or building a lossy adapter. Instead, _portfolio_eig_nmc below is the same nested-Monte-Carlo EIG estimator (Ryan 2003), rewritten one level down against the portfolio’s own discrete weighted draws – same math, the right data shape.

class EpistemicStep(observation, portfolio_before, portfolio_after, surprise, next_action, next_action_eig)[source]

Bases: object

The full outcome of one loop iteration – everything EpistemicJournal logs.

Parameters:
  • observation (Any)

  • portfolio_before (HypothesisPortfolio)

  • portfolio_after (HypothesisPortfolio)

  • surprise (float)

  • next_action (Any | None)

  • next_action_eig (float | None)

step(portfolio, observation, likelihood, *, action_space=None, simulate_fn=None, cost_fn=None, lam=1.0, surprise_threshold=None, propose_fn=None, n_outer=64, n_inner=64, rng=None)[source]

One loop iteration: reweight on observation, optionally abduce on surprise, optionally act.

UPDATE: portfolio.reweight(observation, likelihood). ABDUCE: only when surprise_threshold is set and the portfolio’s surprise_score() on observation meets or exceeds it, propose_fn(updated_portfolio) is called; a non-None return is folded in via _add_hypothesis() (program plan §3.5’s surprise trigger, at the scope this plan covers – schema-expansion / human-checkpoint semantics are not modeled here). ACT: when action_space is given, each candidate is scored by EIG(a) - lam * cost_fn(a) (program plan §2’s a* = argmax_a EIG(a) - lambda*cost(a)) via _portfolio_eig_nmc against the updated portfolio, and the argmax is returned; action_space=None is a valid “just update the belief” call and returns next_action=None. Raises ValueError if action_space is given without simulate_fn – EIG estimation needs a way to generate a predicted observation per hypothesis per action, and there’s no honest default for that.

Parameters:
Return type:

EpistemicStep