Skip to content

[REF] CBMR internal and public API refactor - #1005

Merged
jdkent merged 8 commits into
neurostuff:mainfrom
jdkent:refactor/cbmr
Apr 7, 2026
Merged

[REF] CBMR internal and public API refactor#1005
jdkent merged 8 commits into
neurostuff:mainfrom
jdkent:refactor/cbmr

Conversation

@jdkent

@jdkent jdkent commented Apr 3, 2026

Copy link
Copy Markdown
Member

Closes # .

Changes proposed in this pull request:

  • speed up CBMR and cache appropriately (many algorithmic improvements)
  • correlate CBMA estimator methods with CBMR estimator methods
  • make the public API for CBMR more easily consumable for users (add infer method)

Summary by Sourcery

Refactor CBMR meta-regression and inference to operate at the experiment level, improve numerical stability and performance (including CUDA support and tensor caching), and strengthen reproducibility and test coverage.

New Features:

  • Support running CBMR estimation and inference on CUDA devices with consistent device handling between estimators and models.
  • Add deterministic seeding for CBMR model initialization and optimization to make fits repeatable.
  • Introduce cached tensorized CBMR inputs and reusable Fisher information computations to support efficient multi-contrast inference.

Enhancements:

  • Rework CBMR preprocessing to group by experiment IDs instead of study IDs and to keep experiment-level arrays aligned even when all foci for an experiment fall outside the mask.
  • Improve coordinate validation and masking, including explicit checks for coordinate space consistency and safe filtering of out-of-mask coordinates.
  • Clarify and standardize terminology and docstrings from study-level to experiment-level throughout CBMR estimators, models, and inference classes.
  • Optimize B-spline basis construction to avoid sparse Kronecker products and build the masked design matrix directly for better performance and memory use.
  • Refactor GLM estimators to manage tensor conversion/caching centrally, share data between fit and inference, and provide analytic Fisher information implementations for the Poisson model.
  • Ensure CBMR inference copies results without mutating the input MetaResult and maintains internal caches for spatial and moderator covariance and log-intensity quantities.

Tests:

  • Expand CBMR tests to cover deterministic repeatability of fits, CUDA-based fit and inference, description generation, and preservation of input MetaResult during inference.
  • Add tests verifying that optimized summary table construction matches the legacy DataFrame-from-dict behavior.
  • Add targeted unit tests for vectorized chi-square computation, Poisson multigroup log-likelihood, and agreement between analytic and generic Fisher information implementations.
  • Add tests to ensure experiment-level grouping behavior (including experiments without in-mask foci) and that experiment IDs, moderators, and foci arrays remain aligned across groups.
  • Adjust CBMR test dataset simulation parameters and randomization to keep tests stable and reproducible.

jdkent added 2 commits April 3, 2026 17:12
Profile and refactor the CBMR hot paths while preserving numerical
behavior within tolerance.

- speed up summary table construction and inference result copying
- cache tensorized inputs, log-intensity values, and covariance terms
- vectorize multi-contrast GLH paths and add analytic Poisson Fisher info
- trim the synthetic CBMR fixture and expand regression coverage
- remove stale CBMR/model code, fix lint issues, and support CUDA devices
@sourcery-ai

sourcery-ai Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the CBMR estimator and GLM model stack for experiment-level handling, deterministic/cuda-aware execution, and more efficient tensor/Fisher computations, while tightening coordinate/mask preprocessing and greatly expanding tests for reproducibility and numerical correctness.

Sequence diagram for CBMR fitting with tensor input caching and cuda aware execution

sequenceDiagram
    actor User
    participant Estimator as CBMREstimator
    participant Model as GeneralLinearModelEstimator
    participant Torch as torch

    User->>Estimator: instantiate(model, device)
    Estimator->>Estimator: check device with _uses_cuda
    Estimator->>Torch: cuda.is_available()
    Torch-->>Estimator: availability
    Estimator->>Model: set device

    User->>Estimator: fit(dataset)
    Estimator->>Estimator: _preprocess_input(dataset)
    Estimator->>Model: init_weights(groups, moderators, spatial_coef_dim, moderators_coef_dim)
    Estimator->>Torch: manual_seed(100)
    Estimator->>Torch: cuda.manual_seed_all(100) if _uses_cuda
    Estimator->>Model: fit(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment)

    Model->>Model: _prepare_tensor_inputs(...)
    Model->>Model: _optimizer(tensor_inputs)
    Model->>Model: extract_optimized_params(coef_spline_bases, moderators_by_group)
    Model->>Model: standard_error_estimation(tensor_inputs)
    Model->>Model: _cache_tensor_inputs(original_inputs, tensor_inputs)
    Model-->>Estimator: maps, tables via summary()
Loading

Sequence diagram for CBMR inference with Fisher caching and tensor reuse

sequenceDiagram
    actor User
    participant Inference as CBMRInference
    participant Result as CBMRResult
    participant Estimator as CBMREstimator
    participant Model as GeneralLinearModelEstimator

    User->>Inference: __init__(device)
    Inference->>Inference: _reset_inference_caches()

    User->>Inference: fit(result)
    Inference->>Inference: _copy_result_for_inference(result)
    Inference->>Estimator: set device
    Inference->>Model: set device and to(device)
    Inference->>Model: _invalidate_tensor_inputs_cache()
    Inference->>Inference: build group_reference_dict and moderator_reference_dict

    User->>Inference: transform(t_con_groups, t_con_moderators)
    Inference->>Inference: _preprocess_t_con_regressor(groups)
    Inference->>Inference: _glh_con_group()
    Inference->>Inference: _get_group_spatial_covariance(involved_groups)
    Inference->>Model: fisher_info_multiple_group_spatial(...)
    Model->>Model: _resolve_tensor_inputs(...)
    Model-->>Inference: Fisher matrix
    Inference->>Inference: cache covariance in _group_spatial_covariance_cache
    Inference->>Inference: _chi_square_log_intensity(...)

    Inference->>Inference: _glh_con_moderator()
    Inference->>Inference: _get_moderator_covariance()
    Inference->>Model: fisher_info_multiple_group_moderator(...)
    Model-->>Inference: moderator Fisher matrix
    Inference->>Inference: cache covariance and variance
    Inference-->>User: updated maps and tables for contrasts
Loading

Class diagram for refactored CBMR estimator and GLM model stack

classDiagram
    class CBMREstimator {
        +dict _required_inputs
        +string _group_column
        +list groups
        +list moderators
        +string device
        +dict inputs_
        +__init__(model, penalty, spline_spacing, sample_size_moderator, group_categories, moderators, lr, lr_decay, tol, device)
        +_generate_description()
        +_get_mask_img(dataset)
        +_validate_coordinates()
        +_filter_coordinates_to_mask(coordinates, mask_img, mask_data, mask_lookup)
        +_build_experiment_annotations(dataset)
        +_build_group_moderators(experiment_annotations)
        +_build_group_foci(coordinates, ids_by_group, n_mask_voxels)
        +_preprocess_input(dataset)
        +_fit(dataset)
    }

    class CBMRInference {
        +string device
        +result
        +list groups
        +list moderators
        +dict group_reference_dict
        +dict moderator_reference_dict
        +__init__(device)
        +fit(result)
        +display()
        +create_regular_expressions()
        +create_contrast(contrast_name, source)
        +transform(t_con_groups, t_con_moderators)
        +_copy_result_for_inference(result)
        +_reset_inference_caches()
        +_get_group_log_intensity(group)
        +_get_group_null_log_intensity(group)
        +_get_group_spatial_covariance(involved_groups)
        +_get_moderator_covariance()
        +_glh_con_group()
        +_glh_con_moderator()
        +_chi_square_log_intensity(m, n_brain_voxel, n_con_group_involved, simp_con_group, contrast_log_intensity, cov_log_intensity)
    }

    class _CBMRTensorInputs {
        +Tensor coef_spline_bases
        +object moderators_by_group
        +dict foci_per_voxel
        +dict foci_per_experiment
        +subset(groups) _CBMRTensorInputs
    }

    class GeneralLinearModelEstimator {
        +list groups
        +list moderators
        +int spatial_coef_dim
        +int moderators_coef_dim
        +bool penalty
        +float lr
        +float lr_decay
        +float tol
        +string device
        +dict spatial_regression_coef
        +dict spatial_intensity_estimation
        +dict spatial_regression_coef_se
        +dict log_spatial_intensity_se
        +dict spatial_intensity_se
        +numpy.ndarray moderators_coef
        +dict moderators_effect
        +Tensor coef_spline_bases
        +object _tensor_inputs_cache
        +tuple _tensor_inputs_cache_keys
        +__init__(penalty, lr, lr_decay, tol, device)
        +init_spatial_weights()
        +init_moderator_weights()
        +init_weights(groups, moderators, spatial_coef_dim, moderators_coef_dim)
        +_invalidate_tensor_inputs_cache()
        +_to_numpy_array(array_like)
        +_flatten_tensor(tensor)
        +_as_float_tensor(array_like)
        +_prepare_tensor_inputs(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment)
        +_cache_tensor_inputs(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment, tensor_inputs)
        +_resolve_tensor_inputs(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment)
        +_frame_from_uniform_group_dict(group_values) DataFrame
        +_optimizer(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment)
        +fit(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment)
        +extract_optimized_params(coef_spline_bases, moderators_by_group)
        +standard_error_estimation(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment)
        +summary() maps tables
        +fisher_info_multiple_group_spatial(involved_groups, coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment) numpy.ndarray
        +fisher_info_multiple_group_moderator(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment) numpy.ndarray
        +firth_penalty(foci_per_voxel, foci_per_experiment, moderators, coef_spline_bases, overdispersion, overdispersion_coef, square_root)
        <<abstract>> _log_likelihood_single_group(...)
        <<abstract>> _log_likelihood_mult_group(...)
        <<abstract>> forward(coef_spline_bases, moderators, foci_per_voxel, foci_per_experiment)
    }

    class OverdispersionModelEstimator {
        +dict overdispersion
        +bool square_root
        +init_overdispersion_weights()
        +init_weights(groups, moderators, spatial_coef_dim, moderators_coef_dim)
        +inference_outcome(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment) maps tables
    }

    class PoissonEstimator {
        +fisher_info_multiple_group_spatial(involved_groups, coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment) numpy.ndarray
        +fisher_info_multiple_group_moderator(coef_spline_bases, moderators_by_group, foci_per_voxel, foci_per_experiment) numpy.ndarray
        +_log_likelihood_single_group(group_spatial_coef, moderators_coef, coef_spline_bases, group_moderators, group_foci_per_voxel, group_foci_per_experiment, device)
        +_log_likelihood_mult_group(spatial_coef, moderator_coef, coef_spline_bases, foci_per_voxel, foci_per_experiment, moderators, device)
        +forward(coef_spline_bases, moderators, foci_per_voxel, foci_per_experiment)
    }

    class NegativeBinomialEstimator {
        +_log_likelihood_single_group(group_spatial_coef, moderators_coef, group_overdispersion, coef_spline_bases, group_moderators, group_foci_per_voxel, group_foci_per_experiment, device)
        +_log_likelihood_mult_group(overdispersion_coef, spatial_coef, coef_spline_bases, foci_per_voxel, foci_per_experiment, moderator_coef, moderators, device)
        +forward(coef_spline_bases, moderators, foci_per_voxel, foci_per_experiment)
    }

    class ClusteredNegativeBinomialEstimator {
        +_log_likelihood_single_group(group_spatial_coef, moderators_coef, group_overdispersion, coef_spline_bases, group_moderators, group_foci_per_voxel, group_foci_per_experiment, device)
        +_log_likelihood_mult_group(overdispersion_coef, spatial_coef, coef_spline_bases, foci_per_voxel, foci_per_experiment, moderator_coef, moderators, device)
        +forward(coef_spline_bases, moderators, foci_per_voxel, foci_per_experiment)
    }

    CBMREstimator ..> GeneralLinearModelEstimator : uses model
    CBMRInference ..> CBMREstimator : uses estimator
    CBMRInference ..> GeneralLinearModelEstimator : calls fisher_info_*
    GeneralLinearModelEstimator ..> _CBMRTensorInputs : creates
    OverdispersionModelEstimator --|> GeneralLinearModelEstimator
    PoissonEstimator --|> GeneralLinearModelEstimator
    NegativeBinomialEstimator --|> OverdispersionModelEstimator
    ClusteredNegativeBinomialEstimator --|> OverdispersionModelEstimator
Loading

Flow diagram for refactored CBMR preprocessing and input construction

flowchart TD
    A[Start _preprocess_input] --> B[Get masker and mask image using _get_mask_img]
    B --> C[Validate coordinate spaces with _validate_coordinates]
    C --> D[Compute boolean mask_data from mask_img]
    D --> E[Build mask_lookup and count n_mask_voxels]
    E --> F[Compute B spline bases X with b_spline_bases using mask_data and spacing]
    F --> G[Store coef_spline_bases in inputs_]
    G --> H[Filter coordinates to mask with _filter_coordinates_to_mask]
    H --> I[Drop helper index from stored coordinates]
    I --> J[Build per experiment annotations and ids_by_group with _build_experiment_annotations]
    J --> K[Store ids_by_group and groups in estimator]
    K --> L[Build moderators_by_group with _build_group_moderators]
    L --> M[Build foci_per_voxel and foci_per_experiment with _build_group_foci]
    M --> N[Store foci_per_voxel and foci_per_experiment in inputs_]
    N --> O[End _preprocess_input]
Loading

File-Level Changes

Change Details Files
Refactor CBMREstimator preprocessing and terminology to operate on experiment-level data with robust masking and grouping.
  • Introduce helper methods for masker selection, coordinate-space validation, coordinate-to-mask filtering, and group/moderator/foci aggregation.
  • Switch from study-level to experiment-level terminology and inputs, adding a dedicated group column and ids_by_group mapping.
  • Compute foci_per_voxel and foci_per_experiment directly from masked voxel indices, ensuring alignment even when experiments have no in-mask foci.
  • Seed torch (and CUDA when used) before model initialization to make fits deterministic and pass experiment-wise foci arrays into the model fit path.
nimare/meta/cbmr.py
Optimize CBMRInference to be device-aware, non-mutating, and reuse cached covariance/log-intensity structures for multi-contrast inference.
  • Add a shallow-copy helper for MetaResult that retypes maps, plus cache-reset helpers for group and moderator covariances.
  • Cache and reuse group log-intensity, null log-intensity, and Fisher-based spatial covariances across contrasts; refactor chi-square computation into fully vectorized einsum-based implementation.
  • Cache moderator Fisher information and coefficient tables, and reuse them across moderator contrasts, avoiding repeated expensive Fisher computations.
  • Ensure inference moves the model to the requested device, invalidates tensor-input caches appropriately, and does not mutate the input MetaResult’s maps/tables.
nimare/meta/cbmr.py
GeneralLinearModelEstimator and CBMR model classes gain tensor-input caching, device handling, and more efficient Fisher and likelihood implementations, plus Poisson-specific analytic Fisher methods.
  • Introduce a _CBMRTensorInputs dataclass and caching helpers to normalize, cache, subset, and reuse tensorized inputs for repeated fit/inference calls.
  • Centralize tensor/numpy conversion utilities, enforce float64 on the correct device, and ensure modules move to the estimator’s device while invalidating caches on reinit.
  • Refactor fit, standard_error_estimation, Fisher-information, Firth penalty, and forward/log-likelihood functions to operate on experiment-level foci arrays and cached tensor inputs instead of rebuilding tensors each call.
  • Add PoissonEstimator-specific analytic Fisher calculations for spatial and moderator parameters that match the generic Hessian-based route, and use vectorized multigroup likelihood (spatial and moderator terms) via batched operations.
nimare/meta/models.py
Simplify and speed up B-spline basis construction by removing sparse.kron usage and computing only brain-voxel rows directly.
  • Drop the sparse dependency and compute mask margins using fast summed axes instead of apply_over_axes.
  • Construct the design matrix by indexing spline rows at in-brain coordinates and forming tensor products only for those voxels.
  • Apply a simple max-based threshold to drop weakly supported spline bases instead of iterating over basis indices in three nested loops.
nimare/utils.py
Substantially expand and adjust tests to validate determinism, CUDA paths, numerical consistency, and new experiment-level behavior.
  • Adjust the CBMR simulation fixture to use fewer studies and deterministic shuffles, and lower n_iter while exposing generate_description control.
  • Add tests for deterministic repeated fits, CUDA end-to-end fit+inference, optional description generation, and that optimized summary tables match legacy DataFrame constructors.
  • Introduce unit tests validating vectorized chi-square vs legacy loops, refactored multigroup Poisson likelihood vs legacy, Poisson analytic Fisher vs generic Hessian, and multi-contrast inference consistency with per-contrast runs.
  • Add regression tests ensuring inference does not mutate input MetaResult, experiment arrays stay aligned when experiments have no in-mask foci, experiments are grouped by id rather than collapsed per-study, and that StandardizeField uses population standard deviation.
nimare/tests/test_meta_cbmr.py
nimare/tests/conftest.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue, and left some high level feedback:

  • CBMREstimator._fit and GeneralLinearModelEstimator._optimizer both call torch.manual_seed(100), which globally resets the RNG on every fit; consider seeding only once (e.g., in tests or via an explicit parameter) to avoid surprising users who rely on PyTorch randomness elsewhere.
  • In CBMRInference._copy_result_for_inference you downcast maps to DEFAULT_FLOAT_DTYPE when they were previously double tensors; if DEFAULT_FLOAT_DTYPE is float32 this silently reduces precision—consider preserving the original dtype or making the cast opt-in.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- CBMREstimator._fit and GeneralLinearModelEstimator._optimizer both call torch.manual_seed(100), which globally resets the RNG on every fit; consider seeding only once (e.g., in tests or via an explicit parameter) to avoid surprising users who rely on PyTorch randomness elsewhere.
- In CBMRInference._copy_result_for_inference you downcast maps to DEFAULT_FLOAT_DTYPE when they were previously double tensors; if DEFAULT_FLOAT_DTYPE is float32 this silently reduces precision—consider preserving the original dtype or making the cast opt-in.

## Individual Comments

### Comment 1
<location path="nimare/tests/test_meta_cbmr.py" line_range="386-395" />
<code_context>
+def test_poisson_analytic_fisher_matches_generic_hessian():
</code_context>
<issue_to_address>
**suggestion (testing):** Use explicit tolerances when comparing analytic and Hessian-based Fisher information matrices.

This test compares analytic and Hessian-based Fisher matrices using the default `assert_allclose` tolerances. Because both paths use numerics/AD, small floating-point differences are expected even when correct. Please set explicit `rtol`/`atol` (e.g., `rtol=1e-6, atol=1e-8`) for both spatial and moderator Fisher checks to reduce flakiness.

Suggested implementation:

```python
    np.testing.assert_allclose(
        spatial_fisher_analytic,
        spatial_fisher_hessian,
        rtol=1e-6,
        atol=1e-8,
    )

```

```python
    np.testing.assert_allclose(
        moderator_fisher_analytic,
        moderator_fisher_hessian,
        rtol=1e-6,
        atol=1e-8,
    )

```

If the test currently uses a different assertion helper (e.g., `numpy.testing.assert_allclose` imported as `assert_allclose`, or `torch.testing.assert_close`), adjust the two SEARCH patterns to match the existing calls and add `rtol=1e-6, atol=1e-8` (or the equivalent keyword argument names for that helper). Make sure you apply these explicit tolerances to *both* the spatial and moderator Fisher comparisons within `test_poisson_analytic_fisher_matches_generic_hessian`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +386 to +395
def test_poisson_analytic_fisher_matches_generic_hessian():
"""Analytic Poisson Fisher matrices should match the generic Hessian route."""
model = models.PoissonEstimator(device="cpu")
groups = ["A", "B"]
moderators = ["m1", "m2"]
model.init_weights(
groups=groups,
moderators=moderators,
spatial_coef_dim=2,
moderators_coef_dim=2,

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.

suggestion (testing): Use explicit tolerances when comparing analytic and Hessian-based Fisher information matrices.

This test compares analytic and Hessian-based Fisher matrices using the default assert_allclose tolerances. Because both paths use numerics/AD, small floating-point differences are expected even when correct. Please set explicit rtol/atol (e.g., rtol=1e-6, atol=1e-8) for both spatial and moderator Fisher checks to reduce flakiness.

Suggested implementation:

    np.testing.assert_allclose(
        spatial_fisher_analytic,
        spatial_fisher_hessian,
        rtol=1e-6,
        atol=1e-8,
    )
    np.testing.assert_allclose(
        moderator_fisher_analytic,
        moderator_fisher_hessian,
        rtol=1e-6,
        atol=1e-8,
    )

If the test currently uses a different assertion helper (e.g., numpy.testing.assert_allclose imported as assert_allclose, or torch.testing.assert_close), adjust the two SEARCH patterns to match the existing calls and add rtol=1e-6, atol=1e-8 (or the equivalent keyword argument names for that helper). Make sure you apply these explicit tolerances to both the spatial and moderator Fisher comparisons within test_poisson_analytic_fisher_matches_generic_hessian.

@codecov

codecov Bot commented Apr 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.22222% with 93 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.90%. Comparing base (4a6b76e) to head (4f4f21d).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
nimare/meta/cbmr.py 85.20% 62 Missing ⚠️
nimare/meta/models.py 89.82% 17 Missing ⚠️
nimare/meta/__init__.py 53.33% 7 Missing ⚠️
nimare/io.py 81.81% 4 Missing ⚠️
nimare/utils.py 92.50% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1005      +/-   ##
==========================================
+ Coverage   85.38%   85.90%   +0.52%     
==========================================
  Files          52       52              
  Lines        9256     9818     +562     
==========================================
+ Hits         7903     8434     +531     
- Misses       1353     1384      +31     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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

@jdkent jdkent left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I have a couple comments/questions about the implementation.

Comment thread nimare/meta/cbmr.py Outdated
return masker, mask_img

def _validate_coordinates(self):
"""Validate coordinate space metadata consistently with other CBMA estimators."""

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This should be a universal utility function or on a base class if this is being checked for most estimators.

Comment thread nimare/meta/cbmr.py Outdated
Comment on lines +69 to +75
def available_groups(self):
"""Return the fitted groups that can be used in CBMR contrasts."""
return list(self.groups)

def available_moderators(self):
"""Return the fitted moderators that can be used in CBMR contrasts."""
return list(self.moderators)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

these helpers are not helpful, remove

Comment thread nimare/meta/cbmr.py Outdated
Comment on lines +80 to +81
"groups": self.available_groups(),
"moderators": self.available_moderators(),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

use self.groups and self.moderators

Comment thread nimare/meta/cbmr.py
Comment thread nimare/meta/cbmr.py Outdated
Comment on lines +505 to +506
if self.group_categories is None:
experiment_annotations[self._group_column] = "Default"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

"Default" should be a global variable stated at the top of the file and referenced here.

Comment thread nimare/meta/cbmr.py Outdated
Comment on lines +208 to +214
ClusteredNegativeBinomial This method is also an efficient but less
accurate approach. Clustered NB model is
"random effect" Poisson model, which asserts
that the random effects are latent
characteristics of each experiment, and
represent a shared effect over the entire brain
for a given experiment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

does this still render appropriately?

Comment thread nimare/meta/cbmr.py Outdated
self.model.device = self.device

# Initialize optimisation parameters
self.iter = 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

when is self.iter used? should it be a self attribute?

Comment thread nimare/meta/cbmr.py Outdated
dataset = normalize_collection(dataset)
self._collect_inputs(dataset, drop_invalid=drop_invalid)
self._preprocess_input(dataset)
maps, tables, description = self._cache(self._fit, func_memory_level=1)(dataset)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

where is the definition of the _cache function

Comment thread nimare/meta/cbmr.py Outdated
Comment on lines +710 to +712
torch.manual_seed(100)
if _uses_cuda(self.device):
torch.cuda.manual_seed_all(100)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

is there a general purpose way to set seeds for either cpu/gpu depending on request?

Comment thread nimare/utils.py Outdated
Comment on lines +1414 to +1415
# Build the masked design matrix directly instead of materializing the
# full sparse Kronecker product and then indexing into it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this comment should just explain the current implementation without referencing the previous implementation.

@jdkent

jdkent commented Apr 7, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai

sourcery-ai Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Sorry @jdkent, your pull request is larger than the review limit of 150000 diff characters

@jdkent jdkent changed the title [REF] CBMR [REF] CBMR internal and public API refactor Apr 7, 2026
@jdkent
jdkent merged commit 4264eb9 into neurostuff:main Apr 7, 2026
25 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.

1 participant