Skip to content

Add spatial CBMR estimator and tutorial#1080

Open
yifan0330 wants to merge 11 commits into
neurostuff:mainfrom
yifan0330:spatial-cbmr
Open

Add spatial CBMR estimator and tutorial#1080
yifan0330 wants to merge 11 commits into
neurostuff:mainfrom
yifan0330:spatial-cbmr

Conversation

@yifan0330
Copy link
Copy Markdown
Contributor

@yifan0330 yifan0330 commented May 18, 2026

Closes # .

Changes proposed in this pull request:

Summary by Sourcery

Introduce spatially varying coordinate-based meta-regression (Spatial CBMR) to NiMARE, including core estimator, inference utilities, and example usage.

New Features:

  • Add a SpatialCBMRModel torch module to model spatially varying Poisson CBMR with group-wise spatial and moderator coefficients.
  • Add SpatialCBMREstimator, SpatialCBMRResult, and SpatialCBMRInference classes to run, summarize, and perform inference on spatially varying CBMR analyses, with both full and approximate backends.
  • Expose spatially varying CBMR estimators and results through the nimare.meta namespace and a new spatial_cbmr submodule.
  • Add a public approximate solver for spatially varying log-Poisson CBMR in meta.utils, supporting preconditioned gradient descent with Kronecker structure.
  • Include a new tutorial example demonstrating spatial CBMR workflows, including fitting, group and moderator inference, and multiple comparison correction.

Enhancements:

  • Extend meta utilities with numerically stable helpers (e.g., safe exponentiation) and Kronecker-structured operations to support spatial CBMR.
  • Align spatial CBMR result and inference APIs with existing CBMR patterns, including contrast specification, group/moderator helpers, and metadata recording.

Tests:

  • Add a comprehensive spatial CBMR test suite covering preprocessing, approximate solver behavior, model result extraction, inference contrast handling, covariance computations, and agreement with explicit Kronecker formulations.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 18, 2026

Reviewer's Guide

Introduce a new spatially varying CBMR (SpatialCBMR) framework, including approximate and full torch backends, inference utilities, tests, and an example tutorial, and wire it into NiMARE’s public meta-analysis API.

File-Level Changes

Change Details Files
Add approximate numerical utilities to fit a spatially varying log-Poisson GLM used by SpatialCBMR.
  • Introduce numerically stable helpers for exponentiation and Kronecker-structured products for spatially varying coefficients.
  • Implement a Poisson negative log-likelihood and gradient that operate on moderator, basis, and foci inputs without materializing the full Kronecker design matrix.
  • Add an additive log-Poisson approximation and Kronecker preconditioner to initialize and accelerate gradient-based optimization.
  • Expose a high-level fit_spatial_cbmr_approximate function with convergence logging, step-size control, and damping to return flattened coefficients.
nimare/meta/utils.py
Add a torch-based SpatialCBMRModel representing spatially varying CBMR in the modeling layer.
  • Define SpatialCBMRModel as a torch.nn.Module with separate per-group linear layers for spatial baseline coefficients and spatially varying moderator coefficients.
  • Implement a group-wise linear predictor that combines spatial baseline intensity with moderator-dependent spatial effects.
  • Provide a Poisson negative log-likelihood and a forward pass that aggregates loss across groups given shared bases and group-specific moderators and foci.
nimare/meta/models.py
Expose SpatialCBMR estimator, inference, and result types through the public meta-analysis API.
  • Register spatial_cbmr as an optional meta submodule and add it to the exported namespace list.
  • Export SpatialCBMREstimator, SpatialCBMRInference, and SpatialCBMRResult from nimare.meta.init so users can import them like existing CBMR classes.
nimare/meta/__init__.py
Implement the SpatialCBMR estimator, result type, and inference engine, including both full (torch) and approximate backends.
  • Create SpatialCBMRResult as a CBMRResult subclass that preserves spatial CBMR semantics, adds helpers for spatially varying moderator maps, and provides inference helpers (test/compare groups and moderators).
  • Extend CBMR preprocessing in _SpatialCBMRBase to build experiment-by-voxel foci matrices per group and to prepare dense torch tensors for bases, moderators, and responses.
  • Implement SpatialCBMREstimator with backend selection between a full torch L-BFGS model (via SpatialCBMRModel) and the approximate NumPy backend (via fit_spatial_cbmr_approximate), and add utilities to extract spatial intensity and spatially varying moderator maps plus CBMR-style coefficient tables from either backend.
  • Add SpatialCBMRInference, mirroring CBMRInference’s interface, to compute voxel-wise Wald and GLH tests for group baselines and spatially varying moderators using either robust sandwich covariance (cluster/iid with HC corrections) or inverse Fisher information, exploiting Kronecker structure for efficiency.
  • Provide internal utilities for contrast parsing, contrast preprocessing/standardization, Kronecker-structured Fisher information and sandwich covariance computation, projection of coefficient covariance into voxel space, and writing inference maps into result objects.
nimare/meta/spatial_cbmr.py
Add an extensive test suite for SpatialCBMR utilities, estimator behavior, result helpers, and inference computations.
  • Add unit tests for group foci-matrix construction, torch input preparation, and approximate solver output finiteness.
  • Test SpatialCBMRResult helper methods, result copying behavior, and adherence to CBMR-style table conventions.
  • Verify estimator backend routing, torch and approximate result extraction, and integration of fit_spatial_cbmr_approximate via monkeypatching.
  • Add inference tests covering contrast creation and preprocessing, transform behavior and metadata, Kronecker Fisher information and sandwich covariance correctness, and spatial Wald/GLH statistic implementations against explicit or legacy computations.
nimare/tests/test_meta_spatial_cbmr.py
Add a user-facing example demonstrating SpatialCBMR usage, inference, and visualization.
  • Create a tutorial that simulates a coordinate Studyset with group labels and continuous moderators, then fits SpatialCBMREstimator with the approximate backend and coarse spline spacing for speed.
  • Demonstrate how to inspect spatially varying moderator maps, plot baseline group intensity and moderator effects, run group and moderator tests (including pairwise contrasts) with both sandwich and FI standard errors, and apply FDR correction to inference maps.
  • Show how SpatialCBMR integrates with existing NiMARE workflows (StandardizeField, FDRCorrector, plotting helpers) while highlighting the differences from standard CBMR (spatially varying covariate effects).
examples/02_meta-analyses/12_plot_spatial_cbmr.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

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The approximate backend builds a full Kronecker preconditioner with np.kron(moderator_info_inv, basis_info_inv), which will become extremely large for realistic numbers of moderators/bases; consider using a linear-operator style preconditioner (e.g., solving with the Kronecker factors via reshape/MatMul) instead of materializing the full matrix.
  • Both _fit_full and _fit_approximate accept a dataset argument that is never used; consider dropping the parameter or renaming it to _dataset to make it clear it is intentionally unused and avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The approximate backend builds a full Kronecker preconditioner with `np.kron(moderator_info_inv, basis_info_inv)`, which will become extremely large for realistic numbers of moderators/bases; consider using a linear-operator style preconditioner (e.g., solving with the Kronecker factors via reshape/MatMul) instead of materializing the full matrix.
- Both `_fit_full` and `_fit_approximate` accept a `dataset` argument that is never used; consider dropping the parameter or renaming it to `_dataset` to make it clear it is intentionally unused and avoid confusion.

## Individual Comments

### Comment 1
<location path="nimare/meta/utils.py" line_range="129-135" />
<code_context>
+    return result.x[:n_bases], result.x[n_bases:]
+
+
+def _compute_spatial_cbmr_preconditioner(moderators, bases, mean_moderator, mean_basis, damping):
+    """Build an approximate Kronecker preconditioner for the gradient step."""
+    moderator_info = moderators.T @ (moderators * mean_moderator)
+    basis_info = bases.T @ (bases * mean_basis)
+    moderator_info_inv = np.linalg.pinv(moderator_info + damping * np.eye(moderators.shape[1]))
+    basis_info_inv = np.linalg.pinv(basis_info + damping * np.eye(bases.shape[1]))
+    return np.kron(moderator_info_inv, basis_info_inv)
+
+
</code_context>
<issue_to_address>
**issue (performance):** Preconditioner forms a potentially huge Kronecker matrix, which may not scale for typical moderator/basis sizes.

Because the preconditioner is a dense (n_moderators * n_bases)² matrix, even moderate settings (e.g., 10–20 moderators, 50–100 bases) lead to 10^8–10^10 entries, which is likely infeasible in memory and slow to apply. Since it’s only used as `preconditioner @ gradient`, consider keeping `moderator_info_inv` and `basis_info_inv` separate and implementing an implicit Kronecker–vector product instead of materializing `np.kron(...)`.
</issue_to_address>

### Comment 2
<location path="nimare/meta/models.py" line_range="43-52" />
<code_context>
+class SpatialCBMRModel(torch.nn.Module):
</code_context>
<issue_to_address>
**suggestion (performance):** Forward path densifies group foci, which can be very memory-intensive for realistic voxel grids.

`_prepare_torch_inputs` converts `foci_by_experiment_voxel[group]` to dense tensors before passing them here. For whole-brain masks (tens of thousands of voxels) and many experiments, this structure is extremely sparse, so densifying per group can cause large memory overhead and slower kernels. If you plan to support larger datasets, consider keeping foci sparse with a sparse-aware loss (e.g., summing over nonzero entries plus a background term), or at least adding a warning/config flag to control this densification.

Suggested implementation:

```python
class SpatialCBMRModel(torch.nn.Module):
    """Torch log-Poisson model for spatially varying CBMR.

    This model is used by :class:`~nimare.meta.spatial_cbmr.SpatialCBMREstimator`.
    For experiment ``m`` and voxel ``v`` in group ``g``, the linear predictor is
    ``B(v) @ alpha_g + Z_m @ beta_g @ B(v).T``.

    Parameters
    ----------
    groups : :obj:`list` of :obj:`str`
        Ordered group names.
    densify_foci : :obj:`bool`, optional
        If ``True``, group-wise foci are converted to dense voxel × experiment
        tensors before being passed through the model, which is simpler but can be
        very memory-intensive for realistic whole-brain voxel grids.
        If ``False``, inputs are expected to remain sparse and the estimator/model
        should use a sparse-aware loss that operates on nonzero entries plus a
        background term, avoiding per-group densification.

```

To fully implement this configuration instead of only documenting it, you should:

1. Update the `SpatialCBMRModel` constructor to accept the new flag, store it, and optionally warn when densifying large inputs:
   - Change the `__init__` signature to include a `densify_foci: bool = True` (or similarly named) keyword argument.
   - Store it on the instance (e.g., `self.densify_foci = densify_foci`).
   - Optionally, when `densify_foci` is ``True`` and the voxel × experiment tensor size exceeds a heuristic threshold, issue a `warnings.warn` about potential memory usage.

2. Wire the flag into the data-preparation pipeline:
   - In the `_prepare_torch_inputs` logic that currently converts `foci_by_experiment_voxel[group]` to dense tensors, branch on this flag:
     - If `densify_foci` is ``True``, keep the current behavior.
     - If `densify_foci` is ``False``, keep the inputs sparse (e.g., in a sparse `torch` layout or as index/value pairs) and adjust what is passed to `SpatialCBMRModel.forward`.

3. Make `forward` sparse-aware when `densify_foci` is ``False``:
   - Add a code path in `SpatialCBMRModel.forward` that operates directly on sparse inputs (e.g., summing over nonzero entries plus a background term) without calling `.to_dense()` anywhere.
   - Ensure the loss computation is compatible with sparse tensors and does not densify internally.

4. Propagate the configuration from the estimator:
   - In `SpatialCBMREstimator` (or whatever constructs `SpatialCBMRModel`), add a matching `densify_foci` argument and pass it into the model constructor so users can control the behavior from the public API.
</issue_to_address>

### Comment 3
<location path="nimare/meta/spatial_cbmr.py" line_range="918-927" />
<code_context>
+    @classmethod
</code_context>
<issue_to_address>
**issue:** Chi-square / Wald computations rely on `np.linalg.solve` without guarding against singular or ill-conditioned covariance blocks.

In `_chi_square_log_intensity` and `_compute_spatial_coefficient_statistics`, `np.linalg.solve` is applied voxel-wise to contrast covariance matrices that can be singular or ill-conditioned due to B-spline multicollinearity and weakly informed moderators/groups. This risks `LinAlgError` or numerically unstable statistics. Consider adding a small voxel-wise ridge term to `contrast_cov` or falling back to `np.linalg.pinv` (optionally with a warning or by masking voxels with excessive condition numbers).
</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 thread nimare/meta/utils.py
Comment on lines +129 to +135
def _compute_spatial_cbmr_preconditioner(moderators, bases, mean_moderator, mean_basis, damping):
"""Build an approximate Kronecker preconditioner for the gradient step."""
moderator_info = moderators.T @ (moderators * mean_moderator)
basis_info = bases.T @ (bases * mean_basis)
moderator_info_inv = np.linalg.pinv(moderator_info + damping * np.eye(moderators.shape[1]))
basis_info_inv = np.linalg.pinv(basis_info + damping * np.eye(bases.shape[1]))
return np.kron(moderator_info_inv, basis_info_inv)
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.

issue (performance): Preconditioner forms a potentially huge Kronecker matrix, which may not scale for typical moderator/basis sizes.

Because the preconditioner is a dense (n_moderators * n_bases)² matrix, even moderate settings (e.g., 10–20 moderators, 50–100 bases) lead to 10^8–10^10 entries, which is likely infeasible in memory and slow to apply. Since it’s only used as preconditioner @ gradient, consider keeping moderator_info_inv and basis_info_inv separate and implementing an implicit Kronecker–vector product instead of materializing np.kron(...).

Comment thread nimare/meta/models.py
Comment on lines +43 to +52
class SpatialCBMRModel(torch.nn.Module):
"""Torch log-Poisson model for spatially varying CBMR.

This model is used by :class:`~nimare.meta.spatial_cbmr.SpatialCBMREstimator`.
For experiment ``m`` and voxel ``v`` in group ``g``, the linear predictor is
``B(v) @ alpha_g + Z_m @ beta_g @ B(v).T``.

Parameters
----------
groups : :obj:`list` of :obj:`str`
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 (performance): Forward path densifies group foci, which can be very memory-intensive for realistic voxel grids.

_prepare_torch_inputs converts foci_by_experiment_voxel[group] to dense tensors before passing them here. For whole-brain masks (tens of thousands of voxels) and many experiments, this structure is extremely sparse, so densifying per group can cause large memory overhead and slower kernels. If you plan to support larger datasets, consider keeping foci sparse with a sparse-aware loss (e.g., summing over nonzero entries plus a background term), or at least adding a warning/config flag to control this densification.

Suggested implementation:

class SpatialCBMRModel(torch.nn.Module):
    """Torch log-Poisson model for spatially varying CBMR.

    This model is used by :class:`~nimare.meta.spatial_cbmr.SpatialCBMREstimator`.
    For experiment ``m`` and voxel ``v`` in group ``g``, the linear predictor is
    ``B(v) @ alpha_g + Z_m @ beta_g @ B(v).T``.

    Parameters
    ----------
    groups : :obj:`list` of :obj:`str`
        Ordered group names.
    densify_foci : :obj:`bool`, optional
        If ``True``, group-wise foci are converted to dense voxel × experiment
        tensors before being passed through the model, which is simpler but can be
        very memory-intensive for realistic whole-brain voxel grids.
        If ``False``, inputs are expected to remain sparse and the estimator/model
        should use a sparse-aware loss that operates on nonzero entries plus a
        background term, avoiding per-group densification.

To fully implement this configuration instead of only documenting it, you should:

  1. Update the SpatialCBMRModel constructor to accept the new flag, store it, and optionally warn when densifying large inputs:

    • Change the __init__ signature to include a densify_foci: bool = True (or similarly named) keyword argument.
    • Store it on the instance (e.g., self.densify_foci = densify_foci).
    • Optionally, when densify_foci is True and the voxel × experiment tensor size exceeds a heuristic threshold, issue a warnings.warn about potential memory usage.
  2. Wire the flag into the data-preparation pipeline:

    • In the _prepare_torch_inputs logic that currently converts foci_by_experiment_voxel[group] to dense tensors, branch on this flag:
      • If densify_foci is True, keep the current behavior.
      • If densify_foci is False, keep the inputs sparse (e.g., in a sparse torch layout or as index/value pairs) and adjust what is passed to SpatialCBMRModel.forward.
  3. Make forward sparse-aware when densify_foci is False:

    • Add a code path in SpatialCBMRModel.forward that operates directly on sparse inputs (e.g., summing over nonzero entries plus a background term) without calling .to_dense() anywhere.
    • Ensure the loss computation is compatible with sparse tensors and does not densify internally.
  4. Propagate the configuration from the estimator:

    • In SpatialCBMREstimator (or whatever constructs SpatialCBMRModel), add a matching densify_foci argument and pass it into the model constructor so users can control the behavior from the public API.

Comment thread nimare/meta/spatial_cbmr.py Outdated
Comment on lines +918 to +927
@classmethod
def _validate_method(cls, method):
"""Validate and normalize an inference standard-error method."""
if isinstance(method, str):
if method.lower() == "fi":
return "FI"
if method.lower() == "sandwich":
return "sandwich"
raise ValueError("method must be one of {'sandwich', 'FI'}.")

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.

issue: Chi-square / Wald computations rely on np.linalg.solve without guarding against singular or ill-conditioned covariance blocks.

In _chi_square_log_intensity and _compute_spatial_coefficient_statistics, np.linalg.solve is applied voxel-wise to contrast covariance matrices that can be singular or ill-conditioned due to B-spline multicollinearity and weakly informed moderators/groups. This risks LinAlgError or numerically unstable statistics. Consider adding a small voxel-wise ridge term to contrast_cov or falling back to np.linalg.pinv (optionally with a warning or by masking voxels with excessive condition numbers).

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

❌ Patch coverage is 91.90840% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.96%. Comparing base (a3f4ae6) to head (6a0fd9b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
nimare/meta/cbmr.py 93.27% 36 Missing ⚠️
nimare/meta/utils.py 83.51% 15 Missing ⚠️
nimare/meta/models.py 93.10% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1080      +/-   ##
==========================================
+ Coverage   85.50%   85.96%   +0.45%     
==========================================
  Files          56       56              
  Lines       11248    11849     +601     
==========================================
+ Hits         9618    10186     +568     
- Misses       1630     1663      +33     

☔ 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.

Copy link
Copy Markdown
Member

@jdkent jdkent left a comment

Choose a reason for hiding this comment

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

Should SpatialCBMR be its own class? I think this should be a variant/flag of the original CBMR class, but I'm open to discussion if there is some technical barrier making it difficult to incorporate into the original CBMR class.

(How much code is reimplemented/borrowed from the original class/how much new code is being introduced?)

Copy link
Copy Markdown
Member

@jdkent jdkent left a comment

Choose a reason for hiding this comment

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

Okay, taking a deeper look, my opinion on the user consuming API hasn't changed, I still want a single CBMREstimator class. with the effect being specified as follows:

CBMREstimator(moderator_effect="voxelwise|global")

the moderator effect can be voxelwise or global.

However, my opinion on the underlying code has changed. There is a significant difference in the calculation of likelihood. There can be more separation between CBMR and SpatialCBMR in the code. I'm still unsure on how different Result and Inference classes should be, and if you're adding a whole new class with duplicate methods (but different implementations of those methods), then you should define a base class with those methods stated, and then inherit that base class for CBMR and SpatialCBMR classes.

# We simulate a coordinate-based Studyset with the same structure used in the
# CBMR tutorial: studies have reported foci, sample sizes, diagnosis labels,
# drug-status labels, and continuous study-level moderators. Spatial CBMR can be
# computationally heavier than standard CBMR because it keeps experiment-by-voxel
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are the experiment-by-voxel matrices kept in sparse/dense format? I've found some big speedups/memory reduction when using Compressed Sparsed Row (CSR) matrices.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The other CBMR example should just be added to, a whole new example is not needed.

@yifan0330
Copy link
Copy Markdown
Contributor Author

Okay, taking a deeper look, my opinion on the user consuming API hasn't changed, I still want a single CBMREstimator class. with the effect being specified as follows:

CBMREstimator(moderator_effect="voxelwise|global")

the moderator effect can be voxelwise or global.

However, my opinion on the underlying code has changed. There is a significant difference in the calculation of likelihood. There can be more separation between CBMR and SpatialCBMR in the code. I'm still unsure on how different Result and Inference classes should be, and if you're adding a whole new class with duplicate methods (but different implementations of those methods), then you should define a base class with those methods stated, and then inherit that base class for CBMR and SpatialCBMR classes.

Thanks for the detailed feedback! I agree and have updated the implementation accordingly. Now, the user-facing API still uses a single CBMREstimator interface with:

CBMREstimator(moderator_effect="voxelwise|global")

Copy link
Copy Markdown
Member

@jdkent jdkent left a comment

Choose a reason for hiding this comment

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

Still working through the code, nice work!

I have some comments on structure to keep it in line with the philosophy of NiMARE.

Comment thread nimare/meta/cbmr.py
Comment on lines +2316 to +2343
if scipy.sparse.issparse(foci):
residual_scale, correction_factor = cls._sandwich_correction_scale(
correction,
bread_inverse,
moderators,
bases,
mean,
)
meat_matrix = cls._sandwich_meat_matrix_sparse_response(
moderators,
bases,
foci,
mean,
meat,
residual_scale=residual_scale,
)
else:
y = cls._as_dense_response(foci)
residuals = np.nan_to_num(y - mean, nan=0.0, posinf=0.0, neginf=0.0)
residuals, correction_factor = cls._apply_sandwich_correction(
correction,
bread_inverse,
moderators,
bases,
mean,
residuals,
)
meat_matrix = cls._sandwich_meat_matrix(moderators, bases, residuals, meat)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

when will the response be dense?

Comment on lines +16 to +18
* ``moderator_effect="global"`` estimates one scalar coefficient per moderator.
This is the classic CBMR model and answers whether a study-level covariate has
an overall effect on the spatial intensity function.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* ``moderator_effect="global"`` estimates one scalar coefficient per moderator.
This is the classic CBMR model and answers whether a study-level covariate has
an overall effect on the spatial intensity function.
* ``moderator_effect="global"`` estimates one scalar coefficient per moderator. This assumes the effect of the moderator has a uniform impact across the entire brain.

Is this accurate to say? I want to make the description more applicable to the users of the algorithm, which will be focused on analyzing the brain.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I also want to avoid using the word "study-level covariate", since the covariate/moderator could represent something about the study, or about the specific analysis within the study.

Comment on lines +19 to +21
* ``moderator_effect="voxelwise"`` estimates a smooth map for each moderator.
This spatially varying model asks where the study-level covariate effect is
stronger or weaker over voxels.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* ``moderator_effect="voxelwise"`` estimates a smooth map for each moderator.
This spatially varying model asks where the study-level covariate effect is
stronger or weaker over voxels.
* ``moderator_effect="voxelwise"`` estimates a scalar coefficient _per voxel_ per moderator.
This assumes the effect of the moderator differentially impacts voxels throughout the brain. This is likely a more accurate assumption, but requires a lot more data for estimation.

Comment on lines -21 to -24
For a more detailed introduction to the elements of a coordinate-based meta-regression,
see the
`online course <https://www.coursera.org/lecture/functional-mri-2/module-3-meta-analysis-Vd4zz>`_
or a `brief overview <https://libguides.princeton.edu/neuroimaging_meta>`_.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like these links were tangential, so it looks good to remove those.

@@ -1,427 +1,315 @@
"""
r"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

other documents do not use the raw text marker, unless you have a specific reason you need raw text and another solution will not work, I would remove this marker.

Comment on lines +302 to +303
# The same voxelwise inference helpers can use inverse Fisher information rather
# than the sandwich estimator.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why would a user want to do this? Explain what they gain, what different information do they get if they use inverse fisher information versus the sandwich estimator. Does the inverse Fisher information have more assumptions about the model? Is it more computationally efficient than the sandwich estimator.

Comment thread nimare/meta/cbmr.py Outdated
Comment on lines +98 to +101
def sv_moderator_names(self):
"""Return spatially varying moderator map names."""
return tuple(name for name in self.maps if name.startswith("svModerator_"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

instead of spatially varying, the consistent language used more throughout NiMARE is "voxelwise".

Comment thread nimare/meta/cbmr.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

for public methods (that will be consumed by people using the tool), I want to keep docstrings of the functions that desribe the parameters:

      Parameters
        ----------
        group_contrasts : bool, dict, list, tuple, str, or None, optional
            Group homogeneity or comparison specification. Use ``False`` to skip group inference.
        moderator_contrasts : bool, dict, list, tuple, str, or None, optional
            Moderator effect or comparison specification. Use ``False`` to skip moderator
            inference.
        device : str, optional
            Compute device to use for inference. Defaults to the device recorded on the fitted
            estimator.
        """

for internal methods that are only called by other methods within the class (and are not meant to be seen by users, add a leading underscore.

Comment thread nimare/meta/cbmr.py
_required_inputs = {"coordinates": ("coordinates", None)}
_group_column = "_cbmr_group"
_valid_moderator_effects = ("global", "voxelwise")
_valid_backends = ("full", "approximate")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is the difference between full and approximate in the documentation/in the docstring?

Comment thread nimare/meta/cbmr.py Outdated
Comment on lines +941 to +944
@staticmethod
def _get_spatial_cbmr_approximate_solver():
"""Return the approximate solver used by the voxelwise backend."""
return fit_spatial_cbmr_approximate
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be a propery/attribute, not a method.

Yifan Yu and others added 6 commits May 28, 2026 21:43
Store sparse experiment-by-voxel focus matrices during CBMR preprocessing so experiment-level focus counts remain aligned with grouped experiment metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

3 participants