diff --git a/psm_utils/io/percolator.py b/psm_utils/io/percolator.py index 07919ad..41f86ac 100644 --- a/psm_utils/io/percolator.py +++ b/psm_utils/io/percolator.py @@ -145,9 +145,19 @@ def _infer_charge_columns(fieldnames: list[str]) -> tuple[str | None, dict[int, @staticmethod def _parse_peptidoform(percolator_peptide: str, charge: int | None) -> Peptidoform: """Parse Percolator TSV peptide notation to Peptidoform.""" - # Remove leading and trailing amino acids + # Remove leading and trailing amino acids (e.g., R.PEPTIDE.S -> PEPTIDE) match = re.match(r"^(?:[A-Z-])?\.(.+)\.(?:[A-Z-])?$", percolator_peptide) - peptidoform = match[1] if match else percolator_peptide + peptidoform: str = match[1] if match else percolator_peptide + + # Handle Comet's n-terminal modification format: n[42.0106]PEPTIDE -> [42.0106]-PEPTIDE + peptidoform = re.sub(r"^n\[([+-]?[\w\.]*?)\]", r"[\1]-", peptidoform) + + # Handle Comet's c-terminal modification format: PEPTIDEc[-0.9840] -> PEPTIDE-[-0.9840] + peptidoform = re.sub(r"c\[([+-]?[\w\.]*?)\]$", r"-[\1]", peptidoform) + + # Ensure positive values inside square brackets have a '+' sign + peptidoform = re.sub(r"\[(\d+[\.]*\d*)]", r"[+\1]", peptidoform) + if charge: peptidoform += f"/{charge}" return Peptidoform(peptidoform) diff --git a/tests/test_io/test_percolator.py b/tests/test_io/test_percolator.py index fb5a794..ec93359 100644 --- a/tests/test_io/test_percolator.py +++ b/tests/test_io/test_percolator.py @@ -28,11 +28,32 @@ def test__infer_charge_columns(self): def test_parse_peptidoform(self): test_cases = [ + # Basic cases (("ACDEFGHR", None), "ACDEFGHR"), (("K.ACDEFGHR.I", 1), "ACDEFGHR/1"), (("K.ACDEFGHR.-", 2), "ACDEFGHR/2"), (("-.ACDEFGHR.I", 3), "ACDEFGHR/3"), (("-.ACDEFGHR.-", None), "ACDEFGHR"), + # N-terminal modifications + (("-.n[42.0106]ACDEFGHR.-", None), "[+42.0106]-ACDEFGHR"), + (("n[42.0106]ACDEFGHR", None), "[+42.0106]-ACDEFGHR"), # Without flanking + (("-.n[43]ACDEFGHR.-", 2), "[+43]-ACDEFGHR/2"), # Integer mass + # C-terminal modifications + (("-.ACDEFGHRc[-0.9840].-", None), "ACDEFGHR-[-0.984]"), + (("ACDEFGHRc[-0.9840]", None), "ACDEFGHR-[-0.984]"), # Without flanking + (("-.ACDEFGHRc[17.0265].-", 2), "ACDEFGHR-[+17.0265]/2"), # Positive C-term + # Internal modifications + (("-.ACDEFM[15.9949]GHR.-", None), "ACDEFM[+15.9949]GHR"), + (("-.ACDEM[-18.010565]GHR.-", None), "ACDEM[-18.010565]GHR"), # Negative internal + (("-.AC[57.021]DEFGHR.-", None), "AC[+57.021]DEFGHR"), # Carbamidomethyl + # Multiple modifications + (("-.n[43]ACDEFM[16]GHR.-", None), "[+43]-ACDEFM[+16]GHR"), # N-term + internal + (("-.ACDEFM[16]GHRc[-1].-", None), "ACDEFM[+16]GHR-[-1]"), # Internal + C-term + (("-.n[42]ACDEFM[16]GHRc[-1].-", 2), "[+42]-ACDEFM[+16]GHR-[-1]/2"), # All three + (("-.AC[57]DEM[16]GHK.-", None), "AC[+57]DEM[+16]GHK"), # Multiple internal + # Already has '+' sign (should not add another) + (("-.ACDEFM[+15.9949]GHR.-", None), "ACDEFM[+15.9949]GHR"), + (("-.n[+42.0106]ACDEFGHR.-", None), "[+42.0106]-ACDEFGHR"), ] for test_in, expected_out in test_cases: assert expected_out == PercolatorTabReader._parse_peptidoform(*test_in).proforma