Skip to content

Show posterior uncertainty in plot_model_cartoon as graded bands; declutter samples mode - #1133

Merged
AlexanderFengler merged 9 commits into
mainfrom
1122-improve-model-cartoon
Aug 3, 2026
Merged

Show posterior uncertainty in plot_model_cartoon as graded bands; declutter samples mode#1133
AlexanderFengler merged 9 commits into
mainfrom
1122-improve-model-cartoon

Conversation

@AlexanderFengler

@AlexanderFengler AlexanderFengler commented Aug 3, 2026

Copy link
Copy Markdown
Member

Supersedes #1130, which was auto-closed when the #1123 base branch was deleted and cannot be reopened after the rebase onto main (same commits, content unchanged — rebased past the #1123 squash). All review context lives on #1130.

Closes #1124. Stacked on #1123 (reuses its helpers and uncertainty= vocabulary); retarget to main when #1123 merges.

What changed

  • uncertainty="band" | "samples" | "both" | None on plot_model_cartoon, default "band" — same vocabulary as plot_predictive. None reproduces the old mean-only look (test-pinned).
  • RT histograms: per-draw defective-density matrices on shared bin edges → graded pointwise quantile bands + mean, replacing N overlaid ax.hist calls (~0.98 accumulated opacity at defaults). Makes the previously dead bins/step/colors/linestyles/linewidths parameters real; adds hist_height.
  • Geometry: boundary fan-chart ribbons, dashed drift-quantile cone (truncated at first absorption — no survivor-biased tails), graded NDT axvspans + rug replacing the per-draw axvline smear, starting-point whisker. Geometry stays neutral black (color_model); predictions/data keep the predictive palette.
  • samples mode: auto per-curve alpha clip(4/n_draws, .02, .25) instead of fixed 0.05.
  • 2-choice renderer: per-choice bands in trajectory colors, boundary ribbon, NDT spans; removes a latent NameError on data-only calls.

  • Deprecation shim: plot_predictive_mean/plot_predictive_samplesFutureWarning; both spellings together error.
  • Legends harvested from labeled artists (1D + FacetGrid), replacing hard-coded proxies.
  • New fixture-driven tutorial: docs/tutorials/cartoon_gallery.ipynb (sibling of ppc_gallery.ipynb, executed, 9 figures). Changelog entry included.
  • Tests: 30 new fast artist-structure tests (tests/unit/plotting/test_model_cartoon.py); slow suite converted to the new vocabulary + one shim-coverage case.

Scope notes

Commands run

  • uv run pytest tests/unit/plotting/ → 74/74
  • targeted slow cases (prior-predictive band, boolean shim) → pass
  • uv run mypy src/hssm (CI's gate) → clean; uv run ruff check . → clean except a pre-existing unused import in ppc_gallery.ipynb on the base branch
  • Visual acceptance: all four modes + facets rendered from the cavanagh fixture and eyeballed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced model cartoon plots with uncertainty bands, configurable intervals, opacity, histograms, step rendering, scaling, and legends.
    • Added support for uncertainty visualization across binary and multi-choice models, including geometry and starting-point uncertainty.
    • Added a new model cartoon plotting tutorial to the documentation navigation.
  • Documentation

    • Updated plotting tutorials for the new uncertainty options and clarified plotting behavior.
    • Documented deprecated predictive plotting options and compatibility guidance.
  • Bug Fixes

    • Improved consistency of histogram curves and uncertainty-band rendering across plotting features.

Both the predictive and model-cartoon plot families need the bandable
step-curve geometry and the graded-band opacity ladder. Move _curve_xy out
of predictive.py and extract the inline alpha linspace into _band_alphas,
so the cartoon redesign consumes the same helpers instead of copying them.

No behavior change: 44/44 fast plotting tests pass unchanged.
…#1124)

plot_func_model gains the #1123 uncertainty vocabulary. RT histograms are
now drawn from precomputed per-draw defective-density matrices (shared bin
edges, _curve_xy geometry) instead of N overlaid ax.hist calls, enabling
graded pointwise quantile bands, auto-alpha spaghetti, a real step/bins
story, and a hist_height normalizer. Geometry uncertainty renders as
boundary fan ribbons, a dashed drift cone (decision-time aligned,
NaN-masked past absorption, truncated at <50% survival), graded ndt
axvspans + rug replacing the per-draw axvline smear, and a start-point
whisker. uncertainty=None preserves the legacy rendering for GUI-heritage
callers; add_histograms_to_twin_axes is deleted; legend is assembled from
labeled artists instead of hardcoded proxies.
Public signature adopts the #1123 vocabulary: uncertainty='band' default,
hdi, alpha_mean/alpha_uncertainty, hist_height, legend, live bins/step/
colors/linestyles/linewidths (previously documented but silently dead).
The deprecated plot_predictive_mean/plot_predictive_samples booleans map
through a FutureWarning shim; passing both spellings errors.

Geometry gets its own color_model (neutral black) so the figure encodes
structure=neutral, predictions=predicted color, data=observed color. The
drift cone truncates at the first absorption across draws — the >=50%
survival rule showed selection-biased tails. Histogram baselines anchor to
the same no-noise sim that draws the reference boundary, so histograms sit
exactly on the drawn bound. Hard-coded proxy legends replaced by harvested,
deduped, _legend_order-ranked legends on both the 1D and FacetGrid paths.
…#1124)

plot_func_model_n adopts the same machinery: per-choice defective-density
matrices (jointly integrating to 1 across choices) rendered as graded bands
in the per-choice trajectory colors from the single shared baseline, plus a
boundary ribbon and ndt spans/rug on the geometry. uncertainty=None keeps
the legacy per-draw overlays. The rewrite removes the latent NameError when
only observed data was passed (bottom now always defined), anchors the
baseline to the reference no-noise boundary like the 2-choice renderer, and
gives the legacy legend's dashed ndt proxy its missing label.
…ng (#1124)

tests/unit/plotting/test_model_cartoon.py: 30 unmarked tests in the #1123
style — pure math pins (defective-density mass identities incl. -999
exclusion and non-uniform edges, per-choice masses, geometry matrices,
band-alpha ladder, shim mappings) plus artist-census assertions per mode
(collection/line counts, alpha values, zorders, legend labels, live
bins/step/hist_height/styles, seeded reproducibility, legacy-census parity
for uncertainty=None, and the n-choice data-only NameError regression).

The slow structural suite converts to the uncertainty vocabulary, keeps one
boolean case covering the shim end-to-end (FutureWarning + ValueError), and
adds a facet-level PolyCollection check. tests/test_model_cartoon_colors.py
is kept, deviating from the original plan: _add_trajectories and its colors
support survived the redesign, so the test still covers live code.
docs/tutorials/cartoon_gallery.ipynb mirrors ppc_gallery.ipynb: fixture-
driven (runs in seconds, no sampling), one option per section — default
graded bands, samples, both, mean-only, custom hdi, hist_height, styling,
facets, the deprecated-boolean shim, and the regression-model trial-
provenance caveat. Executed end-to-end: 9 figures, zero errors. Wired into
mkdocs nav + execute_ignore beside its sibling.

Also fixes the plotting.ipynb cell that passed long-removed kwargs
(plot_pp_mean/plot_pp_samples/alpha_pp, silently swallowed since their
removal) and adds the changelog entry. Full re-execution of the cartoon
sections in plotting.ipynb / scientific_workflow_hssm.ipynb is deferred to
merge time to avoid double churn against the #1123 notebook regeneration.
end-of-file/whitespace auto-fixes in the two gallery notebooks, and the
legend dedup rewritten as an explicit loop — mypy rejects the
set.add()-in-boolean-context idiom. CI's gate (mypy src/hssm) is clean;
the 14 remaining local-hook findings are pre-existing in tests/addm and
tests/rl on the base branch.
…#1124)

Three fixes from first review of the gallery:

1. Warnings: attach_trialwise_params_to_df now sorts the (chain, draw,
   obs_n) MultiIndex once before its per-draw assignments — previously every
   assignment emitted a pandas lexsort PerformanceWarning (1236 per gallery
   run). The remaining numba object-mode notices trace to the fixture
   shipping no predictive groups (#1080); filtered in the gallery setup cell
   with a comment naming the cause.

2. Legend: the combined histogram + geometry legend is too tall to float
   inside the axes; it now sits outside on the right (center-left anchor at
   x=1.02).

3. Geometry coherence: all four no-noise geometry sims (reference and
   per-draw, both renderers) now use the trial-0 row as a single-row theta.
   Previously the reference simulated an unseeded RANDOM trial while the
   per-draw curves inherited the simulator's last-trial boundary scratch
   buffer — for hierarchical models the ribbons and the drawn mean bound sat
   at visibly different levels. One consistent trial also makes every
   metadata element (boundary/trajectory/ndt/z) describe the same trial and
   the reference deterministic. #1125 narrows to the reduction convention
   (trial-mean, obs=), seeded trajectories, and max_t.

4. NDT display: the graded axvspan envelope now renders in every uncertainty
   mode, replacing the bottom-edge rug in samples mode — an envelope hugging
   the dashed reference line is the natural reading; the rug read as a
   stray artifact.

Gallery re-executed: zero warnings, 9 figures, no errors. 74/74 fast tests.
#1124)

- _render_drift_band now draws fill_between quantile bands per HDI
  interval (widest first, _band_alphas ladder), matching the boundary
  ribbons and histogram bands; dashed linestyle is reserved for the
  non-decision-time reference line
- band census test distinguishes boundary ribbons (zorder 1010) from
  drift bands (zorder 1012) and asserts no dashed non-vertical lines
- gallery, changelog, and docstring wording updated; changelog note on
  trial provenance refreshed to the trial-0 convention
@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

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds uncertainty-aware plot_model_cartoon rendering with graded bands, predictive samples, configurable histogram behavior, aggregated model geometry, deprecated-argument compatibility, shared plotting helpers, tests, and updated tutorial navigation.

Changes

Model cartoon uncertainty

Layer / File(s) Summary
Plotting API and uncertainty pipeline
src/hssm/plotting/model_cartoon.py
Plotting methods now resolve uncertainty modes, validate deprecated flags, support n_reps, and forward histogram, style, opacity, and legend settings.
Density and geometry rendering
src/hssm/plotting/model_cartoon.py, src/hssm/plotting/utils.py, src/hssm/plotting/predictive.py
Rendering now uses shared density and curve helpers, graded uncertainty bands, sample overlays, aggregated geometry, neutral colors, and deduplicated legends.
Multichoice rendering
src/hssm/plotting/model_cartoon.py
Multichoice plots now use shared bins, choice-specific density matrices, uncertainty layers, observed overlays, and aggregated geometry.
Plotting validation
tests/test_plotting_cartoon.py, tests/unit/plotting/test_model_cartoon.py
Tests cover uncertainty modes, compatibility warnings, density and geometry helpers, Matplotlib artists, legends, styles, reproducibility, and binary and multinomial plots.
Documentation and tutorial wiring
docs/changelog.md, docs/tutorials/plotting.ipynb, docs/tutorials/ppc_gallery.ipynb, mkdocs.yml
Documentation describes the updated plotting interface, tutorial examples use uncertainty=None, and the cartoon gallery is added to navigation and execution exclusions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant plot_model_cartoon
  participant plot_func_model
  participant Matplotlib
  User->>plot_model_cartoon: select uncertainty and styling options
  plot_model_cartoon->>plot_func_model: forward resolved configuration
  plot_func_model->>Matplotlib: render density bands, samples, means, and geometry
  Matplotlib-->>User: display model cartoon
Loading

Possibly related issues

Possibly related PRs

  • lnccbrown/HSSM#1123 — Introduces related predictive uncertainty visualization concepts and shared plotting helpers.
  • lnccbrown/HSSM#1132 — Adds a convenience plotting method extended by this cartoon-plotting work.
  • lnccbrown/HSSM#1019 — Provides related model_cartoon.py and plotting utility changes for the xarray.DataTree migration.

Suggested reviewers: digicosmos86, cpaniaguam

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: graded posterior uncertainty bands and reduced sample clutter in plot_model_cartoon.
Linked Issues check ✅ Passed The changes implement the linked issue objectives for uncertainty modes, graded bands, geometry rendering, styling, compatibility, and legacy mean-only behavior [#1124].
Out of Scope Changes check ✅ Passed The changes remain within scope; documentation, shared utilities, compatibility updates, and tests support the requested visual redesign [#1124].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 1122-improve-model-cartoon

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hssm/plotting/model_cartoon.py (1)

158-176: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A string colors is indexed per character.

colors is typed str | list[str] | None. Line 160 rejects a string only when plot_data=True. When plot_data=False and colors="red", line 169 sets color_predictive="r" and line 171 sets color_data="e". The style pair is then wrong.

Normalize a string to a pair before building style_kwargs.

🐛 Proposed fix
     colors = colors or list(DEFAULT_PREDICTIVE_COLORS)

     if plot_data and isinstance(colors, str):
         raise ValueError("When `plot_data=True`, `colors` must be a list or dict.")
+    if isinstance(colors, str):
+        colors = [colors, DEFAULT_PREDICTIVE_COLORS[1]]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hssm/plotting/model_cartoon.py` around lines 158 - 176, Normalize a
string colors value to a two-entry style pair before constructing style_kwargs,
so colors="red" applies the same color to predictive and data artists when
plot_data=False. Update the colors handling near the existing plot_data
validation, preserve list behavior and defaults, and ensure the later
color_predictive and color_data indexing operates only on the normalized pair.
🧹 Nitpick comments (2)
src/hssm/plotting/model_cartoon.py (2)

2376-2414: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The plot_func_model_n docstring is out of date.

Line 2380 still documents n_samples : int, default=10, but the parameter is now n_reps and n_samples is only accepted through the deprecation shim at line 2416. The docstring also omits the new bins, step, intervals, hist_height, linestyle_histogram_data, linewidth_histogram_data, and alpha_uncertainty parameters. alpha_predictive is documented as default=0.05, but the signature default is None.

The sibling plot_func_model docstring documents all of these. Align this one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hssm/plotting/model_cartoon.py` around lines 2376 - 2414, Update the
plot_func_model_n docstring to match its current signature and the sibling
plot_func_model documentation: document n_reps instead of n_samples, add bins,
step, intervals, hist_height, linestyle_histogram_data,
linewidth_histogram_data, and alpha_uncertainty, and change alpha_predictive’s
default to None. Keep n_samples documented only as the deprecated compatibility
alias handled by the shim.

2148-2178: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

_geometry_arrays uses mapping keys as row indices.

Line 2156 iterates sims.items() and lines 2167-2176 assign into pre-allocated arrays at position i, where i is the dictionary key. The arrays are sized len(sims). The current callers build posterior_pred_no_noise with contiguous keys 0..n-1, so this works. A caller that passes a mapping with non-contiguous or non-zero-based keys raises IndexError, or silently leaves uninitialized np.empty rows in the quantile computation.

Index by enumeration position instead.

♻️ Proposed refactor
-    for i, sample in sims.items():
+    for i, sample in enumerate(sims.values()):
         meta = sample["metadata"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hssm/plotting/model_cartoon.py` around lines 2148 - 2178, Update
_geometry_arrays to enumerate sims.items() and use the enumeration position for
all assignments into b_high_m, b_low_m, drifts, ndts, and z_abs, while retaining
each mapping value as sample. This must support mappings with arbitrary,
non-contiguous keys without leaving uninitialized rows or raising IndexError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/changelog.md`:
- Line 3: Update the Unreleased heading in the changelog from h3 to h2 by using
the document’s expected level, while preserving the heading text and surrounding
content.

In `@src/hssm/plotting/model_cartoon.py`:
- Around line 92-98: Update the uncertainty argument handling in the relevant
plotting helper to distinguish an omitted value from an explicit
uncertainty="band", using a sentinel default and resolving omitted values to
"band" after conflict validation. Ensure any explicitly supplied uncertainty
together with plot_predictive_mean or plot_predictive_samples raises ValueError,
preserving the documented API behavior and preventing deprecated booleans from
overriding the explicit request.

In `@tests/test_plotting_cartoon.py`:
- Around line 129-173: Restore coverage for the grouped 3-choice rendering path
in test_plot_model_cartoon_3_choice by adding at least one parametrized case
with a groups value supported by race_model_cartoon, confirming whether the
fixture expects a string or list before choosing it. Ensure the case reaches the
existing groups-not-None branch and validates the returned per-group axes; do
not remove that branch unless grouped rendering is intentionally unsupported.

In `@tests/unit/plotting/test_model_cartoon.py`:
- Around line 371-395: Update test_legacy_value_pin_plugin_mean to assert the
mean curve’s bottom anchoring using the computed bottom value instead of
normalizing it away, while retaining the shape comparison. Update
test_reproducible_with_seed to compare the collected traj_lines between seeded
renders and verify trajectory reproducibility; do not leave either local unused,
and ensure both tests pass Ruff F841.

---

Outside diff comments:
In `@src/hssm/plotting/model_cartoon.py`:
- Around line 158-176: Normalize a string colors value to a two-entry style pair
before constructing style_kwargs, so colors="red" applies the same color to
predictive and data artists when plot_data=False. Update the colors handling
near the existing plot_data validation, preserve list behavior and defaults, and
ensure the later color_predictive and color_data indexing operates only on the
normalized pair.

---

Nitpick comments:
In `@src/hssm/plotting/model_cartoon.py`:
- Around line 2376-2414: Update the plot_func_model_n docstring to match its
current signature and the sibling plot_func_model documentation: document n_reps
instead of n_samples, add bins, step, intervals, hist_height,
linestyle_histogram_data, linewidth_histogram_data, and alpha_uncertainty, and
change alpha_predictive’s default to None. Keep n_samples documented only as the
deprecated compatibility alias handled by the shim.
- Around line 2148-2178: Update _geometry_arrays to enumerate sims.items() and
use the enumeration position for all assignments into b_high_m, b_low_m, drifts,
ndts, and z_abs, while retaining each mapping value as sample. This must support
mappings with arbitrary, non-contiguous keys without leaving uninitialized rows
or raising IndexError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7bca1e1-bc3b-431f-8333-e24877558db0

📥 Commits

Reviewing files that changed from the base of the PR and between 584b984 and 0615b94.

📒 Files selected for processing (10)
  • docs/changelog.md
  • docs/tutorials/cartoon_gallery.ipynb
  • docs/tutorials/plotting.ipynb
  • docs/tutorials/ppc_gallery.ipynb
  • mkdocs.yml
  • src/hssm/plotting/model_cartoon.py
  • src/hssm/plotting/predictive.py
  • src/hssm/plotting/utils.py
  • tests/test_plotting_cartoon.py
  • tests/unit/plotting/test_model_cartoon.py
💤 Files with no reviewable changes (1)
  • docs/tutorials/ppc_gallery.ipynb
👮 Files not reviewed due to content moderation or server errors (1)
  • docs/tutorials/cartoon_gallery.ipynb

Comment thread docs/changelog.md
@@ -1,5 +1,9 @@
# Changelog

### Unreleased

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the correct heading level for Unreleased.

### Unreleased skips the document’s expected h2 level and triggers markdownlint MD001. Change it to ## Unreleased.

Proposed fix
-### Unreleased
+## Unreleased
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Unreleased
## Unreleased
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 3-3: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/changelog.md` at line 3, Update the Unreleased heading in the changelog
from h3 to h2 by using the document’s expected level, while preserving the
heading text and surrounding content.

Source: Linters/SAST tools

Comment on lines +92 to +98
if plot_predictive_mean is None and plot_predictive_samples is None:
return uncertainty, alpha_mean
if uncertainty != "band":
raise ValueError(
"Pass either `uncertainty=` or the deprecated `plot_predictive_mean`/"
"`plot_predictive_samples` booleans, not both."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Explicit uncertainty="band" plus a deprecated boolean is silently accepted.

The conflict check compares uncertainty against the default value "band". A caller who passes uncertainty="band" explicitly together with plot_predictive_mean or plot_predictive_samples does not get a ValueError. The booleans then override the explicit request. The public docstring at lines 624-629 states that this combination raises ValueError.

Use a sentinel default to detect an explicit argument, or relax the docstring wording.

♻️ Sentinel-based detection
+_UNSET = object()
+
 def _resolve_uncertainty_from_legacy(
     plot_predictive_mean: bool | None,
     plot_predictive_samples: bool | None,
-    uncertainty: Literal["band", "samples", "both"] | None,
+    uncertainty: Literal["band", "samples", "both"] | None | object,
     alpha_mean: float,
 ) -> tuple[Literal["band", "samples", "both"] | None, float]:

The public signature would then default uncertainty=_UNSET and resolve it to "band" after this call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hssm/plotting/model_cartoon.py` around lines 92 - 98, Update the
uncertainty argument handling in the relevant plotting helper to distinguish an
omitted value from an explicit uncertainty="band", using a sentinel default and
resolving omitted values to "band" after conflict validation. Ensure any
explicitly supplied uncertainty together with plot_predictive_mean or
plot_predictive_samples raises ValueError, preserving the documented API
behavior and preventing deprecated booleans from overriding the explicit
request.

Comment on lines 129 to +173
[
(2, None, False, False, "posterior_predictive", "participant_id", "stim"),
(2, None, False, False, "prior_predictive", "participant_id", "stim"),
(2, None, True, True, "posterior_predictive", "participant_id", "stim"),
(2, None, True, True, "prior_predictive", "participant_id", "stim"),
(2, None, False, True, "posterior_predictive", "participant_id", "stim"),
(2, None, False, True, "prior_predictive", "participant_id", "stim"),
(0, None, False, True, "posterior_predictive", "participant_id", "stim"),
(0, None, False, True, "prior_predictive", "participant_id", "stim"),
(0, None, True, False, "posterior_predictive", "participant_id", "stim"),
(0, None, True, False, "prior_predictive", "participant_id", "stim"),
(2, None, True, False, "posterior_predictive", "participant_id", None),
(2, None, True, False, "prior_predictive", "participant_id", None),
(2, None, True, False, "posterior_predictive", "participant_id", "stim"),
(2, None, True, False, "prior_predictive", "participant_id", "stim"),
(2, None, True, False, "posterior_predictive", None, None),
(2, None, True, False, "prior_predictive", None, None),
(2, None, "both", "posterior_predictive", "participant_id", "stim"),
(2, None, "both", "prior_predictive", "participant_id", "stim"),
(2, None, "samples", "posterior_predictive", "participant_id", "stim"),
(2, None, "samples", "prior_predictive", "participant_id", "stim"),
(0, None, "band", "posterior_predictive", "participant_id", "stim"),
(0, None, "band", "prior_predictive", "participant_id", "stim"),
(0, None, None, "posterior_predictive", "participant_id", "stim"),
(0, None, None, "prior_predictive", "participant_id", "stim"),
(2, None, None, "posterior_predictive", "participant_id", None),
(2, None, None, "prior_predictive", "participant_id", None),
(2, None, "band", "posterior_predictive", None, None),
(2, None, "band", "prior_predictive", None, None),
],
)
def test_plot_model_cartoon_3_choice(
race_model_cartoon,
n_trajectories,
groups,
plot_predictive_mean,
plot_predictive_samples,
uncertainty,
predictive_group,
row,
col,
):
"""Test plot_model_cartoon for 3-choice data."""
if (not plot_predictive_mean) and (not plot_predictive_samples):
with pytest.raises(ValueError):
ax = hssm.plotting.plot_model_cartoon(
race_model_cartoon,
n_samples=10,
n_samples_prior=10,
bins=30,
col=col,
row=row,
groups=groups,
plot_predictive_mean=plot_predictive_mean,
plot_predictive_samples=plot_predictive_samples,
predictive_group=predictive_group,
alpha_mean=0.025,
n_trajectories=n_trajectories,
)
else:
ax = hssm.plotting.plot_model_cartoon(
race_model_cartoon,
n_samples=10,
n_samples_prior=10,
bins=30,
col=col,
row=row,
groups=groups,
plot_predictive_mean=plot_predictive_mean,
plot_predictive_samples=plot_predictive_samples,
predictive_group=predictive_group,
alpha_mean=0.025,
n_trajectories=n_trajectories,
)
ax = hssm.plotting.plot_model_cartoon(
race_model_cartoon,
n_samples=10,
n_samples_prior=10,
col=col,
row=row,
groups=groups,
uncertainty=uncertainty,
predictive_group=predictive_group,
n_trajectories=n_trajectories,
)

if groups is None:
if row is not None:
assert np.all(ax.row_names == race_model_cartoon.data[row].unique())
if col is not None:
assert np.all(ax.col_names == race_model_cartoon.data[col].unique())
else:
assert isinstance(ax, list)
assert len(ax) == len(race_model_cartoon.data[groups].unique())
if groups is None:
if row is not None:
assert np.all(ax.row_names == race_model_cartoon.data[row].unique())
if col is not None:
assert np.all(ax.col_names == race_model_cartoon.data[col].unique())
else:
assert isinstance(ax, list)
assert len(ax) == len(race_model_cartoon.data[groups].unique())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The 3-choice grouped path lost coverage and the else branch is now dead.

Every case in the parametrization at lines 129-142 sets groups=None. The else branch at lines 171-173 therefore never runs. The 2-choice matrix still exercises the grouped path with ["dbs"] at lines 31-32.

This PR changes the grouped rendering route for multi-choice models: plot_model_cartoon forwards shared_plot_kwargs and the per-group legend flag into _plot_model_cartoon_2D at lines 925-935 of src/hssm/plotting/model_cartoon.py, and plot_func_model_n gained new geometry and legend code. No integration test now covers that combination.

Restore a grouped case, or remove the unreachable branch.

💚 Restore one grouped case
         (0, None, None, "posterior_predictive", "participant_id", "stim"),
         (0, None, None, "prior_predictive", "participant_id", "stim"),
+        (2, "dbs", "band", "posterior_predictive", "participant_id", "stim"),
         (2, None, None, "posterior_predictive", "participant_id", None),

Line 173 indexes race_model_cartoon.data[groups], so this test expects groups to be a string, unlike the 2-choice test which uses a list. Confirm which form the fixture supports before restoring the case.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[
(2, None, False, False, "posterior_predictive", "participant_id", "stim"),
(2, None, False, False, "prior_predictive", "participant_id", "stim"),
(2, None, True, True, "posterior_predictive", "participant_id", "stim"),
(2, None, True, True, "prior_predictive", "participant_id", "stim"),
(2, None, False, True, "posterior_predictive", "participant_id", "stim"),
(2, None, False, True, "prior_predictive", "participant_id", "stim"),
(0, None, False, True, "posterior_predictive", "participant_id", "stim"),
(0, None, False, True, "prior_predictive", "participant_id", "stim"),
(0, None, True, False, "posterior_predictive", "participant_id", "stim"),
(0, None, True, False, "prior_predictive", "participant_id", "stim"),
(2, None, True, False, "posterior_predictive", "participant_id", None),
(2, None, True, False, "prior_predictive", "participant_id", None),
(2, None, True, False, "posterior_predictive", "participant_id", "stim"),
(2, None, True, False, "prior_predictive", "participant_id", "stim"),
(2, None, True, False, "posterior_predictive", None, None),
(2, None, True, False, "prior_predictive", None, None),
(2, None, "both", "posterior_predictive", "participant_id", "stim"),
(2, None, "both", "prior_predictive", "participant_id", "stim"),
(2, None, "samples", "posterior_predictive", "participant_id", "stim"),
(2, None, "samples", "prior_predictive", "participant_id", "stim"),
(0, None, "band", "posterior_predictive", "participant_id", "stim"),
(0, None, "band", "prior_predictive", "participant_id", "stim"),
(0, None, None, "posterior_predictive", "participant_id", "stim"),
(0, None, None, "prior_predictive", "participant_id", "stim"),
(2, None, None, "posterior_predictive", "participant_id", None),
(2, None, None, "prior_predictive", "participant_id", None),
(2, None, "band", "posterior_predictive", None, None),
(2, None, "band", "prior_predictive", None, None),
],
)
def test_plot_model_cartoon_3_choice(
race_model_cartoon,
n_trajectories,
groups,
plot_predictive_mean,
plot_predictive_samples,
uncertainty,
predictive_group,
row,
col,
):
"""Test plot_model_cartoon for 3-choice data."""
if (not plot_predictive_mean) and (not plot_predictive_samples):
with pytest.raises(ValueError):
ax = hssm.plotting.plot_model_cartoon(
race_model_cartoon,
n_samples=10,
n_samples_prior=10,
bins=30,
col=col,
row=row,
groups=groups,
plot_predictive_mean=plot_predictive_mean,
plot_predictive_samples=plot_predictive_samples,
predictive_group=predictive_group,
alpha_mean=0.025,
n_trajectories=n_trajectories,
)
else:
ax = hssm.plotting.plot_model_cartoon(
race_model_cartoon,
n_samples=10,
n_samples_prior=10,
bins=30,
col=col,
row=row,
groups=groups,
plot_predictive_mean=plot_predictive_mean,
plot_predictive_samples=plot_predictive_samples,
predictive_group=predictive_group,
alpha_mean=0.025,
n_trajectories=n_trajectories,
)
ax = hssm.plotting.plot_model_cartoon(
race_model_cartoon,
n_samples=10,
n_samples_prior=10,
col=col,
row=row,
groups=groups,
uncertainty=uncertainty,
predictive_group=predictive_group,
n_trajectories=n_trajectories,
)
if groups is None:
if row is not None:
assert np.all(ax.row_names == race_model_cartoon.data[row].unique())
if col is not None:
assert np.all(ax.col_names == race_model_cartoon.data[col].unique())
else:
assert isinstance(ax, list)
assert len(ax) == len(race_model_cartoon.data[groups].unique())
if groups is None:
if row is not None:
assert np.all(ax.row_names == race_model_cartoon.data[row].unique())
if col is not None:
assert np.all(ax.col_names == race_model_cartoon.data[col].unique())
else:
assert isinstance(ax, list)
assert len(ax) == len(race_model_cartoon.data[groups].unique())
[
(2, None, "both", "posterior_predictive", "participant_id", "stim"),
(2, None, "both", "prior_predictive", "participant_id", "stim"),
(2, None, "samples", "posterior_predictive", "participant_id", "stim"),
(2, None, "samples", "prior_predictive", "participant_id", "stim"),
(0, None, "band", "posterior_predictive", "participant_id", "stim"),
(0, None, "band", "prior_predictive", "participant_id", "stim"),
(0, None, None, "posterior_predictive", "participant_id", "stim"),
(0, None, None, "prior_predictive", "participant_id", "stim"),
(2, "dbs", "band", "posterior_predictive", "participant_id", "stim"),
(2, None, None, "posterior_predictive", "participant_id", None),
(2, None, None, "prior_predictive", "participant_id", None),
(2, None, "band", "posterior_predictive", None, None),
(2, None, "band", "prior_predictive", None, None),
],
)
def test_plot_model_cartoon_3_choice(
race_model_cartoon,
n_trajectories,
groups,
uncertainty,
predictive_group,
row,
col,
):
"""Test plot_model_cartoon for 3-choice data."""
ax = hssm.plotting.plot_model_cartoon(
race_model_cartoon,
n_samples=10,
n_samples_prior=10,
col=col,
row=row,
groups=groups,
uncertainty=uncertainty,
predictive_group=predictive_group,
n_trajectories=n_trajectories,
)
if groups is None:
if row is not None:
assert np.all(ax.row_names == race_model_cartoon.data[row].unique())
if col is not None:
assert np.all(ax.col_names == race_model_cartoon.data[col].unique())
else:
assert isinstance(ax, list)
assert len(ax) == len(race_model_cartoon.data[groups].unique())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_plotting_cartoon.py` around lines 129 - 173, Restore coverage for
the grouped 3-choice rendering path in test_plot_model_cartoon_3_choice by
adding at least one parametrized case with a groups value supported by
race_model_cartoon, confirming whether the fixture expects a string or list
before choosing it. Ensure the case reaches the existing groups-not-None branch
and validates the returned per-group axes; do not remove that branch unless
grouped rendering is intentionally unsupported.

Comment on lines +371 to +395
def test_legacy_value_pin_plugin_mean(self):
"""The None-mode mean curve equals the independently computed plug-in."""
from ssms.basic_simulators.simulator import simulator

theta_mean, _ = _theta_frames()
np.random.seed(0)
rand_int = np.random.randint(0, 400000000)
sim_out = simulator(
model="ddm",
theta=theta_mean.values,
n_samples=2,
no_noise=False,
delta_t=0.01,
random_state=rand_int,
)
edges = np.arange(-0.05, 5, 0.05)
expected_up, _ = _defective_densities(sim_out["rts"], sim_out["choices"], edges)
fig, ax, up, down = _render(None, data=None)
x, y = _curve_xy(edges, expected_up, True)
mean_line = up.get_lines()[0]
bottom = mean_line.get_ydata().min() if expected_up.min() == 0 else None
np.testing.assert_allclose(
mean_line.get_ydata() - (mean_line.get_ydata() - y).min(), y, atol=1e-8
)
plt.close("all")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Two tests contain unused locals that mark missing assertions.

Line 391 assigns bottom and never uses it. Line 473 assigns traj_lines and never uses it. Ruff reports F841 for both, and the coding guidelines require Ruff compliance.

Each unused local also marks an assertion that is absent:

  • test_legacy_value_pin_plugin_mean computes the expected bottom offset but then normalizes it away at line 393 with - (mean_line.get_ydata() - y).min(). The test therefore verifies curve shape but not the bound anchoring, which is the behavior lines 1215-1225 of src/hssm/plotting/model_cartoon.py changed.
  • test_reproducible_with_seed passes n_trajectories=2 and collects the trajectory lines, but line 474 compares only ax.get_lines()[:2]. Trajectory reproducibility is not asserted, although plot_func_model seeds trajectory simulation with rand_int + i at line 1211.

Add the missing assertions, or remove the unused locals.

💚 Assert trajectory reproducibility
     def test_reproducible_with_seed(self):
         results = []
         for _ in range(2):
             fig, ax, up, down = _render("band", n_trajectories=2)
-            traj_lines = [ln for ln in ax.get_lines() if 2000 <= ln.get_zorder() < 3000]
-            results.append([ln.get_ydata().copy() for ln in ax.get_lines()[:2]])
+            results.append([ln.get_ydata().copy() for ln in ax.get_lines()])
             plt.close("all")
+        assert len(results[0]) == len(results[1])
         for a, b in zip(*results):
             np.testing.assert_array_equal(a, b)

As per coding guidelines: "Use Ruff for linting and formatting, and comply with the configured Ruff rules."

Also applies to: 469-477

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/plotting/test_model_cartoon.py` around lines 371 - 395, Update
test_legacy_value_pin_plugin_mean to assert the mean curve’s bottom anchoring
using the computed bottom value instead of normalizing it away, while retaining
the shape comparison. Update test_reproducible_with_seed to compare the
collected traj_lines between seeded renders and verify trajectory
reproducibility; do not leave either local unused, and ensure both tests pass
Ruff F841.

Source: Coding guidelines

AlexanderFengler added a commit that referenced this pull request Aug 3, 2026
…cal (#1125)

CodeRabbit findings on #1133, applied here at the stack tip to avoid a
rebase round: the 3-choice parametrization never sets groups (the race
fixture has no grouping column; the renderer-agnostic grouped path is
covered by the 2-choice ['dbs'] rows), and the value-pin test carried an
unused 'bottom' local.
@AlexanderFengler

Copy link
Copy Markdown
Member Author

CodeRabbit adjudication (all four findings are minor; none block this PR):

  1. ### Unreleased heading leveldiscarded: every version heading in docs/changelog.md is h3 (### 0.4.0, ### 0.3.1, …); switching only Unreleased to h2 would break the file's own convention, and markdownlint is not a gate in this repo.
  2. Explicit uncertainty="band" + deprecated boolean silently accepteddiscarded with reason: detecting that case needs a sentinel default on a public parameter, purely to catch a caller who explicitly types the default value alongside a deprecated boolean; the FutureWarning still fires and tells them the booleans are driving, and the whole shim is scheduled for removal in plot_model_cartoon: remove the deprecation shims (two minor releases after #1124) #1128.
  3. Dead grouped-else in the 3-choice slow testfixed in the stacked follow-up plot_model_cartoon correctness: θ-reduction, seeded RNG, max_t ownership, coherent trajectories #1131 (commit 51a26db), where this file is already being modified: the race fixture has no grouping column and the renderer-agnostic grouped path is covered by the 2-choice ["dbs"] rows. Fixed at the stack tip to avoid another rebase/force-push round on this branch.
  4. Unused test locals (bottom, traj_lines)fixed in plot_model_cartoon correctness: θ-reduction, seeded RNG, max_t ownership, coherent trajectories #1131: traj_lines was removed when the reproducibility test was strengthened to actually assert trajectory equality (its absence was exactly the missing-assertion smell CodeRabbit points at), and bottom is dropped in commit 51a26db.

@AlexanderFengler
AlexanderFengler merged commit 7e05738 into main Aug 3, 2026
7 checks passed
@AlexanderFengler
AlexanderFengler deleted the 1122-improve-model-cartoon branch August 3, 2026 02:11
AlexanderFengler added a commit that referenced this pull request Aug 3, 2026
…cal (#1125)

CodeRabbit findings on #1133, applied here at the stack tip to avoid a
rebase round: the 3-choice parametrization never sets groups (the race
fixture has no grouping column; the renderer-agnostic grouped path is
covered by the 2-choice ['dbs'] rows), and the value-pin test carried an
unused 'bottom' local.
AlexanderFengler added a commit that referenced this pull request Aug 3, 2026
Deferred from #1133 to avoid double churn: the cartoon cells now render
with the final #1124 + #1125 behavior (graded bands, trial-mean
geometry, reference-θ trajectories). Both notebooks are execute_ignore
in mkdocs, so these committed outputs are the rendered docs.
AlexanderFengler added a commit that referenced this pull request Aug 3, 2026
…hip, coherent trajectories (#1131)

* refactor: single t_s/max_t ownership derived from xlims (#1125)

- one geometry horizon per renderer: max_t_geom = xlim_high + 0.5, passed
  to every no-noise geometry and trajectory simulation (was: simulator's
  20 s default, ~75% of every polyline outside the axes)
- t_s computed once per renderer; the four scattered metadata-derived
  assignments removed (also fixes the _add_trajectories branch leak)
- noisy RT sims deliberately keep the long default horizon: shortening it
  would censor slow RTs into -999 and re-normalize defective densities
- _geometry_arrays hardened: constant-extends short boundary arrays and
  no longer assumes dict keys are 0..n-1

* feat: _reduce_theta — trial-mean geometry by default, obs= conditioning (#1125)

- new _reduce_theta(theta, obs): one θ vector per (chain, draw) — the
  across-trials mean by default, or the obs_n-labeled trial's row — so
  boundary, drift, ndt, and starting point describe the same coherent θ
  by construction (replaces the interim trial-0 convention)
- RT histograms stay marginal over trials by default (they are a check
  against pooled observed data); obs= conditions every simulated layer
  on that trial, with a warning that the observed histogram stays pooled
- theta_mean keeps its obs_n labels (droplevel instead of reset_index);
  trajectory trial picks switched to positional iloc so facet-local,
  non-contiguous labels cannot mis-select
- new obs parameter on plot_model_cartoon and both renderers, validated
  at the public level and label-checked per facet inside the reduction

* feat: seeded RNG end-to-end via np.random.default_rng (#1125)

- plot_model_cartoon gains random_state (int or Generator); one Generator
  now drives which posterior draws are displayed (previously selected by
  the unseeded global RNG before the renderer-level seed ever applied),
  every simulator seed, and the trajectories
- rng= threaded through _use... draw selection helpers
  (_get_plotting_df/_xarray_to_df/_random_sample/_generate_random_indices)
  with None defaults, so plot_predictive and other callers are untouched
- renderers replace the np.random.seed + randint protocol with positional
  draws from the Generator, eliminating the rand_int + i seed collision
  between per-draw sims and trajectories (and, in the n-choice renderer,
  the constant seed shared by all draws)
- facets consume successive segments of one stream: distinct per facet,
  reproducible across calls; stream order documented on random_state
- tests: value pin re-pinned to the new protocol; reproducibility test
  now asserts trajectories, bands, and ndt spans (it previously computed
  trajectory lines and never asserted them); Generator statefulness and
  seeded _generate_random_indices covered

* fix: n-choice per-draw sims respect n_reps (#1125)

- plot_func_model_n passed n_samples=1 to every per-draw RT simulation,
  silently ignoring n_reps: the per-choice uncertainty bands summarized
  single-rep, near-degenerate histograms (the companion constant-seed
  bug fell with the Generator protocol in the previous commit)
- recording-wrapper test pins n_reps and pairwise-distinct seeds

* fix: trajectories realize the reduced reference θ; per-trajectory anchoring (#1125)

- trajectories are noisy realizations of the SAME reduced θ that draws
  the reference geometry, so they illustrate diffusion noise around the
  bounds actually on screen and their crossing markers land on the drawn
  boundary by construction (was: each trajectory simulated a different
  random trial's θ but was rendered against the reference geometry)
- the dead theta_samples trajectory branch is removed: unreachable from
  the public entry point (run_mean is always True) and incoherent — it
  composed θ from independent chain/draw/obs picks; direct renderer
  calls without theta_mean now use one real draw's reduced θ
- _add_trajectories/_add_trajectories_n derive bounds and ndt-roll per
  trajectory from its own metadata instead of sample[0]

* feat: seeded draw subsampling in _use_traces_or_sample (#1125)

- when predictive sampling is triggered with an rng and an integer
  n_samples, pass a sorted seeded random subset of posterior draw
  indices to sample_posterior_predictive instead of the legacy first-n
  (deterministic but biased toward early, warm-up-adjacent draws)
- rng=None (every non-cartoon caller) keeps the legacy behavior exactly

* docs+api: promote n_trajectories/xlims/ylims; changelog + gallery sections (#1125)

- n_trajectories/xlims/ylims promoted from undocumented **kwargs to
  documented parameters with None sentinels (renderer defaults kept);
  xlims is now load-bearing — it determines the geometry max_t horizon
- named parameters win over the legacy kwargs spellings (popped, no
  duplicate-keyword errors)
- changelog: #1125 entry with the deliberate value changes; the #1124
  entry's trial-provenance note now points at it
- cartoon_gallery: new 'Reproducible figures with random_state' and
  'Conditioning on one trial with obs=' sections; caveat section
  rewritten for the trial-mean convention; re-executed (11 figures,
  zero warnings)

* test: slow-suite random_state/obs coverage (#1125)

- random_state=42 on every 2-/3-choice parametrization row (end-to-end
  threading incl. facets and groups)
- double-render equality test: same seed => identical figure through the
  full public path (after a warm-up render materializes the predictive
  group so both snapshots take the identical code path)
- obs=0 conditioning on the cavanagh regression model; out-of-range obs
  raises the labeled ValueError

* test: drop dead grouped-else in the 3-choice suite and a vestigial local (#1125)

CodeRabbit findings on #1133, applied here at the stack tip to avoid a
rebase round: the 3-choice parametrization never sets groups (the race
fixture has no grouping column; the renderer-agnostic grouped path is
covered by the 2-choice ['dbs'] rows), and the value-pin test carried an
unused 'bottom' local.

* docs: re-execute plotting and scientific-workflow tutorials (#1125)

Deferred from #1133 to avoid double churn: the cartoon cells now render
with the final #1124 + #1125 behavior (graded bands, trial-mean
geometry, reference-θ trajectories). Both notebooks are execute_ignore
in mkdocs, so these committed outputs are the rendered docs.
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.

plot_model_cartoon: posterior uncertainty display is too messy

1 participant