mixle.epistemic.portfolio module¶
HypothesisPortfolio – a weighted set of typed hypotheses plus an explicit open-world mass.
The program-plan’s H_t = {(h_i, w_i)} u {(_|_, w_|_)}: a sequential-Monte-Carlo particle cloud
generalized from mixle.inference.mcmc.particle_filter()’s numeric-state-only particles to
arbitrary typed hypothesis payloads, with the reserved “none of the above” mass carried as a
first-class field rather than folded into the particle list. Weights always satisfy w_open +
sum(active weights) == 1 – every mutating method returns a new portfolio with the invariant
already restored, and the constructor validates it on every construction (no silent drift).
Pruning (prune()) never deletes a hypothesis, only deactivates it – the same
never-truly-forget philosophy already used by mixle.substrate.belief’s cascading retraction –
and its freed mass folds into w_open: a pruned hypothesis was one we could no longer defend, which
is exactly what growing the “we don’t currently have an explanation” mass means (see
notes/epistemic-loop-integration-workplan.md §5 Q1). resample() delegates to the same
systematic/multinomial resampling math mixle.inference.mcmc.particle_filter() uses, applied only
to the active mass – w_open is untouched by resampling, since it isn’t a particle to resample.
- class Hypothesis(id, payload, active=True)[source]
Bases:
objectOne typed hypothesis in a portfolio.
payloadis opaque to the portfolio itself.
- class HypothesisPortfolio(hypotheses, weights, w_open=0.0)[source]
Bases:
objectA weighted, typed hypothesis set with an explicit reserved open-world mass
w_open.weightsis a private copy of what the constructor was given, exposed read-only: the constructor’s invariant checks run once, and a writable or caller-aliased array would let them be undone immediately afterwards. Build a new portfolio (or usereweight()/prune()/resample()/resurrect(), which all return one) to change weights;weights.copy()gives a writable array when a caller genuinely wants scratch space.- Parameters:
hypotheses (Sequence[Hypothesis])
weights (np.ndarray)
w_open (float)
- reweight(observation, likelihood_fn, *, open_world_likelihood=None)[source]
Bayesian-reweight every active hypothesis by
likelihood_fn(h, observation).open_world_likelihood(observation)reweightsw_opentoo; it defaults to a flat constant baseline of1.0– an implicit “moderately plausible, independent of how badly the current hypotheses fit” prior – which is what makes the surprise mechanism work without extra wiring: when every active hypothesis’s likelihood collapses toward zero on an out-of-support observation, the (unchanged) open-world baseline dominates the renormalization andw_opengrows on its own, exactly the “the residual resists the current hypothesis schema” signal the program plan’s surprise trigger names. If every likelihood (including the open-world baseline) is zero, all mass moves tow_open– the honest “nothing, including the reserved slot, explains this” outcome, rather than raising or producing NaNs.Every likelihood (including the open-world one) must be finite and non-negative – see
_checked_likelihood(). That all-zero branch is reserved for genuinely unsupported observations; invalid evidence raisesValueErrorinstead of being routed through it.
- resample(*, method='systematic', ess_threshold=0.5, rng=None)[source]
Resample the active particle set if effective sample size drops below
ess_threshold * n.w_openis untouched – it is a reserved mass, not a particle. Resampled duplicates of the same source hypothesis get id-suffixed copies ("h2","h2#1", …) so every hypothesis id in the returned portfolio stays unique, whichresurrect()/the journal rely on.methodandess_thresholdare validated up front, before any early return (MXR-080-1759). Validating them lazily – at the point each is first used – made the contract depend on the current weights: an unknownmethodwas silently accepted whenever ESS happened to clear the threshold, so the identical call raised or did not depending on data.ess_thresholdcompares againstess / n, which lies in(0, 1], so the meaningful domain is[0, 1](0disables resampling,1resamples anything short of uniform weights); a negative threshold silently disabled resampling, and NaN made every comparison false and forced an unrequested resample on every call.- Raises:
ValueError – if
methodis not a known resampler, oress_thresholdis not a finite number in[0, 1].- Parameters:
- Return type:
HypothesisPortfolio
- prune(*, min_weight)[source]
Deactivate (never delete) active hypotheses below
min_weight; their mass folds intow_open.min_weightmust be finite and non-negative.weight < NaNis false for every weight, so a NaN threshold pruned nothing at all and returned an unchanged portfolio – outwardly identical to “no hypothesis was below the threshold”, which is the one reading that makes a caller move on.- Parameters:
min_weight (float)
- Return type:
HypothesisPortfolio
- resurrect(hypothesis_id, *, floor_weight=1e-3)[source]
Reactivate a deactivated hypothesis, taking its floor weight out of
w_open(mass-conserving).
- surprise_score(observation, likelihood_fn)[source]
Joint improbability of
observationunder every active hypothesis, in[0, 1).baseline / (baseline + weighted_mean_likelihood)against the same flatbaseline = 1.0reweight()uses by default – close to 0 when some active hypothesis explains the observation well, close to 1 when every active hypothesis assigns it near-zero likelihood (program plan §3.5’s “improbable under every live hypothesis” surprise condition). A heuristic scalar, not a calibrated probability – callers threshold it, this method just computes it.The
[0, 1)range is only a range if every likelihood is finite and non-negative, which_checked_likelihood()enforces here:weighted_likof-1makes the denominator zero and the scoreinf, anything below-1makes it negative, NaN propagates, andinfcollapses it to exactly0. Each of those is a number outside the advertised range that a caller’s threshold test would nonetheless silently accept.