Skip to content

Trial Placement

Trial placement strategies for adaptive psychophysical experiments.

Package Overview


trial_placement

trial_placement

Non-acquisition-based trial placement strategies.

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

  • GridPlacement: Fixed grid designs for systematic exploration
  • SobolPlacement: Quasi-random low-discrepancy sequences (space-filling)
  • StaircasePlacement: Adaptive staircase procedures (e.g., 1-up-2-down)

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.

StaircasePlacement

Staircase procedure.

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.

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

Ignored in MVP (grid is non-adaptive).

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
        Ignored in MVP (grid is non-adaptive).
    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

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.

Notes

MVP: Pure exploration of space. Full WPPM mode: Use Sobol as initialization, then switch to InfoGain.

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.

    Notes
    -----
    MVP:
        Pure exploration of space.
    Full WPPM mode:
        Use Sobol as initialization, then switch to InfoGain.
    """
    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
    probes = [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 probes]
    return TrialBatch.from_stimuli(trials)

StaircasePlacement

StaircasePlacement(
    start_level: float,
    step_size: float,
    rule: str = "1up-2down",
)

Staircase procedure.

Parameters:

Name Type Description Default
start_level float

Starting stimulus intensity.

required
step_size float

Step increment.

required
rule str

Adaptive rule.

"1up-2down"

Methods:

Name Description
propose

Return next trial(s) based on staircase rule.

update

Update staircase level given last response.

Attributes:

Name Type Description
correct_counter
current_level
rule
step_size
Source code in src/psyphy/trial_placement/staircase.py
def __init__(self, start_level: float, step_size: float, rule: str = "1up-2down"):
    self.current_level = start_level
    self.step_size = step_size
    self.rule = rule
    self.correct_counter = 0

correct_counter

correct_counter = 0

current_level

current_level = start_level

rule

rule = rule

step_size

step_size = step_size

propose

propose(posterior, batch_size: int) -> TrialBatch

Return next trial(s) based on staircase rule.

Parameters:

Name Type Description Default
posterior Posterior

Ignored in MVP (not posterior-aware).

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Batch of trials with current staircase level.

Source code in src/psyphy/trial_placement/staircase.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Return next trial(s) based on staircase rule.

    Parameters
    ----------
    posterior : Posterior
        Ignored in MVP (not posterior-aware).
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Batch of trials with current staircase level.
    """
    trials = [(0.0, self.current_level)] * batch_size  # Stub: (ref=0, probe=level)
    return TrialBatch.from_stimuli(trials)

update

update(response: int)

Update staircase level given last response.

Parameters:

Name Type Description Default
response int

1 = correct, 0 = incorrect.

required
Source code in src/psyphy/trial_placement/staircase.py
def update(self, response: int):
    """
    Update staircase level given last response.

    Parameters
    ----------
    response : int
        1 = correct, 0 = incorrect.
    """
    if response == 1:
        self.correct_counter += 1
        if self.rule == "1up-2down" and self.correct_counter >= 2:
            self.current_level -= self.step_size
            self.correct_counter = 0
    else:
        self.current_level += self.step_size
        self.correct_counter = 0

Grid


grid

grid.py

Grid-based placement strategy.

MVP: - Iterates through a fixed list of grid points. - Ignores the posterior (non-adaptive).

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.

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

Ignored in MVP (grid is non-adaptive).

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
        Ignored in MVP (grid is non-adaptive).
    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).

Full WPPM mode: - 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

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.

Notes

MVP: Pure exploration of space. Full WPPM mode: Use Sobol as initialization, then switch to InfoGain.

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.

    Notes
    -----
    MVP:
        Pure exploration of space.
    Full WPPM mode:
        Use Sobol as initialization, then switch to InfoGain.
    """
    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
    probes = [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 probes]
    return TrialBatch.from_stimuli(trials)

Staircase


staircase

staircase.py

Classical staircase placement (1-up, 2-down).

MVP: - Purely response-driven, 1D only. - Ignores posterior.

Full WPPM mode: - Extend to multi-D tasks, integrate with WPPM-based discriminability thresholds.

Classes:

Name Description
StaircasePlacement

Staircase procedure.

StaircasePlacement

StaircasePlacement(
    start_level: float,
    step_size: float,
    rule: str = "1up-2down",
)

Staircase procedure.

Parameters:

Name Type Description Default
start_level float

Starting stimulus intensity.

required
step_size float

Step increment.

required
rule str

Adaptive rule.

"1up-2down"

Methods:

Name Description
propose

Return next trial(s) based on staircase rule.

update

Update staircase level given last response.

Attributes:

Name Type Description
correct_counter
current_level
rule
step_size
Source code in src/psyphy/trial_placement/staircase.py
def __init__(self, start_level: float, step_size: float, rule: str = "1up-2down"):
    self.current_level = start_level
    self.step_size = step_size
    self.rule = rule
    self.correct_counter = 0

correct_counter

correct_counter = 0

current_level

current_level = start_level

rule

rule = rule

step_size

step_size = step_size

propose

propose(posterior, batch_size: int) -> TrialBatch

Return next trial(s) based on staircase rule.

Parameters:

Name Type Description Default
posterior Posterior

Ignored in MVP (not posterior-aware).

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Batch of trials with current staircase level.

Source code in src/psyphy/trial_placement/staircase.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Return next trial(s) based on staircase rule.

    Parameters
    ----------
    posterior : Posterior
        Ignored in MVP (not posterior-aware).
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Batch of trials with current staircase level.
    """
    trials = [(0.0, self.current_level)] * batch_size  # Stub: (ref=0, probe=level)
    return TrialBatch.from_stimuli(trials)

update

update(response: int)

Update staircase level given last response.

Parameters:

Name Type Description Default
response int

1 = correct, 0 = incorrect.

required
Source code in src/psyphy/trial_placement/staircase.py
def update(self, response: int):
    """
    Update staircase level given last response.

    Parameters
    ----------
    response : int
        1 = correct, 0 = incorrect.
    """
    if response == 1:
        self.correct_counter += 1
        if self.rule == "1up-2down" and self.correct_counter >= 2:
            self.current_level -= self.step_size
            self.correct_counter = 0
    else:
        self.current_level += self.step_size
        self.correct_counter = 0