# 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 `":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.