Changelog¶
All notable changes to psyphy are documented here.
The format follows Keep a Changelog. psyphy is
pre-1.0: while the version is 0.0.x, any release may change the public API, so
breaking changes are called out below but do not force a minor-version bump.
[Unreleased]¶
Breaking¶
- **
ThresholdConfig.target_pcis nowtarget_prob_correct(cleanup only, no behavior change)
PosteriorMeanCovarianceFieldandcontour_bandare gone, and their reductions are methods on the newWPPMCovarianceFieldSamples. The draws are taken once, when the object is built, so the ensemble, the mean and the band describe one sample of the posterior:
fields(ref_points) gives Σ for every draw, (n_samples, n_points, d, d), which
replaces a Python loop over posterior.sample. fields.samples is the draws pytree, for
a per-draw quantity psyphy does not ship. WPPMCovarianceFieldSamples.from_prior(model,
n=..., key=...) is the same object from a prior.
n_samples and key move from the call to the construction, so one check covers every
reduction. There is no sqrt_cov: an average of U Uᵀ is not U Uᵀ for any single U. Use
jnp.linalg.cholesky(fields.mean(x)) for a factor of the mean matrix, or
WPPMCovarianceField for one parameter set. The numbers are unchanged: the mean and the
band reproduce what the removed entry points returned, bit for bit.
WPPMPredictivePosteriortakes a requiredkey, andcov_fieldis gone.meanandvarianceare properties, so the constructor is the only place their randomness can enter. The key is split once into a key for drawing parameters and a key for the threshold sweep:
This removed 2 hardcoded keys, at predictive_posterior.py:414 (which carried a
# TODO: Make configurable via init) and :397. A third, the direction grid for
input_dim > 2, stays fixed on purpose and says so in a comment.
cov_field is removed from WPPMPredictivePosterior and from the
PredictivePosterior protocol. It computed PosteriorMeanCovarianceField's quantity at
a different default under its own key, so there were 2 public paths to
E[Σ(X) | data] returning numbers up to a few percent apart. Use
WPPMCovarianceFieldSamples.from_posterior(posterior).mean(x). A model with no
covariance field can now satisfy PredictivePosterior.
Threshold predictions shift. The sweep key was jax.random.PRNGKey(1) and is now
derived from the key you pass, so threshold_pred=True results move. At the settings
the Hong et al. reproduction uses (n_theta=16, n_length=1000, mc=2000), 4 keys
spread the recovered threshold trace by 4.3%, so this is the inversion's own Monte Carlo
noise rather than a change of method. Record the key alongside any figure built from the
sweep.
- Drawing parameters now names its key
MCMCPosterior.sampleandMAPPosterior.samplerequire bothnandkey!MCMCPosterior.sampleused to fall back tojax.random.PRNGKey(0)andMAPPosterior.sampleused to defaultn=1, so, the 3 implementations now match theParameterSourceprotocol they claim:
Changed¶
- The every-stored-sample path no longer subsamples.
PosteriorMeanCovarianceField(posterior)andcontour_band(posterior, points)pool the stored samples in order instead of askingposterior.samplefor a full-size draw. Both keep their current call shape and need no key, and the numbers are unchanged, because a full-size draw without replacement is a permutation (neither a mean nor a quantile depends on order)
A key is now required when n_samples is given, since that genuinely subsamples:
This also drops a jax.random.choice and a gather over every stored sample, run before
only to shuffle something about to be averaged.
Added¶
ParameterSourceinpsyphy.posterior, a one-method protocol for anything we can draw parameters from:sample(n, *, key)returning a PyTree with a leading axis ofn.Prior,MAPPosteriorandMCMCPosteriorall satisfy it, andParameterPosteriornow extends it withparamsandmodel.
Code written against ParameterSource accepts a prior and a fitted posterior alike, so a
prior predictive check and a posterior predictive check become one function called twice.
Chain structure and a density stay off it: sample pools the chains, and MCMCPosterior
has no log_prob.
Prior.sample(n, *, key), the vectorized form ofsample_params. Drawiequalssample_params(jax.random.split(key, n)[i]), so a prior ensemble matches the Python loop it replaces:
sample_params(key) keeps its single-draw signature, because model.init_params is on
the inference path and wants one unbatched draw rather than a batch of 1 to index.
Breaking¶
cov()andcov_batch()are gone from the covariance fields. Both methods have raisedDeprecationWarningsince the single entry point was introduced, andfield(x)replaces both.
The removal covers WPPMCovarianceField, PosteriorMeanCovarianceField and the
CovarianceField Protocol, which now states __call__ and sqrt_cov. The numbers are
unchanged: cov() forwarded to the same evaluation field(x) uses. sqrt_cov and
sqrt_cov_batch are untouched.
Added¶
- Credible bands on the contour radii:
contour_radii,contour_bandandContourBandinpsyphy.model.contour_radiigives the radius of a covariance's Mahalanobis-1 contour per direction; for the oddity task that radius is the discrimination threshold.contour_bandreduces those radii over posterior draws to a pointwise credible interval.
These names replace threshold_radii, threshold_band and ThresholdBand, which were
never released: they compute a property of any covariance matrix, so the names no longer
say "threshold". Nothing in 0.0.5 or earlier exposed them, so no installed version
breaks. threshold_pred and ThresholdConfig in psyphy.posterior keep their names,
because the contour they recover is defined by a target probability correct.
Fixed¶
OddityTask.predictdeclared the wrong return type. It was annotated-> jnp.ndarraywhileTaskLikelihood.predictspecifies-> tuple[jnp.ndarray, ...], and its helper_simulate_trial_mcwas annotated the same way. Both already returned the one-tuple(p_correct,)that the base class documents, so behaviour is unchanged; only the declarations were wrong.PredictivePosterior.meanand.variancelooked like they could returnNone. The lazy cache they read is initialized toNoneand populated by_ensure_computed(); it is now annotated and the invariant is asserted.- Three
list[np.array]annotations inpsyphy.data.datasetused thenp.arrayfunction in type position instead ofnp.ndarray.
Internal¶
- mypy now means one thing. The commit hook, CI and local runs previously used three
different mypy configurations that disagreed with each other. CI now runs the same
pre-commit hook a developer runs, mypy is pinned to one version in both
.pre-commit-config.yamland thedevextra, and the hook checks the whole project rather than only staged files. - The commit hook can now see JAX. It ran with
--no-site-packages, under which everyjnp.ndarrayannotation in psyphy resolved toAny; array types were effectively unchecked by the only gate that blocked a commit. - The mypy CI job no longer carries
continue-on-error: true. It reported 19 errors on every run and failed nothing. Those are fixed and the gate is live.
0.0.5 — 2026-10-09¶
Two new subpackages
(psyphy.viz, psyphy.data.published), a reproduction of a published figure, and two
breaking API changes in the model layer.
Breaking¶
TaskLikelihood.predictsignature and return type changed (#139). Stimuli are now passed as one packed, slot-indexed array instead of separate arguments, and the return is a tuple of sufficient statistics instead of a bare scalar:
If you wrote a custom TaskLikelihood, it must be updated. Unpack stimuli
yourself and wrap the return in a tuple. Returning a bare array no longer works
correctly: BernoulliTaskLikelihood.loglik indexes element 0 of the returned tuple, so
a bare scalar silently collapses every trial onto the first trial's probability.
TrialDatanow stores stimuli as one array (#136).refs=/comparisons=are replaced by a singlestimuliarray of shape(N, K, d), withresponsesof shape(N, R);Kis the number of stimuli per trial, so tasks with more than two slots are now representable.
A 1-D responses array of shape (N,) is still accepted and normalized to (N, 1).
Slots may optionally be named, e.g., stimulus_names=("ref", "comp") enables
data.stimulus("ref") alongside positional data.stimuli[:, 0, :].
For the oddity task, K is 2, not 3. The observer is shown three stimuli, but only
the two distinct means are stored; presenting the reference twice is encoded in
OddityTask, which draws two samples from the reference distribution and one from the
comparison. K counts stored stimuli, not presentations.
Added¶
psyphy.data.publishedloaders for published datasets, starting withhong2025:fetch,fetch_calibration_matrix,load_calibration_matrix,load_trials,load_reference_W,load_sigma_table,build_paper_model,w2d_to_rgb,default_data_dir. Datasets are downloaded on demand (resumable, cached outside the repo) rather than shipped with the package.psyphy.viz:plot_ellipses, plusauto_scaleandellipse_segments. The geometry is separated from the drawing layer so it is testable without a plotting backend, andmatplotlibis imported lazily.- Threshold prediction as first-class API ,
WPPMPredictivePosterior(..., threshold_pred=True)with aThresholdConfig, recovering threshold contours by numerically invertingP(correct). In this modeXis bare reference points of shape(n_test, input_dim)rather than paired stimuli. - 1-D WPPM support
input_dim=1is now handled by the Chebyshev basis, the prior, and the covariance-field computation, alongside the existing 2-D and 3-D cases. - A distributional layer in the likelihood hierarchy
BernoulliTaskLikelihoodandGaussianTaskLikelihoodsit betweenTaskLikelihoodand concrete tasks, each providingloglikandsimulateso a new task only implementspredict.OddityTaskis now aBernoulliTaskLikelihood. reductiononMAPOptimizer"mean"(new default) or"sum", controlling how the per-trial objective is aggregated. This interacts with gradient clipping: under"sum"the gradient scales with the number of trials, so a learning rate tuned on one dataset size does not transfer."mean"makes learning rates comparable to those reported by Hong et al 2025.- Tutorials reproduction of Hong et al. (2025) Figure 2B (threshold contours) from the published data, recovery of Weber's law with a flexible WPPM, and ellipse-field plotting.
- Tests:
test_data_published_hong2025.py,test_viz.py,test_docs_recipe.py(parses the code quoted in the docs and checks every call against the live API),test_quick_start_recovery.py,test_map_optimizer_clipping.py,test_likelihood_logic.py,test_data_format.py.
Fixed¶
- Response-shape broadcasting in the Bernoulli log-likelihood (#140). With
responsesof shape(N, 1)and probabilities of shape(N,),jnp.wherebroadcast to(N, N), so the summed objective mixed every trial's response with every trial's probability and the gradient was wrong. Responses are now reduced to(N,)first. - Deprecated
jax.tree_mapreplaced withjax.tree.map(#71 — thanks @rohansood10).
Changed¶
requires-pythonis>=3.10; CI runs lint, type checks, and tests on 3.10, 3.11, and 3.12.- Documentation pages now quote code from the runnable scripts beside them via snippet includes, so a page and its script cannot drift apart.
0.0.4 — 2026-03-31¶
Likelihood refactor. loglik and simulate became concrete methods on
TaskLikelihood, so a concrete task implements only predict; OddityTask lost its own
loglik and the duplicated vectorised Monte Carlo path (likelihood.py net −200 lines).
Documentation restructuring.
0.0.2 — 2026-03-27¶
First release published to PyPI. Full initial module set: psyphy.model (WPPM, priors,
noise models, the oddity task), psyphy.inference (MAP optimizer), psyphy.posterior,
psyphy.data, psyphy.utils.