Skip to content

initial poisson race implementation#840

Merged
AlexanderFengler merged 19 commits intomainfrom
add-poisson-race
Mar 3, 2026
Merged

initial poisson race implementation#840
AlexanderFengler merged 19 commits intomainfrom
add-poisson-race

Conversation

@hayden-johnson
Copy link
Copy Markdown
Collaborator

@hayden-johnson hayden-johnson commented Oct 28, 2025

Issue #801

This PR introduces a preliminary implementation of the Poisson race model as described in Appendix A of:

A comparison of two response time models applied to perceptual matching

Key Differences from the original formulation

Stimulus-dependent rate parameters are not implemented. This would require trial-by-trial stimulus identifiers for log-probability evaluation.

Shape parameter generalization: we do not restrict the shape parameter to be an integer. This simplifies the implementation but requires evaluation of the incomplete gamma function using scipy, rather than using factorials.

@codecov
Copy link
Copy Markdown

codecov bot commented Oct 28, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/hssm/_types.py 100.00% <ø> (ø)
src/hssm/likelihoods/__init__.py 100.00% <ø> (ø)
src/hssm/likelihoods/analytical.py 100.00% <100.00%> (ø)
src/hssm/modelconfig/poisson_race_config.py 100.00% <100.00%> (ø)
tests/test_likelihoods_poisson_race.py 100.00% <100.00%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@AlexanderFengler AlexanderFengler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @hayden-johnson this looks reasonable as far as the likelihood is concerned. We might want to check in on the trial-wise covariate issues if it remains unresolved.

Apart from that a few more high level comments to get the PR over the finish line.

  1. We need some tests for the basic likelihood
    2 We need the basic simulator implemented in ssm-simulators (separate small PR)
  2. We need a small notebook to that illustrates the model and shows a model fit (goal is to start having these for all incoming models and backfill for existing ones, so people have a much easier time getting starting with using specific models in the toolbox).

@AlexanderFengler AlexanderFengler marked this pull request as ready for review November 17, 2025 22:32
@cpaniaguam cpaniaguam requested a review from Copilot November 18, 2025 21:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements the Poisson race model for two-choice response time modeling, based on a comparison of response time models. The implementation uses continuous shape parameters and evaluates log-likelihoods analytically using gamma distributions.

Key changes:

  • Adds analytical log-likelihood computation for the Poisson race model with two accumulators
  • Includes comprehensive unit tests validating vectorization, exponential edge cases, and parameter validation
  • Registers the new model in HSSM's type system and likelihood exports

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/hssm/likelihoods/analytical.py Core implementation of logp_poisson_race function with parameter bounds and distribution registration
src/hssm/modelconfig/poisson_race_config.py Configuration defining model parameters, priors, and choices for the Poisson race model
tests/test_likelihoods_poisson_race.py Unit tests covering vectorization, exponential special case, parameter validation, and edge cases
src/hssm/likelihoods/__init__.py Exports the new logp_poisson_race function
src/hssm/_types.py Registers "poisson_race" as a valid model type
Comments suppressed due to low confidence (2)

src/hssm/likelihoods/analytical.py:1

  • Corrected grammar: 'We do not condition on the underlying stimulus condition.'
"""pytensor implementation of the Wiener First Passage Time Distribution.

src/hssm/likelihoods/analytical.py:1

  • The documentation states 'Response > 0 indicates accumulator 1', but the code at line 618 uses r_c = pt.switch(flip, r2, r1) where flip = response > 0. This means positive responses select r2/k2 (accumulator 2), not accumulator 1. The documentation should be corrected to match the implementation, or clarify the accumulator indexing convention.
"""pytensor implementation of the Wiener First Passage Time Distribution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Collaborator

@cpaniaguam cpaniaguam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests look great! I added a suggestion for adopting the more declarative and pythonic list comprehension pattern with a function to do the number crunching.

@review-notebook-app
Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@hayden-johnson
Copy link
Copy Markdown
Collaborator Author

@AlexanderFengler

Added an implementation of the Poisson race model in ssm-simulators (see PR 246), along with a minimal example notebook in HSSM/docs/poisson_race.ipynb.

Lmk what you think! Feel free to suggest additions to the tutorial notebook.

hayden-johnson and others added 3 commits December 29, 2025 19:55
fix shape bounds

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
add pythonic syntax

Co-authored-by: Carlos Paniagua <cpaniaguam@gmail.com>
Copy link
Copy Markdown
Collaborator

@cpaniaguam cpaniaguam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking real good! Left a few more observations. I haven't looked at the notebook yet.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +90 to +104
@pytest.mark.parametrize(
"param,bad_value",
[
("r1", 0.0),
("r2", -0.1),
("k1", 0.0),
("k2", -1.0),
("t", -0.2),
],
)
def test_poisson_race_parameter_validation(poisson_race_data, param, bad_value):
"""Invalid parameter values should produce a ParameterValueError."""
assert_parameter_value_error(
poisson_race_data, theta_poisson_race, **{param: bad_value}
)
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only validates that 0.0 and negative values are rejected, but doesn't test boundary values like very small positive numbers (e.g., 1e-10). Additionally, there's no test case for when t equals 0, which is a valid edge case according to the bounds (t >= 0) but might behave differently than other values.

Copilot uses AI. Check for mistakes.
@AlexanderFengler
Copy link
Copy Markdown
Member

Overall looks like this is basically in shape, apart from some of the CoPilot comments (some of which seem worth addressing). thanks @hayden-johnson

fix doctring typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Member

@AlexanderFengler AlexanderFengler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good now, thanks @hayden-johnson

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +38
return np.array(
[
(0.45, 1.0),
(0.55, -1.0),
(0.65, 0.0),
(0.75, 1.0),
],
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test data includes response == 0.0, but the Poisson race config declares choices as [-1, 1] and the simulator typically emits -1/1. Using 0.0 here makes the test exercise behavior for an out-of-spec response code; consider switching these responses to -1.0 or 1.0 (or, if 0 is intended to be supported, update the model config/validation accordingly).

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +67
data = np.array(
[
(0.4, 1.0),
(0.5, -1.0),
(0.8, 0.0),
(0.9, 1.0),
],
dtype="float32",
)
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parametrized dataset includes response == 0.0, but the model config advertises only [-1, 1] choices and most downstream code assumes binary signed choices. Recommend changing 0.0 to a valid choice value (e.g. -1.0) unless 0 is an explicitly supported third outcome (in which case the config and likelihood should validate/document it).

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +22
"choices": [-1, 1],
"description": "The Poisson Race Model",
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

choices is set to [-1, 1], but the analytical likelihood routes all response <= 0 trials to accumulator 1 (so 0 is treated as a valid code) and the new tests/tutorial examples include response == 0.0. Either tighten the likelihood to validate/enforce response ∈ {-1, 1} or expand choices/documentation to reflect that 0 is a supported value.

Suggested change
"choices": [-1, 1],
"description": "The Poisson Race Model",
"choices": [-1, 0, 1],
"description": "The Poisson Race Model (supports responses -1, 0, 1; responses <= 0 map to accumulator 1 in the analytical likelihood)",

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make one last change in this regard, then all set from my perspective ;)

Comment on lines +55 to +60
# The analytical Poisson-race likelihood assumes exact race finishing times.
# ssms defaults to smooth_unif=True; override to False unless the caller
# explicitly requests otherwise.
if model == "poisson_race" and "smooth_unif" not in kwargs:
kwargs["smooth_unif"] = False

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes simulate_data behavior by implicitly forcing smooth_unif=False for the poisson_race model unless the caller overrides it. Since tests/test_simulator.py already covers simulator defaults, consider adding a focused test that asserts (1) smooth_unif defaults to False for poisson_race, and (2) an explicit smooth_unif=True/False passed by the caller is respected.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@cpaniaguam cpaniaguam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

"k1": (np.finfo(float).eps, np.inf),
"k2": (np.finfo(float).eps, np.inf),
"t": (0.0, np.inf),
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One can always retrieve the poisson_race_params object from poisson_race_bounds with list(mydict.keys())

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

tests/test_likelihoods_poisson_race.py:78

  • In _compute_exponential_logp, the response parameter is never used. This makes the helper signature misleading and can trigger unused-argument linting in some setups. Consider removing the response argument (or using it if it was intended for a check).
    def _compute_exponential_logp(rt, response, winner_rate, loser_rate):
        log_pdf = np.log(winner_rate) - winner_rate * rt
        log_survival = -loser_rate * rt
        return log_pdf + log_survival

tests/test_likelihoods_poisson_race.py:115

  • The test name test_poisson_race_negative_rt_returns_logp_lb is misleading: the test data uses a positive RT (0.02) and is really exercising the rt <= t (non-decision time) clipping behavior. Renaming the test (and/or updating the docstring) would make the intent clearer.
def test_poisson_race_negative_rt_returns_logp_lb():
    """Trials with rt <= t should clip to LOGP_LB."""
    data = np.array([(0.02, 1.0)], dtype="float32")
    theta = dict(r1=2.0, r2=3.0, k1=1.2, k2=1.4, t=0.05)
    logp = logp_poisson_race(data, **theta).eval()
    assert np.allclose(logp, LOGP_LB)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@AlexanderFengler AlexanderFengler merged commit ad82147 into main Mar 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants