mixle.evolve.closed_loop module¶
L1: closed-loop self-evolution with operator credit – the loop at five altitudes, wired end to end.
Router-harvested failures -> acquisition (A5’s mixle.task.acquire.acquire()) -> challenger
production (a distill/refine/evolve operator, all reused
ImprovementOperator instances, not reimplemented) -> the held-out
challenger_beats_champion() gate -> deploy, as ONE budgeted background
loop (ClosedLoopSelfEvolution). A per-context meta-bandit (OperatorCreditBandit,
wrapping mixle.task.bandit.UCB1 – the same UCB1 machinery reused across this codebase for
meta-bandits) learns which challenger-production operator wins for which kind of failure, and every
ADOPTED champion gets a genealogy receipt (GenealogyLedger, built directly on
EvolutionLedger) recording its parent, the operator that produced it,
and the measured gap – a real, walk-backable lineage.
Which subsystems this wires, not rebuilds:
mixle.task.router–harvested_from_router()reads a realRouter’s.harvested()(the frontier-answered cases the cheap tiers could not handle) as the harvested-failure source when a caller already runs one. For domains without a live Router (e.g. this module’s own tests, which evolve a marginal label model rather than a routed per-input classifier),harvest_failures()is the same idea generalized to anyObjective: the observations where the current champion did NOT score at its own best-attainable pointwise value – literally “the cases it got wrong.”mixle.task.acquire.acquire()(A5) ranks the harvested pool before it is spent on challenger production.acquireneeds a “scoreable” model (predict_proba-shaped); a champion that is a bare marginal distribution (does not condition on the item) is wrapped by_ConstantProbaAdapterso the realacquire()code path runs (entropy strategy) instead of being skipped – honestly, this degenerates to “prioritize while the champion overall is unconfident” since the per-item probability is constant, but it is the real primitive, not a mock.mixle.evolve.operators– three EXISTING operators stand in for L1’s “distill / refine / evolve ops” triad (seedefault_challenger_operators()):AutoSelect(cold-start refit – distillation’s “train a fresh model from the teacher-labeled pool” shape) as"distill",Refit(warm-started refit of the champion’s own parameters) as"refine", andMutate(the genetic-programming structure-edit operator) as"evolve".mixle.evolve.verify.challenger_beats_champion()is the held-out gate, used exactly as-is.mixle.evolve.ledger.EvolutionLedgeris the genealogy substrate, wrapped (not replaced) byGenealogyLedger.mixle.task.bandit.UCB1is the meta-bandit’s arm-selection machinery, one instance per context.
Principled crossover. If a challenger-production operator ever combines two existing champions
(a “crossover” in evolutionary-algorithm terms), it MUST do so via mixture composition
(mixle.ops.mixture(), exactly what Mutate’s grow move and
Recompose already do) – never by cutting and pasting incompatible
weight sub-blocks from two unrelated models. principled_crossover() is the explicit primitive for
that case.
- accuracy_objective()[source]
Higher-is-better accuracy for a model exposing a single “most likely label” prediction (a fitted
CategoricalDistribution’sargmax(pmap)): per-observation1{y == predicted_label}.- Return type:
Objective
- harvest_failures(champion, batch, objective)[source]
The objective-generic analog of
mixle.task.router.Router.harvested(): the observations inbatchwherechampiondid NOT score at its own best-attainable pointwise value underobjective– foraccuracy_objective()this is exactly “the cases it got wrong.” A scalar-only objective (no honest per-observation vector) harvests the whole batch – there is no finer signal to rank on.
- harvested_from_router(router)[source]
Pull the REAL harvested failure pool from a live
mixle.task.router.Router: every input that escalated all the way to the frontier, paired with its frontier label – exactlyrouter.harvested(), just zipped into one pool formixle.task.acquire.acquire().
- default_challenger_operators()[source]
The three L1 challenger-production operators, each a reused
ImprovementOperator(no bespoke fitting logic):"distill"->AutoSelect– cold-start: fit a fresh model from the harvested pool, the “train a new model from teacher-labeled data” shape of distillation."refine"->Refit– warm-started refit of the champion’s own parameters on the harvested pool."evolve"->Mutate– genetic-programming structure edit (grow/shrink/perturb), mixle.evolve’s own structure-search operator.
- principled_crossover(model_a, model_b, *, weight_a=0.5)[source]
Combine two champions the ONLY principled way this framework allows: mixture composition (
mixle.ops.mixture()), never gene-splicing (cutting/pasting incompatible weight sub-blocks). Returns an unfitted mixture prototype; refit it against data (e.g. via its own.estimator()) before treating it as a challenger.
- class OperatorCreditBandit(operator_names, *, c=1.0, seed=0)[source]
Bases:
objectA per-context meta-bandit over challenger-production operators, wrapping one
mixle.task.bandit.UCB1per context (e.g. a failure type/domain) – so the loop learns, independently for each context, which operator actually produces winning challengers there.- select(context)[source]
The bandit-chosen operator name for
context.
- reward(context, operator, reward)[source]
Fold an observed (non-negative, anti-regression) reward back into
operator’s arm.
- class GenealogyLedger(ledger=<factory>, _model_ids=<factory>, _counter=0)[source]
Bases:
objectGenealogy receipts for adopted (gate-passing) champions, built directly on
EvolutionLedger(never storing model objects in the ledger rows themselves – only their operator, measured gap, and a stable id – exactly the ledger’s own JSON-serializability discipline).- record_adoption(*, parent, child, operator, gap, context, meta=None)[source]
Record ONE adoption:
childreplacedparentviaoperator, measuredgap(the verified challenger-beats-champion delta).parent=Nonemarks the root of a lineage.
- lineage(model)[source]
Reconstruct
model’s full lineage: the ordered (root-first) chain of adoption receipts{operator, delta (the measured gap), parent_hash, meta: {child_hash, context, ...}}back to the first recorded ancestor. Returns[]ifmodelwas never recorded as an adoption.
- class LoopStepResult(context, operator, promoted, delta, champion_score, champion)[source]
Bases:
objectOne
ClosedLoopSelfEvolution.step()outcome.
- class ClosedLoopSelfEvolution(champion, *, objective, operators=None, context_fn=None, acquire_k=32, acquire_strategy='entropy', bandit_c=1.0, seed=0)[source]
Bases:
objectThe whole L1 loop, one budgeted step at a time: harvest -> acquire -> propose -> gate -> deploy, with per-context operator credit and genealogy receipts on every adoption.
- Parameters:
champion (Any) – the initial fitted model (e.g. a
CategoricalDistribution).objective (Objective) – the
Objectivethe loop optimizes (e.g.accuracy_objective()).operators (dict[str, ImprovementOperator] | None) – challenger-production operators by name; defaults to
default_challenger_operators().context_fn (Callable[[Sequence[Any]], str] | None) –
batch -> context keyfor the per-context bandit; defaults to a single global context ("default") when the caller has no domain/failure-type signal to condition on.acquire_k (int) – how many harvested failures A5’s
acquireprioritizes per step.acquire_strategy (str) – the
acquire()ranking strategy (default"entropy", the one that works for a marginal/non-conditional champion via_ConstantProbaAdapter).seed (int) – RNG seed threaded through the bandit and the gate.
bandit_c (float)
- step(batch, *, verify=None, context=None)[source]
One cycle of the loop on one arriving
batchof held-out-shaped observations.harvest failures under the CURRENT champion,
rank them with A5’s
acquire,pick a challenger-production operator via the per-context bandit,
propose + gate the challenger,
reward the bandit with the verified (anti-regression) delta,
deploy + record a genealogy receipt iff the gate promotes.
- run(stream, *, verify_batches=None, contexts=None, budget=None)[source]
Run the loop over a SEQUENCE of batches (a stream) – the budgeted background loop’s outer iteration.
budget(if given) is a ceiling on the totalcost_hintof operators actually applied (proposal attempts that were skipped as inapplicable don’t spend budget); the loop stops early once it is exhausted, leaving the champion at whatever it last became.