Skip to content

Commit 429e491

Browse files
authored
Merge pull request #98 from hgb-bin-proteomics/feat-exculde_redundant_annotations
Excude redundant internal ions in fragannot
2 parents b690ada + dcc2671 commit 429e491

5 files changed

Lines changed: 95 additions & 38 deletions

File tree

internal_ions/fragannot/fragannot_call.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
from .fragannot_numba import FragannotNumba
22

3-
from typing import Dict
4-
from typing import List
53
from ..util.spectrumio import SpectrumFile
64
from psm_utils.psm_list import PSMList
75

86

97
def fragannot_call(spectrum_file: SpectrumFile,
108
psms: PSMList,
119
tolerance: float,
12-
fragment_types: List[str],
13-
charges: List[str],
14-
losses: List[str],
10+
nterm_fragment_types: list[str],
11+
cterm_fragment_types: list[str],
12+
charges: list[str],
13+
losses: list[str],
1514
deisotope: bool,
16-
verbose: bool = False) -> Dict:
15+
verbose: bool = False) -> dict:
1716

1817
frag = FragannotNumba(do_parallel = True)
18+
1919
fragannot_dict = frag.fragment_annotation(psms,
2020
spectrum_file,
2121
tolerance,
22-
fragment_types,
22+
nterm_fragment_types,
23+
cterm_fragment_types,
2324
charges,
2425
losses,
2526
deisotope,

internal_ions/fragannot/fragannot_numba.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
import re
44
from pyteomics import mass, parser
5-
from itertools import tee
5+
from itertools import tee, product
66
import ms_deisotope
77
import logging
88

@@ -38,7 +38,8 @@ def fragment_annotation(
3838
psms: PSMList,
3939
spectra_file: SpectrumFile,
4040
tolerance: float,
41-
fragment_types: list[str],
41+
nterm_fragment_types: list[str],
42+
cterm_fragment_types: list[str],
4243
charges: list[str],
4344
losses: list[str],
4445
deisotope: bool,
@@ -48,7 +49,8 @@ def fragment_annotation(
4849
psms=psms,
4950
spectra_file=spectra_file,
5051
tolerance=tolerance,
51-
fragment_types=fragment_types,
52+
nterm_fragment_types=nterm_fragment_types,
53+
cterm_fragment_types=cterm_fragment_types,
5254
charges=charges,
5355
losses=losses,
5456
deisotope=deisotope,
@@ -62,7 +64,8 @@ def fragment_annotation(
6264
psms: PSMList,
6365
spectra_file: SpectrumFile,
6466
tolerance: float,
65-
fragment_types: list[str],
67+
nterm_fragment_types: list[str],
68+
cterm_fragment_types: list[str],
6669
charges: list[str] | str,
6770
losses: list[str],
6871
deisotope: bool,
@@ -112,6 +115,8 @@ def fragment_annotation(
112115

113116
print(f"\nAnnotating spectra in {'parallel' if do_parallel else 'serial'}...\n")
114117

118+
internal_fragment_types = get_internal_ion_types(nterm_fragment_types, cterm_fragment_types)
119+
115120
if micro_batch:
116121
i = 0
117122
still_spectra_available = True
@@ -125,17 +130,17 @@ def fragment_annotation(
125130
current_batch = psms[i:len(psms)]
126131
still_spectra_available = False
127132
if do_parallel:
128-
p_result = Parallel(n_jobs=nr_used_cores)(delayed(calculate_ions_for_psms)(psm, tolerance, fragment_types, charges, losses, deisotope) for psm in current_batch)
133+
p_result = Parallel(n_jobs=nr_used_cores)(delayed(calculate_ions_for_psms)(psm, tolerance, nterm_fragment_types, cterm_fragment_types, internal_fragment_types, charges, losses, deisotope) for psm in current_batch)
129134
else:
130-
p_result = [calculate_ions_for_psms(psm, tolerance, fragment_types, charges, losses, deisotope) for psm in current_batch]
135+
p_result = [calculate_ions_for_psms(psm, tolerance, nterm_fragment_types, cterm_fragment_types, internal_fragment_types, charges, losses, deisotope) for psm in current_batch]
131136
psms_json += list(p_result)
132137
i += batch_size
133138
else:
134139
p_psms = tqdm(psms) # tqdm is good for cli but bad for streamlit progress
135140
if do_parallel:
136-
p_result = Parallel(n_jobs=nr_used_cores)(delayed(calculate_ions_for_psms)(psm, tolerance, fragment_types, charges, losses, deisotope) for psm in p_psms)
141+
p_result = Parallel(n_jobs=nr_used_cores)(delayed(calculate_ions_for_psms)(psm, tolerance, nterm_fragment_types, cterm_fragment_types, internal_fragment_types, charges, losses, deisotope) for psm in p_psms)
137142
else:
138-
p_result = [calculate_ions_for_psms(psm, tolerance, fragment_types, charges, losses, deisotope) for psm in p_psms]
143+
p_result = [calculate_ions_for_psms(psm, tolerance, nterm_fragment_types, cterm_fragment_types, internal_fragment_types, charges, losses, deisotope) for psm in p_psms]
139144
psms_json = list(p_result)
140145

141146
for psm in psms_json:
@@ -160,9 +165,25 @@ def fragment_annotation(
160165
return psms_dict
161166

162167

168+
def get_internal_ion_types(nterm_fragment_types: list[str], cterm_fragment_types: list[str]) -> list[str]:
169+
"""Get non-redundant internal ion types based on provided n-term and c-term fragment types."""
170+
internal_ions = []
171+
equiv_internal_ions = set()
172+
for n_frag, c_frag in product(nterm_fragment_types, cterm_fragment_types):
173+
equiv = constants.IDENTICAL_INTERNAL_IONS[(n_frag, c_frag)]
174+
if equiv not in equiv_internal_ions:
175+
equiv_internal_ions.add(equiv)
176+
internal_ions.append(f"{n_frag}:{c_frag}")
177+
else:
178+
logger.debug("Skipping redundant internal ion: %s", f"{n_frag}:{c_frag}")
179+
return internal_ions
180+
181+
163182
def calculate_ions_for_psms(psm,
164183
tolerance: float,
165-
fragment_types: list[str],
184+
nterm_fragment_types: list[str],
185+
cterm_fragment_types: list[str],
186+
internal_fragment_types: list[str],
166187
charges: list[str] | str,
167188
losses: list[str],
168189
deisotope: bool) -> dict[str, Any]:
@@ -182,11 +203,12 @@ def calculate_ions_for_psms(psm,
182203

183204
theoretical_fragment_code = compute_theoretical_fragments(
184205
sequence_length=len(psm.peptidoform.sequence),
185-
fragment_types=typed.List(fragment_types),
206+
n_term_ions=typed.List(nterm_fragment_types),
207+
c_term_ions=typed.List(cterm_fragment_types),
186208
charges=typed.List([int(c) for c in charges_used]),
187209
neutral_losses=typed.List(losses),
188-
ion_directions=ion_directions,
189-
internal=True
210+
# ion_directions=ion_directions,
211+
internal_ions=typed.List(internal_fragment_types),
190212
)
191213

192214
theoretical_fragment_dict = {
@@ -234,14 +256,16 @@ def deisotope_peak_list(mzs: list[float], intensities: list[float]) -> tuple[lis
234256
@jit(nopython=True, cache=True)
235257
def compute_theoretical_fragments(
236258
sequence_length: int,
237-
fragment_types: list[str],
259+
n_term_ions: list[str],
260+
c_term_ions: list[str],
238261
charges: list[int] = [1],
239262
neutral_losses: list[str] = [],
240-
ion_directions: dict[str, str] = {},
241-
internal: bool = True) -> list[str]:
263+
# ion_directions: dict[str, str] = {},
264+
internal_ions: list[str] = [],
265+
) -> list[str]:
242266

243-
n_term_ions = [ion_type for ion_type in fragment_types if ion_directions[ion_type] == "n-term"]
244-
c_term_ions = [ion_type for ion_type in fragment_types if ion_directions[ion_type] == "c-term"]
267+
# n_term_ions = [ion_type for ion_type in fragment_types if ion_directions[ion_type] == "n-term"]
268+
# c_term_ions = [ion_type for ion_type in fragment_types if ion_directions[ion_type] == "c-term"]
245269

246270
n_term = ["t:" + ion_type for ion_type in n_term_ions]
247271
c_term = [ion_type + ":t" for ion_type in c_term_ions]
@@ -276,20 +300,18 @@ def compute_theoretical_fragments(
276300

277301
internal_frags_with_nl = ["" for _ in range(0)] # typed empty list for Numba
278302

279-
if internal:
303+
if internal_ions:
280304
# internal fragments
281-
internal_frags = [
282-
f"{n_term_ion}:{c_term_ion}" for n_term_ion in n_term_ions for c_term_ion in c_term_ions
283-
]
305+
284306
internal_pos = [
285307
f"{i}:{j}"
286308
for i in range(2, sequence_length)
287309
for j in range(2, sequence_length)
288310
if i <= j
289311
]
290312
internal_frags = [
291-
f"{internal_ions}@{internal_positions}"
292-
for internal_ions in internal_frags
313+
f"{ions}@{internal_positions}"
314+
for ions in internal_ions
293315
for internal_positions in internal_pos
294316
]
295317

internal_ions/tab1.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def main(argv=None) -> None:
7272
else:
7373
st.warning("Please upload a spectrum file before the identifications file!", icon="⚠️")
7474

75-
st.session_state["fragannot_call_ion_selection"] = st.session_state["selected_ions_nterm"] + st.session_state["selected_ions_cterm"]
75+
# st.session_state["fragannot_call_ion_selection"] = st.session_state["selected_ions_nterm"] + st.session_state["selected_ions_cterm"]
7676

7777
charges_str = st.text_input("Charges to consider [comma delimited]:",
7878
value="+1",
@@ -119,7 +119,7 @@ def main(argv=None) -> None:
119119
run_info_str = f"\tSpectrum file name: {spectrum_file.name}\n" + \
120120
f"\tIdentifications file name: {st.session_state.identifications_file.name}\n" + \
121121
f"\tTolerance: {st.session_state.tolerance}\n" + \
122-
f"\tSelected ions: {', '.join(st.session_state['fragannot_call_ion_selection'])}\n" + \
122+
f"\tSelected ions: {', '.join(st.session_state['selected_ions_nterm'] + st.session_state['selected_ions_cterm'])}\n" + \
123123
f"\tCharges: {', '.join(st.session_state['charges'])}\n" + \
124124
f"\tLosses: {', '.join(st.session_state['losses'])}\n" + \
125125
f"\tDeisotope: {st.session_state.deisotope}"
@@ -130,7 +130,8 @@ def main(argv=None) -> None:
130130
result = fragannot_call(spectrum_file,
131131
psm_list,
132132
float(st.session_state.tolerance),
133-
st.session_state["fragannot_call_ion_selection"],
133+
st.session_state["selected_ions_nterm"],
134+
st.session_state["selected_ions_cterm"],
134135
st.session_state["charges"],
135136
st.session_state["losses"],
136137
st.session_state.deisotope)

internal_ions/tab2.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ def main(argv=None) -> None:
105105
st.markdown("**Figure 1:** Histogram illustrating the frequency distribution of ion types present in the dataset.")
106106

107107
with frag_center_plot_col1_2:
108-
st.markdown("**Ion Type Proportions**")
109-
st.plotly_chart(common_type_pie(st.session_state["frag_center_filtered"]), width="stretch")
110-
st.markdown("**Figure 2:** Pie chart displaying the proportional composition of ion types within the dataset.")
108+
if st.session_state["frag_center_filtered"].shape[0]:
109+
st.markdown("**Ion Type Proportions**")
110+
st.plotly_chart(common_type_pie(st.session_state["frag_center_filtered"]), width="stretch")
111+
st.markdown("**Figure 2:** Pie chart displaying the proportional composition of ion types within the dataset.")
111112

112113
st.markdown("**Distribution of m/z Values Across Ion Types**")
113114
st.plotly_chart(mz_dist_ion_type(st.session_state["frag_center_filtered"]), width="stretch")

internal_ions/util/constants.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pyteomics import mass
2+
import logging
23

34
from psm_utils.io import FILETYPES
45
SUPPORTED_FILETYPES = list(FILETYPES)
@@ -8,11 +9,20 @@
89
DIV_COLOR = "rainbow"
910
FRAGANNOT_ION_NAMES = ["a", "b", "c", "cdot", "c-1", "c+1", "x", "y", "z", "zdot", "z+1", "z+2", "z+3"]
1011

11-
ion_comp = mass.std_ion_comp.copy()
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class HashableComp(mass.Composition):
16+
def __hash__(self):
17+
return hash(tuple(sorted(self.items())))
18+
19+
20+
ion_comp = {key: HashableComp(val) for (key, val) in mass.std_ion_comp.items()}
21+
1222
for k in list(ion_comp):
1323
if k.endswith('dot'):
1424
ion_comp[k.replace('-', '')] = ion_comp.pop(k)
15-
ion_comp['t'] = {}
25+
ion_comp['t'] = HashableComp({})
1626

1727
ion_cap_delta_mass = {
1828
name: mass.calculate_mass(composition=comp, absolute=False) for (name, comp) in ion_comp.items()
@@ -35,6 +45,28 @@
3545
}
3646

3747

48+
def _identical_internal_ions() -> dict[tuple[str, str], tuple[str, str]]:
49+
nterm = [key for key, val in ion_direction.items() if val == "n-term"]
50+
cterm = [key for key, val in ion_direction.items() if val == "c-term"]
51+
internal_ion_comps = {}
52+
ion_mapping = {}
53+
for n in nterm:
54+
for c in cterm:
55+
internal_ion_comps.setdefault((ion_comp[n] + ion_comp[c]), []).append((n, c))
56+
for values in internal_ion_comps.values():
57+
values.sort(key=lambda x: len(x[0]) + len(x[1]))
58+
for v in values:
59+
ion_mapping[v] = values[0]
60+
return ion_mapping
61+
62+
IDENTICAL_INTERNAL_IONS = _identical_internal_ions()
63+
64+
for key, val in IDENTICAL_INTERNAL_IONS.items():
65+
if key != val:
66+
logger.debug(f"Identical internal ions: {key} -> {val}")
67+
68+
69+
3870
# ---------------------------------------------------------------------------- #
3971
# For visualization #
4072
# ---------------------------------------------------------------------------- #

0 commit comments

Comments
 (0)