Skip to content

Trial Placement

Package


trial_placement

trial_placement

Strategies for selecting the next set of trials in an experiment.

This subpackage provides: - TrialPlacement : abstract base class. - GridPlacement : fixed non-adaptive design. - SobolPlacement : quasi-random low-discrepancy exploration. - StaircasePlacement : classical adaptive rule (1-up-2-down). - GreedyMAPPlacement : adaptive design based on MAP point estimate. - InfoGainPlacement : adaptive design based on expected information gain.

MVP implementation
  • Simple grid, Sobol, and staircase procedures.
  • Greedy placement uses MAP only.
  • InfoGain uses entropy-style heuristic with placeholder logic.
Full WPPM mode
  • InfoGainPlacement will integrate with posterior.sample() from LaplacePosterior or MCMCPosterior.
  • StaircasePlacement can be extended to multi-dimensional, task-aware rules.
  • Hybrid strategies: exploration (Sobol) -> exploitation (InfoGain).

Classes:

Name Description
GreedyMAPPlacement

Greedy adaptive placement (MAP-based).

GridPlacement

Fixed grid placement.

InfoGainPlacement

Information-gain adaptive placement.

SobolPlacement

Sobol quasi-random placement.

StaircasePlacement

Staircase procedure.

TrialPlacement

Abstract interface for trial placement strategies.

GreedyMAPPlacement

GreedyMAPPlacement(candidate_pool)

Bases: TrialPlacement

Greedy adaptive placement (MAP-based).

Parameters:

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

Candidate stimuli.

required

Methods:

Name Description
propose

Select trials based on MAP discriminability.

Attributes:

Name Type Description
pool
Source code in src/psyphy/trial_placement/greedy_map.py
def __init__(self, candidate_pool):
    self.pool = candidate_pool

pool

pool = candidate_pool

propose

propose(posterior, batch_size: int) -> TrialBatch

Select trials based on MAP discriminability.

Parameters:

Name Type Description Default
posterior Posterior

Posterior object. Provides MAP params.

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Selected trials.

Notes

MVP: Returns first N candidates. Full WPPM mode: - Score all candidates with _score_candidate(). - Rank candidates by score. - Select top-N.

Source code in src/psyphy/trial_placement/greedy_map.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Select trials based on MAP discriminability.

    Parameters
    ----------
    posterior : Posterior
        Posterior object. Provides MAP params.
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Selected trials.

    Notes
    -----
    MVP:
        Returns first N candidates.
    Full WPPM mode:
        - Score all candidates with _score_candidate().
        - Rank candidates by score.
        - Select top-N.
    """
    # MVP stub
    selected = self.pool[:batch_size]

    # TODO: Uncomment when scoring is implemented
    # scores = [self._score_candidate(posterior, cand) for cand in self.pool]
    # idx = jnp.argsort(jnp.array(scores))[::-1]  # sort descending
    # selected = [self.pool[i] for i in idx[:batch_size]]

    return TrialBatch.from_stimuli(selected)

GridPlacement

GridPlacement(grid_points)

Bases: TrialPlacement

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)

InfoGainPlacement

InfoGainPlacement(candidate_pool)

Bases: TrialPlacement

Information-gain adaptive placement.

Parameters:

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

Candidate stimuli.

required

Methods:

Name Description
propose

Propose trials that maximize information gain.

Attributes:

Name Type Description
pool
Source code in src/psyphy/trial_placement/info_gain.py
def __init__(self, candidate_pool):
    self.pool = candidate_pool

pool

pool = candidate_pool

propose

propose(posterior, batch_size: int) -> TrialBatch

Propose trials that maximize information gain.

Parameters:

Name Type Description Default
posterior Posterior

Posterior object. Must support sample().

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Selected trials.

Notes

MVP: Returns first N candidates. Full WPPM mode: - Score all candidates with _score_candidate(). - Rank candidates by score. - Select top-N.

Source code in src/psyphy/trial_placement/info_gain.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Propose trials that maximize information gain.

    Parameters
    ----------
    posterior : Posterior
        Posterior object. Must support sample().
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Selected trials.

    Notes
    -----
    MVP:
        Returns first N candidates.
    Full WPPM mode:
        - Score all candidates with _score_candidate().
        - Rank candidates by score.
        - Select top-N.
    """
    # MVP stub
    selected = self.pool[:batch_size]

    # TODO: Uncomment when scoring is implemented
    # scores = [self._score_candidate(posterior, cand) for cand in self.pool]
    # idx = jnp.argsort(jnp.array(scores))[::-1]
    # selected = [self.pool[i] for i in idx[:batch_size]]

    return TrialBatch.from_stimuli(selected)

SobolPlacement

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

Bases: TrialPlacement

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",
)

Bases: TrialPlacement

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

TrialPlacement

Bases: ABC

Abstract interface for trial placement strategies.

Methods:

Name Description
propose

Propose the next batch of trials.

All trial placement strategies

propose

propose(posterior: Any, batch_size: int)

Propose the next batch of trials.

Parameters:

Name Type Description Default
posterior Posterior

Posterior distribution (MAP, Laplace, or MCMC).

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Proposed batch of (reference, probe) stimuli.

Source code in src/psyphy/trial_placement/base.py
@abstractmethod
def propose(self, posterior: Any, batch_size: int):
    """
    Propose the next batch of trials.

    Parameters
    ----------
    posterior : Posterior
        Posterior distribution (MAP, Laplace, or MCMC).
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Proposed batch of (reference, probe) stimuli.
    """
    return NotImplementedError()

Base


base

base.py

Abstract base class for trial placement strategies.

Classes:

Name Description
TrialPlacement

Abstract interface for trial placement strategies.

TrialPlacement

Bases: ABC

Abstract interface for trial placement strategies.

Methods:

Name Description
propose

Propose the next batch of trials.

All trial placement strategies

propose

propose(posterior: Any, batch_size: int)

Propose the next batch of trials.

Parameters:

Name Type Description Default
posterior Posterior

Posterior distribution (MAP, Laplace, or MCMC).

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Proposed batch of (reference, probe) stimuli.

Source code in src/psyphy/trial_placement/base.py
@abstractmethod
def propose(self, posterior: Any, batch_size: int):
    """
    Propose the next batch of trials.

    Parameters
    ----------
    posterior : Posterior
        Posterior distribution (MAP, Laplace, or MCMC).
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Proposed batch of (reference, probe) stimuli.
    """
    return NotImplementedError()

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)

Bases: TrialPlacement

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)

Bases: TrialPlacement

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",
)

Bases: TrialPlacement

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

Greedy MAP


greedy_map

greedy_map.py

Greedy placement using MAP estimate.

MVP: - Returns first N candidates (stub). - Ignores posterior discriminability.

Full WPPM mode: - Score each candidate using posterior.MAP_params(). - Rank candidates by informativeness (e.g., discriminability).

Classes:

Name Description
GreedyMAPPlacement

Greedy adaptive placement (MAP-based).

GreedyMAPPlacement

GreedyMAPPlacement(candidate_pool)

Bases: TrialPlacement

Greedy adaptive placement (MAP-based).

Parameters:

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

Candidate stimuli.

required

Methods:

Name Description
propose

Select trials based on MAP discriminability.

Attributes:

Name Type Description
pool
Source code in src/psyphy/trial_placement/greedy_map.py
def __init__(self, candidate_pool):
    self.pool = candidate_pool

pool

pool = candidate_pool

propose

propose(posterior, batch_size: int) -> TrialBatch

Select trials based on MAP discriminability.

Parameters:

Name Type Description Default
posterior Posterior

Posterior object. Provides MAP params.

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Selected trials.

Notes

MVP: Returns first N candidates. Full WPPM mode: - Score all candidates with _score_candidate(). - Rank candidates by score. - Select top-N.

Source code in src/psyphy/trial_placement/greedy_map.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Select trials based on MAP discriminability.

    Parameters
    ----------
    posterior : Posterior
        Posterior object. Provides MAP params.
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Selected trials.

    Notes
    -----
    MVP:
        Returns first N candidates.
    Full WPPM mode:
        - Score all candidates with _score_candidate().
        - Rank candidates by score.
        - Select top-N.
    """
    # MVP stub
    selected = self.pool[:batch_size]

    # TODO: Uncomment when scoring is implemented
    # scores = [self._score_candidate(posterior, cand) for cand in self.pool]
    # idx = jnp.argsort(jnp.array(scores))[::-1]  # sort descending
    # selected = [self.pool[i] for i in idx[:batch_size]]

    return TrialBatch.from_stimuli(selected)

Info Gain


info_gain

info_gain.py

Information-gain placement (e.g., entropy, expected Absolute Volume Change (EAVC)).

MVP: - Returns first N candidates (stub)

Full WPPM mode: - Requires posterior.sample() (Laplace/MCMC). - For each candidate: * Draw posterior samples. * Compute predictive distribution of responses. * Compute expected entropy or EAVC. * Rank candidates by information gain.

Classes:

Name Description
InfoGainPlacement

Information-gain adaptive placement.

InfoGainPlacement

InfoGainPlacement(candidate_pool)

Bases: TrialPlacement

Information-gain adaptive placement.

Parameters:

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

Candidate stimuli.

required

Methods:

Name Description
propose

Propose trials that maximize information gain.

Attributes:

Name Type Description
pool
Source code in src/psyphy/trial_placement/info_gain.py
def __init__(self, candidate_pool):
    self.pool = candidate_pool

pool

pool = candidate_pool

propose

propose(posterior, batch_size: int) -> TrialBatch

Propose trials that maximize information gain.

Parameters:

Name Type Description Default
posterior Posterior

Posterior object. Must support sample().

required
batch_size int

Number of trials to propose.

required

Returns:

Type Description
TrialBatch

Selected trials.

Notes

MVP: Returns first N candidates. Full WPPM mode: - Score all candidates with _score_candidate(). - Rank candidates by score. - Select top-N.

Source code in src/psyphy/trial_placement/info_gain.py
def propose(self, posterior, batch_size: int) -> TrialBatch:
    """
    Propose trials that maximize information gain.

    Parameters
    ----------
    posterior : Posterior
        Posterior object. Must support sample().
    batch_size : int
        Number of trials to propose.

    Returns
    -------
    TrialBatch
        Selected trials.

    Notes
    -----
    MVP:
        Returns first N candidates.
    Full WPPM mode:
        - Score all candidates with _score_candidate().
        - Rank candidates by score.
        - Select top-N.
    """
    # MVP stub
    selected = self.pool[:batch_size]

    # TODO: Uncomment when scoring is implemented
    # scores = [self._score_candidate(posterior, cand) for cand in self.pool]
    # idx = jnp.argsort(jnp.array(scores))[::-1]
    # selected = [self.pool[i] for i in idx[:batch_size]]

    return TrialBatch.from_stimuli(selected)