mixle.evolve.concept_discovery module

Concept discovery: the library itself under selection (CARD L6, the culmination of the L-track loop).

One loop – discrepancy -> propose -> verify -> adopt -> remember – run over the library of families rather than over any one model’s parameters. When several tasks recur with a residual pattern no family in the starting library explains well, this module selects the best-matching family for that pattern from a small set of REGISTERED, already-implemented families, fits it, gates it as a challenger against the status quo, and – only if it survives the gate – admits it to a small, inspectable, reversible registry (ConceptLibrary). “New” here means newly admitted to the library for this task signature – selection, not synthesis: the candidate is always one of a small, closed set of known families (four built in, plus any added via register_family()), never a likelihood, support, sufficient statistic, or inference procedure invented from scratch. A future task whose signature matches records where the concept won (mixle.task.design_prior), so the search reaches for it directly instead of re-discovering it from scratch. Today’s systematic error becomes tomorrow’s primitive.

This module adds no new modeling capability – it is pure wiring over five existing subsystems, each already carrying its own contract:

  • discrepancy – mixle.epistemic.discrepancy.discrepancy_report() measures how far a fitted champion’s predictive distribution is from held-out data; tracked across tasks, a sustained gap is the “recurring unmodeled residual” signal that triggers a proposal.

  • propose – mixle.utils.automatic.profiling._profile_series() (the automatic profiler that landed this session’s six new univariate detectors) is asked which registered family it would recommend for the data; that recommendation is the candidate family – selected from the registry, not synthesized.

  • verify / adopt – mixle.evolve.verify.challenger_beats_champion() is the single gate: the proposed family is only admitted if it significantly, non-regressively beats the current champion on held-out data.

  • remember – mixle.task.design_prior.record_accepted_recipe() over a mixle.task.edge.DesignModel persists where the concept won, tagged by a coarse task signature, so ConceptLibrary.query() can recommend it for a matching future task.

ConceptLibrary is intentionally its own small registry – not a mutation of mixle.stats’s hardcoded distribution list – so admission (and, symmetrically, revocation) is a local, receipted, reversible act rather than a change to shared global state.

class AdmissionEvent(action, family, task_index, evidence=<factory>)[source]

Bases: object

One receipted, timestamped-by-task-index entry in a ConceptLibrary’s audit log.

Parameters:
class ConceptLibrary(base_families=('gaussian',))[source]

Bases: object

A small, inspectable registry of admitted concept families – the “library” of CARD L6.

Starts with base_families (present from the outset, never revocable via revoke() – they are the starting library, not something this loop discovered). Everything admitted afterwards is a first-class, named, evidenced entry that can be queried by task signature and, if it turns out to be a mistake, genuinely revoked – removed from both the active family set and the design-prior ledger that backs query(), not merely hidden.

Parameters:

base_families (Sequence[str])

property families: tuple[str, ...]

the starting base plus everything still admitted.

Type:

Every family currently in the library

admit(family, evidence, *, task_signature, task_index, quality)[source]

Admit family to the library, receipted with evidence and recorded in the design-prior ledger under task_signature so query() can recommend it later.

family must name a REGISTERED family, and quality must be finite. An admission is the library’s promise that a later query() can hand this name back as something the search can actually reach for and fit; admitting an unregistered name made query() recommend a family that does not exist, and a non-finite quality made the ranking that picks between recorded recipes meaningless.

Parameters:
Return type:

None

revoke(family, *, task_index=-1, reason='')[source]

Undo an admission: family leaves the active family set AND the design-prior ledger, so query() can never recommend it again – genuinely reversible, not soft-hidden.

Parameters:
Return type:

None

query(signature)[source]

The design-prior recommendation for signature: the best-recorded, still-admitted family seen for a matching task signature, or None if nothing qualifies.

Parameters:

signature (str)

Return type:

str | None

class TaskResult(task_index, task_signature, discrepancy, challenger_family, reused_concept, admitted_family, verdict, mdl_gain_bits)[source]

Bases: object

One task’s pass through the loop – the receipt the acceptance test reads.

Parameters:
  • task_index (int)

  • task_signature (str)

  • discrepancy (float)

  • challenger_family (str | None)

  • reused_concept (bool)

  • admitted_family (str | None)

  • verdict (Verdict | None)

  • mdl_gain_bits (float)

register_family(name, constructor, *, replace=False)[source]

Register an already-implemented family’s constructor so the loop can select/reuse it (beyond the four built in). This does not define a new family: constructor must build a working, already-implemented distribution – registration only makes an existing family selectable here.

The constructor is invoked once and its prototype checked, so a name can never become selectable without something fittable behind it. Replacing an already-registered name requires replace=True: silently rebinding a name the library may already have admitted (and recorded receipts against) would retroactively change what that recorded evidence was evidence for.

Parameters:
Return type:

None

run_concept_discovery_loop(tasks, *, library=None, champion_family='gaussian', objective=None, train_frac=0.6, min_train=8, min_holdout=8, recurrence_window=2, discrepancy_threshold=0.003, min_effect=0.01, nonnested=False)[source]

Run discrepancy -> propose -> verify -> adopt -> remember over tasks, in order.

For every task: fit the champion_family on a train split, measure discrepancy against a held-out split (mixle.epistemic.discrepancy.discrepancy_report()). If the library already has an admitted concept recommended for this task’s signature (ConceptLibrary.query()), that concept is tried directly – no re-search. Otherwise, once high discrepancy has recurred for recurrence_window consecutive tasks sharing a signature with no admitted concept yet, the automatic profiler proposes a candidate family from its registered set (selection, not synthesis); it is gated via mixle.evolve.verify.challenger_beats_champion() and, only if it passes, admitted.

Each task’s data is split train, held_out = data[:n_train], data[n_train:] with n_train derived from train_frac but clamped so both sides meet an explicit floor: min_train rows to fit the champion/challenger on, min_holdout rows so discrepancy and the verify gate never run on empty (or near-empty) evidence. train_frac must lie in the open interval (0, 1); a task with fewer than min_train + min_holdout rows raises ValueError rather than silently starving the train or held-out side of the split.

Returns the (possibly newly-created) ConceptLibrary and one TaskResult per task.

Parameters:
  • tasks (Sequence[Sequence[float]])

  • library (ConceptLibrary | None)

  • champion_family (str)

  • objective (Objective | None)

  • train_frac (float)

  • min_train (int)

  • min_holdout (int)

  • recurrence_window (int)

  • discrepancy_threshold (float)

  • min_effect (float)

  • nonnested (bool)

Return type:

tuple[ConceptLibrary, list[TaskResult]]

task_signature(data)[source]

A coarse, reusable descriptor of a task’s data-generating shape.

Deliberately coarse (kind + skew sign, not a fingerprint of the exact data): the point is that different tasks sharing the same hidden family land on the same signature, so ConceptLibrary.query() can recommend a concept discovered on an earlier task to a later one.

Parameters:

data (Sequence[float])

Return type:

str