Skip to content

Commit 35fb723

Browse files
authored
Merge pull request #14 from Rappsilber-Laboratory/development
higher level columns pass-down
2 parents 864da26 + e91dd1f commit 35fb723

10 files changed

Lines changed: 628 additions & 766 deletions

File tree

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
22
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
33

4-
name: Python package
4+
name: Python test
55

66
on:
77
push:
@@ -15,18 +15,23 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
python-version: ["3.10", "3.11", "3.12", "3.13"]
18+
python-version: ["3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
1919
steps:
2020
- uses: actions/checkout@v4
2121
- name: Set up Python ${{ matrix.python-version }}
22-
uses: actions/setup-python@v3
22+
uses: actions/setup-python@v5
2323
with:
2424
python-version: ${{ matrix.python-version }}
25+
allow-prereleases: true
2526
- name: Install dependencies
2627
run: |
2728
python -m pip install --upgrade pip
2829
python -m pip install pytest
29-
python -m pip install .
30+
if [ "${{ endsWith(matrix.python-version, 't') }}" == "true" ]; then
31+
python -m pip install ".[nogil]"
32+
else
33+
python -m pip install .
34+
fi
3035
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
3136
- name: Test with pytest
3237
run: |

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.13
1+
3.14t

Pipfile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@ verify_ssl = true
44
name = "pypi"
55

66
[packages]
7-
pandas = "*"
7+
pandas = ">=3.0.0"
88
polars = "*"
9-
xifdr = {file = ".", editable = true}
9+
numpy = ">=2.1.0"
10+
scipy = ">=1.15.0"
11+
psutil = "*"
12+
maturin = "*"
13+
setuptools-rust = "*"
14+
kneed = "*"
1015

1116
[dev-packages]
1217
sphinx = "*"
1318
sphinx-autodoc-typehints = "*"
1419
sphinx-rtd-theme = "*"
1520
pytest = "*"
21+
setuptools-scm = "*"
1622

17-
[requires]
18-
python_version = "3.13"
23+
[pipenv]
24+
allow_prereleases = true

Pipfile.lock

Lines changed: 326 additions & 611 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/basic_usage.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ Use `pip` to install xiFDR from PyPi:
1212
1313
$ pip install xifdr
1414
15+
If you use a free threaded python version install with the nogil option:
16+
17+
.. code-block:: shell
18+
19+
$ pip install 'xifdr[nogil]'
20+
1521
Input format
1622
------------
1723

pyproject.toml

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,34 @@ authors = [
1212
license = { text = "AGPL-3.0-or-later" }
1313
readme = "README.rst"
1414
requires-python = ">=3.9"
15+
16+
# Core dependencies (Standard behavior)
1517
dependencies = [
16-
"numpy",
17-
"pandas",
18-
"polars",
19-
"scipy",
18+
"numpy>=2.1.0",
19+
"pandas>=3.0.0",
20+
"scipy>=1.15.0",
2021
"psutil",
2122
"pyarrow",
2223
"setuptools_scm",
2324
"kneed",
25+
"polars",
26+
]
27+
28+
[project.optional-dependencies]
29+
# Usage: pip install "xifdr[nogil]"
30+
nogil = [
31+
# Free-threaded Polars Bridge for 3.13+
32+
"polars @ https://github.com/Rappsilber-Laboratory/polars-free-threaded/releases/download/v1.36.0-beta.2-ft/polars-1.36.0b2-py3-none-any.whl ; python_version >= '3.13'",
33+
34+
# Platform-specific Polars Runtime for 3.13
35+
"polars-runtime-32 @ https://github.com/Rappsilber-Laboratory/polars-free-threaded/releases/download/v1.36.0-beta.2-ft/polars_runtime_32-1.36.0b2-cp313-cp313t-macosx_11_0_arm64.whl ; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version == '3.13'",
36+
"polars-runtime-32 @ https://github.com/Rappsilber-Laboratory/polars-free-threaded/releases/download/v1.36.0-beta.2-ft/polars_runtime_32-1.36.0b2-cp313-cp313t-manylinux_2_39_x86_64.whl ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.13'",
37+
"polars-runtime-32 @ https://github.com/Rappsilber-Laboratory/polars-free-threaded/releases/download/v1.36.0-beta.2-ft/polars_runtime_32-1.36.0b2-cp313-cp313t-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.13'",
38+
39+
# Platform-specific Polars Runtime for 3.14
40+
"polars-runtime-32 @ https://github.com/Rappsilber-Laboratory/polars-free-threaded/releases/download/v1.36.0-beta.2-ft/polars_runtime_32-1.36.0b2-cp314-cp314t-macosx_11_0_arm64.whl ; sys_platform == 'darwin' and platform_machine == 'arm64' and python_version == '3.14'",
41+
"polars-runtime-32 @ https://github.com/Rappsilber-Laboratory/polars-free-threaded/releases/download/v1.36.0-beta.2-ft/polars_runtime_32-1.36.0b2-cp314-cp314t-manylinux_2_39_x86_64.whl ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.14'",
42+
"polars-runtime-32 @ https://github.com/Rappsilber-Laboratory/polars-free-threaded/releases/download/v1.36.0-beta.2-ft/polars_runtime_32-1.36.0b2-cp314-cp314t-win_amd64.whl ; sys_platform == 'win32' and python_version == '3.14'",
2443
]
2544

2645
[project.urls]
@@ -33,4 +52,4 @@ Issues = "https://github.com/Rappsilber-Laboratory/xiFDR-py/issues"
3352
packages = ["xifdr"]
3453

3554
[tool.setuptools_scm]
36-
write_to = "xifdr/_version.py" # Auto-generates version info
55+
write_to = "xifdr/_version.py"

tests/test_column_prep.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,35 @@ def test_column_prep():
3434
"start_pos_p1": [[1], [1, 2], [1, 2, 3]],
3535
"protein_p2": [['C'], ['E', 'E', 'E'], ['F', 'X']],
3636
"start_pos_p2": [[1], [1, 1, 2], [2, 1]],
37-
"link_pos_p1": [1, 2, 3],
38-
"link_pos_p2": [7, 8, 9],
37+
"link_pos_p1": [7, 2, 3],
38+
"link_pos_p2": [1, 8, 9],
3939
"sequence_p1": ['ABC', 'DEF', 'GHI'],
4040
"sequence_p2": ['ABC', 'AAA', 'DEF'],
4141
"decoy_p1": [False, False, True],
4242
"decoy_p2": [False, True, True],
4343
"score": [0.2, 1.2, 2.2],
4444
"fdr_group": ['between', 'self', 'between'],
4545
"decoy_class": ['TT', 'TD', 'DD'],
46-
"cl_pos_p1": [[1], [2, 3], [3, 4, 5]],
47-
"cl_pos_p2": [[7], [8, 8, 9], [10, 9]],
46+
"cl_pos_p1": [[7], [2, 3], [3, 4, 5]],
47+
"cl_pos_p2": [[1], [8, 8, 9], [10, 9]],
4848
"TT": [True, False, False],
4949
"TD": [False, True, False],
5050
"DD": [False, False, True],
5151
"coverage_p1": [0.5, 0.5, 0.5],
5252
"coverage_p2": [0.5, 0.5, 0.5],
5353
"protein_score_p1": [0.1, 0.6, 1.1],
54-
"protein_score_p2": [0.1, 0.6, 1.1]
54+
"protein_score_p2": [0.1, 0.6, 1.1],
55+
"group_swapped": [True, False, False],
5556
})
5657

5758
df_res = prepare_columns(df)
58-
assert_frame_equal(df_res, df_expect.select(df_res.columns))
59+
assert_frame_equal(
60+
df_res.select(df_expect.columns).sort(df_expect.columns),
61+
df_expect.sort(df_expect.columns)
62+
)
63+
# Check that the resulting state is constant
64+
df_res = prepare_columns(df)
65+
assert_frame_equal(
66+
df_res.sort(df_expect.columns),
67+
df_expect.select(df_res.columns).sort(df_expect.columns)
68+
)

xifdr/boosting.py

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@
99
from contextlib import closing
1010
from multiprocessing import get_context
1111
import multiprocessing.dummy as mp_dummy
12+
import sys
1213
from .fdr import full_fdr
1314
from .utils.column_preparation import prepare_columns
1415
from .utils.knee_finder import find_knees
1516

1617
logger = logging.getLogger(__name__)
1718

19+
def is_gil_enabled():
20+
if hasattr(sys, "_is_gil_enabled"):
21+
return sys._is_gil_enabled()
22+
return True
23+
1824
def boost(df: pl.DataFrame,
1925
csm_fdr: (float, float) = (0.0, 1.0),
2026
pep_fdr: (float, float) = (0.0, 1.0),
@@ -26,6 +32,7 @@ def boost(df: pl.DataFrame,
2632
boost_level: str = "ppi",
2733
boost_between: bool = True,
2834
method: str = "manhattan",
35+
decoy_adjunct: str = "REV_",
2936
countdown: int = 3,
3037
points: int = 10,
3138
n_jobs: int = -1,
@@ -84,6 +91,7 @@ def boost(df: pl.DataFrame,
8491
neg_boost_cols=neg_boost_cols,
8592
boost_level=boost_level,
8693
boost_between=boost_between,
94+
decoy_adjunct=decoy_adjunct,
8795
countdown=countdown,
8896
points=points,
8997
n_jobs=n_jobs,
@@ -102,11 +110,50 @@ def boost_manhattan(df: pl.DataFrame,
102110
neg_boost_cols: list = None,
103111
boost_level: str = "ppi",
104112
boost_between: bool = True,
113+
decoy_adjunct: str = "REV_",
105114
countdown: int = 3,
106115
points: int = 10,
107116
n_jobs: int = -1,
108117
**kwargs):
109-
df = prepare_columns(df)
118+
"""
119+
Core Entry point for Manhattan optimization of FDR.
120+
121+
Parameters
122+
----------
123+
df
124+
Input CSM/PSM dataframe
125+
csm_fdr
126+
Range of CSM-level FDR cutoffs
127+
pep_fdr
128+
Range of peptide-level FDR cutoffs
129+
prot_fdr
130+
Range of protein-level FDR cutoffs
131+
link_fdr
132+
Range of link-level FDR cutoffs
133+
ppi_fdr
134+
Range of protein pair level (PPI) FDR cutoffs
135+
boost_cols
136+
Columns where a HIGHER value is better (e.g. scores)
137+
neg_boost_cols
138+
Columns where a LOWER value is better (e.g. Mass Error)
139+
boost_level
140+
The FDR level to optimize for ('csm', 'pep', 'prot', 'link', 'ppi')
141+
boost_between
142+
Optimize only for between-protein links
143+
countdown
144+
Number of iterations without improvement before stopping
145+
points
146+
Grid points for Manhattan search
147+
n_jobs
148+
Number of parallel jobs. -1 for all available cores or automatic detection based on memory.
149+
kwargs
150+
Other parameters to be passed to `full_fdr`
151+
152+
Returns
153+
-------
154+
Best parameters found for the optimization
155+
"""
156+
df = prepare_columns(df, decoy_adjunct=decoy_adjunct)
110157
param_ranges = (
111158
csm_fdr,
112159
pep_fdr,
@@ -127,8 +174,11 @@ def boost_manhattan(df: pl.DataFrame,
127174
best_params += [maxi]
128175

129176
# Figure out knee points for starting
130-
df = prepare_columns(df)
131-
knee_points = find_knees(df.filter(pl.col ('fdr_group') == 'between'), **kwargs)
177+
knee_points = find_knees(
178+
df.filter(pl.col ('fdr_group') == 'between'),
179+
decoy_adjunct=decoy_adjunct,
180+
**kwargs
181+
)
132182
for i, p in enumerate(knee_points):
133183
best_params[i] = p
134184
# Clip to param max
@@ -161,6 +211,7 @@ def boost_manhattan(df: pl.DataFrame,
161211
neg_boost_cols=neg_boost_cols,
162212
boost_level=boost_level,
163213
boost_between=boost_between,
214+
decoy_adjunct=decoy_adjunct,
164215
**kwargs
165216
)
166217

@@ -173,11 +224,16 @@ def boost_manhattan(df: pl.DataFrame,
173224
n_jobs = max(max_mem_cpu, 1)
174225
n_jobs = min(n_jobs, os.cpu_count())
175226
logger.info(f"Using {n_jobs} CPUs based on available memory.")
176-
if n_jobs == 1:
177-
mp = mp_dummy
227+
if not is_gil_enabled():
228+
logger.info(f"Using {n_jobs} threads (GIL disabled).")
229+
pool_obj = mp_dummy.Pool(n_jobs)
230+
elif n_jobs == 1:
231+
pool_obj = mp_dummy.Pool(1)
178232
else:
179-
mp = get_context('spawn')
180-
with closing(mp.Pool(n_jobs)) as pool:
233+
# spawn is the only method that reliably works across all OSs with PyInstaller
234+
pool_obj = get_context('spawn').Pool(processes=n_jobs)
235+
236+
with closing(pool_obj) as pool:
181237
while True:
182238
grids = []
183239
for param_index in range(n_params):
@@ -255,6 +311,7 @@ def boost_manhattan(df: pl.DataFrame,
255311

256312
def _optimization_template(cutoffs,
257313
df: pl.DataFrame,
314+
decoy_adjunct: str = 'REV_',
258315
min_len: int = 5,
259316
unique_csm: bool = True,
260317
boost_cols: list = [],
@@ -263,7 +320,45 @@ def _optimization_template(cutoffs,
263320
boost_between: bool = True,
264321
td_prob: int = 2,
265322
td_prot_prob: int = 10,
266-
td_dd_ratio: float = 1.0) -> float:
323+
td_dd_ratio: float = 1.0,
324+
custom_aggs: dict = None) -> float:
325+
"""
326+
Template for parallel optimization and calculation of the score.
327+
328+
Parameters
329+
----------
330+
cutoffs
331+
A list of cutoffs for the different levels
332+
df
333+
The input CSM dataframe
334+
decoy_adjunct
335+
The prefix/suffix for decoy proteins
336+
min_len
337+
Minimum peptide length
338+
unique_csm
339+
Unique CSM aggregation
340+
boost_cols
341+
Columns to filter for HIGHER values
342+
neg_boost_cols
343+
Columns to filter for LOWER values
344+
boost_level
345+
The level to optimize for
346+
boost_between
347+
Optimize for between links
348+
td_prob
349+
Minimum threshold for TT/TD counts (except protein)
350+
td_prot_prob
351+
Minimum threshold for TT/TD counts on protein level
352+
td_dd_ratio
353+
Minimum ratio for matching DD/TD
354+
custom_aggs
355+
Custom aggregation expressions for the FDR levels
356+
357+
Returns
358+
-------
359+
Resulting score (negative estimated true positives)
360+
"""
361+
df_height = df.height
267362
fdrs = cutoffs[:5]
268363
col_levels = cutoffs[5:]
269364
neg_col_levels = col_levels[len(boost_cols):]
@@ -285,12 +380,14 @@ def _optimization_template(cutoffs,
285380
)
286381
result_all = full_fdr(
287382
df, *fdrs,
383+
decoy_adjunct=decoy_adjunct,
288384
min_len=min_len,
289385
unique_csm=unique_csm,
290386
prepare_column=False,
291387
td_prob=0,
292388
td_prot_prob=td_prot_prob,
293-
td_dd_ratio=0
389+
td_dd_ratio=0,
390+
custom_aggs=custom_aggs
294391
)
295392
result = result_all[boost_level]
296393
if boost_between:
@@ -316,6 +413,6 @@ def _optimization_template(cutoffs,
316413
td_prob_bad = gl_tt*cutoffs[li] < td_prob
317414
dd_prob_bad = gl_dd*td_dd_ratio > gl_td
318415
if td_prob_bad or dd_prob_bad:
319-
return -tp/df.height
416+
return -tp/df_height
320417

321418
return -tp

0 commit comments

Comments
 (0)