Skip to content

Format-agnostic identification I/O: idXML / mzIdentML / idParquet#52

Merged
ypriverol merged 16 commits into
mainfrom
feat/format-agnostic-io
Jun 21, 2026
Merged

Format-agnostic identification I/O: idXML / mzIdentML / idParquet#52
ypriverol merged 16 commits into
mainfrom
feat/format-agnostic-io

Conversation

@ypriverol

@ypriverol ypriverol commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

Generalizes the identification I/O so the AScore/PhosphoRS/LucXor pipeline reads and writes idXML, mzIdentML, or idParquet by file extension, normalized to the existing idParquet pandas-DataFrame model. Also re-applies the localizer hot-path performance optimizations on top of the current idParquet architecture.

onsite all -in spectra.mzML -id input.<idXML|mzid|idparquet> -out output.<idXML|mzid|idparquet> [--add-decoys]

What's new

  • onsite/id_io.py (new): detect_format, load_identifications, save_identifications.
    • load_identifications: idParquet → load_dataframes (native); idXML/mzid → pyOpenMS load → psms_df (peptidoform via UNIMOD notation, protein_accessions, modifications, and typed psm_metavalues carrying all score keys).
    • save_identifications: dispatch by extension. mzid store strips the non-UNIMOD custom PhosphoDecoy modification from SearchParameters.variable_modifications (otherwise MzIdentMLFile.store rejects UNIMOD:99913); the decoy modification stays on the hits and round-trips back intact.
  • onsite/idparquet.py: new save_psms_from_scratch so idXML/mzid → idParquet works without an existing idParquet template (main's save_dataframes only copy-updates a source).
  • Wiring: the three CLIs and onsitec merge route user-facing input/output through the format layer; the native idParquet→idParquet path is unchanged (copy-template), and internal per-tool intermediates stay idParquet.
  • Performance: re-applies the AScore (numpy peak matching), PhosphoRS (binomial-tail memoization + isoform theo-spectrum cache), and LucXor (per-charge density hoist) optimizations onto main. The earlier ascore/cli.py shared-instance tweak was intentionally dropped — main refactored that CLI to DataFrame groups, so it no longer applies; the core AScore win lives in ascore.py and is preserved.

Testing

Full suite 172 passed, 0 failed. New tests/test_id_io.py covers: format detection, idParquet/idXML/mzid load→df, save round-trips for all three formats, the from-scratch idParquet writer, score-key (AScore_site_scores) survival across every output format, and a real PhosphoDecoy(A) mzid round-trip.

Notes / follow-ups (non-blocking)

  • save_psms_from_scratch currently leaves entirely-empty columns null-typed rather than fixture-typed (e.g. mz_array, cv_params). It is consumable and idempotent for this pipeline; emitting an explicit pyarrow schema would make it type-faithful for strict external idParquet consumers. Tracked as a follow-up.
  • search_params.parquet from-scratch stub writes a subset of columns (the native passthrough copies the full file); template_df on the from-scratch branch is reserved for that follow-up.

Supersedes #51 (the pyOpenMS-object mzIdentML approach), which predated main's idParquet migration.

Summary by CodeRabbit

  • New Features
    • Added unified identification I/O with automatic format detection and roundtrip support across idParquet, idXML, and mzIdentML.
    • Added save_psms_from_scratch for creating new idParquet outputs.
  • Performance Improvements
    • Speeded up phospho site and permutation scoring with precomputed m/z windows, improved ion matching, and shared theoretical spectrum caching.
    • Added caching for binomial tail probabilities and log-space final normalization.
  • Behavior Changes
    • Changed default neutral-loss handling to off and refined FLR ranking/cross-tool intersection logic.
  • Tests
    • Added extensive end-to-end load/save and scoring regression coverage for identification and FLR logic.

yueqixuan and others added 10 commits June 19, 2026 15:53
…to psms_df

Adds onsite/id_io.py (Phase 1 load side):
- detect_format(): idparquet/idxml/mzid by extension or directory
- load_identifications(): 4-tuple dispatch to idparquet.load_dataframes or
  pyOpenMS IdXMLFile/MzIdentMLFile then _peptide_ids_to_psms_df()
- _peptide_ids_to_psms_df(): 29-col schema-compatible DataFrame with int32
  scan/charge/hit_index, psm_metavalues as numpy ndarray of dicts, and a
  score fallback for mzid round-trip that drops the primary score into a
  named metavalue
Adds tests/test_id_io.py: 25 tests covering all three formats and edge cases.
… value_type + empty-df dtypes

- protein_accessions: populate from hit.getPeptideEvidences() as numpy object
  array of {accession, aa_before, aa_after, start, end} dicts matching fixture schema
- modifications: populate via peptidoform_to_modifications(peptidoform) per row
- psm_metavalues value_type: derive from Python type (float→"double", int→"int",
  else "string") matching fixture convention; add _metavalue_type_str() helper
- _empty_psms_df(): enforce int32/float64/bool dtypes via _PSM_DTYPE_MAP so
  empty and non-empty DataFrames are type-consistent
- detect_format: coerce path=str(path) so pathlib.Path works
- _NON_SCORE_KEYS: hoist to module level (was rebuilt per hit); expand to include
  AScore_site_scores, num_matched_peptides, protein_references, isotope_error
- _SCAN_RE: tighten to \bscan=(\d+) to avoid false-matching basescan=123
- tests: add TestDetectFormatPathlib (4), TestProteinAccessions (2),
  TestMetavalueType (3), TestModificationsPopulated (2); 36/36 pass
…writer

- Add save_identifications() to id_io.py dispatching on detect_format():
  idParquet (from-scratch or schema-copy), idXML, mzIdentML
- Add _psms_df_to_peptide_ids() converting psms DataFrame to pyOpenMS objects
- Add save_psms_from_scratch() to idparquet.py for schema-free idParquet write
- Fix latent bug in _copy_or_create_parquet() where src was computed without
  None-guard on source_idparquet
- Add 11 Phase 2 tests (4 classes): idParquet/idXML/mzid roundtrip + scratch write
  All 47 tests pass
…metavalue round-trip tests

BUG 1: save_psms_from_scratch now fills NaN with -1 sentinel before casting
int32 columns (precursor_charge, scan, hit_index, peptide_identification_index).
Previously the silent try/except left NaN-containing columns as float64,
producing schema-mismatched parquet output.

BUG 2: _psms_df_to_peptide_ids now uses pd.isna() throughout for scalar
values that could be None, NaN, or pd.NA: score_type, higher_score_better
(which raised "boolean value of NA is ambiguous"), rt_val, mz_val, charge,
and score_val.

GAP 3: test_phosphodecoy_does_not_raise rewritten with a real
ALS(Phospho)A(PhosphoDecoy)K peptidoform (UNIMOD:21 + UNIMOD:99913).
Empirically confirmed: the PhosphoDecoy strip in save_identifications is
REACHABLE (strips PhosphoDecoy from SearchParams.variable_modifications)
but the hit sequence survives, and PhosphoDecoy round-trips correctly via
the mzid save→load path. Test now asserts "PhosphoDecoy" appears in
unimod_to_pyopenms_notation(reloaded_peptidoform).

GAP 4: Added test_mzid_score_preserved and test_mzid_metavalue_survives
to TestSaveLoadMzidRoundtrip for score precision (< 1e-6) and AScore
metavalue survival through mzid round-trip.

Minor: Added test_from_scratch_peptidoform_preserved and
test_from_scratch_int32_dtypes to TestSaveIdparquetFromScratch.

Added TODO(phase3) comments for search_params.parquet stub and
template_df parameter wiring.
…/onsitec (multi-format I/O)

Phase 3 of unified identification I/O: replace direct load_dataframes/save_dataframes
calls at user-facing I/O boundaries with load_identifications/save_identifications from
onsite.id_io, enabling transparent multi-format support (.idparquet, .idxml, .mzid).

Internal per-tool intermediate files (ascore/phosphors/lucxor .idparquet) continue to
use idparquet.load_dataframes/save_dataframes directly.

Also adds TestRoundTripFormats to tests/test_id_io.py verifying row-count preservation
across all three output formats via the fixture idParquet.
…rmat round-trip

- Remove `save_dataframes as _save_df` from phosphors/cli.py (replaced by _io_save)
- Remove `load_dataframes`, `save_dataframes` from ascore/cli.py (replaced by load/save_identifications)
- Remove `save_dataframes`, `load_dataframes` from lucxor/cli.py (replaced by load/save_identifications)
- Add _inject_ascore_metavalue helper to TestRoundTripFormats
- Add test_round_trip_idxml_score_key_survives: AScore_site_scores survives idXML round-trip
- Add test_round_trip_mzid_score_key_survives: AScore_site_scores survives mzid round-trip
- Add test_round_trip_idparquet_score_key_survives: AScore_site_scores survives idparquet round-trip
…ions onto idParquet main

Cherry-pick of the perf work onto main's idParquet architecture. Core
optimizations apply cleanly (ascore.py numpy matching, phosphors.py binomial
cache + theo cache, lucxor/psm.py density hoist). The ascore/cli.py shared-AScore
-instance tweak was dropped: main refactored that CLI to DataFrame groups
(process_psm_group), so it no longer applies; the primary AScore win lives in
ascore.py and is preserved.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces onsite/id_io.py as a unified identification I/O layer with format detection (idparquet, idXML, mzIdentML), dispatch-based load/save, and bidirectional pyOpenMS↔DataFrame conversions. Adds save_psms_from_scratch to idparquet.py. Wires all tool CLIs (ascore, lucxor, phosphors, onsitec) to the new layer. Independently optimizes AScore (numpy two-pointer matching with precomputed windows), LucXor (hoisted density evaluators), PhosphoRS (binomial-tail and isoform m/z caching with log-space normalization), and decoy FLR (corrected formula and unified PSM keys).

Changes

Unified Identification I/O Layer

Layer / File(s) Summary
PSM schema constants and conversion helpers
onsite/id_io.py
Defines _PSM_COLUMNS, _PSM_DTYPE_MAP, _empty_psms_df(), _NON_SCORE_KEYS/_SCAN_RE, and utilities _metavalue_type_str, _is_sentinel, _prot_ids_to_df underlying all pyOpenMS↔DataFrame conversions.
Format detection and load/save dispatch
onsite/id_io.py
detect_format classifies paths as idparquet/mzid/idxml; load_identifications routes to idParquet loader or internal pyOpenMS loaders (returning 4-tuple); save_identifications routes by format, picks scratch vs template idParquet path, strips PhosphoDecoy mods for mzIdentML.
pyOpenMS ↔ DataFrame conversions
onsite/id_io.py
_load_idxml/_load_mzid load pyOpenMS files to DataFrames. _peptide_ids_to_psms_df extracts scan via regex, reconstructs score from metavalues, converts UNIMOD peptidoforms, enforces dtypes. _psms_df_to_peptide_ids groups by peptide_identification_index, sorts hits, parses AASequence, inserts typed metavalues, derives SearchParameters variable mods from UNIMOD tags.
idParquet from-scratch writer and source-guard fix
onsite/idparquet.py
Adds save_psms_from_scratch to create .idparquet directory from psms_df without source (fills columns per schema, enforces dtypes including int32 and NaN-safe booleans, writes all parquet files). Fixes _copy_or_create_parquet to conditionally construct source path only when source_idparquet is provided.
CLI wiring and orchestration across all tools
onsite/ascore/cli.py, onsite/phosphors/cli.py, onsite/onsitec.py, onsite/lucxor/cli.py
Switches ascore, phosphors, and onsitec to load_identifications/save_identifications. LucXor fully rewritten: uses same I/O, reorganizes error handling/logging, dynamically selects score mode based on posterior_error_probability availability, carries over psm_metavalues while filtering Luciphor-managed keys, and tracks relocation counts.
id_io test suite
tests/test_id_io.py
1207-line suite covering detect_format, load_identifications for all formats (UNIMOD conversion, scan parsing, score recovery, metavalues), save_identifications roundtrips, save_psms_from_scratch, PhosphoDecoy mzid integration, dtype preservation, protein_accessions extraction, and six regression tests.

Algorithm Performance Optimizations

Layer / File(s) Summary
AScore numpy-based window matching
onsite/ascore/ascore.py
Adds spectrumMZArray_, windowDepthMZArrays_, countMatchedMZ_ (two-pointer scan with tolerance/ppm scaling). compute precomputes windows_depth_mz once per PSM; site-determination and calculatePermutationPeptideScores_ use countMatchedMZ_ instead of numberOfMatchedIons_.
LucXor hoisted density evaluators per charge model
onsite/lucxor/psm.py
score_permutations detects HCD vs CID once, binds HCD density methods or computes CID Gaussian log-density constants and defines local _intensity_density/_distance_density helpers, replacing per-peak model dispatch in inner loop.
PhosphoRS binomial-tail and isoform m/z caching
onsite/phosphors/phosphors.py
Adds bounded FIFO _binomial_tail_cache (200k) with early-return lookup in all binomial_tail_probability branches. Extends _isoform_theo_mz with optional cache/cache_tag memoization; threads theo_cache through depth optimization and final scoring. Hoists constant tolerance in _count_matched_ions. Changes neutral-loss default to False. Switches final isomer normalization to log-space (log_p_inv and log-sum-exp).
Decoy FLR formula and PSM intersection unification
onsite/decoy_flr.py, tests/test_decoy_flr.py
Updates flr_curve with decoy-corrected formula dividing by (n - cum_decoy). Modifies PSM intersection in compute_tool_flr/compute_decoy_flr to use (spectrum_ref, unmod_seq) tuples for per-site FLR. Removes score binning from collapsed-site sorting, changes collapse default to False, applies strict q-value filtering. Test expectations updated for new FLR computation and intersection key format.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • bigbio/onsite#40: This PR addresses the issue by updating the decoy-amino-acid FLR estimator formula in onsite/decoy_flr.py to use decoy-corrected form and modifying PSM intersection logic to use (spectrum_ref, unmod_seq) pairs for proper site-level FLR calculation.

Possibly related PRs

  • bigbio/onsite#6: Both PRs modify AScore's ion-matching logic; this PR introduces numpy two-pointer matching with precomputed windows_depth_mz and countMatchedMZ_ as a follow-on optimization to the earlier foundation work.
  • bigbio/onsite#32: Both PRs touch LucXor's PSM scoring in onsite/lucxor/psm.py, specifically PSM.score_permutations density computation refactoring.
  • bigbio/onsite#44: Both PRs modify onsite/ascore/cli.py identification I/O by switching from direct idparquet helpers to the unified load_identifications/save_identifications layer.

Suggested labels

Review effort 4/5

Suggested reviewers

  • weizhongchun
  • timosachsenberg

Poem

🐇 A rabbit hops with purpose today,
I/O unified in every way!
NumPy pointers dance through m/z arrays bright,
Cached binomials and isoforms take flight!
From AScore to LucXor, all tools now align,
This refactor's thoroughness is truly divine! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.11% 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 clearly and concisely describes the main objective: introducing format-agnostic identification I/O with support for idXML, mzIdentML, and idParquet formats, which is the central focus of all changes across multiple modules.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/format-agnostic-io

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.

@codacy-production

codacy-production Bot commented Jun 20, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 9 medium

Alerts:
⚠ 9 issues (≤ 0 issues of at least minor severity)

Results:
9 new issues

Category Results
Complexity 9 medium

View in Codacy

🟢 Metrics 170 complexity · 2 duplication

Metric Results
Complexity 170
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown

Algorithm Comparison Test Results

Click to expand test results
============================= test session starts ==============================
platform linux -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0 -- /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
cachedir: .pytest_cache
rootdir: /home/runner/work/onsite/onsite
configfile: pyproject.toml
plugins: cov-7.1.0
collecting ... Modification already exists in ModificationsDB. Skipping.PhosphoDecoy (A)
Modification already exists in ModificationsDB. Skipping.PhosphoDecoy (G)
Modification already exists in ModificationsDB. Skipping.PhosphoDecoy (L)
collected 3 items

tests/test_algorithm_comparison.py::TestAlgorithmComparison::test_lucxor_produces_phospho_sites [19:37:40] Loading identifications from /tmp/tmp8aatys17/lucxor.idparquet
Loaded 152 PSMs
Loaded 382 proteins
PASSED
tests/test_algorithm_comparison.py::TestAlgorithmComparison::test_ascore_produces_phospho_sites [19:37:41] Loading identifications from /tmp/tmpcitt58ol/ascore.idparquet
Loaded 152 PSMs
Loaded 382 proteins
PASSED
tests/test_algorithm_comparison.py::TestAlgorithmComparison::test_phosphors_produces_phospho_sites [19:37:42] Loading identifications from /tmp/tmp4b0s7fzh/phosphors.idparquet
Loaded 152 PSMs
Loaded 382 proteins
PASSED

============================== 3 passed in 4.53s ===============================

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
onsite/phosphors/cli.py (1)

137-147: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Initialize the debug logger before loading inputs.

With --debug, a failure from load_spectra() or the new _io_load() path leaves local logger undefined, so the outer exception handler can raise UnboundLocalError instead of reporting the real input error.

🐛 Proposed fix
     try:
         # Initialize processing pipeline
+        log_file = f"{out_file}.debug.log"
+        logger = log_debug(log_file, debug)
         exp = load_spectra(in_file)
         psms_df, proteins_df = load_identifications(id_file)
 
         # Build scan number to spectrum mapping for efficient lookup
         lookup = build_scan_to_spectrum_map(exp)
 
         # Initialize debug log (only when --debug)
-        log_file = f"{out_file}.debug.log"
-        logger = log_debug(log_file, debug)
         if debug:
🤖 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 `@onsite/phosphors/cli.py` around lines 137 - 147, The debug logger
initialization using log_debug() happens after load_spectra() and
load_identifications() are called, so if either of these functions fails when
--debug is enabled, the logger variable will be undefined and cause an
UnboundLocalError in the exception handler instead of reporting the actual input
error. Move the logger initialization using log_debug() to the beginning of the
try block, before calling load_spectra() and load_identifications(), so the
logger is always available for exception handling.
🧹 Nitpick comments (4)
onsite/phosphors/phosphors.py (1)

136-140: 💤 Low value

Optional: Optimize FIFO eviction to avoid full key list allocation.

The current eviction creates a full list of all cache keys (potentially 200,000 elements) just to delete 1-2 oldest entries. While functionally correct, using next(iter(_binomial_tail_cache)) would be more efficient.

⚡ Proposed optimization
 def _store_binomial_tail(key, value: float) -> float:
     """Store a computed binomial-tail value under its (k, n, p) key (bounded
     FIFO). Returns the value so callers can ``return _store_binomial_tail(...)``."""
     if len(_binomial_tail_cache) >= BINOMIAL_TAIL_CACHE_SIZE:
-        for old_key in list(_binomial_tail_cache.keys())[
-            : len(_binomial_tail_cache) - BINOMIAL_TAIL_CACHE_SIZE + 1
-        ]:
+        num_to_delete = len(_binomial_tail_cache) - BINOMIAL_TAIL_CACHE_SIZE + 1
+        for _ in range(num_to_delete):
+            old_key = next(iter(_binomial_tail_cache))
             del _binomial_tail_cache[old_key]
     _binomial_tail_cache[key] = value
     return value
🤖 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 `@onsite/phosphors/phosphors.py` around lines 136 - 140, The cache eviction
logic in the _binomial_tail_cache cleanup block creates a full list of all
dictionary keys just to delete one or two oldest entries, which is inefficient
for large caches. Replace the slice-based approach with a loop that repeatedly
uses next(iter(_binomial_tail_cache)) to get and delete the oldest (first) key
from the dictionary in each iteration, since Python dictionaries maintain
insertion order. This avoids allocating a potentially large list and achieves
the same FIFO eviction behavior with better performance.
onsite/lucxor/cli.py (1)

52-63: ⚡ Quick win

Update LucXor help text for format-agnostic IDs.

The command now accepts and writes idParquet, idXML, and mzIdentML through onsite.id_io, but the help still tells users both paths are idParquet-only.

♻️ Proposed fix
     required=True,
     type=click.Path(exists=True),
-    help="Input identification file (idparquet directory)"
+    help="Input identification file (.idparquet, .idXML, .mzid, or .mzIdentML)"
 )
@@
     required=True,
     type=click.Path(),
-    help="Output file (idparquet directory)"
+    help="Output identification file (.idparquet, .idXML, .mzid, or .mzIdentML)"
 )
🤖 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 `@onsite/lucxor/cli.py` around lines 52 - 63, The help text for both the input
and output options in the click decorators is outdated and only mentions
idParquet format support. Update the help parameter in the input option
decorator to indicate it accepts multiple ID formats (idParquet, idXML, and
mzIdentML) instead of just idparquet directory, and similarly update the help
parameter in the output option decorator to reflect the same format-agnostic
capabilities since the command now supports these multiple formats through
onsite.id_io.
tests/test_id_io.py (1)

645-776: ⚡ Quick win

Add save→load coverage for decoy and protein evidence preservation.

The mzIdentML/idXML round-trip tests currently cover rows, peptidoforms, scores, and metavalues, but not is_decoy or protein_accessions; those are high-value schema fields for the new conversion layer.

🧪 Proposed regression coverage
 class TestSaveLoadMzidRoundtrip:
@@
     def test_mzid_metavalue_survives(self, tmp_path):
         """A psm_metavalue injected before save must appear in the reloaded row."""
@@
         assert "AScore_site_scores" in names_rt, (
             f"AScore_site_scores not found in reloaded metavalues: {names_rt}"
         )
+
+    def test_mzid_decoy_and_protein_accessions_survive(self, tmp_path):
+        from onsite.id_io import save_identifications
+        import pyopenms as oms
+
+        prot, pep_list = _build_pep_list_with_protein()
+        path_in = str(tmp_path / "source.idXML")
+        oms.IdXMLFile().store(path_in, prot, pep_list)
+        psms_df, proteins_df, *_ = load_identifications(path_in)
+
+        path_out = str(tmp_path / "out.mzid")
+        save_identifications(path_out, psms_df, proteins_df)
+        psms2, *_ = load_identifications(path_out)
+
+        assert psms2["protein_accessions"].iloc[0][0]["accession"] == "PROT_A"
🤖 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_id_io.py` around lines 645 - 776, Add two new test methods to the
TestSaveLoadMzidRoundtrip class following the same pattern as the existing
round-trip tests (test_row_count_preserved, test_peptidoform_preserved,
test_mzid_score_preserved, etc.). First, create a test_is_decoy_preserved method
that loads identifications from idXML, saves to mzid, reloads the data, and
asserts that the is_decoy column values are identical before and after the
round-trip. Second, create a test_protein_accessions_preserved method that
similarly verifies the protein_accessions field values are preserved through the
save and load cycle. Both tests should use the same pattern: call
_build_pep_list(), store to idXML using pyopenms, load with
load_identifications, save with save_identifications to mzid format, reload with
load_identifications, and assert the field values match between original and
reloaded dataframes.
onsite/ascore/ascore.py (1)

443-443: 💤 Low value

Consider using min() for clarity.

The condition i if size > i else size is equivalent to min(i, size) and reads more directly.

♻️ Suggested simplification
-            k = i if size > i else size
+            k = min(i, size)
🤖 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 `@onsite/ascore/ascore.py` at line 443, The assignment to variable k uses a
ternary conditional operator to get the smaller of two values. Replace the
ternary expression `i if size > i else size` with the built-in min() function by
assigning `k = min(i, size)` instead, which is more concise and clearly
expresses the intent to get the minimum value between i and size.
🤖 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 `@onsite/ascore/cli.py`:
- Around line 114-119: The debug logger is initialized after calling
load_spectra() and load_identifications(), which means if either function raises
an exception during debug mode, the except block will reference an undefined
logger variable and mask the original error. Move the log_debug call that
assigns to the logger variable to occur before the calls to load_spectra() and
load_identifications() so that the logger is available in the except block
regardless of where the exception originates.

In `@onsite/id_io.py`:
- Around line 127-128: The detect_format() function cannot recognize an
idParquet directory format when the path does not yet exist, preventing new
directory-style outputs from being created properly. Before calling
detect_format(path), add logic to handle the case where the directory does not
exist yet. Either modify detect_format() to recognize idParquet format from the
path string itself even when the directory hasn't been created, or add a check
that allows the format detection to infer the intended format for new
directories so that the suffix-normalization path in the scratch writer becomes
reachable for new idParquet outputs.
- Around line 318-334: The fallback score retrieval logic in the nested loop
starting after the check "if score == 0.0" is too permissive and accepts any
numeric metadata value that is not in _NON_SCORE_KEYS, which can corrupt the
score with unrelated numeric data like counts or FLR values. Instead of
iterating through all meta_by_name items and accepting any numeric value,
replace this loop with logic that only accepts score values from an explicit
whitelist of known score key names. Keep the initial attempt to recover the
score from the specific score_type key, but remove or refactor the arbitrary
fallback loop to only consider values from predefined score key candidates.
- Around line 474-512: The _psms_df_to_peptide_ids() function is not restoring
the target_decoy metavalue and PeptideEvidence objects when reconstructing
PeptideHit objects from the DataFrame, causing is_decoy and protein_accessions
data to be lost during idXML/mzIdentML save-load cycles. After setting the score
on the hit object (following hit.setScore), retrieve the is_decoy column value
from the current row and set it as the target_decoy metavalue on the hit, then
retrieve the protein_accessions array from the row and create PeptideEvidence
objects for each protein accession and attach them to the hit before appending
it to the hits list.

In `@onsite/idparquet.py`:
- Around line 402-410: The code silently converts NaN values to True when
casting boolean columns like is_decoy or higher_score_better to bool dtype using
astype(bool), because NaN is treated as True. Modify the casting logic in the
else block after the check for _int32_dtype_cols to identify when the target
dtype is bool and fill missing values with False before calling astype(dtype),
similar to how _int32_dtype_cols columns fill NaN with -1 before casting. This
ensures boolean columns don't have their missing values silently converted to
True during the dtype conversion.

In `@onsite/lucxor/cli.py`:
- Around line 420-435: The code unconditionally uses posterior_error_probability
to score each PeptideHit via the hit.setScore() method without checking if it's
NaN, which occurs for idXML/mzIdentML conversions. Instead, check if the
posterior_error_probability value is NaN, and if so, use the imported PSM score
from the score column. Update both the hit.setScore() call and the
hit.setMetaValue() call to conditionally use either the
posterior_error_probability when it's available, or fall back to the score
column value when it's NaN.

---

Outside diff comments:
In `@onsite/phosphors/cli.py`:
- Around line 137-147: The debug logger initialization using log_debug() happens
after load_spectra() and load_identifications() are called, so if either of
these functions fails when --debug is enabled, the logger variable will be
undefined and cause an UnboundLocalError in the exception handler instead of
reporting the actual input error. Move the logger initialization using
log_debug() to the beginning of the try block, before calling load_spectra() and
load_identifications(), so the logger is always available for exception
handling.

---

Nitpick comments:
In `@onsite/ascore/ascore.py`:
- Line 443: The assignment to variable k uses a ternary conditional operator to
get the smaller of two values. Replace the ternary expression `i if size > i
else size` with the built-in min() function by assigning `k = min(i, size)`
instead, which is more concise and clearly expresses the intent to get the
minimum value between i and size.

In `@onsite/lucxor/cli.py`:
- Around line 52-63: The help text for both the input and output options in the
click decorators is outdated and only mentions idParquet format support. Update
the help parameter in the input option decorator to indicate it accepts multiple
ID formats (idParquet, idXML, and mzIdentML) instead of just idparquet
directory, and similarly update the help parameter in the output option
decorator to reflect the same format-agnostic capabilities since the command now
supports these multiple formats through onsite.id_io.

In `@onsite/phosphors/phosphors.py`:
- Around line 136-140: The cache eviction logic in the _binomial_tail_cache
cleanup block creates a full list of all dictionary keys just to delete one or
two oldest entries, which is inefficient for large caches. Replace the
slice-based approach with a loop that repeatedly uses
next(iter(_binomial_tail_cache)) to get and delete the oldest (first) key from
the dictionary in each iteration, since Python dictionaries maintain insertion
order. This avoids allocating a potentially large list and achieves the same
FIFO eviction behavior with better performance.

In `@tests/test_id_io.py`:
- Around line 645-776: Add two new test methods to the TestSaveLoadMzidRoundtrip
class following the same pattern as the existing round-trip tests
(test_row_count_preserved, test_peptidoform_preserved,
test_mzid_score_preserved, etc.). First, create a test_is_decoy_preserved method
that loads identifications from idXML, saves to mzid, reloads the data, and
asserts that the is_decoy column values are identical before and after the
round-trip. Second, create a test_protein_accessions_preserved method that
similarly verifies the protein_accessions field values are preserved through the
save and load cycle. Both tests should use the same pattern: call
_build_pep_list(), store to idXML using pyopenms, load with
load_identifications, save with save_identifications to mzid format, reload with
load_identifications, and assert the field values match between original and
reloaded dataframes.
🪄 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: f3cb964b-2e42-4882-bc56-3802e7f64954

📥 Commits

Reviewing files that changed from the base of the PR and between 40a56b4 and 3f95e80.

📒 Files selected for processing (10)
  • onsite/ascore/ascore.py
  • onsite/ascore/cli.py
  • onsite/id_io.py
  • onsite/idparquet.py
  • onsite/lucxor/cli.py
  • onsite/lucxor/psm.py
  • onsite/onsitec.py
  • onsite/phosphors/cli.py
  • onsite/phosphors/phosphors.py
  • tests/test_id_io.py

Comment thread onsite/ascore/cli.py
Comment thread onsite/id_io.py Outdated
Comment thread onsite/id_io.py Outdated
Comment thread onsite/id_io.py
Comment thread onsite/idparquet.py
Comment thread onsite/lucxor/cli.py Outdated
…s_decoy/protein restore, score-recovery, extensionless idparquet, debug logger order

#6 (CRITICAL) lucxor/cli.py: when posterior_error_probability is NaN for all
rows, fall back to df's score_type/higher_score_better and use row['score']
instead of NaN for hit.setScore(); skip 'Posterior Error Probability' metavalue.

#5 (Major) idparquet.py save_psms_from_scratch: bool columns (is_decoy,
higher_score_better) now filled with False before astype(bool) to prevent
NaN→True coercion. Uses map-based approach to avoid pandas FutureWarning.

#4 (Major) id_io.py _psms_df_to_peptide_ids: restore is_decoy as
target_decoy metavalue and rebuild PeptideEvidence objects from
protein_accessions dicts on the save path. Also register all referenced
protein accessions as ProteinHit entries in ProteinIdentification (required
by idXML format).

#3 (Major) id_io.py score-recovery: removed the fallback loop scanning
arbitrary numeric metavalues. Only the score_type-named metavalue lookup is
kept. Save path now explicitly stores score under score_type name as a
metavalue to enable recovery after mzIdentML round-trip (where score_type
is renamed). Updated two pre-existing mzid score tests accordingly.

#2 (Major) id_io.py save_identifications: wrap detect_format() in try/except;
on ValueError with no file extension, treat as idparquet. load side unchanged.

#1 (Major) ascore/cli.py: moved logger = log_debug(...) above load_spectra()
and load_identifications() calls so the except block can reference logger when
either load raises.

Tests: 82 passed (test_id_io.py +7 new tests, test_lucxor/ascore/phosphors all green)
@ypriverol

Copy link
Copy Markdown
Member Author

Addressed all 6 review findings in d1dadd7 (+5 regression tests; full suite 177 passing):

  1. ascore/cli.py — initialize the debug logger before the input loads so a load failure under --debug surfaces the real error instead of a NameError.
  2. id_io.save_identifications — accept an extensionless idParquet output path (e.g. -out results) instead of raising.
  3. id_io score recovery — removed the fallback that scanned arbitrary numeric metavalues; a legitimate 0.0 score is no longer overwritten by unrelated metadata (counts/FLR). Recovery now only uses the score_type-named key.
  4. id_io._psms_df_to_peptide_ids — restore is_decoy (target_decoy metavalue) and protein_accessions (PeptideEvidence) on save, so they survive a save→load cycle.
  5. idparquet.save_psms_from_scratch — fill boolean columns (is_decoy/higher_score_better) with False before astype(bool) so NaN isn't silently coerced to True.
  6. lucxor/cli.py (critical) — fall back to the row's score/score_type when posterior_error_probability is NaN (idXML/mzid inputs carry no PEP), preventing NaN PSM scores from breaking model selection.

New tests cover each path (these were previously only exercised by end-to-end tests that skip without a local mzML).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (4)
onsite/id_io.py (1)

292-296: ⚠️ Potential issue | 🟠 Major

Handle bytes returned by pyOpenMS getAABefore()/getAAAfter() to prevent data corruption.

The codebase already demonstrates defensive bytes handling at line 306 using k.decode() if isinstance(k, bytes), but lines 293-294 lack this protection. If pyOpenMS returns bytes (likely, given tests use setAABefore(b"K")), then str(b'K') produces "b'K'" instead of "K", corrupting the flanking residue data on save/load round-trip.

Suggested fix
             pa_list.append({
                 "accession": ev.getProteinAccession(),
-                "aa_before": str(ev.getAABefore()),
-                "aa_after": str(ev.getAAAfter()),
+                "aa_before": ev.getAABefore().decode() if isinstance(ev.getAABefore(), bytes) else ev.getAABefore(),
+                "aa_after": ev.getAAAfter().decode() if isinstance(ev.getAAAfter(), bytes) else ev.getAAAfter(),
                 "start": ev.getStart(),
                 "end": ev.getEnd(),
             })
🤖 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 `@onsite/id_io.py` around lines 292 - 296, The aa_before and aa_after fields on
lines 293-294 do not defensively handle bytes returned by getAABefore() and
getAAAfter() methods. Apply the same bytes-handling pattern already demonstrated
at line 306 by checking if the return value is bytes using isinstance() and
calling decode() if needed, otherwise use the value as-is. Replace the str()
calls around getAABefore() and getAAAfter() with conditional expressions that
properly decode bytes to strings.
onsite/lucxor/cli.py (1)

881-883: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

protein_accessions is unconditionally cleared, discarding any existing protein mappings.

The output DataFrame sets protein_accessions to an empty array, which loses protein-to-peptide associations that were present in the input. If downstream processes rely on protein accessions (e.g., for protein-level reporting or filtering), they will receive no data.

Consider copying the original protein_accessions from the input DataFrame row, similar to how psm_metavalues is preserved:

Suggested approach
+            # Preserve protein_accessions from input if available
+            _prot_acc = np.array([], dtype=object)
+            if _in_rec is not None and "_protein_accessions" in _input_by_ref.get(spec_ref, {}):
+                _prot_acc = _input_by_ref[spec_ref].get("_protein_accessions", np.array([], dtype=object))
+            # or lookup from template by spectrum_reference
+
             result_rows.append({
                 ...
-                "protein_accessions": np.array([], dtype=object),
+                "protein_accessions": _prot_acc,

This would require storing protein_accessions in _input_by_ref during the pre-filter step at lines 776-786.

🤖 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 `@onsite/lucxor/cli.py` around lines 881 - 883, The protein_accessions field in
the output DataFrame is unconditionally set to an empty array, discarding the
original protein accession mappings from the input. To fix this, first modify
the pre-filter step around lines 776-786 to store the original
protein_accessions from the input DataFrame row into the _input_by_ref
dictionary (similar to how psm_metavalues is preserved). Then, in the output
DataFrame construction around lines 881-883, replace the empty array assignment
for protein_accessions with a reference to the stored value from _input_by_ref,
ensuring the original protein-to-peptide associations are preserved in the
output.
onsite/decoy_flr.py (2)

88-95: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Division by zero when leading sites are all decoys.

When cum_target (which equals ranks - cum_decoy) is zero for some prefix of the ranking—i.e., the top-ranked site(s) are all decoys—line 94 divides by zero, producing inf. The subsequent np.minimum(flr_raw, 1.0) does not cap infinity (since inf > 1.0 evaluates to True, minimum keeps inf).

This can occur in edge cases where the highest-confidence localizations are decoy sites (e.g., Ala winning over S/T/Y in ambiguous peptides).

🐛 Proposed fix: guard against zero denominator
     ratio = (t_c / x_c) if x_c > 0 else 0.0
-    flr_raw = (ratio * cum_decoy) / (ranks - cum_decoy)
+    denom = ranks - cum_decoy
+    with np.errstate(divide='ignore', invalid='ignore'):
+        flr_raw = np.where(denom > 0, (ratio * cum_decoy) / denom, 1.0)
     flr_raw = np.minimum(flr_raw, 1.0)  # an FLR cannot exceed 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 `@onsite/decoy_flr.py` around lines 88 - 95, The flr_raw calculation divides by
(ranks - cum_decoy) which can be zero when the top-ranked sites are all decoys,
resulting in infinity values that cannot be properly capped by np.minimum. Add a
guard condition to prevent division by zero by checking if (ranks - cum_decoy)
equals zero before the division in the flr_raw assignment. When the denominator
would be zero, set flr_raw to an appropriate value (such as 1.0, representing
maximum false localization rate). This ensures the result stays within the valid
range [0, 1] and handles the edge case where the highest-confidence
localizations are decoy sites.

78-95: ⚠️ Potential issue | 🔴 Critical

Fix test parameter type and expected FLR values for new formula.

The test at line 183 passes keep = {"s1", "s2", "s4", "s5"} (string set), but the implementation at line 326 expects keep_refs to contain (spectrum_ref, unmod_seq) tuples. The test will fail with a type error; convert keep to a tuple set.

Additionally, the test assertion at line 30 expects flr_raw = [0, 0, 0, 0, 0.4] based on the old formula comment at line 29 (2*(4/4)*1/5), but the new formula yields (4/4)*1/(5-1) = 0.25 for rank 5. Update the assertion to match the new formula. The comment at line 191 ("factor 2*(4/1)=8") also references the old formula and needs updating.

🤖 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 `@onsite/decoy_flr.py` around lines 78 - 95, The test at line 183 passes keep
as a string set but the implementation expects keep_refs to contain
(spectrum_ref, unmod_seq) tuples, causing a type error. Convert the keep
parameter from a string set to a tuple set with the appropriate tuple structure.
Additionally, update the test assertion at line 30 for flr_raw from [0, 0, 0, 0,
0.4] to match the new FLR formula (ratio * cum_decoy) / (ranks - cum_decoy),
which yields 0.25 instead of 0.4 for rank 5. Also update the comment at line 191
that references the old formula multiplier to reflect the new formula
calculation.
🤖 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 `@onsite/decoy_flr.py`:
- Around line 360-362: The comment above the collapsed.sort method call
references "bin score to 2 dp" but the actual sort key no longer includes the
round(t[0], 2) operation - it only sorts by the raw score t[0] and PSM count
t[2]. Update the comment to accurately reflect the current sorting behavior by
removing the reference to 2-decimal precision binning, or replace it with a
comment that describes what the code actually does: sorting by score and
tie-breaking by supporting-PSM count without any binning.

In `@tests/test_id_io.py`:
- Line 996: The assertion on line 996 uses equality comparison with boolean
literals (== False) which violates Ruff E712. Replace the equality check against
False by casting the value to bool() and using identity comparison (is) instead.
The same fix should be applied to the assertion on line 1046. This maintains
scalar compatibility while satisfying the Ruff linter rule.

---

Outside diff comments:
In `@onsite/decoy_flr.py`:
- Around line 88-95: The flr_raw calculation divides by (ranks - cum_decoy)
which can be zero when the top-ranked sites are all decoys, resulting in
infinity values that cannot be properly capped by np.minimum. Add a guard
condition to prevent division by zero by checking if (ranks - cum_decoy) equals
zero before the division in the flr_raw assignment. When the denominator would
be zero, set flr_raw to an appropriate value (such as 1.0, representing maximum
false localization rate). This ensures the result stays within the valid range
[0, 1] and handles the edge case where the highest-confidence localizations are
decoy sites.
- Around line 78-95: The test at line 183 passes keep as a string set but the
implementation expects keep_refs to contain (spectrum_ref, unmod_seq) tuples,
causing a type error. Convert the keep parameter from a string set to a tuple
set with the appropriate tuple structure. Additionally, update the test
assertion at line 30 for flr_raw from [0, 0, 0, 0, 0.4] to match the new FLR
formula (ratio * cum_decoy) / (ranks - cum_decoy), which yields 0.25 instead of
0.4 for rank 5. Also update the comment at line 191 that references the old
formula multiplier to reflect the new formula calculation.

In `@onsite/id_io.py`:
- Around line 292-296: The aa_before and aa_after fields on lines 293-294 do not
defensively handle bytes returned by getAABefore() and getAAAfter() methods.
Apply the same bytes-handling pattern already demonstrated at line 306 by
checking if the return value is bytes using isinstance() and calling decode() if
needed, otherwise use the value as-is. Replace the str() calls around
getAABefore() and getAAAfter() with conditional expressions that properly decode
bytes to strings.

In `@onsite/lucxor/cli.py`:
- Around line 881-883: The protein_accessions field in the output DataFrame is
unconditionally set to an empty array, discarding the original protein accession
mappings from the input. To fix this, first modify the pre-filter step around
lines 776-786 to store the original protein_accessions from the input DataFrame
row into the _input_by_ref dictionary (similar to how psm_metavalues is
preserved). Then, in the output DataFrame construction around lines 881-883,
replace the empty array assignment for protein_accessions with a reference to
the stored value from _input_by_ref, ensuring the original protein-to-peptide
associations are preserved in the output.
🪄 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: fcef327a-a337-40d2-9324-29176b216d19

📥 Commits

Reviewing files that changed from the base of the PR and between 3f95e80 and 96b2fcc.

📒 Files selected for processing (8)
  • onsite/ascore/ascore.py
  • onsite/ascore/cli.py
  • onsite/decoy_flr.py
  • onsite/id_io.py
  • onsite/idparquet.py
  • onsite/lucxor/cli.py
  • onsite/phosphors/phosphors.py
  • tests/test_id_io.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • onsite/ascore/cli.py
  • onsite/ascore/ascore.py
  • onsite/idparquet.py

Comment thread onsite/decoy_flr.py Outdated
Comment thread tests/test_id_io.py

psms2, *_ = load_dataframes(out)
assert len(psms2) == 1
assert psms2["is_decoy"].iloc[0] is False or psms2["is_decoy"].iloc[0] == False, (

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace == True/False assertions to satisfy Ruff E712 and keep scalar compatibility.

Line 996 and Line 1046 use equality checks against boolean literals, which Ruff flags (E712). Cast to bool(...) and assert identity instead.

Suggested patch
-        assert psms2["is_decoy"].iloc[0] is False or psms2["is_decoy"].iloc[0] == False, (
+        assert bool(psms2["is_decoy"].iloc[0]) is False, (
             f"Expected is_decoy=False for NaN input, got {psms2['is_decoy'].iloc[0]!r}"
         )
...
-        assert row2["is_decoy"] is True or row2["is_decoy"] == True, (
+        assert bool(row2["is_decoy"]) is True, (
             f"Expected is_decoy=True after round-trip, got {row2['is_decoy']!r}"
         )

Also applies to: 1046-1046

🧰 Tools
🪛 Ruff (0.15.17)

[error] 996-996: Avoid equality comparisons to False; use not psms2["is_decoy"].iloc[0]: for false checks

Replace with not psms2["is_decoy"].iloc[0]

(E712)

🤖 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_id_io.py` at line 996, The assertion on line 996 uses equality
comparison with boolean literals (== False) which violates Ruff E712. Replace
the equality check against False by casting the value to bool() and using
identity comparison (is) instead. The same fix should be applied to the
assertion on line 1046. This maintains scalar compatibility while satisfying the
Ruff linter rule.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/test_decoy_flr.py`:
- Around line 29-32: The comment on the line before the assert_allclose call for
curve["flr_raw"] contains an arithmetic error. The comment states the
calculation equals 0.4, but the correct result of (4/4)*1/(5-1) is 0.25, which
matches the actual assertion value in
np.testing.assert_allclose(curve["flr_raw"], [0, 0, 0, 0, 0.25]). Update the
comment to show the correct result of 0.25 instead of 0.4 so the documentation
matches the test expectation.
🪄 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: dfeaad05-3f4b-4f31-9710-bd6dab57b07d

📥 Commits

Reviewing files that changed from the base of the PR and between 96b2fcc and 7fc415f.

📒 Files selected for processing (4)
  • onsite/decoy_flr.py
  • onsite/id_io.py
  • onsite/idparquet.py
  • tests/test_decoy_flr.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • onsite/idparquet.py
  • onsite/id_io.py

Comment thread tests/test_decoy_flr.py
Comment on lines +29 to +32
# flr_raw: 0,0,0,0, (4/4)*1/(5-1) = 0.4
np.testing.assert_allclose(curve["flr_raw"], [0, 0, 0, 0, 0.25])
# qval == flr_raw here (already monotone non-decreasing)
np.testing.assert_allclose(curve["qval"], [0, 0, 0, 0, 0.4])
np.testing.assert_allclose(curve["qval"], [0, 0, 0, 0, 0.25])

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Typo in comment: arithmetic result is wrong.

The comment says (4/4)*1/(5-1) = 0.4 but the correct result is 1*1/4 = 0.25, which matches the assertion. The = 0.4 should be = 0.25.

📝 Suggested fix
-    # flr_raw: 0,0,0,0, (4/4)*1/(5-1) = 0.4
+    # flr_raw: 0,0,0,0, (4/4)*1/(5-1) = 0.25
📝 Committable suggestion

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

Suggested change
# flr_raw: 0,0,0,0, (4/4)*1/(5-1) = 0.4
np.testing.assert_allclose(curve["flr_raw"], [0, 0, 0, 0, 0.25])
# qval == flr_raw here (already monotone non-decreasing)
np.testing.assert_allclose(curve["qval"], [0, 0, 0, 0, 0.4])
np.testing.assert_allclose(curve["qval"], [0, 0, 0, 0, 0.25])
# flr_raw: 0,0,0,0, (4/4)*1/(5-1) = 0.25
np.testing.assert_allclose(curve["flr_raw"], [0, 0, 0, 0, 0.25])
# qval == flr_raw here (already monotone non-decreasing)
np.testing.assert_allclose(curve["qval"], [0, 0, 0, 0, 0.25])
🤖 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_decoy_flr.py` around lines 29 - 32, The comment on the line before
the assert_allclose call for curve["flr_raw"] contains an arithmetic error. The
comment states the calculation equals 0.4, but the correct result of
(4/4)*1/(5-1) is 0.25, which matches the actual assertion value in
np.testing.assert_allclose(curve["flr_raw"], [0, 0, 0, 0, 0.25]). Update the
comment to show the correct result of 0.25 instead of 0.4 so the documentation
matches the test expectation.

@ypriverol ypriverol merged commit 4d0093d into main Jun 21, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants