Skip to content

Feature/fast clustering - #31

Open
Janne98 wants to merge 72 commits into
mainfrom
feature/fast-clustering
Open

Feature/fast clustering#31
Janne98 wants to merge 72 commits into
mainfrom
feature/fast-clustering

Conversation

@Janne98

@Janne98 Janne98 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces several significant updates to the Falcon spectrum clustering tool.

Algorithm and Performance Enhancements:

  • Replaced the previous nearest neighbor indexing and DBSCAN clustering approach with hierarchical clustering using a full pairwise cosine distance matrix, computed in parallel with Numba for improved speed and efficiency (falcon/cluster/distance_matrix.py, falcon/cluster/similarity.py).
  • Updated the spectrum similarity calculation to use a greedy matching algorithm instead of the slower Hungarian method, further accelerating distance computations (falcon/cluster/similarity.py).
  • Implemented spectral averaging with outlier rejection as a consensus spectrum algorithm (falcon/cluster/consensus.py).

Configuration and Usability Improvements:

  • Added new command-line options for consensus spectrum computation (--consensus_method, --outlier_cutoff_lower, --outlier_cutoff_upper) (falcon/config.py).
  • Added new command-line option for customizing precursor charge buckets (--precursor_charge_buckets), providing users with greater flexibility (falcon/config.py).
  • Removed legacy code and unused vectorization routines related to the old clustering approach, simplifying the codebase (falcon/cluster/spectrum.py).

Documentation Updates:

  • Revised the README.md to describe the new clustering pipeline, updated parameters and defaults, and improved installation and usage instructions. The documentation now accurately reflects the hierarchical clustering workflow and new configuration options.

Summary by CodeRabbit

  • New Features

    • Charge-bucket clustering with configurable consensus methods (medoid/average), RT-aware refinement, and chunked parallel processing
    • On-disk condensed distance matrix with memmap-backed computation and Numba-accelerated consensus averaging
  • Improvements

    • Faster greedy peak-matching cosine similarity; USI-like identifiers; RT represented as NaN; MGF export scaling and stable cluster titles; logging and seed utilities; added .gitignore; bumped joblib minimum
  • Documentation

    • README rewritten for v2 workflow, updated CLI flags/defaults and installation instructions
  • Tests

    • Extensive new unit/integration tests covering clustering, consensus, distance matrix, similarity, IO, and config parsing

Janne98 and others added 30 commits October 23, 2024 16:19
Copilot AI review requested due to automatic review settings June 9, 2026 04:42
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors clustering to bucketed Lance datasets, computes memmapped condensed distance matrices in parallel, clusters per m/z interval with cost-based chunking and NaN-safe m/z/RT postprocessing, produces medoid/average consensus spectra (Numba-accelerated), replaces assignment similarity with greedy matching, updates I/O identifiers/RT handling, adds utils, and adds extensive tests.

Changes

Falcon v2 Clustering and Consensus Spectrum Pipeline

Layer / File(s) Summary
Documentation and Configuration Updates
README.md, .gitignore, setup.cfg, falcon/config.py
README updated for v2 flow; .gitignore added; joblib requirement bumped; CLI gains --consensus_method, --outlier_cutoff_*, and --precursor_charge_buckets with validation.
Utils and Entry Wiring
falcon/utils.py, falcon/falcon.py
Adds set_seeds and configure_logger, integrates them into main(), enforces multiprocessing "fork", and logs consensus/bucket settings.
Spectrum I/O and MGF Export
falcon/ms_io/mgf_io.py, falcon/ms_io/mzml_io.py, falcon/ms_io/mzxml_io.py, falcon/ms_io/ms_io.py
Readers produce USI-style ids and NaN retention times when missing; MGF export accepts ConsensusTuple and writes scaled intensities and Falcon-specific params.
Similarity and Distance Matrix
falcon/cluster/similarity.py, falcon/cluster/distance_matrix.py
cosine_fast now uses greedy non-overlapping peak-pair matching; adds memmap-backed, Numba-parallel compute_condensed_distance_matrix and a jitted condensed_index.
Consensus Spectrum Module
falcon/cluster/consensus.py
Adds ConsensusTuple and representative-spectrum computation (medoid and average) with Numba-accelerated binning, sigma-clipping, and typed-list conversions.
Clustering Algorithm Refactor
falcon/cluster/cluster.py
generate_clusters loads minimal metadata, deterministically sorts spectra, computes m/z splits, uses cost-based chunking and parallel interval clustering, postprocesses with m/z/RT splitting (NaN-safe mixed-radix encoding), and globally relabels cluster IDs; returns cluster labels and representative spectra.
Main Pipeline and Bucketed I/O
falcon/falcon.py
Main switches to per-bucket processing, per-bucket Lance writes with _PerChargeLockRegistry, cached bucket defs, bucket-key dataset naming, and expanded run metadata.
Spectrum Processing Cleanup
falcon/cluster/spectrum.py
Removes MsmsSpectrumNb and drops filename from process_spectrum output; preserves reader is_processed behavior.
Tests, Fixtures, and Test Data
tests/**, tests/data/*
Large set of new pytest modules and fixtures covering splits, linkage, postprocessing, consensus methods, distance matrix indexing, similarity, I/O round-trips, bucket routing, and config parsing; includes sample mzML/mzXML test files.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

"In m/z splits and buckets, we cluster with grace,
Greedy peaks find matches at a steady pace,
Medoids and averages both have their place,
Consensus spectra bloom—Falcon's new embrace! 🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/fast clustering' is directly related to the main objective of this PR, which implements a faster hierarchical clustering algorithm with performance optimizations (Numba-accelerated distance matrix, greedy peak matching). It clearly identifies the primary change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fast-clustering

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 and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR modernizes Falcon’s clustering pipeline by switching to hierarchical clustering over a full pairwise cosine distance matrix (Numba-parallel), adds consensus spectrum support (medoid/average with outlier rejection), and improves I/O + configuration around charge bucketing and representative export.

Changes:

  • Replaced sparse NN/DBSCAN pipeline with hierarchical clustering over a condensed cosine distance matrix and a faster greedy peak matching similarity.
  • Added consensus spectrum generation (medoid and average + sigma-clipping outlier rejection) and corresponding CLI/config options.
  • Reworked spectrum I/O (mzML/mzXML/MGF identifiers, NaN RT handling) and added extensive tests + updated README.

Reviewed changes

Copilot reviewed 27 out of 29 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
falcon/falcon.py Main orchestration updated for charge buckets, consensus export, and Lance dataset writing.
falcon/config.py Adds CLI/config parsing for consensus options and precursor charge buckets.
falcon/utils.py Centralizes seed + logger configuration (replaces seed.py usage).
falcon/seed.py Removes legacy seed helper (migrated to utils.py).
falcon/ms_io/ms_io.py Adjusts I/O dispatch behavior (removes legacy is_processed flag).
falcon/ms_io/mgf_io.py Adds identifier normalization, representative writing, and intensity scaling for MGF export.
falcon/ms_io/mzml_io.py Builds stable identifiers from mzML scan IDs; uses NaN RT default.
falcon/ms_io/mzxml_io.py Builds stable identifiers; uses NaN RT default; normalizes charge type.
falcon/cluster/spectrum.py Removes legacy vectorization code and unused fields (e.g., filename).
falcon/cluster/similarity.py Replaces Hungarian assignment with greedy peak matching for speed.
falcon/cluster/distance_matrix.py New parallel condensed distance matrix computation (Numba).
falcon/cluster/consensus.py New consensus spectrum algorithms (medoid + averaging with outlier rejection).
falcon/cluster/cluster.py Reworks clustering to use the new distance matrix + consensus selection.
README.md Updates docs to match the new pipeline, parameters, and defaults.
setup.cfg Bumps joblib minimum version.
.gitignore Adds Python and local dev ignores.
tests/conftest.py Adds shared fixtures for new clustering/consensus and Lance dataset tests.
tests/test_config.py Adds config parsing/validation tests for new CLI options.
tests/test_falcon.py Adds tests for charge bucketing and Lance writing/routing behavior.
tests/test_ms_io.py Adds tests for dispatch + MGF read/write roundtrip and identifier behavior.
tests/test_mzml_mzxml_io.py Adds tests for mzML/mzXML parsing and identifiers.
tests/cluster/test_cluster.py Adds extensive clustering behavior tests (splits, RT handling, relabeling, end-to-end).
tests/cluster/test_consensus.py Adds tests for consensus methods and outlier rejection utilities.
tests/cluster/test_distance_matrix.py Adds tests for condensed indexing and distance computation.
tests/cluster/test_similarity.py Adds tests for greedy cosine similarity behavior.
tests/cluster/test_spectrum.py Adds tests for spectrum preprocessing pipeline.
tests/data/sample.mzml Adds minimal mzML fixture for I/O tests.
tests/data/sample.mzxml Adds minimal mzXML fixture for I/O tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread falcon/falcon.py Outdated
Comment thread falcon/falcon.py Outdated
Comment thread falcon/falcon.py Outdated
Comment thread falcon/falcon.py Outdated
Comment thread falcon/ms_io/mgf_io.py Outdated
Comment thread falcon/cluster/cluster.py Outdated
Comment thread falcon/cluster/cluster.py Outdated
Comment thread falcon/cluster/cluster.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
falcon/falcon.py (1)

486-496: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Race condition: lock is keyed by charge but dataset is keyed by bucket_key.

Multiple charges can map to the same bucket_key (e.g., charges 1 and 2 both map to bucket (1, 2)). The code acquires locks using lance_locks.get(charge), but the dataset path is derived from bucket_key. This means:

  • Thread handling charge=1 acquires lock for key 1
  • Thread handling charge=2 acquires lock for key 2
  • Both threads write to the same spectra_charge_1_2.lance dataset concurrently

This can cause data corruption in the Lance dataset.

Proposed fix - use bucket_key for locking
             for charge in spec_to_write.keys():
                 if len(spec_to_write[charge]) == 0:
                     continue
                 _write_to_dataset(
                     spec_to_write[charge],
                     charge,
-                    lance_locks.get(charge),
+                    lance_locks.get(charge),  # Note: charge here IS the bucket_key
                     schema,
                     config.work_dir,
                 )

Wait - looking more carefully, spec_to_write is keyed by bucket_key, not charge. So the iteration variable charge in line 486 is actually a bucket_key. But then the flush path at line 512 uses the original charge variable (the spectrum's charge), not bucket_key.

The fix should be:

-            _write_to_dataset(
-                spec_to_write[bucket_key],
-                bucket_key,
-                lance_locks.get(charge),
-                schema,
-                config.work_dir,
-            )
+            _write_to_dataset(
+                spec_to_write[bucket_key],
+                bucket_key,
+                lance_locks.get(bucket_key),
+                schema,
+                config.work_dir,
+            )

Also applies to: 508-516

🤖 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 `@falcon/falcon.py` around lines 486 - 496, The loop uses the wrong lock key:
spec_to_write is keyed by bucket_key but the code calls lance_locks.get(charge)
and later uses a different `charge` when building the dataset path/flush,
causing concurrent writes to the same dataset; fix by treating the loop variable
as bucket_key (rename if helpful), acquire lance_locks.get(bucket_key) and pass
that lock to _write_to_dataset, and ensure the flush/filename logic and
spec_to_write clearing all use the same bucket_key (not the original spectrum
`charge`), applying the same change in both the earlier loop and the later flush
block (places around _write_to_dataset, spec_to_write.clear(), and any code that
constructs spectra_charge_* .lance).
🧹 Nitpick comments (16)
falcon/cluster/cluster.py (4)

815-815: 💤 Low value

Ambiguous Unicode character in comment.

The comment contains × (MULTIPLICATION SIGN) which may display differently across editors. Consider using x (lowercase letter) instead.

-    # Group reps by mz_split in one pass — O(R) instead of O(S × R).
+    # Group reps by mz_split in one pass -- O(R) instead of O(S x R).
🤖 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 `@falcon/cluster/cluster.py` at line 815, Replace the ambiguous Unicode
multiplication sign in the comment "Group reps by mz_split in one pass — O(R)
instead of O(S × R)." with a plain ASCII 'x' so it reads "O(S x R)"; update the
comment near the grouping logic that references mz_split and reps (the same line
containing "Group reps by mz_split in one pass") to ensure consistent display
across editors.

683-684: 💤 Low value

Unused loop variable.

The loop variable i is not used within the loop body.

-            for i, label in enumerate(cluster_assignments):
+            for _, label in enumerate(cluster_assignments):
                 labels[label] = labels.get(label, 0) + 1

Or simply:

-            for i, label in enumerate(cluster_assignments):
+            for label in cluster_assignments:
                 labels[label] = labels.get(label, 0) + 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 `@falcon/cluster/cluster.py` around lines 683 - 684, The for-loop over
cluster_assignments uses an unused loop variable i; update the loop in
cluster.py so it doesn’t enumerate when not needed (replace "for i, label in
enumerate(cluster_assignments)" with a simple "for label in
cluster_assignments") and keep the body that updates labels (labels[label] =
labels.get(label, 0) + 1) unchanged to avoid the unused variable warning.

264-272: ⚡ Quick win

Remove or resolve commented-out code.

This commented block with # TODO: ask wout about this should either be removed or the decision resolved before merge to avoid confusion.

🤖 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 `@falcon/cluster/cluster.py` around lines 264 - 272, Remove the commented-out
block containing the "TODO: ask wout about this" logic around block_size,
batch_size, splits, n_chunks and chunk_size: either implement the intended
splitting behavior (use the shown algorithm to compute n_chunks, chunk_size and
populate splits) or delete the entire commented section and the TODO so it
doesn't remain in the code; if you choose to keep a short note, replace the TODO
with a concise explanatory comment describing why the code was omitted and link
to an issue/PR that will implement it.

145-161: 💤 Low value

Consider consolidating TODOs and cleaning up redundant data preparation.

The row_ids and idx_interval at lines 150-151 are computed from the same slice. If they serve different purposes (Lance row IDs vs. positional indices), consider adding a clarifying comment. Also, the TODOs at lines 127 and 145 indicate technical debt that could be addressed.

🤖 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 `@falcon/cluster/cluster.py` around lines 145 - 161, The code in cluster.py is
computing the same slice twice (row_ids = idx[interval_start:interval_stop] and
idx_interval = idx[interval_start:interval_stop]) and has TODOs about moving
this into a chunking method; fix by consolidating the duplicate slice into a
single variable (e.g., interval_slice = idx[interval_start:interval_stop]) and
then derive row_ids = interval_slice.tolist() and idx_interval = interval_slice
(or a renamed variable) so intent is explicit, add a short clarifying comment
explaining the difference between Lance row IDs vs positional indices, and move
this data-preparation loop into a dedicated helper (e.g., a new chunking method
used by data_chunks creation) to address the TODOs; update references to
data_chunks, chunks, idx, mzs accordingly.
falcon/cluster/consensus.py (5)

566-575: 💤 Low value

Parameter type annotations are inconsistent with actual usage.

The bins_peaks and bins_mz parameters are declared as nb.typed.List but are actually np.ndarray after processing through _outlier_rejection.

 def _construct_average_spectrum(
     bins_indices: List[int],
-    bins_peaks: nb.typed.List,
-    bins_mz: nb.typed.List,
+    bins_peaks: np.ndarray,
+    bins_mz: np.ndarray,
     avg_precursor_mz: float,
🤖 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 `@falcon/cluster/consensus.py` around lines 566 - 575, The signature of
_construct_average_spectrum incorrectly types bins_peaks and bins_mz as
nb.typed.List while they are np.ndarray after _outlier_rejection; update the
type annotations in the _construct_average_spectrum definition to use
appropriate numpy array types (e.g., np.ndarray with element dtype) and adjust
any internal assumptions accordingly, and ensure any callers (including
_outlier_rejection) and numba usage remain compatible with the new np.ndarray
annotations so the function and `@nb.njit` compile and operate with the actual
array inputs.

74-76: 💤 Low value

Docstring formatting issue: misaligned parameter.

The order_map parameter description is incorrectly indented, appearing to be part of the rts parameter description rather than a separate entry.

     rts : np.ndarray
         The retention times corresponding to the current interval indexes.
-        order_map : np.ndarray
+    order_map : np.ndarray
         Map to convert label indexes to pairwise distance matrix indexes.
🤖 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 `@falcon/cluster/consensus.py` around lines 74 - 76, The docstring currently
nests the `order_map` description under the `rts` parameter; update the
docstring so `order_map : np.ndarray` is a separate parameter entry aligned with
`rts` (same indentation/format as other params) and move its description to a
new line directly under `order_map` so it’s not part of the `rts` paragraph;
ensure parameter names `rts` and `order_map` are clearly separated and follow
the same docstring style used elsewhere in this module.

487-493: ⚡ Quick win

Return type annotation is incorrect.

The function returns a tuple of two arrays (intensities, mzs) but the type hint declares a single np.ndarray.

-) -> np.ndarray:
+) -> Tuple[np.ndarray, np.ndarray]:
🤖 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 `@falcon/cluster/consensus.py` around lines 487 - 493, The _sigma_clipping
function is annotated to return a single np.ndarray but actually returns two
arrays (intensities, mzs); update its return type to a tuple of two arrays
(e.g., Tuple[np.ndarray, np.ndarray] or (np.ndarray, np.ndarray)) and add any
required import (typing.Tuple) so the signature correctly reflects the returned
(intensities, mzs) pair used by callers.

425-432: ⚡ Quick win

Return type annotation is incorrect.

The function returns a tuple of three numpy arrays, but the type hint incorrectly declares Tuple[nb.typed.List].

-) -> Tuple[nb.typed.List]:
+) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
🤖 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 `@falcon/cluster/consensus.py` around lines 425 - 432, The annotated return
type for _outlier_rejection is wrong: it declares Tuple[nb.typed.List] but the
function actually returns three arrays. Update the signature to return a 3-tuple
of typed lists, e.g. change the annotation to Tuple[nb.typed.List,
nb.typed.List, nb.typed.List] (and keep the `@nb.njit`(cache=True) decorator);
also verify the function body of _outlier_rejection still returns exactly three
nb.typed.List objects to match the new annotation.

353-359: ⚡ Quick win

Return type annotation is incomplete.

The function returns three values (bins_indices, bins_peaks_nb, bins_mz_nb) but the type hint only declares two.

 ) -> Tuple[List[int], nb.typed.List]:
+) -> Tuple[np.ndarray, nb.typed.List, nb.typed.List]:
🤖 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 `@falcon/cluster/consensus.py` around lines 353 - 359, The return type
annotation for _spectrum_binning is wrong: the function returns three values
(bins_indices, bins_peaks_nb, bins_mz_nb) but the signature only declares two.
Update the def _spectrum_binning(...) -> Tuple[...] annotation to list all three
return element types (e.g., Tuple[List[int], nb.typed.List, nb.typed.List] or
the concrete typed.List element types you use) so the signature matches the
actual returns; keep the three return variable names (bins_indices,
bins_peaks_nb, bins_mz_nb) in mind when choosing the types.
falcon/falcon.py (2)

374-404: 💤 Low value

Docstring parameter name mismatch.

The docstring says charge : int but the parameter is named charge_bucket.

Proposed fix
 def _create_lance_dataset(
     charge_bucket: int, schema: pa.Schema
 ) -> lance.LanceDataset:
     """
     Create a lance dataset.

     Parameters
     ----------
-    charge : int
-        The precursor charge of the spectra.
+    charge_bucket : Union[Tuple[int, ...], str]
+        The bucket key for the spectra.
     schema : pa.Schema
         The schema of the dataset.
🤖 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 `@falcon/falcon.py` around lines 374 - 404, The docstring for
_create_lance_dataset has a parameter name mismatch: it documents "charge" but
the function parameter is "charge_bucket"; update the docstring to use the
correct parameter name "charge_bucket" and adjust the description accordingly so
the Parameters section matches the function signature (refer to
_create_lance_dataset and the parameter charge_bucket and schema).

31-33: 💤 Low value

Consider explicit type annotation for optional parameter.

The static analysis hint indicates PEP 484 prohibits implicit Optional. While this is a minor style issue, it improves clarity.

Proposed fix
-def main(args: Union[str, List[str]] = None) -> int:
+def main(args: Union[str, List[str], None] = None) -> int:
🤖 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 `@falcon/falcon.py` around lines 31 - 33, The function signature for main uses
an implicitly optional parameter; update it to use an explicit Optional type
annotation (e.g., change the args annotation to Optional[Union[str, List[str]]])
and add the corresponding import (Optional) from typing to the module so static
type checkers accept it; locate the main function and the module-level typing
imports to make these changes.

Source: Linters/SAST tools

falcon/utils.py (1)

8-10: Consider replacing legacy np.random.seed() with np.random.default_rng() for explicit RNG state. In falcon/utils.py (lines 8-10), set_seeds() uses np.random.seed(my_seed); a search for np.random.* under falcon/ only found this call, but switching to Generator would likely require propagating an rng object if other code relies on NumPy’s global RNG.

🤖 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 `@falcon/utils.py` around lines 8 - 10, The current set_seeds(my_seed=42) sets
NumPy's global RNG via np.random.seed, which is legacy; change set_seeds to
create and return an explicit Generator using rng =
np.random.default_rng(my_seed) (keep random.seed(my_seed) for Python's random)
and update call sites to accept/propagate that rng instead of relying on NumPy's
global state (alternatively expose a module-level NUMPY_RNG =
np.random.default_rng(my_seed) from falcon.utils and update callers to use that
RNG). Ensure the function signature (set_seeds) and callers (wherever it is
invoked) are updated to use the returned/exported rng object so code uses
Generator methods instead of np.random.* globals.
tests/cluster/test_cluster.py (3)

271-271: 💤 Low value

Use ASCII 'x' instead of Unicode multiplication sign in docstring.

The docstring contains "×" (MULTIPLICATION SIGN, U+00D7). Use ASCII "x" for better compatibility and to satisfy linter rules.

📝 Proposed fix
-        Expected result: 4 clusters (2 m/z groups × 2 RT sub-groups each:
+        Expected result: 4 clusters (2 m/z groups x 2 RT sub-groups each:
🤖 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/cluster/test_cluster.py` at line 271, Replace the Unicode
multiplication sign "×" in the docstring with the ASCII letter "x" to satisfy
the linter and improve compatibility; locate the docstring in
tests/cluster/test_cluster.py (the comment describing "Expected result: 4
clusters (2 × 2 ...") and change "2 × 2" to "2 x 2" (or any similar occurrence
of "×") so the test docstring uses only ASCII characters.

79-79: 💤 Low value

Remove leftover debug print statement.

This print statement appears to be a debugging artifact and should be removed.

🧹 Proposed fix
         linkage_matrix = cluster._linkage(values, "Da")
         assert linkage_matrix.shape == (4, 4)
-        print(linkage_matrix)
         assert (
🤖 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/cluster/test_cluster.py` at line 79, Remove the leftover debug print
statement by deleting the call print(linkage_matrix) in the test (replace with
nothing or, if needed for debugging, use logging at debug level or an
assertion); locate the print(linkage_matrix) occurrence in the test_cluster test
and remove that line so the test output is clean.

463-463: 💤 Low value

Prefix unused reps variable with underscore.

The reps return value is unpacked but never used in these tests. Prefix with _ to indicate this is intentional and suppress linter warnings.

🔧 Proposed fix
-        labels, reps = cluster.generate_clusters(
+        labels, _reps = cluster.generate_clusters(

Apply the same change to lines 510 and 540.

Also applies to: 510-510, 540-540

🤖 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/cluster/test_cluster.py` at line 463, The test unpacks
cluster.generate_clusters into labels, reps but never uses reps; rename the
unused variable to _reps to signal intentional unused and silence linters.
Update the three call sites where generate_clusters is unpacked (the occurrences
using variables named reps at lines around the calls to
cluster.generate_clusters) to use _reps instead of reps so tests remain
functionally identical while satisfying the linter.
tests/cluster/test_consensus.py (1)

166-179: 💤 Low value

Consider verifying the retention time as well.

The test asserts that the precursor m/z matches the first spectrum, but could also assert retention_times[0] == 10.0 to fully confirm the medoid selection logic for small clusters.

✨ Suggested enhancement
     assert len(precursor_mzs) == 1
     assert precursor_mzs[0] == spectra[0].precursor_mz
+    assert retention_times[0] == 10.0
🤖 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/cluster/test_consensus.py` around lines 166 - 179, Update the
test_small_cluster unit test to also assert the medoid retention time: after
calling consensus._get_cluster_medoids (in test_small_cluster) add an assertion
that retention_times[0] == 10.0 (or use a small tolerance if floating-point
precision is a concern) to confirm the chosen medoid’s retention time matches
the first spectrum’s rts.
🤖 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 `@falcon/cluster/cluster.py`:
- Around line 540-544: The precursor_charge field is being cast to np.float64 in
the ConsensusTuple construction, which conflicts with ConsensusTuple's expected
np.int32 or np.nan and with the _get_representative_spectra behaviour; change
the cast where precursor_charge is set (the precursor_charge=(...) expression)
to produce either an np.int32 value or np.nan (mirror the logic used in
_get_representative_spectra) so the field consistently uses np.int32 for valid
integer charges and numpy.nan otherwise.

In `@falcon/cluster/distance_matrix.py`:
- Around line 33-49: compute_condensed_distance_matrix currently creates the
memmap inside a with NamedTemporaryFile which is closed/deleted on return,
invalidating condensed_dist_matrix; change to create the temp file with
NamedTemporaryFile(delete=False) (or use tempfile.mkstemp) and assign its path
to pdist_filename, construct the memmap (condensed_dist_matrix) from that path,
return the memmap, and add explicit cleanup logic so callers (or a
finally/cleanup function in this module) unlink the temp file path
(pdist_filename) after the memmap is closed; update references to
pdist_file/pdist_filename and ensure callers that delete pdist now unlink the
underlying temp file path.

In `@falcon/config.py`:
- Around line 241-284: In parse_and_validate_charge_buckets, the error message
builds f"Charge value(s) {sorted(overlap)}..." which can TypeError when overlap
mixes ints and "unknown"; change the construction to produce a stable,
comparable list by mapping values to strings before sorting (e.g.,
sorted(map(str, overlap)) or using a key=str) and include that joined/printed
representation in the argparse.ArgumentTypeError so the error shows the
offending values without raising a new exception.

In `@falcon/falcon.py`:
- Around line 502-516: The code currently uses bucket_key (derived from
charge_to_bucket and catch_other_charges) but then unconditionally accesses
spec_to_write[bucket_key], which can create a None key or cause downstream
errors; move the length check and subsequent _write_to_dataset call inside the
existing if bucket_key is not None: block so all uses of
spec_to_write[bucket_key] only happen when bucket_key is valid, and ensure you
pass lance_locks.get(charge), schema, and config.work_dir to _write_to_dataset
only from within that block to avoid handling None keys.

In `@falcon/ms_io/mzml_io.py`:
- Line 31: In get_spectra, avoid directly accessing f_in.name (which can raise
AttributeError for in-memory streams); instead detect whether f_in is a str path
or a file-like object, use os.path.splitext(os.path.basename(...)) when f_in is
a str, otherwise use getattr(f_in, "name", None) and fall back to a safe default
(e.g., "unknown" or None) when name is missing before assigning filename; update
the code around the filename assignment that currently reads filename =
os.path.splitext(os.path.basename(f_in.name))[0] to use this guarded approach.

In `@README.md`:
- Around line 75-76: Update the README entry for the `linkage` option so the
fragment becomes a complete sentence: replace "Should be one of `single`,
`complete`, or `average`." with a full sentence such as "It must be one of
`single`, `complete`, or `average`." Ensure the `linkage` description now reads
as a complete sentence including the default (`complete`) and the allowed
values.

In `@tests/cluster/test_spectrum.py`:
- Around line 130-145: The assertion in test_precursor_removal is allowing a
peak exactly at the precursor m/z due to the unnecessary "or abs(mz_val - 500.0)
== 0" clause; update the assertion in the test_precursor_removal function (which
calls spectrum.process_spectrum with remove_precursor_tolerance=1.5 and
simple_spectrum) to assert that every mz in result["mz"] is farther than the
tolerance from 500.0 by replacing the current condition with a single check that
abs(mz_val - 500.0) > 1.5 so peaks within tolerance (including exactly 500.0)
are considered removed.

---

Outside diff comments:
In `@falcon/falcon.py`:
- Around line 486-496: The loop uses the wrong lock key: spec_to_write is keyed
by bucket_key but the code calls lance_locks.get(charge) and later uses a
different `charge` when building the dataset path/flush, causing concurrent
writes to the same dataset; fix by treating the loop variable as bucket_key
(rename if helpful), acquire lance_locks.get(bucket_key) and pass that lock to
_write_to_dataset, and ensure the flush/filename logic and spec_to_write
clearing all use the same bucket_key (not the original spectrum `charge`),
applying the same change in both the earlier loop and the later flush block
(places around _write_to_dataset, spec_to_write.clear(), and any code that
constructs spectra_charge_* .lance).

---

Nitpick comments:
In `@falcon/cluster/cluster.py`:
- Line 815: Replace the ambiguous Unicode multiplication sign in the comment
"Group reps by mz_split in one pass — O(R) instead of O(S × R)." with a plain
ASCII 'x' so it reads "O(S x R)"; update the comment near the grouping logic
that references mz_split and reps (the same line containing "Group reps by
mz_split in one pass") to ensure consistent display across editors.
- Around line 683-684: The for-loop over cluster_assignments uses an unused loop
variable i; update the loop in cluster.py so it doesn’t enumerate when not
needed (replace "for i, label in enumerate(cluster_assignments)" with a simple
"for label in cluster_assignments") and keep the body that updates labels
(labels[label] = labels.get(label, 0) + 1) unchanged to avoid the unused
variable warning.
- Around line 264-272: Remove the commented-out block containing the "TODO: ask
wout about this" logic around block_size, batch_size, splits, n_chunks and
chunk_size: either implement the intended splitting behavior (use the shown
algorithm to compute n_chunks, chunk_size and populate splits) or delete the
entire commented section and the TODO so it doesn't remain in the code; if you
choose to keep a short note, replace the TODO with a concise explanatory comment
describing why the code was omitted and link to an issue/PR that will implement
it.
- Around line 145-161: The code in cluster.py is computing the same slice twice
(row_ids = idx[interval_start:interval_stop] and idx_interval =
idx[interval_start:interval_stop]) and has TODOs about moving this into a
chunking method; fix by consolidating the duplicate slice into a single variable
(e.g., interval_slice = idx[interval_start:interval_stop]) and then derive
row_ids = interval_slice.tolist() and idx_interval = interval_slice (or a
renamed variable) so intent is explicit, add a short clarifying comment
explaining the difference between Lance row IDs vs positional indices, and move
this data-preparation loop into a dedicated helper (e.g., a new chunking method
used by data_chunks creation) to address the TODOs; update references to
data_chunks, chunks, idx, mzs accordingly.

In `@falcon/cluster/consensus.py`:
- Around line 566-575: The signature of _construct_average_spectrum incorrectly
types bins_peaks and bins_mz as nb.typed.List while they are np.ndarray after
_outlier_rejection; update the type annotations in the
_construct_average_spectrum definition to use appropriate numpy array types
(e.g., np.ndarray with element dtype) and adjust any internal assumptions
accordingly, and ensure any callers (including _outlier_rejection) and numba
usage remain compatible with the new np.ndarray annotations so the function and
`@nb.njit` compile and operate with the actual array inputs.
- Around line 74-76: The docstring currently nests the `order_map` description
under the `rts` parameter; update the docstring so `order_map : np.ndarray` is a
separate parameter entry aligned with `rts` (same indentation/format as other
params) and move its description to a new line directly under `order_map` so
it’s not part of the `rts` paragraph; ensure parameter names `rts` and
`order_map` are clearly separated and follow the same docstring style used
elsewhere in this module.
- Around line 487-493: The _sigma_clipping function is annotated to return a
single np.ndarray but actually returns two arrays (intensities, mzs); update its
return type to a tuple of two arrays (e.g., Tuple[np.ndarray, np.ndarray] or
(np.ndarray, np.ndarray)) and add any required import (typing.Tuple) so the
signature correctly reflects the returned (intensities, mzs) pair used by
callers.
- Around line 425-432: The annotated return type for _outlier_rejection is
wrong: it declares Tuple[nb.typed.List] but the function actually returns three
arrays. Update the signature to return a 3-tuple of typed lists, e.g. change the
annotation to Tuple[nb.typed.List, nb.typed.List, nb.typed.List] (and keep the
`@nb.njit`(cache=True) decorator); also verify the function body of
_outlier_rejection still returns exactly three nb.typed.List objects to match
the new annotation.
- Around line 353-359: The return type annotation for _spectrum_binning is
wrong: the function returns three values (bins_indices, bins_peaks_nb,
bins_mz_nb) but the signature only declares two. Update the def
_spectrum_binning(...) -> Tuple[...] annotation to list all three return element
types (e.g., Tuple[List[int], nb.typed.List, nb.typed.List] or the concrete
typed.List element types you use) so the signature matches the actual returns;
keep the three return variable names (bins_indices, bins_peaks_nb, bins_mz_nb)
in mind when choosing the types.

In `@falcon/falcon.py`:
- Around line 374-404: The docstring for _create_lance_dataset has a parameter
name mismatch: it documents "charge" but the function parameter is
"charge_bucket"; update the docstring to use the correct parameter name
"charge_bucket" and adjust the description accordingly so the Parameters section
matches the function signature (refer to _create_lance_dataset and the parameter
charge_bucket and schema).
- Around line 31-33: The function signature for main uses an implicitly optional
parameter; update it to use an explicit Optional type annotation (e.g., change
the args annotation to Optional[Union[str, List[str]]]) and add the
corresponding import (Optional) from typing to the module so static type
checkers accept it; locate the main function and the module-level typing imports
to make these changes.

In `@falcon/utils.py`:
- Around line 8-10: The current set_seeds(my_seed=42) sets NumPy's global RNG
via np.random.seed, which is legacy; change set_seeds to create and return an
explicit Generator using rng = np.random.default_rng(my_seed) (keep
random.seed(my_seed) for Python's random) and update call sites to
accept/propagate that rng instead of relying on NumPy's global state
(alternatively expose a module-level NUMPY_RNG = np.random.default_rng(my_seed)
from falcon.utils and update callers to use that RNG). Ensure the function
signature (set_seeds) and callers (wherever it is invoked) are updated to use
the returned/exported rng object so code uses Generator methods instead of
np.random.* globals.

In `@tests/cluster/test_cluster.py`:
- Line 271: Replace the Unicode multiplication sign "×" in the docstring with
the ASCII letter "x" to satisfy the linter and improve compatibility; locate the
docstring in tests/cluster/test_cluster.py (the comment describing "Expected
result: 4 clusters (2 × 2 ...") and change "2 × 2" to "2 x 2" (or any similar
occurrence of "×") so the test docstring uses only ASCII characters.
- Line 79: Remove the leftover debug print statement by deleting the call
print(linkage_matrix) in the test (replace with nothing or, if needed for
debugging, use logging at debug level or an assertion); locate the
print(linkage_matrix) occurrence in the test_cluster test and remove that line
so the test output is clean.
- Line 463: The test unpacks cluster.generate_clusters into labels, reps but
never uses reps; rename the unused variable to _reps to signal intentional
unused and silence linters. Update the three call sites where generate_clusters
is unpacked (the occurrences using variables named reps at lines around the
calls to cluster.generate_clusters) to use _reps instead of reps so tests remain
functionally identical while satisfying the linter.

In `@tests/cluster/test_consensus.py`:
- Around line 166-179: Update the test_small_cluster unit test to also assert
the medoid retention time: after calling consensus._get_cluster_medoids (in
test_small_cluster) add an assertion that retention_times[0] == 10.0 (or use a
small tolerance if floating-point precision is a concern) to confirm the chosen
medoid’s retention time matches the first spectrum’s rts.
🪄 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

Run ID: 3b58361a-c414-426c-956d-f89fcb61564d

📥 Commits

Reviewing files that changed from the base of the PR and between a77a9a3 and 63a48f5.

⛔ Files ignored due to path filters (1)
  • falcon_how_v2.png is excluded by !**/*.png
📒 Files selected for processing (28)
  • .gitignore
  • README.md
  • falcon/cluster/cluster.py
  • falcon/cluster/consensus.py
  • falcon/cluster/distance_matrix.py
  • falcon/cluster/similarity.py
  • falcon/cluster/spectrum.py
  • falcon/config.py
  • falcon/falcon.py
  • falcon/ms_io/mgf_io.py
  • falcon/ms_io/ms_io.py
  • falcon/ms_io/mzml_io.py
  • falcon/ms_io/mzxml_io.py
  • falcon/seed.py
  • falcon/utils.py
  • setup.cfg
  • tests/cluster/test_cluster.py
  • tests/cluster/test_consensus.py
  • tests/cluster/test_distance_matrix.py
  • tests/cluster/test_similarity.py
  • tests/cluster/test_spectrum.py
  • tests/conftest.py
  • tests/data/sample.mzml
  • tests/data/sample.mzxml
  • tests/test_config.py
  • tests/test_falcon.py
  • tests/test_ms_io.py
  • tests/test_mzml_mzxml_io.py
💤 Files with no reviewable changes (3)
  • falcon/seed.py
  • falcon/ms_io/ms_io.py
  • falcon/cluster/spectrum.py

Comment thread falcon/cluster/cluster.py Outdated
Comment thread falcon/cluster/distance_matrix.py Outdated
Comment thread falcon/config.py
Comment thread falcon/falcon.py Outdated
Comment thread falcon/ms_io/mzml_io.py Outdated
Comment thread README.md Outdated
Comment thread tests/cluster/test_spectrum.py
Janne98 and others added 2 commits June 9, 2026 08:44
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
falcon/falcon.py (2)

145-153: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't ignore the current --precursor_charge_buckets when reusing a work dir.

When charges.joblib exists, Line 147 reuses the cached bucket layout unconditionally. A rerun with different bucket settings will silently cluster against stale per-bucket datasets, so the output no longer matches the CLI the user asked for. Compare the cached value to config.precursor_charge_buckets and fail fast or rebuild after clearing work_dir/spectra.

Proposed fix
     charge_path = os.path.join(config.work_dir, "spectra", "charges.joblib")
     if os.path.isfile(charge_path) and not config.overwrite:
         charge_buckets = joblib.load(charge_path)
+        if charge_buckets != config.precursor_charge_buckets:
+            raise ValueError(
+                "Cached charge buckets do not match the current "
+                "--precursor_charge_buckets; rerun with --overwrite "
+                "or a different --work_dir."
+            )
     else:
         # Recalculate the charge buckets and recreate dataset.
         charge_buckets = _prepare_spectra(
             process_spectrum, config.precursor_charge_buckets
         )
🤖 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 `@falcon/falcon.py` around lines 145 - 153, The code currently reuses
charges.joblib without validating its stored bucket configuration; update the
logic around charge_path/charge_buckets to load the cached metadata and compare
its stored precursor bucket layout to config.precursor_charge_buckets, and if
they differ either (a) delete/clear the work_dir/spectra cache and recompute by
calling _prepare_spectra(process_spectrum, config.precursor_charge_buckets) and
joblib.dump the new charge_buckets, or (b) raise a clear error indicating bucket
mismatch so the caller can remove the spectra directory; ensure the stored
metadata in charges.joblib (or alongside it) is read to perform this comparison
before reusing the cached charge_buckets.

141-143: ⚠️ Potential issue | 🟠 Major

Fix --overwrite cleanup for Lance dataset directories (falcon/falcon.py lines 141-143)

os.remove() will raise IsADirectoryError if spectra/ contains Lance dataset roots like spectra_charge*.lance (Lance stores datasets under a directory root). Remove directories with shutil.rmtree() instead of os.remove().

Proposed fix
     if config.overwrite:
         for filename in os.listdir(os.path.join(config.work_dir, "spectra")):
-            os.remove(os.path.join(config.work_dir, "spectra", filename))
+            path = os.path.join(config.work_dir, "spectra", filename)
+            if os.path.isdir(path):
+                shutil.rmtree(path)
+            else:
+                os.remove(path)
🤖 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 `@falcon/falcon.py` around lines 141 - 143, The overwrite cleanup loop in
falcon.py uses os.remove on entries inside the spectra directory which fails for
Lance dataset directories (e.g., spectra_charge*.lance); update the block where
config.overwrite is checked to iterate entries under
os.path.join(config.work_dir, "spectra") and for each entry use os.path.isdir to
detect directories and call shutil.rmtree to remove them, otherwise call
os.remove for files; ensure you import shutil at the top if not already present
and preserve existing behavior when entries are regular files.
🤖 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.

Outside diff comments:
In `@falcon/falcon.py`:
- Around line 145-153: The code currently reuses charges.joblib without
validating its stored bucket configuration; update the logic around
charge_path/charge_buckets to load the cached metadata and compare its stored
precursor bucket layout to config.precursor_charge_buckets, and if they differ
either (a) delete/clear the work_dir/spectra cache and recompute by calling
_prepare_spectra(process_spectrum, config.precursor_charge_buckets) and
joblib.dump the new charge_buckets, or (b) raise a clear error indicating bucket
mismatch so the caller can remove the spectra directory; ensure the stored
metadata in charges.joblib (or alongside it) is read to perform this comparison
before reusing the cached charge_buckets.
- Around line 141-143: The overwrite cleanup loop in falcon.py uses os.remove on
entries inside the spectra directory which fails for Lance dataset directories
(e.g., spectra_charge*.lance); update the block where config.overwrite is
checked to iterate entries under os.path.join(config.work_dir, "spectra") and
for each entry use os.path.isdir to detect directories and call shutil.rmtree to
remove them, otherwise call os.remove for files; ensure you import shutil at the
top if not already present and preserve existing behavior when entries are
regular files.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7105b1ea-9c25-45fe-818b-e0200e92361d

📥 Commits

Reviewing files that changed from the base of the PR and between 63a48f5 and 859a4b3.

📒 Files selected for processing (1)
  • falcon/falcon.py

@Janne98

Janne98 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

How should clusters with known and unknown retention times be handled when rt_tol is not None?
Currently spectra with an unknown RT do not participate in RT splitting and will end up in a separate cluster.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
falcon/falcon.py (2)

530-567: 💤 Low value

Docstring incorrectly states lock type.

The docstring says lock : multiprocessing.synchronize.Lock but the actual type annotation correctly shows threading.Lock.

📝 Suggested fix
-    lock : multiprocessing.synchronize.Lock
+    lock : threading.Lock
         Lock to synchronize writing to the dataset.
🤖 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 `@falcon/falcon.py` around lines 530 - 567, The docstring for _write_to_dataset
incorrectly documents the lock parameter as multiprocessing.synchronize.Lock
while the function signature uses threading.Lock; update the docstring's lock
parameter description to match the annotation (threading.Lock) and adjust its
wording to indicate it's a threading.Lock used to synchronize writes to the
Lance dataset (reference: function _write_to_dataset and parameter lock).

385-415: 💤 Low value

Type hint for charge_bucket is incomplete.

The parameter can also be str (e.g., "other"), but the type hint only shows Union[int, Tuple[int, ...]]. The caller _write_to_dataset correctly types it as Union[Tuple[int, ...], str].

📝 Suggested fix
 def _create_lance_dataset(
-    charge_bucket: Union[int, Tuple[int, ...]], schema: pa.Schema
+    charge_bucket: Union[Tuple[int, ...], str], schema: pa.Schema
 ) -> lance.LanceDataset:
🤖 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 `@falcon/falcon.py` around lines 385 - 415, The type hint for parameter
charge_bucket in _create_lance_dataset is missing str; update its annotation
from Union[int, Tuple[int, ...]] to Union[Tuple[int, ...], str, int] (or
Union[int, Tuple[int, ...], str]) to match callers like _write_to_dataset, and
update the corresponding docstring "charge_bucket" type description to include
str (e.g., "int | Tuple[int, ...] | str") so bucket_key_to_str usage remains
correct.
falcon/cluster/consensus.py (1)

571-625: 💤 Low value

Docstring type hints don't match actual parameter types.

The docstring states bins_peaks and bins_mz are nb.typed.List, but the actual parameter type annotations (lines 574-575) correctly show them as np.ndarray. This matches how _outlier_rejection returns them.

📝 Suggested docstring fix
     Parameters
     ----------
     bins_indices : List[int]
         The indices of the non-empty bins.
-    bins_peaks : nb.typed.List
+    bins_peaks : np.ndarray
         The intensities for each bin.
-    bins_mz : nb.typed.List
+    bins_mz : np.ndarray
         The m/z values for each bin
🤖 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 `@falcon/cluster/consensus.py` around lines 571 - 625, Update the
_construct_average_spectrum docstring to reflect the actual types used: change
bins_peaks and bins_mz from nb.typed.List to np.ndarray (matching the function
signature and what _outlier_rejection returns), and ensure the Returns
section/type summary aligns with the function signature (e.g., charge is int or
np.nan handling) so the documented types match the annotated types in
_construct_average_spectrum.
tests/conftest.py (1)

115-140: 💤 Low value

Unused loop variable i.

The loop variable i on line 121 is not used within the loop body. Rename it to _ to indicate it's intentionally unused.

📝 Suggested fix
-        for i in range(n):
+        for _ in range(n):
🤖 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/conftest.py` around lines 115 - 140, The loop in the pytest fixture
factory _make inside mock_spectra uses an unused loop variable i; update the for
loop to use a conventional unused name (for example change "for i in range(n):"
to "for _ in range(n):") so the intent is clear and linting warnings are
removed. Leave the rest of _make and the creation of similarity.SpectrumTuple
unchanged.

Source: Linters/SAST tools

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

Nitpick comments:
In `@falcon/cluster/consensus.py`:
- Around line 571-625: Update the _construct_average_spectrum docstring to
reflect the actual types used: change bins_peaks and bins_mz from nb.typed.List
to np.ndarray (matching the function signature and what _outlier_rejection
returns), and ensure the Returns section/type summary aligns with the function
signature (e.g., charge is int or np.nan handling) so the documented types match
the annotated types in _construct_average_spectrum.

In `@falcon/falcon.py`:
- Around line 530-567: The docstring for _write_to_dataset incorrectly documents
the lock parameter as multiprocessing.synchronize.Lock while the function
signature uses threading.Lock; update the docstring's lock parameter description
to match the annotation (threading.Lock) and adjust its wording to indicate it's
a threading.Lock used to synchronize writes to the Lance dataset (reference:
function _write_to_dataset and parameter lock).
- Around line 385-415: The type hint for parameter charge_bucket in
_create_lance_dataset is missing str; update its annotation from Union[int,
Tuple[int, ...]] to Union[Tuple[int, ...], str, int] (or Union[int, Tuple[int,
...], str]) to match callers like _write_to_dataset, and update the
corresponding docstring "charge_bucket" type description to include str (e.g.,
"int | Tuple[int, ...] | str") so bucket_key_to_str usage remains correct.

In `@tests/conftest.py`:
- Around line 115-140: The loop in the pytest fixture factory _make inside
mock_spectra uses an unused loop variable i; update the for loop to use a
conventional unused name (for example change "for i in range(n):" to "for _ in
range(n):") so the intent is clear and linting warnings are removed. Leave the
rest of _make and the creation of similarity.SpectrumTuple unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9b10b92-9d6d-4143-add7-a07fd2474eca

📥 Commits

Reviewing files that changed from the base of the PR and between 2225511 and ec9c32a.

📒 Files selected for processing (15)
  • falcon/cluster/cluster.py
  • falcon/cluster/consensus.py
  • falcon/cluster/similarity.py
  • falcon/falcon.py
  • falcon/ms_io/mzxml_io.py
  • falcon/utils.py
  • tests/cluster/test_cluster.py
  • tests/cluster/test_consensus.py
  • tests/cluster/test_distance_matrix.py
  • tests/cluster/test_similarity.py
  • tests/conftest.py
  • tests/test_config.py
  • tests/test_falcon.py
  • tests/test_ms_io.py
  • tests/test_mzml_mzxml_io.py
💤 Files with no reviewable changes (2)
  • falcon/cluster/similarity.py
  • falcon/ms_io/mzxml_io.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/cluster/test_distance_matrix.py
  • tests/cluster/test_similarity.py
  • falcon/utils.py
  • tests/test_mzml_mzxml_io.py
  • tests/test_falcon.py
  • tests/test_ms_io.py
  • tests/cluster/test_cluster.py
  • tests/test_config.py
  • falcon/cluster/cluster.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
tests/test_falcon.py (1)

27-36: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore prior config.work_dir value instead of always deleting it.

The fixture teardown at Line 36 unconditionally deletes work_dir, which can leak state across tests if config.work_dir was already set before this fixture ran. Save the previous value and restore it after yield.

Proposed fix
 `@pytest.fixture`
 def work_dir(tmp_path):
@@
-    (tmp_path / "spectra").mkdir()
-    _global_config.__dict__["work_dir"] = str(tmp_path)
-    yield tmp_path
-    del _global_config.__dict__["work_dir"]
+    (tmp_path / "spectra").mkdir()
+    had_prev = "work_dir" in _global_config.__dict__
+    prev = _global_config.__dict__.get("work_dir")
+    _global_config.__dict__["work_dir"] = str(tmp_path)
+    try:
+        yield tmp_path
+    finally:
+        if had_prev:
+            _global_config.__dict__["work_dir"] = prev
+        else:
+            _global_config.__dict__.pop("work_dir", None)
🤖 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_falcon.py` around lines 27 - 36, The fixture work_dir currently
unconditionally deletes _global_config.__dict__["work_dir"] in teardown which
can remove a previously set value; modify work_dir to capture the prior presence
and value before setting (e.g. store a sentinel like _prev_exists and
_prev_value), yield as before, and in the teardown restore the previous value if
it existed or delete the key only if it did not exist prior—use the same
_global_config.__dict__["work_dir"] symbol to locate and revert state.
🤖 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.

Outside diff comments:
In `@tests/test_falcon.py`:
- Around line 27-36: The fixture work_dir currently unconditionally deletes
_global_config.__dict__["work_dir"] in teardown which can remove a previously
set value; modify work_dir to capture the prior presence and value before
setting (e.g. store a sentinel like _prev_exists and _prev_value), yield as
before, and in the teardown restore the previous value if it existed or delete
the key only if it did not exist prior—use the same
_global_config.__dict__["work_dir"] symbol to locate and revert state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6aea1122-465c-47c3-9e80-cb5ebd21c105

📥 Commits

Reviewing files that changed from the base of the PR and between ec9c32a and 3cda72f.

📒 Files selected for processing (5)
  • README.md
  • falcon/config.py
  • falcon/falcon.py
  • tests/test_config.py
  • tests/test_falcon.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • README.md
  • tests/test_config.py
  • falcon/config.py
  • falcon/falcon.py

@bittremieux

Copy link
Copy Markdown
Member

How should clusters with known and unknown retention times be handled when rt_tol is not None?
Currently spectra with an unknown RT do not participate in RT splitting and will end up in a separate cluster.

Yes, that sounds fine.

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