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.
- ActionLikelihood
(hypothesis, action, observation) -> p(y | h, a)– the ACTION-CONDITIONED outcome law.MXR-080-1896. ACT’s
EIG = E[log p(y|h,a) - log sum_h' w_h' p(y|h',a)]is an expectation over the outcome law of the experiment ``a``, andsimulate_fn(h, a, rng)already draws from that law. The matching density had nowhere to live:step()’slikelihoodis a two-argument(hypothesis, observation)LikelihoodStrategy, so wheneversimulate_fn’s law genuinely depended onathe estimator was pairing draws fromq(y|h,a)with a densityp(y|h)from a different distribution. That ratio is not a mutual information and is not even sign-constrained:mixle.task.discrepancy_invention_loop’s probe, which rejection-samples the truncated lawp(y|h, |y-a| <= w), reported “EIG” values of-1.15nats for four of five candidate actions where the true action-conditioned EIG was+0.04– so the number being maximized was not the information gain of the experiment being commissioned.This is an optional second density, not a replacement:
action_likelihood=Nonekeeps the existing two-argument behaviour verbatim, which stays exactly right whenever the outcome law does not depend on the action (p(y|h,a) == p(y|h)) – the common case for asimulate_fnthat merely predicts from the hypothesis. Nothing here can detect which case a caller is in (asimulate_fnis opaque), so declaring it is the caller’s job and there is deliberately no guard that rejects the two-argument form.
- class EpistemicStep(observation, portfolio_before, portfolio_after, surprise, next_action, next_action_eig)[source]
Bases:
objectThe full outcome of one loop iteration – everything
EpistemicJournallogs.
- step(portfolio, observation, likelihood, *, action_space=None, simulate_fn=None, action_likelihood=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 whensurprise_thresholdis set and the portfolio’ssurprise_score()onobservationmeets or exceeds it,propose_fn(updated_portfolio)is called; a non-Nonereturn 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: whenaction_spaceis given, each candidate is scored byEIG(a) - lam * cost_fn(a)(program plan §2’sa* = argmax_a EIG(a) - lambda*cost(a)) via_portfolio_eig_nmcagainst the updated portfolio, and the argmax is returned;action_space=Noneis a valid “just update the belief” call and returnsnext_action=None. RaisesValueErrorifaction_spaceis given withoutsimulate_fn– EIG estimation needs a way to generate a predicted observation per hypothesis per action, and there’s no honest default for that.UPDATE evaluates each likelihood exactly once (see
_OnceLikelihood): surprise and the posterior are derived from the same evidence, so a stateful or stochastic likelihood can no longer make one step report two different beliefs, and an expensive likelihood is not paid for twice.action_likelihood(MXR-080-1896, seeActionLikelihood) declares the ACTION-CONDITIONED outcome densityp(y|h,a)that ACT’s own formula names. Supply it wheneversimulate_fn’s outcome law depends on the action – with it left atNonethe two-argumentlikelihoodis used for scoring, which is right only when it does not. UPDATE is unaffected either way: it conditions on an observation the caller has already obtained, not on a simulated one, so it keeps usinglikelihood.MXR-080-1896, selection vs. final evidence:
next_action_eigis NOT the score that won the argmax.max_a EIG_hat(a)over noisy Monte Carlo estimates is the winner’s curse – it is biased high for the winner precisely because an upward error is what made a candidate win (measured at+0.054nats on a~0.2nat probe, a ~27% overstatement). The argmax selects; the winner’s EIG is then re-estimated once on an INDEPENDENT draw stream and that unbiased number is what gets reported and journaled. Only the reported magnitude changes – which action is chosen is still decided by the first pass, since re-scoring every candidate would just recreate the same curse.The ACT economics are validated rather than trusted:
lamand everycost_fnresult must be finite and non-negative, andn_outer/n_innermust be exact positive integers. Without those checks a NaN cost or EIG madescore > best_scorefalse for every candidate, so a one-candidate action space returned “no action worth taking” instead of reporting that its evidence was invalid; a negative cost was scored as a reward, and a negativelaminverted the declared cost penalty into a bonus.- Parameters:
portfolio (HypothesisPortfolio)
observation (Any)
likelihood (LikelihoodStrategy)
simulate_fn (Callable[[Hypothesis, Any, RandomState], Any] | None)
action_likelihood (Callable[[Hypothesis, Any, Any], float] | None)
lam (float)
surprise_threshold (float | None)
propose_fn (Callable[[HypothesisPortfolio], Hypothesis | None] | None)
n_outer (int)
n_inner (int)
rng (Any)
- Return type:
EpistemicStep