Skip to content

Trial Placement

Trial placement strategies for adaptive psychophysical experiments.

Package Overview


trial_placement

trial_placement

Non-acquisition-based trial placement strategies.

NOTE: this modeule is currently untested and still in active development.

This module provides classical (non-Bayesian-optimization) placement strategies:

  • GridPlacement: Fixed grid designs for systematic exploration
  • SobolPlacement: Quasi-random low-discrepancy sequences (space-filling)

For acquisition-based adaptive designs (Bayesian optimization), use: - psyphy.acquisition: Expected Improvement, UCB, Mutual Information - See: psyphy.acquisition.optimize_acqf_discrete() for trial selection

Examples:

1
2
3
4
>>> # Fixed grid design
>>> from psyphy.trial_placement import GridPlacement
>>> placement = GridPlacement(n_points_per_dim=10, bounds=[[-1, 1], [-1, 1]])
>>> trials = placement.propose(batch_size=5)
1
2
3
4
>>> # Quasi-random exploration
>>> from psyphy.trial_placement import SobolPlacement
>>> placement = SobolPlacement(bounds=[[-1, 1], [-1, 1]])
>>> trials = placement.propose(batch_size=10)
>>> # For adaptive designs with acquisition functions, see:
>>> from psyphy.acquisition import expected_improvement, optimize_acqf_discrete

Classes:

Name Description
GridPlacement

Fixed grid placement.

SobolPlacement

Sobol quasi-random placement.

GridPlacement

GridPlacement(grid_points)

Fixed grid placement.

Parameters:

Name Type Description Default
grid_points list of (ref, probe)

Predefined set of trial stimuli.

required
Notes
  • grid = your set of allowable trials; this class simply walks through that set.
  • Not yet tested. Pending the same TrialBatch redesign as SobolPlacement: each trial's stimuli should be a single np.ndarray of shape (K, d) rather than a two-element tuple, to align with the generalised TrialData layout introduced in the data-object refactor.

Methods:

Name Description
propose

Return the next batch of trials from the grid.

Attributes:

Name Type Description
grid_points
Source code in src/psyphy/trial_placement/grid.py
def __init__(self, grid_points):
    self.grid_points = list(grid_points)
    self._index = 0

grid_points

grid_points = list(grid_points)

propose

propose(posterior, batch_size: int) -> TrialBatch

Return the next batch of trials from the grid.

Parameters:

Name Type Description Default
posterior Posterior
required
batch_size int

Number of trials to return.

required

Returns:

Type Description
TrialBatch

Fixed batch of (ref, probe).

Source code in src/psyphy/trial_placement/grid.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Return the next batch of trials from the grid.

    Parameters
    ----------
    posterior : Posterior
    batch_size : int
        Number of trials to return.

    Returns
    -------
    TrialBatch
        Fixed batch of (ref, probe).
    """
    start, end = self._index, self._index + batch_size
    batch = self.grid_points[start:end]
    self._index = end
    return TrialBatch.from_stimuli(batch)

SobolPlacement

SobolPlacement(dim: int, bounds, seed: int = 0)

Sobol quasi-random placement.

Parameters:

Name Type Description Default
dim int

Dimensionality of stimulus space.

required
bounds list of (low, high)

Bounds per dimension.

required
seed int

RNG seed.

0
Notes

Not yet tested. Pending two design changes tracked in the trial-placement follow-up issue:

  1. TrialBatch currently stores stimuli as list[tuple[Any, Any]] (two-stimulus tuples). It should be updated to list[np.ndarray] each of shape (K, d) to align with TrialData.stimuli and allow ResponseData.add_batch to consume it without conversion.

  2. The zero reference vector is hardcoded here. It should be an explicit parameter so the caller controls which point in stimulus space acts as the reference, rather than always using the origin.

Methods:

Name Description
propose

Propose Sobol points (ignores posterior).

Attributes:

Name Type Description
bounds
engine
Source code in src/psyphy/trial_placement/sobol.py
def __init__(self, dim: int, bounds, seed: int = 0):
    self.engine = Sobol(d=dim, scramble=True, seed=seed)
    self.bounds = bounds

bounds

bounds = bounds

engine

engine = Sobol(d=dim, scramble=True, seed=seed)

propose

propose(posterior, batch_size: int) -> TrialBatch

Propose Sobol points (ignores posterior).

Parameters:

Name Type Description Default
posterior Posterior

Ignored in MVP.

required
batch_size int

Number of trials to return.

required

Returns:

Type Description
TrialBatch

Candidate trials from Sobol sequence.

Source code in src/psyphy/trial_placement/sobol.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Propose Sobol points (ignores posterior).

    Parameters
    ----------
    posterior : Posterior
        Ignored in MVP.
    batch_size : int
        Number of trials to return.

    Returns
    -------
    TrialBatch
        Candidate trials from Sobol sequence.

    """
    raw = self.engine.random(batch_size)
    scaled = [
        low + (high - low) * raw[:, i] for i, (low, high) in enumerate(self.bounds)
    ]
    # Convert column-wise scaled arrays into list of probe vectors
    comparisons = [tuple(vals) for vals in zip(*scaled)]
    # MVP: use a zero reference vector of matching dimension
    dim = len(self.bounds)
    zero_ref = 0.0 if dim == 1 else tuple(0.0 for _ in range(dim))
    trials = [(zero_ref, p) for p in comparisons]
    return TrialBatch.from_stimuli(trials)

Grid


grid

grid.py

Grid-based placement strategy.

Full WPPM mode: - Could refine the grid adaptively around regions of high posterior uncertainty.

Classes:

Name Description
GridPlacement

Fixed grid placement.

GridPlacement

GridPlacement(grid_points)

Fixed grid placement.

Parameters:

Name Type Description Default
grid_points list of (ref, probe)

Predefined set of trial stimuli.

required
Notes
  • grid = your set of allowable trials; this class simply walks through that set.
  • Not yet tested. Pending the same TrialBatch redesign as SobolPlacement: each trial's stimuli should be a single np.ndarray of shape (K, d) rather than a two-element tuple, to align with the generalised TrialData layout introduced in the data-object refactor.

Methods:

Name Description
propose

Return the next batch of trials from the grid.

Attributes:

Name Type Description
grid_points
Source code in src/psyphy/trial_placement/grid.py
def __init__(self, grid_points):
    self.grid_points = list(grid_points)
    self._index = 0

grid_points

grid_points = list(grid_points)

propose

propose(posterior, batch_size: int) -> TrialBatch

Return the next batch of trials from the grid.

Parameters:

Name Type Description Default
posterior Posterior
required
batch_size int

Number of trials to return.

required

Returns:

Type Description
TrialBatch

Fixed batch of (ref, probe).

Source code in src/psyphy/trial_placement/grid.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Return the next batch of trials from the grid.

    Parameters
    ----------
    posterior : Posterior
    batch_size : int
        Number of trials to return.

    Returns
    -------
    TrialBatch
        Fixed batch of (ref, probe).
    """
    start, end = self._index, self._index + batch_size
    batch = self.grid_points[start:end]
    self._index = end
    return TrialBatch.from_stimuli(batch)

Sobol


sobol

sobol.py

Sobol quasi-random placement.

MVP: - Uses a Sobol engine to generate low-discrepancy points. - Ignores the posterior (pure exploration).

Levaraging WPPM's posterior: - Could combine Sobol exploration (early) with posterior-aware exploitation (later).

Classes:

Name Description
SobolPlacement

Sobol quasi-random placement.

SobolPlacement

SobolPlacement(dim: int, bounds, seed: int = 0)

Sobol quasi-random placement.

Parameters:

Name Type Description Default
dim int

Dimensionality of stimulus space.

required
bounds list of (low, high)

Bounds per dimension.

required
seed int

RNG seed.

0
Notes

Not yet tested. Pending two design changes tracked in the trial-placement follow-up issue:

  1. TrialBatch currently stores stimuli as list[tuple[Any, Any]] (two-stimulus tuples). It should be updated to list[np.ndarray] each of shape (K, d) to align with TrialData.stimuli and allow ResponseData.add_batch to consume it without conversion.

  2. The zero reference vector is hardcoded here. It should be an explicit parameter so the caller controls which point in stimulus space acts as the reference, rather than always using the origin.

Methods:

Name Description
propose

Propose Sobol points (ignores posterior).

Attributes:

Name Type Description
bounds
engine
Source code in src/psyphy/trial_placement/sobol.py
def __init__(self, dim: int, bounds, seed: int = 0):
    self.engine = Sobol(d=dim, scramble=True, seed=seed)
    self.bounds = bounds

bounds

bounds = bounds

engine

engine = Sobol(d=dim, scramble=True, seed=seed)

propose

propose(posterior, batch_size: int) -> TrialBatch

Propose Sobol points (ignores posterior).

Parameters:

Name Type Description Default
posterior Posterior

Ignored in MVP.

required
batch_size int

Number of trials to return.

required

Returns:

Type Description
TrialBatch

Candidate trials from Sobol sequence.

Source code in src/psyphy/trial_placement/sobol.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Propose Sobol points (ignores posterior).

    Parameters
    ----------
    posterior : Posterior
        Ignored in MVP.
    batch_size : int
        Number of trials to return.

    Returns
    -------
    TrialBatch
        Candidate trials from Sobol sequence.

    """
    raw = self.engine.random(batch_size)
    scaled = [
        low + (high - low) * raw[:, i] for i, (low, high) in enumerate(self.bounds)
    ]
    # Convert column-wise scaled arrays into list of probe vectors
    comparisons = [tuple(vals) for vals in zip(*scaled)]
    # MVP: use a zero reference vector of matching dimension
    dim = len(self.bounds)
    zero_ref = 0.0 if dim == 1 else tuple(0.0 for _ in range(dim))
    trials = [(zero_ref, p) for p in comparisons]
    return TrialBatch.from_stimuli(trials)