Migrating to 0.8.2

0.8.2 is a patch release of 0.8.1 with the same public surface. It repairs the findings the ten adversarial reviews of the 0.8.1 candidate deferred (release-checklists/0.8.2-followups.md, decision D-0213), and the blocking defects a second ten-pass review found on the 0.8.2 candidate itself (release-checklists/0.8.2-reviews/, decision D-0217). No documented public API was renamed or removed; what was added is disclosure and control on existing objects (for example numerical_repairs(), surrogate_repairs, filtered= and tie_tolerance; the CHANGELOG lists each), plus internal helper names that are not part of the documented API. Every 0.8.1 pickle, JSON model and checkpoint loads, with one visible difference: the eight families that now serialize their constructor parameters (Dirichlet, DirichletProcessMixture, MultivariateStudentT, ProbabilisticPCA, HierarchicalMixture, JointMixture, GaussianCopula, RVineCopula) have a different content hash than on 0.8.1. A model_hash recorded for one of them on 0.8.1 – in a provenance header, a deploy manifest or a registry record – will not match the same model on 0.8.2, and Model.load of a 0.8.1 deploy artifact for one of them emits its content-hash integrity note. That note is expected for these eight families, not a sign of tampering; the model itself is unchanged. What changed otherwise is that a number of inputs 0.8.1 accepted and mis-scored in silence now raise. Code that never sent those inputs is unaffected; code that did was getting a wrong answer, and the error names what to change. The full list, by surface:

Univariate estimators refuse observations outside their support

ExponentialEstimator, PoissonEstimator, GeometricEstimator, UniformEstimator and LogSeriesEstimator used to drop a negative, NaN, infinite, or fractional observation from the sufficient statistics and fit the rest. They now raise ValueError naming the family and its support. Clean the column (x[np.isfinite(x)], or fix the sign error) before fitting; a zero-weight row is still exempt. WeibullEstimator and the other clamped estimators report their shape ceiling, scale floor and rate floor through numerical_repairs() instead of silently.

Scoring is the other half, and it moved the opposite way. Every support-limited family’s encoder now admits a finite value outside the support and scores it -inf, as the scalar log_density already did, so a mixture or HMM can encode one batch against every component and the component that owns a row takes it. On 0.8.1 the encoders of GeneralizedPareto, Nakagami, Rician, Tweedie, Pareto, NegativeBinomial, BetaBinomial and Bernoulli (among others) refused such a row outright, so a Mixture[Gaussian, Nakagami] could not even be encoded on data the Gaussian owns. Code that relied on that encode-time error to catch bad data will now get it at fit time instead, from the accumulator, and a mixture in which NO component supports a row names how many rows score -inf. Two refusals stay at encode time on purpose: NaN (and, for most continuous families, an infinity), which marks missing or malformed evidence rather than an observation, and a non-integer value passed to a count family (Poisson, Geometric, LogSeries, NegativeBinomial, BetaBinomial, Binomial, Bernoulli, Skellam), which has no support off the integers. An out-of-range integer, such as a negative count, is admitted and scored -inf.

quantile(q) refuses a q outside [0, 1] on every family

Most families used to answer an out-of-domain q: many returned NaN (scipy’s ppf convention), BernoulliDistribution and BetaBinomialDistribution returned a plausible support point (1.0 for q = 1.5, 10 for q = 1.1), and HalfNormalDistribution returned a negative number for q = -0.1. Every family that defines quantile – thirty-one of them – now raises ValueError naming the family, which is what four discrete families already did. Clamp the level, or fix the computation that produced it; q = 0 and q = 1 are still answered, with the support’s own bounds.

optimize() refuses inputs it used to fit meaninglessly

  • optimize(enc_data=...) with a zero-row batch, and optimize(data) on an empty sequence, raise instead of returning an unfitted model marked converged.

  • optimize(delta=0) raises: the test is gain < delta, so zero could never stop. Pass delta=None to remove the convergence stop. That is not a guaranteed iteration count: a rejected update (a non-improving or non-finite step) still ends the run, with a note saying how many of the max_its iterations ran, and fit_provenance().iterations records the count.

  • rng= and prev_estimate= raise TypeError by name when given the wrong type. (So does potentials= on the probabilistic-programming sampler fits, which is where that argument lives; optimize has no potentials parameter.)

  • A one-shot iterator with estimator=None is materialized and fitted (200 rows fit 200 rows) instead of fitting an empty IgnoredDistribution.

  • print_iter= without out= prints to sys.stdout instead of doing nothing. Its default is now None rather than 1, so a call that does not mention it stays quiet exactly as before; print_iter=0 still asks for no periodic lines. Only an explicit value starts printing.

  • The cap note for a run that went downhill under best-seen selection says so, and names the caller’s line; library-internal capped calls no longer warn from inside distill.

  • A bare str or bytes raises instead of being fitted as a categorical over its individual characters: optimize('hello world hello') modelled nine characters. Pass a sequence of observations (['hello', 'world', 'hello']).

  • A datetime64 or timedelta64 array raises, naming the dtype and suggesting data.astype('int64'). On 0.8.1 a datetime64 array was fitted as an ignored categorical over its timestamps, and a timedelta64 array crashed inside the profiler.

  • The same front door – those refusals, the refusals of a masked array, a numpy.matrix and a 0-dimensional array, and the normalization of a one-shot iterator, a structured array, a mapping of columns and a DataFrame into rows – now applies to every verb that reads data: Model().fit, Model().evaluate, propose, best_of, the vdata= validation set, learn_structure, learn_bayesian_network, fit_with_provenance, Monitor, Service.score, detect_drift and score_drift. Several of these read a DataFrame or a mapping as its column NAMES on 0.8.1 – fit_with_provenance(df) fitted a categorical over two labels and recorded n_records=2, detect_drift(model, reference, df) scored the labels, and Monitor.update(df) retrained on them – so results from those calls on a table change, and are now the ones optimize gives. A DataFrame handed to a record model arrives as rows keyed the way its encoder reads them; a mapping of columns or a structured array handed to a record model is refused on every verb, where the drift verbs used to report DRIFT on two identical batches of it. learn_mixture_structure and mixture_structure_health read a DataFrame or a mapping as rows too, and the structure learners refuse a sequence of scalars or of dict rows, which they cannot read as fields.

Latent models

  • MixtureDistribution and the HMM families refuse a component that is a likelihood factor rather than a generative law (a SequenceDistribution with no length model). Pass len_estimator=/len_dist= so every component models everything it emits.

  • An (n, 1) column array into a univariate estimator or a lookback HMM raises naming the shape and numpy.ravel; df['x'] gives the one-dimensional form.

  • HiddenMarkovModelDistribution.seq_posterior returns the documented smoothing marginals on a base install and with terminal_states, where it returned None.

  • LookbackHiddenMarkovModelDistribution.seq_posterior returns different numbers. It ran the forward-only kernel, so it returned the filtered P(state_t | observations up to t) under the smoothed name; it now returns the smoothing marginals P(state_t | the whole sequence), like every other seq_posterior in the package. The two agree at the last position and differ everywhere else (0.16 on a four-step chain). Pass filtered=True for the old values – the flag and its default match HiddenMarkovModelDistribution.seq_posterior.

  • A terminal-state HMM’s latent_posterior, viterbi, seq_viterbi, mode(), sample() and posterior_predictive return different answers. They answered for a model without the full restriction – including Viterbi paths that visit a terminal state mid-sequence, and paths and samples that END in a non-terminal state, both of which the model gives probability zero. A length-1 sequence is now assigned a terminal state. Models without terminal_states are unaffected.

  • LookbackHiddenMarkovModelDistribution.seq_posterior raises NotImplementedError when the model sets terminal_states, where it silently ran the unrestricted recursion. At lag=0 the same emissions can go to HiddenMarkovModelDistribution, which runs the restricted one.

  • LDAEstimator returns the last valid alpha iterate with a warning where it raised LDAConvergenceError out of optimize(); the strict solver still raises.

  • Variance floors applied inside mixtures and HMMs are disclosed in fit_provenance().repairs.

  • The hidden-Markov families start from a symmetry-broken initialization when their emissions are Gaussian or vector-valued. Gamma, Weibull, Beta, Poisson and categorical emissions decline that start and stay bit-identical to 0.8.1, including the slow initialization plateau where they had one: a categorical-emission HMM needs the same iteration budget it did.

  • Mixture fits warn when a component ends with fewer effective rows than parameters. The note can also fire from an intermediate EM iterate or an initialization subsample and describe a starvation the returned model does not have; check MixtureDistribution.component_row_mass on the returned model before acting on it.

Structure learning, proposals and lifecycle

  • learn_bayesian_network and the dependency screen raise on an empty corpus or ragged records.

  • propose() raises when the holdout would leave fewer records than a fit needs (holdout= or more rows), and names multimodal fields with the $['key'] spelling.

  • Registry, checkpoint and deploy paths refuse a non-finite header value, a corrupt version file, and an invalid trust_code flag before touching data.

  • Service(keep=...) requires a positive integer, and Registry.checkpointer(every=...) likewise, with resume= required to be a bool. A keep or every of 0 or a negative number asked for a buffer or a cadence that cannot exist.

  • Monitor(...) validates its thresholds at construction, with the same rules detect_drift applies: loglik_shift_threshold must be <= 0 (it is how far the mean log-likelihood may FALL, so a positive one flags drift on identical data) and psi_threshold must be non-negative. Both used to construct cleanly and raise from every check() and update() afterwards, naming a constructor argument from a method that does not take one.

Seeds derived from a prompt

mixle.task derives a per-prompt seed from a base seed. A prompt whose value has no canonical encoding (a custom class, say) now seeds from its repr, which is the fallback the API documented all along; it used to seed from the literal string "<base_seed>:None", so every such prompt in a run shared one seed. Draws recorded on 0.8.1 for those prompts will not reproduce – they were never per-prompt draws. The same applies to mixle models and posteriors used as prompts: on 0.8.1 they had no canonical encoding and fell into that shared seed; on 0.8.2 they seed from their canonical key, so their draws differ from 0.8.1’s. Prompts with a canonical encoding on both versions (str, bytes, numbers, None, and containers of those) are unaffected and seed exactly as before.

Bayesian optimization discloses a repaired surrogate

BayesOptResult gained numerical_repairs() (and the surrogate_repairs field behind it), which reports any covariance ridging the Gaussian-process surrogate needed during the run. It is () when none was needed, so reading it is always safe. Nothing else about the result changed, and the field is keyword-only with a default, so existing construction of a BayesOptResult is unaffected.

Compute kernels

NumbaKernelFactory.build raises KernelCapabilityDeclinedError when numba is not installed (the [numba] extra) instead of running an interpreted kernel under the numba name. Ask for GeneratedNumbaKernelFactory to fall back to the generic kernel, or install the extra.

Probabilistic programs and surrogates

  • .fit() on a program whose parameters are all constants warns instead of returning result=None SILENTLY – the return is unchanged, and the warning is the repair; an unfitted vector free() raises the same message a scalar one does.

  • TransformDistribution.mean()/.variance() are exact for an affine transform and raise NotImplementedError for a transform with no closed form, instead of returning a sampled number labelled exact. RandomVariable.mean/.var still answer for any transform: they catch that refusal and fall back to sampling, which is deliberate and is what the regression test pins.

  • The GP surrogate escalates its jitter and reports it; a matrix no jitter can rescue raises a named error instead of a raw torch _LinAlgError.

  • A sampler that accepted none of its proposals is refused rather than returned as a fit.

  • A sampler or Laplace fit (how='mcmc'|'nuts'|'hmc'|'ensemble'|'laplace') is refused when the model has parameters that NO prior and no data pin: more free parameters without a prior than there are numbers in the data. how='map' is not guarded and still fits such a model, returning the degenerate point estimate – for Normal(free, free) on one observation, a scale at its floor. Parameters carrying a proper prior are not counted – a one-observation Bayesian update is fitted as before – and a row of a vector model counts as the scalars it carries, not as one observation. Previously such a model wandered to scales that overflowed double precision and ended in an OverflowError raised from inside the lowering table. The guard sits in the numerical-target builder, so the default how='auto' route (which resolves to closed-form EM for an all-free model) and how='vi' do not pass through it and still fit.

  • how='ensemble' returns different posteriors at its default budget, and takes longer. Its burn default is now None, resolved to max(1500, 300 * d) sweeps for a d-parameter model; the old fixed 500 did not contract the walkers’ deliberately overdispersed start, and on ordinary 5- and 18-parameter models the returned posteriors were several posterior standard deviations off and several times too wide, with no warning. The walkers also start differently: around the log-target’s maximum over the location parameters, instead of every location at one shared data mean, which left a model whose location parameters sit far apart 141 posterior sds off at any burn-in. how='sample' dispatches to the ensemble for small models, so its answers change the same way. An explicit burn= is still honoured as given, so a call that passes a short one keeps that behaviour: run chains=2 or more and read the R-hat when the budget is not the default.

  • predict() on a posterior restored from a pickle raises instead of silently sampling the plug-in (posterior-mean) model. That now includes the conjugate route, the default for a conjugate model: on 0.8.1 a conjugate fit could not be pickled at all.