Psyphy¶
psyphy
¶
psyphy¶
Psychophysical modeling and adaptive trial placement.
This package implements the Wishart Process Psychophysical Model (WPPM) with modular components for priors, task likelihoods, and noise models, which can be fitted to incoming subject data and used to adaptively select new trials to present to the subject next. This is useful for efficiently estimating psychophysical parameters (e.g. threshold contours) with minimal trials.
Workflow
Core design
- WPPM (model/wppm.py):
- Structural definition of the psychophysical model.
- Maintains parameterization of local covariance fields.
- Computes discriminability between stimuli.
-
Delegates trial likelihoods and predictions to the task.
-
Prior (model/prior.py):
- Defines the distribution over model parameters.
- MVP: Gaussian prior over diagonal log-variances.
-
Full WPPM mode: structured prior over basis weights and lengthscale-controlled covariance fields.
-
TaskLikelihood (model/task.py):
- Encodes the psychophysical decision rule.
- MVP: OddityTask (3AFC) and TwoAFC with sigmoid mappings.
-
Full WPPM mode: loglik and predict implemented via Monte Carlo observer simulations, using the noise model explicitly.
-
NoiseModel (model/noise.py):
- Defines the distribution of internal representation noise.
- MVP: GaussianNoise (zero mean, isotropic).
- Full WPPM mode: add StudentTNoise option and beyond.
Unified import style
Top-level (core models + session): from psyphy import WPPM, Prior, OddityTask, GaussianNoise, MAPOptimizer from psyphy import ExperimentSession, ResponseData, TrialBatch
Subpackages: from psyphy.model import WPPM, Prior, OddityTask, TwoAFC, GaussianNoise, StudentTNoise from psyphy.inference import MAPOptimizer, LangevinSampler, LaplaceApproximation from psyphy.posterior import Posterior, effective_sample_size, rhat from psyphy.trial_placement import GridPlacement, GreedyMAPPlacement, InfoGainPlacement, SobolPlacement, StaircasePlacement from psyphy.utils import grid_candidates, sobol_candidates, custom_candidates, chebyshev_basis
Data flow
- A ResponseData object (psyphy.data) contains trial stimuli and responses.
- WPPM.init_params(prior) samples parameter initialization.
- Inference engines optimize the log posterior: log_posterior = task.loglik(params, data, model=WPPM, noise=NoiseModel) + prior.log_prob(params)
- Posterior predictions (p(correct), threshold ellipses) are always obtained through WPPM delegating to TaskLikelihood.
Extensibility
- To add a new task: subclass TaskLikelihood, implement predict/loglik.
- To add a new noise model: subclass NoiseModel, implement logpdf/sample.
- To upgrade from MVP -> Full WPPM mode: replace local_covariance and discriminability with basis-expansion Wishart process + MC simulation.
MVP vs Full WPPM mode
- MVP is a diagonal-covariance, closed-form scaffold that runs out of the box.
- Full WPPM mode matches the published research model:
- Smooth covariance fields (Wishart process priors).
- Monte Carlo likelihood evaluation.
- Explicit noise model in predictions.
Classes:
Name | Description |
---|---|
ExperimentSession |
High-level experiment orchestrator. |
GaussianNoise |
|
LangevinSampler |
Langevin sampler (stub). |
LaplaceApproximation |
Laplace approximation around MAP estimate. |
MAPOptimizer |
MAP (Maximum A Posteriori) optimizer. |
OddityTask |
Three-alternative forced-choice oddity task (MVP placeholder) ("pick the odd-one out). |
Posterior |
MVP Posterior (MAP only). |
Prior |
Prior distribution over WPPM parameters |
ResponseData |
Container for psychophysical trial data. |
StudentTNoise |
|
TrialBatch |
Container for a proposed batch of trials |
TwoAFC |
2-alternative forced-choice task (MVP placeholder). |
WPPM |
Wishart Process Psychophysical Model (WPPM). |
ExperimentSession
¶
High-level experiment orchestrator.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
model
|
WPPM
|
(Psychophysical) model instance. |
required |
inference
|
InferenceEngine
|
Inference engine (MAP, Langevin, etc.). |
required |
placement
|
TrialPlacement
|
Adaptive trial placement strategy. |
required |
init_placement
|
TrialPlacement
|
Initial placement strategy (e.g., Sobol exploration). |
None
|
Attributes:
Name | Type | Description |
---|---|---|
data |
ResponseData
|
Stores all collected trials. |
posterior |
Posterior or None
|
Current posterior estimate (None before initialization). |
Methods:
Name | Description |
---|---|
initialize |
Fit an initial posterior before any adaptive placement. |
next_batch |
Propose the next batch of trials. |
update |
Refit posterior with accumulated data. |
Source code in src/psyphy/session/experiment_session.py
initialize
¶
Fit an initial posterior before any adaptive placement.
Returns:
Type | Description |
---|---|
Posterior
|
Posterior object wrapping fitted parameters. |
Notes
MVP: Posterior is fitted to empty data (prior only). Full WPPM mode: Could use pilot data or pre-collected trials along grid etc.
Source code in src/psyphy/session/experiment_session.py
next_batch
¶
next_batch(batch_size: int)
Propose the next batch of trials.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
batch_size
|
int
|
Number of trials to propose. |
required |
Returns:
Type | Description |
---|---|
TrialBatch
|
Batch of proposed (reference, probe) stimuli. |
Notes
MVP: Always calls placement.propose() on current posterior. Full WPPM mode: Could support hybrid placement (init strategy -> adaptive strategy).
Source code in src/psyphy/session/experiment_session.py
update
¶
Refit posterior with accumulated data.
Returns:
Type | Description |
---|---|
Posterior
|
Updated posterior. |
Notes
MVP: Re-optimizes from scratch using all data. Full WPPM mode: Could support warm-start or online parameter updates.
Source code in src/psyphy/session/experiment_session.py
LangevinSampler
¶
Langevin sampler (stub).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
steps
|
int
|
Number of Langevin steps. |
1000
|
step_size
|
float
|
Integration step size. |
1e-3
|
temperature
|
float
|
Noise scale (temperature). |
1.0
|
Methods:
Name | Description |
---|---|
fit |
Fit model parameters with Langevin dynamics (stub). |
Attributes:
Name | Type | Description |
---|---|---|
step_size |
|
|
steps |
|
|
temperature |
|
Source code in src/psyphy/inference/langevin.py
fit
¶
fit(model, data) -> Posterior
Fit model parameters with Langevin dynamics (stub).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
model
|
WPPM
|
Model instance. |
required |
data
|
ResponseData
|
Observed trials. |
required |
Returns:
Type | Description |
---|---|
Posterior
|
Posterior wrapper (MVP: params from init). |
Source code in src/psyphy/inference/langevin.py
LaplaceApproximation
¶
Laplace approximation around MAP estimate.
Methods:
Name | Description |
---|---|
from_map |
Construct a Gaussian approximation centered at MAP. |
MAPOptimizer
¶
MAPOptimizer(
steps: int = 500,
learning_rate: float = 5e-05,
momentum: float = 0.9,
optimizer: GradientTransformation | None = None,
*,
track_history: bool = False,
log_every: int = 10
)
Bases: InferenceEngine
MAP (Maximum A Posteriori) optimizer.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
steps
|
int
|
Number of optimization steps. |
500
|
optimizer
|
GradientTransformation
|
Optax optimizer to use. Default: SGD with momentum. |
None
|
Notes
- Loss function = negative log posterior.
- Gradients computed with jax.grad.
Create a MAP optimizer.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
steps
|
int
|
Number of optimization steps. |
500
|
optimizer
|
GradientTransformation | None
|
Optax optimizer to use. |
None
|
learning_rate
|
float
|
Learning rate for the default optimizer (SGD with momentum). |
5e-05
|
momentum
|
float
|
Momentum for the default optimizer (SGD with momentum). |
0.9
|
track_history
|
bool
|
When True, record loss history during fitting for plotting. |
False
|
log_every
|
int
|
Record every N steps (also records the last step). |
10
|
Methods:
Name | Description |
---|---|
fit |
Fit model parameters with MAP optimization. |
get_history |
Return (steps, losses) recorded during the last fit when tracking was enabled. |
Attributes:
Name | Type | Description |
---|---|---|
log_every |
|
|
loss_history |
list[float]
|
|
loss_steps |
list[int]
|
|
optimizer |
|
|
steps |
|
|
track_history |
|
Source code in src/psyphy/inference/map_optimizer.py
fit
¶
Fit model parameters with MAP optimization.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
model
|
WPPM
|
Model instance. |
required |
data
|
ResponseData
|
Observed trials. |
required |
init_params
|
dict | None
|
Initial parameter PyTree to start optimization from. If provided, this takes precedence over the seed. |
None
|
seed
|
int | None
|
PRNG seed used to draw initial parameters from the model's prior when init_params is not provided. If None, defaults to 0. |
None
|
Returns:
Type | Description |
---|---|
Posterior
|
Posterior wrapper around MAP params and model. |
Source code in src/psyphy/inference/map_optimizer.py
get_history
¶
Return (steps, losses) recorded during the last fit when tracking was enabled.
OddityTask
¶
OddityTask(slope: float = 1.5)
Bases: TaskLikelihood
Three-alternative forced-choice oddity task (MVP placeholder) ("pick the odd-one out).
Methods:
Name | Description |
---|---|
loglik |
|
predict |
|
Attributes:
Name | Type | Description |
---|---|---|
chance_level |
float
|
|
performance_range |
float
|
|
slope |
|
Source code in src/psyphy/model/task.py
loglik
¶
Source code in src/psyphy/model/task.py
predict
¶
Posterior
¶
Bases: BasePosterior
MVP Posterior (MAP only).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params
|
dict
|
MAP parameter dictionary. |
required |
model
|
WPPM
|
Model instance used for predictions. |
required |
Notes
- This is effectively a MAPPosterior.
- Future subclasses (LaplacePosterior, MCMCPosterior) will extend BasePosterior with real sampling logic.
Methods:
Name | Description |
---|---|
MAP_params |
Return the MAP parameters. |
predict_prob |
Predict probability of correct response for a stimulus. |
predict_thresholds |
Predict discrimination threshold contour around a reference stimulus. |
sample |
Draw parameter samples from the posterior. |
Attributes:
Name | Type | Description |
---|---|---|
model |
|
|
params |
|
Source code in src/psyphy/posterior/posterior.py
MAP_params
¶
predict_prob
¶
Predict probability of correct response for a stimulus.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
stimulus
|
tuple
|
(reference, probe). |
required |
Returns:
Type | Description |
---|---|
ndarray
|
Probability of correct response. |
Notes
Delegates to WPPM.predict_prob(). This is not recursion: Posterior calls WPPM’s method with stored params.
Source code in src/psyphy/posterior/posterior.py
predict_thresholds
¶
Predict discrimination threshold contour around a reference stimulus.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
reference
|
ndarray
|
Reference point in model space. |
required |
criterion
|
float
|
Target performance (e.g., 2/3 for oddity). |
0.667
|
directions
|
int
|
Number of directions to probe. |
16
|
Returns:
Type | Description |
---|---|
ndarray
|
Contour points (MVP: unit circle). |
MVP
Returns a placeholder unit circle.
Future
- Search outward in each direction until performance crosses criterion.
- Average over posterior samples (Laplace, MCMC) to get credible intervals.
Source code in src/psyphy/posterior/posterior.py
sample
¶
sample(num_samples: int = 1)
Draw parameter samples from the posterior.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
num_samples
|
int
|
Number of samples. |
1
|
Returns:
Type | Description |
---|---|
list of dict
|
Parameter sets. |
MVP
Returns MAP params repeated n times.
Future
- LaplacePosterior: draw from N(mean, cov).
- MCMCPosterior: return stored samples.
Source code in src/psyphy/posterior/posterior.py
Prior
¶
Prior(
input_dim: int,
scale: float = 0.5,
variance_scale: float = 1.0,
lengthscale: float = 1.0,
extra_embedding_dims: int = 0,
)
Prior distribution over WPPM parameters
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_dim
|
int
|
Dimensionality of the model space (same as WPPM.input_dim) |
required |
scale
|
float
|
Stddev of Gaussian prior for log_diag entries (MVP only). |
0.5
|
variance_scale
|
float
|
Forward-compatible stub for Full WPPM mode. Will scale covariance magnitudes |
1.0
|
lengthscale
|
float
|
Forward-compatible stub for Full WPPM mode; controls smoothness of covariance field: - small lengthscale --> rapid variation across space - large lengthscale --> smoother field, long-range correlations. |
1.0
|
extra_embedding_dims
|
int
|
Forward-compatible stub for Full WPPM mode. Will expand embedding space. |
0
|
Methods:
Name | Description |
---|---|
default |
Convenience constructor with MVP defaults. |
log_prob |
Compute log prior density (up to a constant) |
sample_params |
Sample initial parameters from the prior. |
Attributes:
Name | Type | Description |
---|---|---|
extra_embedding_dims |
int
|
|
input_dim |
int
|
|
lengthscale |
float
|
|
scale |
float
|
|
variance_scale |
float
|
|
default
¶
log_prob
¶
log_prob(params: Params) -> ndarray
Compute log prior density (up to a constant)
MVP: Isotropic Gaussian on log_diag Full WPPM mode: Will implement structured prior over basis weights and lengthscale-regularized covariance fields
Source code in src/psyphy/model/prior.py
sample_params
¶
sample_params(key: KeyArray) -> Params
Sample initial parameters from the prior.
MVP: Returns {"log_diag": shape (input_dim,)}. Full WPPM mode: Will also include basis weights, structured covariance params, and hyperparameters for GP (variance_scale, lengthscale).
Source code in src/psyphy/model/prior.py
ResponseData
¶
Container for psychophysical trial data.
Attributes:
Name | Type | Description |
---|---|---|
refs |
List[Any]
|
List of reference stimuli. |
probes |
List[Any]
|
List of probe stimuli. |
responses |
List[int]
|
List of subject responses (e.g., 0/1 or categorical). |
Methods:
Name | Description |
---|---|
add_batch |
Append responses for a batch of trials. |
add_trial |
append a single trial. |
to_numpy |
Return refs, probes, responses as numpy arrays. |
Source code in src/psyphy/data/dataset.py
add_batch
¶
add_batch(
responses: List[int], trial_batch: TrialBatch
) -> None
Append responses for a batch of trials.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
responses
|
List[int]
|
Responses corresponding to each (ref, probe) in the trial batch. |
required |
trial_batch
|
TrialBatch
|
The batch of proposed trials. |
required |
Source code in src/psyphy/data/dataset.py
add_trial
¶
append a single trial.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
ref
|
Any
|
Reference stimulus (numpy array, list, etc.) |
required |
probe
|
Any
|
Probe stimulus |
required |
resp
|
int
|
Subject response (binary or categorical) |
required |
Source code in src/psyphy/data/dataset.py
StudentTNoise
¶
TrialBatch
¶
Container for a proposed batch of trials
Attributes:
Name | Type | Description |
---|---|---|
stimuli |
List[Tuple[Any, Any]]
|
Each trial is a (reference, probe) tuple. |
Methods:
Name | Description |
---|---|
from_stimuli |
Construct a TrialBatch from a list of stimuli (ref, probe) pairs. |
Source code in src/psyphy/data/dataset.py
TwoAFC
¶
TwoAFC(slope: float = 2.0)
Bases: TaskLikelihood
2-alternative forced-choice task (MVP placeholder).
Methods:
Name | Description |
---|---|
loglik |
|
predict |
|
Attributes:
Name | Type | Description |
---|---|---|
chance_level |
float
|
|
performance_range |
float
|
|
slope |
|
Source code in src/psyphy/model/task.py
loglik
¶
Source code in src/psyphy/model/task.py
predict
¶
WPPM
¶
WPPM(
input_dim: int,
prior: Prior,
task: TaskLikelihood,
noise: Any | None = None,
*,
extra_dims: int = 0,
variance_scale: float = 1.0,
lengthscale: float = 1.0,
diag_term: float = 1e-06
)
Wishart Process Psychophysical Model (WPPM).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_dim
|
int
|
Dimensionality of the input stimulus space (e.g., 2 for isoluminant plane, 3 for RGB). Both reference and probe live in R^{input_dim}. |
required |
prior
|
Prior
|
Prior distribution over model parameters. MVP uses a simple Gaussian prior over diagonal log-variances (see Prior.sample_params()). |
required |
task
|
TaskLikelihood
|
Psychophysical task mapping that defines how discriminability translates to p(correct) and how log-likelihood of responses is computed. (e.g., OddityTask, TwoAFC) |
required |
noise
|
Any
|
Noise model describing internal representation noise (e.g., GaussianNoise). Not used in MVP mapping but passed to the task interface for future MC sims. |
None
|
Forward-compatible hyperparameters (MVP stubs)
extra_dims : int, default=0 Additional embedding dimensions for basis expansions (unused in MVP). variance_scale : float, default=1.0 Global scaling factor for covariance magnitude (unused in MVP). lengthscale : float, default=1.0 Smoothness/length-scale for spatial covariance variation (unused in MVP). (formerly "decay_rate") diag_term : float, default=1e-6 Small positive value added to the covariance diagonal for numerical stability. MVP uses this in matrix solves; the research model will also use it.
Methods:
Name | Description |
---|---|
discriminability |
Compute scalar discriminability d >= 0 for a (reference, probe) pair |
init_params |
Sample initial parameters from the prior. |
local_covariance |
Return local covariance Σ(x) at stimulus location x. |
log_likelihood |
Compute the log-likelihood for arrays of trials. |
log_likelihood_from_data |
Compute log-likelihood directly from a ResponseData object. |
log_posterior_from_data |
Convenience helper if you want log posterior in one call (MVP). |
predict_prob |
Predict probability of a correct response for a single stimulus. |
Attributes:
Name | Type | Description |
---|---|---|
diag_term |
|
|
extra_dims |
|
|
input_dim |
|
|
lengthscale |
|
|
noise |
|
|
prior |
|
|
task |
|
|
variance_scale |
|
Source code in src/psyphy/model/wppm.py
discriminability
¶
Compute scalar discriminability d >= 0 for a (reference, probe) pair
MVP:
d = sqrt( (probe - ref)^T Σ(ref)^{-1} (probe - ref) )
with Σ(ref) the local covariance at the reference,
- We add diag_term * I
for numerical stability before inversion
Future (full WPPM mode):
d is implicit via Monte Carlo simulation of internal noisy responses
under the task's decision rule (no closed form). In that case, tasks
will directly implement predict/loglik with MC, and this method may be
used only for diagnostics.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params
|
dict
|
Model parameters. |
required |
stimulus
|
tuple
|
(reference, probe) arrays of shape (input_dim,). |
required |
Returns:
Name | Type | Description |
---|---|---|
d |
ndarray
|
Nonnegative scalar discriminability. |
Source code in src/psyphy/model/wppm.py
init_params
¶
init_params(key: KeyArray) -> Params
local_covariance
¶
local_covariance(params: Params, x: ndarray) -> ndarray
Return local covariance Σ(x) at stimulus location x.
MVP: Σ(x) = diag(exp(log_diag)), constant across x. - Positive-definite because exp(log_diag) > 0. Future (full WPPM mode): Σ(x) varies smoothly with x via basis expansions and a Wishart-process prior controlled by (extra_dims, variance_scale, lengthscale). Those hyperparameters are exposed here but not used in MVP.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params
|
dict
|
model parameters (MVP expects "log_diag": (input_dim,)). |
required |
x
|
ndarray
|
Stimulus location (unused in MVP because Σ is constant). |
required |
Returns:
Type | Description |
---|---|
Σ : jnp.ndarray, shape (input_dim, input_dim)
|
|
Source code in src/psyphy/model/wppm.py
log_likelihood
¶
log_likelihood(
params: Params,
refs: ndarray,
probes: ndarray,
responses: ndarray,
) -> ndarray
Compute the log-likelihood for arrays of trials.
IMPORTANT: We delegate to the TaskLikelihood to avoid duplicating Bernoulli (MPV) or MC likelihood logic in multiple places. This keeps responsibilities clean and makes adding new tasks straightforward.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params
|
dict
|
Model parameters. |
required |
refs
|
(ndarray, shape(N, input_dim))
|
|
required |
probes
|
(ndarray, shape(N, input_dim))
|
|
required |
responses
|
(ndarray, shape(N))
|
Typically 0/1; task may support richer encodings. |
required |
Returns:
Name | Type | Description |
---|---|---|
loglik |
ndarray
|
Scalar log-likelihood (task-only; add prior outside if needed) |
Source code in src/psyphy/model/wppm.py
log_likelihood_from_data
¶
Compute log-likelihood directly from a ResponseData object.
Why delegate to the task? - The task knows the decision rule (oddity, 2AFC, ...). - The task can use the model (this WPPM) to fetch discriminabilities - and the task can use the noise model if it needs MC simulation
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params
|
dict
|
Model parameters. |
required |
data
|
ResponseData
|
Collected trial data. |
required |
Returns:
Name | Type | Description |
---|---|---|
loglik |
ndarray
|
scalar log-likelihood (task-only; add prior outside if needed) |
Source code in src/psyphy/model/wppm.py
log_posterior_from_data
¶
Convenience helper if you want log posterior in one call (MVP).
This simply adds the prior log-probability to the task log-likelihood. Inference engines (e.g., MAP optimizer) typically optimize this quantity.
Returns:
Type | Description |
---|---|
jnp.ndarray : scalar log posterior = loglik(params | data) + log_prior(params)
|
|
Source code in src/psyphy/model/wppm.py
predict_prob
¶
Predict probability of a correct response for a single stimulus.
Design choice: WPPM computes discriminability & covariance; the TASK defines how that translates to performance. We therefore delegate to: task.predict(params, stimulus, model=self, noise=self.noise)
Parameters:
Name | Type | Description | Default |
---|---|---|---|
params
|
dict
|
|
required |
stimulus
|
(reference, probe)
|
|
required |
Returns:
Name | Type | Description |
---|---|---|
p_correct |
ndarray
|
|