initial poisson race implementation#840
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 4 files with indirect coverage changes 🚀 New features to boost your workflow:
|
AlexanderFengler
left a comment
There was a problem hiding this comment.
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.
- We need some tests for the basic likelihood
2 We need the basic simulator implemented inssm-simulators(separate small PR) - 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).
There was a problem hiding this comment.
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)whereflip = 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.
cpaniaguam
left a comment
There was a problem hiding this comment.
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.
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
|
Added an implementation of the Poisson race model in Lmk what you think! Feel free to suggest additions to the tutorial notebook. |
fix shape bounds Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
add pythonic syntax Co-authored-by: Carlos Paniagua <cpaniaguam@gmail.com>
cpaniaguam
left a comment
There was a problem hiding this comment.
Looking real good! Left a few more observations. I haven't looked at the notebook yet.
Co-authored-by: Carlos Paniagua <cpaniaguam@gmail.com>
There was a problem hiding this comment.
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.
| @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} | ||
| ) |
There was a problem hiding this comment.
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.
|
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>
AlexanderFengler
left a comment
There was a problem hiding this comment.
This looks good now, thanks @hayden-johnson
There was a problem hiding this comment.
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.
| return np.array( | ||
| [ | ||
| (0.45, 1.0), | ||
| (0.55, -1.0), | ||
| (0.65, 0.0), | ||
| (0.75, 1.0), | ||
| ], |
There was a problem hiding this comment.
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).
| data = np.array( | ||
| [ | ||
| (0.4, 1.0), | ||
| (0.5, -1.0), | ||
| (0.8, 0.0), | ||
| (0.9, 1.0), | ||
| ], | ||
| dtype="float32", | ||
| ) |
There was a problem hiding this comment.
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).
| "choices": [-1, 1], | ||
| "description": "The Poisson Race Model", |
There was a problem hiding this comment.
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.
| "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)", |
There was a problem hiding this comment.
I will make one last change in this regard, then all set from my perspective ;)
| # 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 | ||
|
|
There was a problem hiding this comment.
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.
| "k1": (np.finfo(float).eps, np.inf), | ||
| "k2": (np.finfo(float).eps, np.inf), | ||
| "t": (0.0, np.inf), | ||
| } |
There was a problem hiding this comment.
One can always retrieve the poisson_race_params object from poisson_race_bounds with list(mydict.keys())
There was a problem hiding this comment.
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, theresponseparameter is never used. This makes the helper signature misleading and can trigger unused-argument linting in some setups. Consider removing theresponseargument (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_lbis misleading: the test data uses a positive RT (0.02) and is really exercising thert <= 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.
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.