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.
src/hssm/likelihoods/analytical.py
Outdated
| Each accumulator time follows a Gamma distribution (with continuous | ||
| shape parameter) with shape parameter k and rate r. The per-trial | ||
| likelihood decomposes into the density of the winning accumulator | ||
| evaluated at the observed decision time and the survival function of | ||
| the losing accumulator at the same time. |
There was a problem hiding this comment.
Would it be okay to update the first sentence from Each accumulator time follows a Gamma distribution (with continuous shape parameter) with shape parameter k and rate r. to Each accumulator time follows a Gamma distribution with continuous shape parameter k and rate r.?
src/hssm/likelihoods/analytical.py
Outdated
| pytensor.tensor.TensorVariable | ||
| Per-trial log-likelihoods compatible with PyTensor graphs. |
There was a problem hiding this comment.
The return type in the function's signature says it's np.ndarray. Consider revising this.
There was a problem hiding this comment.
Resolved, and added some more detail in the docstring.
src/hssm/likelihoods/analytical.py
Outdated
| k_l = pt.switch(flip, k1, k2) | ||
|
|
||
| log_pdf = ( | ||
| k_c * pt.log(pt.maximum(r_c, epsilon)) |
There was a problem hiding this comment.
Why not define r_c_safe as well?
There was a problem hiding this comment.
I was originally only using the safe option for problematic computations (i.e., logarithms). For other computations, I was using unsafe variables; in either case, the evaluation is invalid and should be caught by the parameter checks.
I was inconsistent with this, however, in that I used rt_safe outside of the log function later in the pdf - I changed this now.
src/hssm/likelihoods/analytical.py
Outdated
| log_pdf = ( | ||
| k_c * pt.log(pt.maximum(r_c, epsilon)) | ||
| + (k_c - 1.0) * pt.log(rt_safe) | ||
| - r_c * rt_safe |
There was a problem hiding this comment.
Just curious: why use safe versions for a standard product for one variable but not the other? It seems like it should be fine to use the untreated versions.
There was a problem hiding this comment.
True. I meant to only use safe values inside the log function. I changed this for consistency.
There was a problem hiding this comment.
Notebooks looks good to me!
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.
| " \"r1\": ssms_params[\"r1\"],\n", | ||
| " \"r2\": ssms_params[\"r0\"],\n", | ||
| " \"k1\": ssms_params[\"k1\"],\n", | ||
| " \"k2\": ssms_params[\"k0\"],\n", |
There was a problem hiding this comment.
The notebook creates a mapping between ssms_params (with r0/r1/k0/k1 naming) and true_params (with r1/r2/k1/k2 naming), where ssms r1 maps to HSSM r1 and ssms r0 maps to HSSM r2. However, there's an inconsistency: ssms k1 maps to HSSM k1, but ssms k0 maps to HSSM k2. This creates confusion because r1 from ssms goes to r1 in HSSM, but k1 from ssms goes to k1 in HSSM while k0 goes to k2. For clarity and consistency, the mapping should be: ssms r0->HSSM r1, ssms r1->HSSM r2, ssms k0->HSSM k1, ssms k1->HSSM k2 (consistently mapping lower accumulator to 1 and upper to 2, or vice versa).
| " \"r1\": ssms_params[\"r1\"],\n", | |
| " \"r2\": ssms_params[\"r0\"],\n", | |
| " \"k1\": ssms_params[\"k1\"],\n", | |
| " \"k2\": ssms_params[\"k0\"],\n", | |
| " \"r1\": ssms_params[\"r0\"],\n", | |
| " \"r2\": ssms_params[\"r1\"],\n", | |
| " \"k1\": ssms_params[\"k0\"],\n", | |
| " \"k2\": ssms_params[\"k1\"],\n", |
| "logp_ddm_bbox", | ||
| "logp_ddm_sdv_bbox", | ||
| "logp_full_ddm", | ||
| "logp_poisson_race", |
There was a problem hiding this comment.
The POISSON_RACE distribution object is created but not exported in the init.py file. The init.py only exports logp_poisson_race function but not the POISSON_RACE distribution object itself. Looking at similar models like LBA2 and LBA3, both the logp function and the distribution object should be exported for consistency.
| log_pdf = ( | ||
| k_c * pt.log(r_c_safe) | ||
| + (k_c - 1.0) * pt.log(rt_safe) | ||
| - r_c * rt |
There was a problem hiding this comment.
In the log_pdf computation, the term "- r_c * rt" should use "rt_safe" instead of "rt" for consistency with the rest of the computation. When rt is negative (rt <= 0), rt_safe is set to epsilon, but the original rt (which is negative) is still used in this calculation. This could lead to incorrect log-likelihood values for edge cases where rt is barely positive after subtracting t.
| - r_c * rt | |
| - r_c * rt_safe |
|
|
||
| # set bounds | ||
| poisson_race_params = ["r1", "r2", "k1", "k2", "t"] | ||
| poisson_race_bounds = {param: (0.0, np.inf) for param in poisson_race_params} |
There was a problem hiding this comment.
The bounds for all parameters are set to (0.0, np.inf), which means the lower bound is 0.0 (inclusive). However, the parameter validation checks r1 > 0, r2 > 0, k1 > 0, and k2 > 0 (strictly greater than), meaning 0 is not actually a valid value. The bounds should be defined with an exclusive lower bound, or the implementation should use epsilon as the lower bound to be consistent with the parameter checks.
| poisson_race_bounds = {param: (0.0, np.inf) for param in poisson_race_params} | |
| poisson_race_bounds = { | |
| "r1": (np.finfo(float).eps, np.inf), | |
| "r2": (np.finfo(float).eps, np.inf), | |
| "k1": (np.finfo(float).eps, np.inf), | |
| "k2": (np.finfo(float).eps, np.inf), | |
| "t": (0.0, np.inf), | |
| } |
| @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.
| "logp_full_ddm", | ||
| "logp_poisson_race", |
There was a problem hiding this comment.
The name 'logp_poisson_race' is exported by all but is not defined.
| "logp_full_ddm", | |
| "logp_poisson_race", | |
| "logp_full_ddm", |
|
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>
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.