From 2c80a19a6e89e37f4ed63b57db47cbc96cc41733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20M=C3=A4dler?= <15019107+sophiamaedler@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:51:35 +0200 Subject: [PATCH 01/37] add support for fixed string arrays --- alphabase/io/tempmmap.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/alphabase/io/tempmmap.py b/alphabase/io/tempmmap.py index 62b572a4..4b5c177b 100644 --- a/alphabase/io/tempmmap.py +++ b/alphabase/io/tempmmap.py @@ -163,10 +163,16 @@ def array(shape: tuple, dtype: np.dtype, tmp_dir_abs_path: str = None) -> np.nda TEMP_DIR_NAME, f"temp_mmap_{np.random.randint(2**63, dtype=np.int64)}.hdf" ) - with h5py.File(temp_file_name, "w") as hdf_file: - array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = 0 - offset = array.id.get_offset() + if isinstance(dtype, np.dtypes.ObjectDType): + with h5py.File(temp_file_name, "w") as hdf_file: + array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) + array[0] = np.string_("") + offset = array.id.get_offset() + else: + with h5py.File(temp_file_name, "w") as hdf_file: + array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) + array[0] = 0 + offset = array.id.get_offset() with open(temp_file_name, "rb+") as raw_hdf_file: mmap_obj = mmap.mmap(raw_hdf_file.fileno(), 0, access=mmap.ACCESS_WRITE) @@ -223,10 +229,15 @@ def create_empty_mmap( else: temp_file_name = _get_file_location(file_path, overwrite=False) - with h5py.File(temp_file_name, "w") as hdf_file: - array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = 0 - + if isinstance(dtype, np.dtypes.ObjectDType): + with h5py.File(temp_file_name, "w") as hdf_file: + array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) + array[0] = np.string_("") + else: + with h5py.File(temp_file_name, "w") as hdf_file: + array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) + array[0] = 0 + return temp_file_name From 146c2cf1f37a6f68b73a03eae6ccbe257f6c9286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20M=C3=A4dler?= <15019107+sophiamaedler@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:57:32 +0200 Subject: [PATCH 02/37] run pre-commit hooks --- alphabase/io/tempmmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alphabase/io/tempmmap.py b/alphabase/io/tempmmap.py index 4b5c177b..a6779119 100644 --- a/alphabase/io/tempmmap.py +++ b/alphabase/io/tempmmap.py @@ -237,7 +237,7 @@ def create_empty_mmap( with h5py.File(temp_file_name, "w") as hdf_file: array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) array[0] = 0 - + return temp_file_name From 6ba9c1639190cf1a88c61bc18d32b95e9416871d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20M=C3=A4dler?= <15019107+sophiamaedler@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:26:50 +0200 Subject: [PATCH 03/37] implement suggested changes --- alphabase/io/tempmmap.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/alphabase/io/tempmmap.py b/alphabase/io/tempmmap.py index a6779119..dc2abb7c 100644 --- a/alphabase/io/tempmmap.py +++ b/alphabase/io/tempmmap.py @@ -163,16 +163,10 @@ def array(shape: tuple, dtype: np.dtype, tmp_dir_abs_path: str = None) -> np.nda TEMP_DIR_NAME, f"temp_mmap_{np.random.randint(2**63, dtype=np.int64)}.hdf" ) - if isinstance(dtype, np.dtypes.ObjectDType): - with h5py.File(temp_file_name, "w") as hdf_file: - array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = np.string_("") - offset = array.id.get_offset() - else: - with h5py.File(temp_file_name, "w") as hdf_file: - array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = 0 - offset = array.id.get_offset() + with h5py.File(temp_file_name, "w") as hdf_file: + array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) + array[0] = np.string_("") if isinstance(dtype, np.dtypes.ObjectDType) else 0 + offset = array.id.get_offset() with open(temp_file_name, "rb+") as raw_hdf_file: mmap_obj = mmap.mmap(raw_hdf_file.fileno(), 0, access=mmap.ACCESS_WRITE) @@ -229,14 +223,9 @@ def create_empty_mmap( else: temp_file_name = _get_file_location(file_path, overwrite=False) - if isinstance(dtype, np.dtypes.ObjectDType): - with h5py.File(temp_file_name, "w") as hdf_file: - array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = np.string_("") - else: - with h5py.File(temp_file_name, "w") as hdf_file: - array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = 0 + with h5py.File(temp_file_name, "w") as hdf_file: + array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) + array[0] = np.string_("") if isinstance(dtype, np.dtypes.ObjectDType) else 0 return temp_file_name From 483eaab529487909d034181207e5c2152b0a04c9 Mon Sep 17 00:00:00 2001 From: Mikhail Lebedev Date: Mon, 23 Sep 2024 21:34:47 +0200 Subject: [PATCH 04/37] NEW: smiles for peptides --- alphabase/smiles/peptide.py | 200 +++++++++++++++ docs/nbs/peptide_smiles.ipynb | 457 ++++++++++++++++++++++++++++++++++ tests/test_peptide_smiles.py | 107 ++++++++ 3 files changed, 764 insertions(+) create mode 100644 alphabase/smiles/peptide.py create mode 100644 docs/nbs/peptide_smiles.ipynb create mode 100644 tests/test_peptide_smiles.py diff --git a/alphabase/smiles/peptide.py b/alphabase/smiles/peptide.py new file mode 100644 index 00000000..ddbe502a --- /dev/null +++ b/alphabase/smiles/peptide.py @@ -0,0 +1,200 @@ +from typing import Dict, Optional + +from rdkit import Chem + +from alphabase.smiles.smiles import AminoAcidModifier + + +class PeptideSmilesEncoder: + """ + A class to encode peptide sequences into SMILES strings or RDKit molecule objects. + """ + + def __init__(self): + self.amino_acid_modifier = AminoAcidModifier() + + def encode_peptide( + self, + sequence: str, + mods: Optional[str] = "", + mod_site: Optional[str] = "", + ) -> str: + """ + Encode a peptide sequence into an RDKit molecule object. + + Parameters + ---------- + sequence : str + Peptide sequence, e.g., "AFVKMCK". + mods : Optional[str] + Modifications in the format "GG@K;Oxidation@M;Carbamidomethyl@C". + mod_site : Optional[str] + Corresponding modification sites in the format "4;5;6". + + Returns + ------- + Chem.Mol + RDKit molecule object of the peptide. + """ + if mods and mod_site: + mods = {int(m): mod for m, mod in zip(mod_site.split(";"), mods.split(";"))} + else: + mods = {} + peptide_mol = self._build_peptide(sequence, mods) + return Chem.MolToSmiles(peptide_mol) + + def _build_peptide( + self, + sequence: str, + mods: Dict[int, str], + ) -> Chem.Mol: + """ + Build the peptide molecule from the sequence and modifications. + + Parameters + ---------- + sequence : str + Peptide sequence. + mods : Dict[int, str] + Modifications dictionary with the site as key and the modification as value. + + Returns + ------- + Chem.Mol + The peptide molecule. + """ + # List to hold the amino acid molecules + amino_acid_mols = [] + + n_term_placeholder_mol = Chem.MolFromSmiles( + self.amino_acid_modifier.N_TERM_PLACEHOLDER, sanitize=False + ) + c_term_placeholder_mol = Chem.MolFromSmiles( + self.amino_acid_modifier.C_TERM_PLACEHOLDER, sanitize=False + ) + n_term_mod = None + c_term_mod = None + + if 0 in mods: + n_term_mod = mods[0] + if -1 in mods: + c_term_mod = mods[-1] + # Process each amino acid in the sequence + for idx, aa in enumerate(sequence): + # Get the amino acid SMILES + if idx + 1 in mods: + aa_smiles = self.amino_acid_modifier.ptm_dict.get(mods[idx + 1], None) + else: + aa_smiles = self.amino_acid_modifier.aa_smiles.get(aa) + if not aa_smiles: + raise ValueError( + f"Unknown amino acid code: {aa} or modification: {mods.get(idx + 1, 'no mod')} (SMILES for it might be missing)" + ) + # Convert the amino acid SMILES to a molecule + aa_mol = Chem.MolFromSmiles(aa_smiles) + if aa_mol is None: + raise ValueError( + f"Invalid SMILES for amino acid {aa}, mod {mods.get(idx + 1, 'no mod')}: {aa_smiles}" + ) + amino_acid_mols.append(aa_mol) + + # Now, assemble the peptide + peptide_mol = amino_acid_mols[0] + peptide_mol = self.amino_acid_modifier._apply_n_terminal_modification( + peptide_mol, + n_term_placeholder_mol=n_term_placeholder_mol, + n_term_mod=n_term_mod, + ) + for idx in range(1, len(amino_acid_mols)): + peptide_mol = self._connect_amino_acids(peptide_mol, amino_acid_mols[idx]) + # if idx == 1: + + peptide_mol = self.amino_acid_modifier._apply_n_terminal_modification( + peptide_mol, + n_term_placeholder_mol=n_term_placeholder_mol, + n_term_mod=None, + ) + peptide_mol = self.amino_acid_modifier._apply_c_terminal_modification( + peptide_mol, + c_term_placeholder_mol=c_term_placeholder_mol, + c_term_mod=c_term_mod, + ) + Chem.SanitizeMol(peptide_mol) + print(Chem.MolToSmiles(peptide_mol)) + return peptide_mol + + def _connect_amino_acids(self, mol1: Chem.Mol, mol2: Chem.Mol) -> Chem.Mol: + """ + Connect two amino acids via a peptide bond. + + Parameters + ---------- + mol1 : Chem.Mol + The first amino acid molecule (C-terminal). + mol2 : Chem.Mol + The second amino acid molecule (N-terminal). + + Returns + ------- + Chem.Mol + The combined molecule. + """ + # Combine the two molecules + combined = Chem.CombineMols(mol1, mol2) + rw_mol = Chem.RWMol(combined) + mol1_num_atoms = mol1.GetNumAtoms() + + # Find the placeholder hydrogens and their neighboring atoms + c_term_placeholder = self.amino_acid_modifier.C_TERM_PLACEHOLDER.strip("[]") + n_term_placeholder = self.amino_acid_modifier.N_TERM_PLACEHOLDER.strip("[]") + + c_term_h_idx = None + c_atom_idx = None + n_term_h_idx = None + n_atom_idx = None + + # Find C-terminal placeholder hydrogen in mol1 + for atom in mol1.GetAtoms(): + if atom.GetSymbol() == c_term_placeholder: + c_term_h_idx = atom.GetIdx() + neighbors = atom.GetNeighbors() + if len(neighbors) != 1: + raise ValueError( + "C-terminal placeholder hydrogen should have exactly one neighbor." + ) + c_atom_idx = neighbors[0].GetIdx() + break + + # Find N-terminal placeholder hydrogen in mol2 + for atom in mol2.GetAtoms(): + if atom.GetSymbol() == n_term_placeholder: + n_term_h_idx = atom.GetIdx() + mol1_num_atoms + neighbors = atom.GetNeighbors() + if len(neighbors) != 1: + raise ValueError( + "N-terminal placeholder hydrogen should have exactly one neighbor." + ) + n_atom_idx = neighbors[0].GetIdx() + mol1_num_atoms + break + + if c_term_h_idx is None or n_term_h_idx is None: + raise ValueError( + "Failed to find terminal placeholders for peptide bond formation." + ) + + # Remove placeholder hydrogens, higher index first to avoid index shifting + indices_to_remove = sorted([c_term_h_idx, n_term_h_idx], reverse=True) + for idx in indices_to_remove: + rw_mol.RemoveAtom(idx) + + # Adjust atom indices after removal + if n_term_h_idx > c_term_h_idx: + n_atom_idx -= 1 + + # Add peptide bond between the carbon and nitrogen atoms + rw_mol.AddBond(c_atom_idx, n_atom_idx, Chem.rdchem.BondType.SINGLE) + + # Fix valencies if necessary + Chem.SanitizeMol(rw_mol) + + return rw_mol diff --git a/docs/nbs/peptide_smiles.ipynb b/docs/nbs/peptide_smiles.ipynb new file mode 100644 index 00000000..d451ec9b --- /dev/null +++ b/docs/nbs/peptide_smiles.ipynb @@ -0,0 +1,457 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PeptideSmilesEncoder: Encoding Peptides to SMILES\n", + "\n", + "SMILES representations of peptides with PTMs enable advanced ML applications in proteomics:\n", + "\n", + "* Graph Neural Networks (GNNs): SMILES convert to molecular graphs, allowing GNNs to capture complex structural relationships, including PTM arrangements.\n", + "* Transformer Models: Treating SMILES as sequences enables use of state-of-the-art transformer architectures, adept at learning long-range dependencies in peptide structures.\n", + "* Transfer Learning: Pre-trained models on large chemical datasets can be fine-tuned for specific proteomic tasks, leveraging broader chemical knowledge.\n", + "* Interpretability: Graph-based models provide insights into which parts of the peptide, including PTMs, contribute most to predicted properties.\n", + "\n", + "This notebook demonstrates the functionality of the `PeptideSmilesEncoder` class, which is used to encode peptide sequences into SMILES (Simplified Molecular Input Line Entry System) strings. We'll go through various examples, including peptides with and without modifications, and visualize the resulting molecules." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from rdkit import Chem\n", + "from rdkit.Chem import Descriptors, Draw\n", + "from rdkit.Chem import rdCoordGen\n", + "from rdkit.Chem import rdFingerprintGenerator\n", + "from rdkit.Chem import MACCSkeys\n", + "from rdkit.Chem.AtomPairs import Pairs\n", + "\n", + "from alphabase.peptide.precursor import get_mod_seq_formula\n", + "from alphabase.constants.atom import CHEM_MONO_MASS, MASS_H2O\n", + "from alphabase.smiles.peptide import PeptideSmilesEncoder\n", + "\n", + "# Initialize the encoder\n", + "encoder = PeptideSmilesEncoder()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Encoding a Peptide without Modifications" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[H]N([H])[C@@H](CCC(N)=O)C(=O)N([H])[C@@H](CCSC)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Peptide sequence: QMNPHIR\n" + ] + }, + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEsASwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA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iTQUbTXEOtafKt3p0+cbZV52k+jDg9umelY6P8WNWjXdH4d0JCBuLF7mYH2A+SgDW+KF19j+GPiKXON1m8X/AH38n/s1bXhy1+w+F9ItMY8iyhix6bUA/pXG3Hwx1HXoPJ8V+NdW1OBiDJa2yJawvgg4ZVByMivRAAqhQMADAFAC0UUUAcxpfhZ9Pm0mbzmaS1aUzBrmV0O5WA2Kx2jqOgFdPRRWtWtOq+abu/8Ag3/USVgrzP4h6bc3niSylhERRbZFYtMi4/fKeckYGAeelemViahp168+ofZkt5I7+ARM0rlTGQGGcAHcMNnGRzn1yOWtDnjyno5Zifq1f2qtouv9dtTbopkMflQxx7i2xQuT1OBTZ7qC2MQnmSPzZBHHvbG5z0Uep4PFannswfH/APyTrxL/ANgy4/8ARbVN4J/5ELw7/wBgy2/9FLVrULFfEGjXen3ccsFrdwzW0yHAkwcruUgkDjJHXqOnIrhYJPGHw2t4raa3fxN4Zt1CRy2yBby1jAwAU6SKB6c+uKAPTaZMhkgkQYyykDP0rL8PeKNG8VWAvNGv4rmMffUHDxn0ZTyp+ta9C0A5rRfDL6Td6TN5zv8AZtNe1nD3Mkg3kwkFAxIC/u26Y7cenS0UVrVrTqvmm7v+mJKwVymsEf8ACwNBl82IRwRzLLmVQVLqAgIJzycgV1dYN5pV7KdStolgMF/KshmZyGi+VFPy45I2ZHPftjnmqJtaHZgpRjNuT6Nffo/uTb+RsLd27iIrMhErmNMH7zDOQPcbW/I1NXP2mgzW82nzGZ2aC7mmkQysUCuJQNo6Z+df1roKqLbWplXhThK1N3X/AAX+gUUUVRiFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFVIdOggubqdd7G5kWV1dtyq6qFBUHpwo6dxmrdFABRRRQB5n4k0mw0z4x+Cr+wtY7W5v3vFu3hGzzwsORvA4JyTz/AICvTK8/8Y/8lU+Hn/XW/wD/AEQK9AoAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAhls7We4guJraGSeDcYZHQFo8jB2k8jI4OKmoooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD//2Q==", + "image/png": "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sQIAhg5Fp0747jt89x1at1a9Z8kSjBkDAMOHF5dv798fjgZYGEiq07L/+c9/AGzevFnENj//nAAaPVqc1pKSkoRbWAHcu3dPnEbZn44epTVrSv7Tw4f0/vskkxFAvr7h27Zte/n+wejoaGHvqX37ZV270tixZG5OAFla0pw59Pd3eRguyUK4YMECAJMnTxaxzdhYAsjJiXS/5TM/P1+Yod7Y2NjHxyczM1OMDjKV2Fj6/HMqKirtPceP01tvJctkcgCdOnWKjY29deuW35819t54443Jkyd3754CkExGgYG63KcoMclCKFx/E/eWFKWSatcmgC5d0rWp9957z8jIyNraetiwYXwbt7iSkqhPHwoJoV27/uGdSqUyPDzc0dERgEwmE24brlKlyocffti+fXsArq7vtWunPHOmXPqtN5KFMDU1VSaTWVpaFqpvZ9BQQQFlZ6seK5X0/DkR0ZIlFBZGqalEpHpFC8uWLatSpUq1atW0voOHiej58+fTpk0zNTUFYGVlNWjQICGNTk5OYWFhZbzJ0ZBJedta48aNocNIhX37aOxY1eOkJGrbloioalVaulT14htvaNPs6dOn69ata29v/9VXX2nXMaYP8fHxVn+WKTE3N581a1a2+je4gpPyjplWrVoB+P3330Vs094e27Zpf/tFamrqqFGjCgsLFy5cOH36dBE7xnTk4uLi4uICoHPnztevX1+wYIGVhqWDDJaUIWzdujV0C+H9+9i1C7t24eBB1SvGxpg/H5MmqZ7m5KDsF0EUCsWAAQOysrI2bNjwv//9T+teMb1avnx5Jav9I30IhWHs2snJwZMnePIEycnFL/bpAwAHDgDA6tWwtESrVpg0CZs2/e20h4JRo0bdv39/586dffv21bpLjGlKyjL4Xl5eRkZGcXFxeXl55upK9Zpo2hTjxgHA48fYs6f49dBQDByIwkI8fIiiIsTGQl0pqlEjRfv2I1q3btmuXbsWLVoIh/sAvvnmm3379p05c+aNN97Q6VMxpiEpQ2htbe3m5nbt2rW4uLg2bcQscV23Lt59F7NnY8UKLFiAuDicPIkTJ3D6NExMrm3Z8sOWLT8AMDc39/Lyateunb29/bp16y5fvixUtWCGqWHDtjKZnVwuygQJhkTa80LDhg0DsGrVKi2WffCAjh9XPX7xQnXRaft21SsFBfT9939dRKGg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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SMILES: [H]N([H])[C@@H](CCC(N)=O)C(=O)N([H])[C@@H](CCSC)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Exact Mass: 876.4388\n", + "Expected mass: 876.4388\n", + "Mass difference: 0.0000\n" + ] + } + ], + "source": [ + "def visualize_peptide(smiles, title):\n", + " mol = Chem.MolFromSmiles(smiles)\n", + " rdCoordGen.AddCoords(mol)\n", + " img = Draw.MolToImage(mol)\n", + " display(img)\n", + " print(f\"SMILES: {smiles}\")\n", + " print(f\"Exact Mass: {Descriptors.ExactMolWt(mol) - MASS_H2O:.4f}\")\n", + "\n", + "sequence = \"QMNPHIR\"\n", + "smiles = encoder.encode_peptide(sequence)\n", + "\n", + "print(f\"Peptide sequence: {sequence}\")\n", + "visualize_peptide(smiles, \"Peptide without modifications\")\n", + "\n", + "# Verify mass\n", + "expected_mass = np.sum([CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, \"\")])\n", + "actual_mass = Descriptors.ExactMolWt(Chem.MolFromSmiles(smiles)) - MASS_H2O\n", + "print(f\"Expected mass: {expected_mass:.4f}\")\n", + "print(f\"Mass difference: {abs(actual_mass - expected_mass):.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Encoding a Peptide with Modifications" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[H]N(C(=O)[C@@H]1CCC(=O)N1[H])C(CCS(C)=O)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Peptide sequence: QMNPHIR\n", + "Modifications: Gln->pyro-Glu@Q^Any_N-term;Oxidation@M\n", + "Modification sites: 1;2\n" + ] + }, + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEsASwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAGTTRW8TSzypFGvLO7BQPqTRFNFcRLLDI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ssXQhfmB7An8K36K0p1HTd0Jq5keF7Wey8L6bbXKMk8UCq6t1BrXoopVJuc3N9XcaVgrktJhC/EvxFN5QUvbWw8zbgtgHv3rra4iND9jsxFHONaBb7c2xwSNjb9x6Fc429vu4rnqOzR3YOClGon1VvTW9/wt8zt6K5jSo9SW+0Q3bxtGLCQYjiZSpxF94ljk/l3rp6uMuY569JUmknf/AIdr9AoooqjEKKKKACiiigAooooAKKKKACiiigAooooAK5LxV4Bs/FGq2epjU9S0u+t42ha406bypJYm5KM2DxnkfU/h1tFAHE6f8JvBtjN9om0r+0bo/en1GRrhm+oY7f0rsLWztbGAQWltDbwr0jhQIo/AVNRQAUUUUAFFFFABRRRQAUUUUAZsttqFxfyB7lYbSOSKWBoDh2xnzI5AQQVPHI557YBNy1tYLK2S3tolihTO1EGAOc8VNRQAjKrqVYBlIwQRkEV5p4o8FDwtaal4r8G3z6LdWsEl1c2aLutboIpYho+inAOCOn616ZXP+Ozj4eeJf+wVdf8AopqALvhvU5da8L6Tqs6JHLe2cNw6JnapdAxAz25rTrn/AAKMfD3w0P8AqFWv/opa6CgAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACsvxLpkuteFtW0qB0Sa9s5rdHfO1WdCoJx25rUooAzfDunSaP4Z0rTJnR5bOzht3ZM7WZECkjPbitKiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SMILES: [H]N(C(=O)[C@@H]1CCC(=O)N1[H])C(CCS(C)=O)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Exact Mass: 875.4072\n", + "Expected mass: 875.4072\n", + "Mass difference: 0.0000\n" + ] + } + ], + "source": [ + "sequence = \"QMNPHIR\"\n", + "mods = \"Gln->pyro-Glu@Q^Any_N-term;Oxidation@M\"\n", + "mod_sites = \"1;2\"\n", + "\n", + "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "\n", + "print(f\"Peptide sequence: {sequence}\")\n", + "print(f\"Modifications: {mods}\")\n", + "print(f\"Modification sites: {mod_sites}\")\n", + "visualize_peptide(smiles, \"Peptide with modifications\")\n", + "\n", + "# Verify mass\n", + "expected_mass = np.sum([CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)])\n", + "actual_mass = Descriptors.ExactMolWt(Chem.MolFromSmiles(smiles)) - MASS_H2O\n", + "print(f\"Expected mass: {expected_mass:.4f}\")\n", + "print(f\"Mass difference: {abs(actual_mass - expected_mass):.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. N-terminal Modification" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[H]N(C(=O)[C@H](Cc1cnc[nH]1)N([H])C(=O)[C@@H]1CCCN1C(=O)[C@H](CC(N)=O)N([H])C(=O)[C@H](CCSC)N([H])C(=O)[C@H](CCC(N)=O)N([H])C(C)=O)[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Peptide sequence: QMNPHIR\n", + "Modifications: Acetyl@Any_N-term\n", + "Modification sites: 0\n" + ] + }, + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEsASwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoorM8RtcL4X1ZrMyC6FlMYTFneH2HbtxznOMYqoR55KPcGadFc9b3t7Z63qMurMEtltLcoYVdolO6fcenBwFyfQLmucTUvEejNaubDUtWjJnnSNGO8gvgxsT/d+8ueoOB92uuGClNtJrp87q++2hLkeiUVxtn8UPDEsy22o3UujXhyPs+qRmBhjHUn5e/rXGat8R5/Gusnw/4V8/ytxKyodjXQBwWB6pGD1PBPbvjjrxnQbU1quh04SisRVjT5lFPq9j00azMZFl+zJ9ia7NoH8w79wcx7tuMY3jHXpz7VsVj6Zoggt7aS/cT3yfvJWR3ETS45YJnbn3x154zWxWcOa2o8R7LmtT6f1/W3ogoooqznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAK19ZJfwCGSSRFDBjsI+bHY5BBHtVmiindtWAp6lpOnaxam21OxtryA/8s54g4/WvPPgzo+n6LaeI7GG1jjvrPV5rWWXGXeNcGPk9sHgfWvT68wttY03wf8Y/ENtql7BY2utWtteQPO4RDIoMbDceATgn8KQHp9FMhmiuIVlhkSSJxlXRgQR7EU+gAooooAKKKKACiiuQuE1e2+LFnOklw+jXelvC8Zl/dxzq+4MFJ6leMgUnJLVgdfRRRTAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKiubiK0t5Lid9kSDLHGf0HU+1S1W1CzF9YyW+8oWwyuBnawIZTjvyBxSd7aFQUXJKW3ULS/gvGkSPzFkjxvjljKMAehwexwefY+lM1LSdO1i2NvqdhbXkJ/guIlcfqK5fwb4mj8QazqLPG6XPkxgZQICis4xt3Nghi3OT1HpXaVNOanHmR0YzDSw1Z05KzVvxR59L8J9PsZmufCusan4cnJzttZjJAx/2onJB+mQKl+HHiHXdTuvEOj+IZobi90a8EAuI4vL81CDhio45xnj1rvK5Czs2034r6jIgIh1fTI52z0MkDiM4/wCAyJ+dWcp19FFIzBFLMQFAySTwBQAtFcxdazbXkrzQ6v5Vl9m3WssDDEsoZwwHB3EYTC993Q10Nm80llA9wgSdo1Mi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KtSdvaNu2iv0QUUUVRkFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABXL+PPC7+J/D+2zk8jV7KQXWnXA4Mcy8jn0PQ/n2rqKKAPO9L+MPhg+Hre61y/jsNTGYrqw2M0scynaw2KCcZHH1+tJP8RtT1m3kh8MeCtcvWkUqlxdotpDyOCGc8j8q7eHRdKtr6W+g0yyivJW3SXCQKsjn1LAZJq9QBzHw80K78NeAdI0i/REu7eI+aqNuCszM2M9/vduM+tdPRRQAUUUUAFVtQtTe6bdWgkMZnheMOP4dwIz+tWaKGr6DjJxakuhlWdrdtqKXdzDBbiK38gJC5bdkg56DAG3j6npWrRVa7sorw27SGRWt5hNGUcqdwBHOOoIJBHvSSsVUm5u7GpfJdopsisySRu0dwpDRbgcYJBznP8jXDan4Z8TeG/EupeJvCc8N+moOkl7pF3hPMKrgGKT+FuvB4+vAr0JEWNdqKFGScAY5PJp1Mg5Twx8QNI8SXDaewl03WYuJtMvV8uZT3wD94e47dQK6uvOvjPo2nXfw91LVJrSM6hYoj210BiSI716MOccnjpXeaezNptqzsWYwoSzHJJwOTQBZooooAKwtT027ml1FYYIpkv7ZYA7vjyiAw5GOV+bPHOc/Wt2iplHmRrSqulK6/rr+hz1tod3DNaSNcyMIr+adoyy7djeZgjjOfnHfua6GiiiMVHYK1aVV3l/XUKKKKoyCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA4j4wHHwo1//rin/oxK6+wGNOtR/wBMU/kKmdElQpIiup6qwyDTugwKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA/9k=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAIAAAD2HxkiAAAzNUlEQVR4nO3de1yO9/8H8NfdXZGOEsshJCwqOZsOzmwIW3PbsMxhX2MzzOY0m2yM2hdr2DBsv5xGmK+yOZRjiDnknJBTopUOOqn79P79cd2KLXTf15WreD8f/ri6u6/P/bl1v+7r+Hl/FEQExph8zOTuAGMvOw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzIzl7sDT1BYiIsXYWWFpk2hVMrdG8bKUYUM4cWLGDECPXsiJwfnzmHbNtjZyd0nxsqLgojk7sO/BARg5ky0bw8Aixbh/n189ZXcfWKsvFTIY8KLFw0JBNC1K06ckLU3jJWvChlCMzPo9YZljQaWlrL2hrHyVZFCeOkSduwAgI4d8eefhge3bkWXLvL1ibFyZ9wxYYsWOHu2lGUAERGYOBEJCbC3xw8/wNoaH3xgTEeOHkVAAAoKEBsLZ2eMGIGaNZGfDzs7rFzJG0P2AjPu7KhaXfqyoH59BAcjLMz4XkRH4+23kZuL7t0xZgxWrcLu3cjKQpUqqFbN+OYYq0yMC2FeHn7/3bBcfNRWrH9/HDiAU6eM6wGtWqUYMwZaLQIDEReHu3fx5ZeIjET16sY1xFjlZNwxoU6H7GzDv1J3Y8PCMGECiKDXo08fLFyImzef1mBoaOiyX3+FTofBg7FnD+7eRdeuWLPGqF4xVrmRMV59tfRlItq4kb79loho2jRq144mTSLA8K9tW5o3jy5f1j36fK1WO3bsWABKpXL3+PFkaUkADR5MRUVGdYmxyk7sHTMbNyIhAS1bljzy1VfYsAGNGyMyEps2Yds2nDiBEyewc+ep9PT3VSpVv379PD09hw0bFhERUaVKlSFDhryxZMkJP79WLVvi++9hVpFO2DL2HBgV2bi4fy6nphIRjRpFqal0/brhV5cv0+3bhuUHD2jbNho2jDp0GFf8ovb29gDs7OyGDBkCQKFQLFywQOz3CWOVkwS3re3fj6tXn31BQqfTxcXFbdq0KSIiIjU1VaFQWFhYqNVqS0vLX3/9VUgjYy8hsSGMiEBKCkaOhL19WVfR6XT79+8PCAgoLCy0trbeunVrz549xfSBsUpN7AFYURGsrbFzpxGrKJXK7t27DxgwAMBnn33GCWQvObEnZoKCTFyxXbt2GzduTEtLE9kBxio72U5Ftm/fHsBff/0lVwcYqyBkG09YUFBgb2+vUCju379vZWUlSx8Yqwhk2xJWq1bNw8NDo9HEx8fL1QfGKgI5r4zzHiljkDeE7dq1A3D8+HEZ+yBeQUHBrVu35O4Fq8R4SyjKvn377O3tR4wYIXdHWCUmZ6EnnU5nb29fUFCQnp5eo0YNubohRlpa2iuvvGJra5uVlaXk0ozMJHJuCZVKZevWrYmo8u6R1qpVy9XVNTc3NyEhQe6+sMpK5iELL8Ae6WuvvQbg6NGjcneEVVYyh/AFODfToUMHAMeOHZO7I6yykrkCd4sWLQDs2LEjODh40KBBHh4e8vbHBL17++h0vq1b35C7I6yykr8Cd6tWrU6fPi0su7q69uvXT6VS+fr6KhQKWftVVkRF8fH2RJqWLbOUSi7Xz4wmfwgLCwt37dq1d+9eYZyh8GDDhg379+9fWdJ46VKH/Py/mjbda2vbVe6+sMpH/hAWKx71u3nz5jt37ggP1q9f/80336zgaUxOHp+Wtrhu3XnOztPk7gurfCpQCIvp9fojR45s2rRpy5YtKSkpwoP16tXr06dPQEBA7969zc0r1mRSmZnrrl9/z8HhTTe3rXL3hVU+FTGExfR6/dGjRzdv3vz777/ffFg7cePGjYMGDZK3Y/9QVHTtxo3h9vZ9eEvITFChS5uZmZn5+PgsXLjw+vXra9eurVKlCirkFbkqVRrVqBGUm7vv8uXuubn75e4Oq2TKOYQ6HU6fRlwc8vLENKNQKIYOHeru7g6geAe14tBoUtPSfmzceHuTJjutrdvJ3R1WyZRnCHNy0L071q7F3r3o1g2ixw26ubkByMrKkqJzUlIqbbXaezk5uwEzMzNrubvDKpnyDOHixQgMxPz5mDEDK1Zg8mSR7alUKnNz88zMTEl6JyEzM+smTf7Izt6akOBdUHBS7u6wSqY8Q3jqFPz9Dcve3rh6VWR7/fr1c3FxycrK0ul0YvsmHb0+D4CVlXeDBivr1g39+++FcveIVTLlGcKqVVFUZFgmguiRPtbW1sKQBckOC8+fx+LFWLkSphZ9y8mJOXfOLTt76717K/Pzj9+//4eVVeW7847JqzxD2LUrNm82LP/xB157TXyTHTp0yMjISExMFN8UIiMxcSKaNoW1NV5/HcaPjs/MXHv1ah+tNi03d5+ZmVVOzp82Nv6vvDJFgr6xl0l5XifUavHRR7h9G7a2yMnBqlWoU0dkk9euXWvTps3UqVOnTRN9Ra5tW0RFoXZtAFi3DqdP47//LfvaaWmLk5MnAnpn56l1684DKujdPKziK89bT8zN8fPPuHkT58/j2DEEBWHbNtjYiGmyUaNGjRo1kmYEbUaGIYEAvLwQEVHG9Yjo7NlQrXY6YObiElar1gQJOsNeYuV8nTAvD25ueOstbN+OvXtxUoIzh+7u7n///bf4dmBhAa3WsJyRgbLV19DpdB9++KGvb7BG065hw5WcQCZeOYfQxgbu7tBo4OoKAFKMoB86dOilS5dMX3/nTqxYAQADBmDRIgDQaPDDD1CpnrnqgwcPAgMDV6xYAVjk5MyuUYPrOzEJlP+d0O3a4cIFVK0KAFKMoO/Zs2eeyfffrF+P4cOh06FJE3zzDb76CgEB0Onw9tvo3fvpq2ZnZ/fv3z82NtbR0TEqKsrHx8fEPjD2D+U+A+KPPxJA/foRQA0aSNJkp06dMjMzjV5tyRIyMyOABg0iZ2faudOotf38/ADUr18/ISHB6Jdm7MnK/wbu9u0B4MoV2NpSbu4D0YdzaWlp6enpDRs2nDJlytGjR6lsZ3c133yDceNAhBEjEBWF1FRs3Fj2F83Lyzty5IhCodi7d69wCytjkin3mBcVne7WbZGv7wd9+wKIiooS01hSUlLjxo0f7X/NmjWDgoIiIyPVanWpq2i12jFjxnzg6am3taVRo8jCggAaPpw0GqNe2svLC8ChQ4fE9J+xfyv/EBIJ9cjeeecdADNnzjS5nTNnztSpUweAl5dXREREcHBwkyZNitPo6OgopLGwsLB4lcLCQpVKBcDKymrZxx8bEvjVVya8+kcffQQgNDTU5P4zVqrnEcJPPvkEwODBgwH07t3btEZ2746xsbEB0KtXr5ycnOLHz58/Hxwc3KxZs+I0VqtWLSAgIDw8PDU19fXXXwfg4OAwZMgQAL/4+9OiRaZ1YN26dQD69+9v2uqMPcnzCOHq1auF8AjbK71eb2wLa9ZQ8+YFdnYu77333pN2O8+fP//11197e3sXp1GogmFvbx8YGCj8+Msvv5j8Lm7cuAGgRo0aJvSfsad4HiEULutVq1ZNGBp/6tQpo1YPDSWFggCaM+dOWQJw48aNsLAwX19fMzMzYUdUePU///zT1HdgUK9ePQB8dpRJ63mEUK1WW1paAhBSoVQqfX19w8LCUlNTn76iXk+TJxNACgV9953Rr3v79m0hNjY2NnFxcSb2/hHCYe3KlSvFN/US2rCBliwxLG/dSvPnP/ZblYqKTxf4+5NWa/oL7dlD7dpRXh4R0fr1FB5OW7aUHIVs327KZ6lclfsliry8vAEDBqjVamtr64EDB/bs2VOhUBw+fHjixInt2/v07Inly0sGEmVmIi7OsJyaihkz8N//okoVbNhgypDgunXrCpOWqVSq16QYw+Hr6wvg8OHD4pt6CRUUlBQ5efDgnwVP8vKwYwcuXgSAnBxRL5Sfj8JCzJplaOr+feTn4/79km4UL1cQ5XvHzN9//923b9+TJ0++8sorf/zxR5s2bQBkZGRs27Zt8+bNRUVeMTGIicHHH8PfHwMHwtkZgwdj3z74+mLPHlSpgu7dMWMGuppaU1e4r0XUbW6PEEJ46NAhSVp7CSUlYe9eALhwARYW//ztt9/ik08QEyPBC735Jo4cwdmzJY/cugXh7ybRZ0FK5bglvHbtmr+//8mTJxs1ahQbGyskEECNGjVGjhz5559/btkSGh4OlQpVqmD/fowbh6NH8fbbmDwZGg0AWFoiJsb0BALw8fFRKpWnTp0qLCwU/468vb1tbW2vXLlSXCmcGeXOHcTHIz4eN26U8lsvL3h7Y80aExvPzsann2LZMgBQKBAWhvHjUXwrx82bOHQIhw69TCE8fvx4x44dr1y50q5du7i4uEcv6BVzcMCwYYiIwN27WLsWgYHw9UWdOhgwAN9/L0037Ozsmjdvbm1tc/r0dfGtKZVK4ZpnBSy7WCn4++Ozz/DZZ+jbt/QnfP01Fi9Gfj62b0ePHjh/vkzN6nRYtgxNmiAsDDNnQq0GAE9PtG1bcluUvz+mTcO0aXjzTQneiLTKJYTR0dHdu3dPS0vr0aPHnj17atWq9fTn29lh6FBs2QI7OwD49FNs3oyHhfDF6tkzJisrPTa22bOfWgZ8WCghjQbz5+OLL1Bcu8vWFp99hqtX8e232LMHrVvj00+Rnf20RvbtQ5s2GDsW9+7B1xc9eyI83PCr4GBcuVKu70Aikp/qCQ8Pt7CwADBs2LAnXdN7kj17aNIkIqK9e6lmTZo7V4L+rF5NAA0YIEFTRLR7924Ar732mjTNvUxu3KBLlwzLt27RxYuUl0d//01799KaNXTwID14YPjt//5HaWk0ZgwplQRQzZq0fLlW+68TpklJSYGBgZ6eOQA1bEjDhpG9PQFUtSrFxhqec/YsXblC16/T+fMlL332bLm/WaNIHMKwsDBh2pbx48ebcFH74EGaMcOwPHw4ff+9BF26epUAcnIiSa6x37x5U6FQmJub5+fnS9AcI5o+nW7cKP1XFy/S668TQJ07H23WrNnOhwNfcnNzp0+fXrVqVQBt234UFERNmhBAAPXtS4mJz6/zkpAshFqtVri7UqlU/vjjj6Y1cvkyHT5sWL52jQ4elKZvtWsTQJcvi20nPz9fmN8bwEGpOvcSU6tp7lw6efIZT9u8We/h0VH4b1epVD/++GP9+vUBKBSKN954o1u3br6+VwFq2pS2b38u/ZaaZCE8efKkpaVl1apVt2zZYnIjERElW8LISPrsM2n6FhhIAP36q6hG1Gp1t27dAJiZmbVs2fLGk769WZklJVFICIWE0JEjz3hmQUHB7Nmzra2thewBaNu27aBBg4Q7E199tWtYmN7IQ58KRMrd0Q0bNojcPogPoVZLd+4YlnU6SkkhIvr1V1q2jG7efOy3RtHr9SqVSqlU2tnZDRkyxNhjXSaJlJSUoKAgAFWqVLG1tRV2u8aOHZueni5310R5HretlV1EBDVuTH37Ut++1K6dKSG8e5c6dzYsZ2VRhw5EREolCbeapaZS9+6mdGzEiBGWlpbOzs4hISGmrM+k06lTJ2HXtGvXrmfOnJG7OxKoWLNtAnjnHcyZAwBRUThwQJo23d2xYgUGDDBx9WnTpm3atMnFxWXRokV9+vSRpk/MVMKY0kmTJi1YsEDuvkijwoVQvFu38O23AFB8k4ylJWbMwOTJCAkBgKIiVKlS1tYWLVq0bNmyJk2a/P777w0bNpS+u8wk7dq9OFPQVaxJQh0dUa+eYbl6ddSvb0ojDg7o1g3duuHhbgsA9OuHzEwI19jffRdubpgwATExJZVHS7V+/frZs2d36tTp8OHDnEBWXuTeH5ZYqceErVoREV2/Tl5e1K0b1a1ruKYEUK1aNHw4bdsW++hofUF0dLS9vf0XX3zxHLvPnu3dd98F8Ntvv8ndEclUrC1huWrYEIMGQaHAzZuIjcXUqWjWDGlp2LaN3n67Z/Xq1f38/EJDQ4UhF2fOnHn33XdXrlz5rbBryyoMa+uVNWtmWlgEyt0RyZTnhDBy0OuRlWUoaU+EjAw4OSElBXXrAoBGg3v3SmagAJCQgP37k3/7beiRI0eKpz1s3rw5EUVERHh6ej7/t8Ce7t13sXEjfvsN774rd1ck8qJtCc3MSiaVUCjg5ATAkEAAFhaPJRBAs2YYO9bl4MGDaWlpERERQUFBtra2ly9fLigo4ASy5+NFC6HJHB0dVSrV6tWrz507p9fr09PTi4pnOGWsPHEI/6lBgwYeHh4FBQXHpZg5g7Fn4hCWokuXLgD2798vcz9YaTp1QlCQYZqvFwOHsBSdO3cGcECqG3aYSZKTMWyYYTktDe+8A40Gbm5o2xarV6NuXfznP7L2TzocwlJ06dLFzMzsyJEjfFgoI60W6emGZZ0OaWkgAhE+/xw6HbRavDCFfjiEpahRo4anp2dBQcFfUsxqykyWlYXYWMTG4tgxwyO1auH117F4sazdkhqHsHR8WFgRZGXh8GEcPvzY7LKff461ayUrQVQRcAhLx4eFkktKMtxAD+D0aZTl+61RI0OJtHHjSh6sUgVz52LGDAD45BNMmCC2WLDsOISl48NCyd28iblzsW4dAFy4ADFVI3v1Qs2aePAAy5Zh0SI0a4Y1a1B5b/3iEJbO0dHR09PzwYMHfFgooSFDEBaGrKwyPblWLUybZliuXh1ffQVzcxTfybtkCWbNwqFDaN8ed+5g2DD4+OD4cU259LuccQifiA8LxcvLw4EDmD8fKhXS02FtjSlT8MUXZVrX2hqdOxuWq1ZFt24wM0P37oZHatWCnx86dMDRo4iIQP36OHoUo0dPHjRo0K1bt8rlzZQbDuET+fn5gUNoJI1GEx8fv2zZsmnTUry84OCALl0weTI2b0ZCAgCoVLhxA/Hxkr2iQgGVChcuICTkdkLCsk2bNnl6eoaEhFSm4wi5x1JVUPn5+d27dwegVCrj4+Pl7k5Ft3HjxgkTJnTs2FGYDRJAly57ALKwoDZtaOxY+uUXiogwVHZOTCQ7O5o3T/puXL169c2HZe7d3Nwqy5hDDmEpcnNzi6sbCkX1GjVqNH78+OjoaK6z9m/bt2+3eDjHkkKhaNq06dChQ3/+efeRIyVFtYkoPp6WLjUsh4XRqlVUTvWT9+7d26JFC6VSWa1atYKCgnJ5DUlxCP8pMzNTmPWldu3aQmEvpVJZvOPg5OT0+edxERF0/37JKrdv0+3bhuXr1ykvj5KTKTfX8EhKymNPfvH88MMPAFxdXXft2pWVlfXM52u1tGEDOTnRl1+WV5c0Go3wvZBb/GeowPiY8HF378aPHXvs2DE3NzcvL6+DBw/a29vv27cvNjZ26tSpzZs3v3///ooV7oMGwdERfn4IDUViIn76CV27GupKffMNLl7EvHklN3ksWIDYWBnfUrnLy8sD0Llz523btv3xxx9Pf/KJE2jdGlu3IiMD8+fjugSTZZXC3Nxc2IV59Au0wuIQPuLGDfj7d9u48X9vv+3s7Lx79+6aNWvu3bvX39/fz88vJCTkwoUL588nzZjhIJSQOnwY06bB3R2ZmWjZEnPnyt1/mdy9exeAWq3+6aefdu3a9fQnW1nh4kVs2YK+fVFYiClTnksXKzYO4UOJiejUCUlJaNlywK1bY8zMnJ2d9+zZ07p160ef1bSpy+TJOHAAaWlYuxbvvIPWreHoiGHDsG/fYxNQrliBKVMwZcoLvhkEkJKSAkCv1+NhUdCn8PDAqFHQanH/PmxssHkzoqOfRycrMg4hAODCBXTtiuRkdOgAjQbHj7939+6pI0e8vLyetIajI4YOxYYNOHkSAMzM8P33mDix5Am9emH4cAwfDg+Pcu++vO7cuQNAo9GgDCEEMHs2HBxw9Kjm7bev16t3Y8WKycXVfV5OHEIAQG4ucnPRpQsyM3HhAtzdsW9fbSPHjbZtCzc37Nlj+LFBAzRvjubN4egofX8rFCGEwpFhWUJYsyZCQ6+7uHjHxfW2tu69adP8lStXlnsvK7CXOIRE2LkTP/2EAwfw2mvYuxcpKbhyBa1b4+DBkiLExpg7F5pKeeOU6fR6fWpqqkKhyMzMRNlCCGDkSBcrK7PLlxNfe60DgC+++CKzeLbel5Dcp2flo1LR9OkUFUVjx9K4cURE27aRjw9lZBjbUkEBqdV09ix9+y399htpNFRURDqd4bdFRfSvSWZfHKmpqQCcnJzq1asH4Pr162VcMTo6GoCdnZ2Pjw+ATz/9VNqOCbcN8HXCCuzYscdm0G7f3jBbrIjpfH/9lQAaPFhs1yqXkydPAmjRooWFhYVCoXjw6OX5Z+nTp4+lpeX8+fOVSqWFhcXJZ04XaoxKFMIXcEKYMklMhLd3yY/e3khMRIMGUChMblIoU3runOi+VSrCAaGTk5NGo3FychKmsC6jJUuWaLXa/Px8c3NzOzu7ynS3p6Re1mNCBwdkZ5f8mJ2N6tVFNunhAaUSiYlQq0W2VJkIIbSzswNQt7jKctm4urpWrVq1X79+RUVFXbt2fZEmWjLKyxpCf3/s3WsoFXT16j83jCaxskKjRtBocPmyBB00OHsW69fj8OEKO2RVCKGwASzjWZliGRkZvXr1un37dpcuXVavXi3c4/ISeknfNhwcsGIFRo2CQgFzc6xfD0tL8a16eeH+fX1i4n1PT7HbVQCYMwfnzqF/f0REYMkS/PabBG1KTQhh9erV3d3d3dzcyr5iQQE++WTDpUuXvL29t23bVqXsU0a+eOQ+KH2hfPNNCIDp06dL0FZ6Onl5lZxjDQykQ4ckaFZS+fn5Hh4ewlvWFXe1DDQaCgggS0vq3z/8zp075dE34Qbu06dPl0fj0npZd0fLh7t7IwDnz5+XoK3Ll+HlBbOHf6B27SBJs9KJiory9PS8cOGCnZ3dvHnzmjRpEhoaev/+/WeuSIQxY7B9O2xtERo6rPY/5uiRSN26dRUKhY+Pz6xZsx48eFAeLyEZub8FXigJCQkAXF1dJWjr3Dnq37/kxy++oPXrJWhWCklJSQEBAcLnx9vbe9KkSY0aNRJ+rF69enDw3JSUp63+1VcEkJUVHT5cjp2Mi4vr0aOH0CtXV9fNmzeX44uJwyGUklartbKyUigU900eQahW03/+Q7t3k1pNLVrQtWtERPfvk7c33b0rYVdNo1arw8LCbGxsANjb24eFhWk0GiLS6XSRkZHCh75z56kWFqRS0bFjpbSQnk61apG5OUVGPo8O79u3z/vhKbfOnTtXzL1TDqHEWrZsCeDzzz/XmnCbTHY29ehBANWuTcnJdPo09e5NvXtTt27055/l0Fnj7Nmzp1mzZsIHOiAgIDk5+d/PiYuLGzXqjlJpmI28e3f64w/q359mzzY8wceHrlyh51l3QqfThYeH16pVC4CZmVlQUNDff//9/F6+DDiEEhs9erRCoQBQp06dqVOn3rx5s6xr3r5NLVsSQM7ONGgQ1a1LpX3KZZGSkhIUFCTEr2nTprt37376869fp6lTycGBAHJ0pM6dqUMHunyZiMjTU1xX8vNp8WL67DP69VfSaMq+XlZW1tSpUy0tLQE4ODiEhIQUFhaK64pkOISSSUtLI6IHDx4MHTq0SZMmwkfW3Nw8MDBw165dTz95eO3UKapdmwByd6du3QigqlUrwtZP2P+0tbUFUK1ateDg4KKiojKum51N331HYWHUrRvt2UO9ehGJDKFWS35+9MsvdPEihYSQSmVsA4mJiX379i3+NomKihLRG8lwCKWRkZHx8ccfP/rIiRMngoKCiisg1atXLzg4uNQdoZ07d9ra2u7t0oX8/AwbQ0dHOnDAlH789Rd99BGNGCHJDt/+/fubN29evP9pxFb9cd26UWYmffQR/fabuBBGR9P775f86OdnOGY2uploj4ejPLt3737+/HkRfZIAh1CU9PT0p3+bpqamhoSEFJ88tLS0VKlU0dHR+od3ii9atEiog/LmgAGX+/YlgJo0oStXTOnNqVPUoQMlJFBqKg0fTt9/b0ojDwUHBwt9btSo0TP3P59OCGFWFrVtS25uIhpasYLmzCn5ceRI2rPHtJaKior++9//CvfoWFpaltO1yjLiEIpy/fr1Bg0aPLOkl06ni46OVqlUxRvGJk2azJs3b/LkyQAUCsV7771nZ2dnY2l5d8gQunfPxN6MHUvF3wg5OeTubmI7REQ0bNgwALa2tuI/oEIIiSg8nMzNRTQUGWkYdCZ44w26cEFMxwYOHAjA2tpaY8zhpeQ4hKaIioqaP3++sGzUYJnbt2/PmjWr3iMjhs3MzEaMGCGEc+DAgaKG3vTvT2fOlPzYoIHpTRHNmTMHgIuLS77o8qA7dpBwIKnXi9tNzs8nLy86d46IaO9e8vERM/SMiMaNGwfg1VdfFdOIeBxCUyQnJ9eqVcvkY4niDaNCoSjeNk6fPl0v7iNFH31UcvUtJ4eaNRPTWGRkpEKhaNiwoSnXWh7XogUVn4ls356ys0W0dfUqDRtGffrQxx9TWprIjs2bNw9Aq1atRLYjEt+2Zop69ept377dqPuVH2VmZtajR4+IiIiZM2dqNBqFQvHTTz/NnTtXIWI0IwB88AHmzkVSErKz8emn+PBDMY25ubnZ2dkplcqKVbpz/XrcuYO2bZGYiKgokY25uLgAKC7dLxcOoRHCw8P79OkTEREBoF27dkYNYC3ViBEjANjZ2X0oLjAGrVrh++/x3Xf4+GN064YJE8Q0Vr9+fVtbW/HDixISkJKCn3/G0qWYPdswLYzpzp5FTAyuXEFMDK5dE9k3FxcXc3PzatWqiWxHJA6hEa5fv75jx46LFy9K1WCDBg3q1at3//79xMREaVo8dgxJSXB2xrRpmDVLTEs2NjaShFCthk4HFxe4uKBWrYo1LtLJycna2pq3hJVJzZo1AaSnp0vYZseOHQEcPnxYmuYuXcKePUhLQ3IyUlJENmZnZyfJQFutFo6OqFEDDg54eAhsKklDXKNGDSsrK94SViZOTk6QOoS+vr4Ajhw5Ik1zNWoAgFBL9949kY3Z29tLFcJ9+7B3r6gpsh8jRFHkITTg6OhoaWkp3I8uo5d1ZL1JhC3hPdEf7kf5+XVp375fbq6fNM0JIdRqASAjQ2RjdnZ2ZRkf+EzdumHmTCgUSEzEuXOwtRXfpDQsLCyUSmU5DWgsO94SGqE8dke9vb0vXozcsmWkNK0KIRQmiBIdwldeeUV0hwDAxqZko2VtXTJQ2QSjq1Rp5ODwkYVFIweHUIlO2wrnSGXEITRCeYTQ3Bzt2oFIol01IYQFBWRjoxZ7+IXmzZvn5+eLbKRBA3zyiWG5dm1MmiSqtYzCwuvZ2VkazfXs7Hy9XmTfAOj1eg5hZVKjRg2FQpGRkaGX4s9fzNcXACQ5NXOuZk0PJye//Hxlfr7NxYsk7jSGl5eXVtizFSEvD507G75iLl3C6tWiWhPekdgLqo+wtrZ2dnaWoKHVqxEQgN698cUXhj2RMuMQGsHCwsLe3l6r1WY/WrNUNAlDWLV69Yv37v19756NjY1Go8nNzRXTWoMGDRwcHMT3qlUrTJkCCWdekjCKtWvXriHsPoixaxciIrB5M3bsgJ0dZswwam0OoXHKY4/UxwdKJY4fh/gK1MLnKSMjQziRmyHusDAzM7OwsDA5OVlkr+rUQc+eWLJEZDPAw/jt379fgraAM2fO3LhxY8aMGRqR8/hERGDSJAg3b3z+OSIjjVqbQ2ic8jhBameHVq3Qti3S0sQ25eDgoFQqs7OzHR0dIaKfRPTzzz/7+/ufO3fO09MzNDTUtBr1xbvtkydj9WpDsWWTCfOW4+GX4IYNG0wubJeZmTlhwoQ2bdqkpKSsXbvWw8Nj06ZNRreiVmPVKuj1yMwsqeBubg5j9+FlvXO1ksnJyWnYsCGArVu3StjsuXM0ahSp1UREBw7Q//4nqjVhYygUg/nTpLH5SUlJXbp0ET4eQn17AI0bN960aZNRt5jv20deXhQRQf36ERHt3Ene3jRypAk9oqtXr7711ltCT1xdXfv06VO9enUAFhYWkyZNyjbmlnC1Wr1gwQJhN9vCwqJTp07F9wD36dPn0qVLZW0oOpqaNyeAVq2iyZNp7VrD43fvUuvWRr07DmFZnT17tmnTpsJf6/XXX88wfga1J4mJoTp1KDSUiGj1agoJMb0pvV7v5+fXrFkz4S6Qhg0bhoeHl30YhE5HK1bssra2BuDs7Pz7778TUXR0dIsWLYQ33r59+wNlGPJ/4wYNHGio9dSrlyGERDRwYEkIExPp4Wiwp8nLywsODhZu07W2tg4ODhYmfkpPT//www+Fm8t79Vq6YgWVpfjw9u00cGCo8F569+6dkJBARGq1evny5cIOvIWFxfjx47Oysp7Wyvnz1LOn4e25u9MHH9CFC9SqFW3dSvv30+uv05Ytz+7KIziEZbJmzRrho+nk5CQUbK9SpYpKpYqMjBQ/0icmhiZOpI4d6cYNUSFcsGDBrFmziEitVv/888/CRhuAh4fH1q2nn/kZvXaNunalunU1Nja1VSpVenp68a+EgmXFF7V79OhxThjU9y8FBQXz5uVUq0YAWVvT3LmUk1NSJyAryzADHRH16UMLFxIRPWlArfCiwqlLhUIRFBT07+HF8fHxPXsOcHbWAtSmzdNqlCcmUkCA0KscH5+uf/zxxz+ekJGRMX78eCHVjo6OYWFh//7LZmRkBE+apLezM5QgGT2aGjQggKZMoZQUWrKE5s0j4yd44xA+y4MHp776Svjwvfnmm0IFJwsLCzMzMwDVq7u5uemnTiVhbszERNq+3bBefDxduUKnT1NCguGR8+ep1BGIMTH0+ecUG0v9+xtCmJBgymjVlJSUWrVqFU/TqVarw8PDGzdubG1t4+iY3rw5hYeXPl2pTkdhYSQkp3Zt2rEjtdT28/PzQ0JChB1UCwuL0aNHp6Y+9szIyMhGjRp16jQOoICAkryV+oo//0xqNR05cqRTp07/fsK+ffuE4pHC5vfIkSNPeeORkdSwoWHLFBBAMTEl29ijR2n3bpo6lSwtCSAHBwoJoaeUWYuPj+/UqZPwui1btize7Gs0muXLlwtnBKK7daNBg+i11wwv2bo1HTz4lO49E4fwqW7epPbtSaFY4Ov7wQcfCBvDli1bXrly5ebNm7NmzXrzzT+EP4RSSb1706xZZG1N8fFERHPn0tq19N13tGKFobHFi2nx4lJeRAghEb3/Pg0damikSRMKCaFHtkZPlJSUNHPmTGE55V+1r9Vq9bp1x11dDR+Y5s1p/XoaM6akVyqVobwbQMOGGepQPEV6evr48eOFe0qtra2nTp2ak5OTkJDQq1cv4bPbqlXrAwfKWpHt9OnTu3btIqLY2FjhkVu3bgUFBQmXH+rVqxceHl6WA9H8fAoOJisrAmj4cLKzM+wSLl1Kw4cb/kAffljWYcCRkZGurq7C2wkICFi3bp2Xl5fwo5+f39sDBmhcXAzFKX/5pUz7wU/FIXyyqCiqXp0Aql9frVL51K0LYOTIkY9WoNDpKDaWRo82bEa+/po++YQ6dyadzpQQ/v03OTrSuHEk/ImFWvEjRtDx40+rkJmfn//GG288/TBGrabwcGrSxLA16NKFWrYkIbCenvTNN+TsTEadbLpw4UJxJXwrKythL6569erCvJ9GNGTonjokJESr1b711lvC3r6Njc2cOXOMLfZx4waNH09bttDEidSmDeXk0NKltGgRTZxIxpbeLigo+Prrr4VDa+Ebwc3NLSgoSLjbe6GfH02dSjk5xjX6BBzCJ5g/nxQKw4kFb28Cclq3/uWXX5709Hv3aPVq+vVX+v57+uILWrq0JIQtWlDfvtS3L3l4/DOEDx7Q6NF06BAVH6Fs307795NOR9HRpFKRUMq6U6exbdq0Wb58eV5e3qOrb9261ahPvFpNK1fSsmXUowdt3mwo2+npSWo1Pf1MxJMsXbq0+DNqbm4usrL1nj17hNZUKpXJ5RWJaNs2Cg6m//s/mjiRli4t/YuvjG7fvi1slpVKpXDmRtg2Xr161fRG/4VD+Ai9nq5fpwsXqKiITp8mW1saPNiwMWzQgP7665kNCCHMz6fWrenzz5+xJdTracgQAsjH54kNXr1KU6boGjQw7Bo5ODhMmDDh4sWLRKTT6VatWmVambAePejePRo8mHbsEFUIVLhq5+7uLuyamt4QERHFxMQIp5FEtiOEUK+nrl1p/HhRIRR0795d+P93d3c37arP0/HF+ocePEDfvpg9G+Hh8PeHuTk2bcKGDcjKwltv4cwZlHky52rVEByM5cuf+ARhzO0332D9etjaYunSJz7TzQ2hoWaXLl1cvXp1x44ds7Ozf/jhh06dOm3fvt3MzGzkyJFixvstWIBZs4y+sPwo4eyUcM1N/P20Qo366qLnLRcoFPjhB6xaJUFTQgXkQYMGnT9/vnfv3hK0+DgeT/jQ0qXw98f06QBw8iQmTcKuXRgxAh4e+PTTMo4frVEDwoSz/fvjnXfg6AiNBg8vdxtGOKSkYONG7N6N3bthZoZ16/DwItwTVa1aNSgoKCgo6PTp08OHDz9z5sycOXOKD8lMVrs2Bg8WNaxB2BElIkgRQqEdMzEjnQAAdeoY7qD28sK336JxY5HtGfj5+ZVTzSsO4UN//VXyeWzTBleuADD2i7Rfv5LlFSv++dvBgw0Lvr7Hfv65wMKia2joY6s8U8uWLQcPHnzmzJnOnTsb1bF/+OADCCUdPv4YYrIjBEYID4kuPCHEWHwIc3JKimi0aYM7d0S2V+44hA+Zmz92n7/oj8KT3Lp1KyBg2r17/lOnXpk4cbSxq2dmZgIQbg012U8/YcAAADA3x86dGDkS9vamtFPHwmKNv3/VevUKq1RRiN5KSBXCGzdQfIN9cjIuXxbZXrnjED7k54ft29GxIwAcOICHM0tKKy8vr2fPqffuhTRqdObtt0eY0IIQQpGjb3JySgom5eSYvjF00evfi41FmzY4eRIVJoQAtFoIM2SLHB0hCLGyWuDiorW0lKCt0nAIHxo1CsOHIzAQTk5ITMT//Z/kr6DRaAYPHnz58vZXX42Pi4urXt2Uke/C6CSRW0IA0dEQPlSihkYKgRECLfqYUMIQbtiA+HgASE5Gnz5iW6tWUIDkZAtJAl0aDuFDFhZYtw45OSgogCRDrf/lypUrcXFx5ubmUVFRJp8DlGRLCCApyXDgZOQo8McJ56uEEFaYY0IA772HqVMB4LffpNgdlai425NwCB9nZ1dyNlNqNjY2+fn5Wq325MmTxbOIGkuqLeGYMRBq3q5bJ6IVITB6PRQKEIFIzCdVwhBWLi/dG5ZR/fr1Fy9eDODTTz81uUCGVFtCaRRvCYvTKIJUIfTyQvv2hmV3d/j4iGyv3PGW8LkaNWrUunXr9u/f/+WXXy4xqd6DJGdHf/jBcD0TwPz5ML34be3a+O471KuHnBwoFCJ32KQK4aN3VbRqJbKxh62oVJJdcPwX3hI+VwqFYsmSJRYWFkuXLo2LizN29fz8/MLCwmrVqomci6ZTp5JLML6+IkrTm5nh0CFs2oSoKBw8KLJITgXdHT1xAlFRyM3F2rW4dQsA/vMfZGYafjtunNiiHRzC58/Dw2PSpEl6vX7MmDHGFhSsWPuiAGbOxMCB+P13bN+OZs3www9iGquIIczIwMiRWLwYO3bgk08QGAidDomJJZc+rlwRX5+rIr3hl8bMmTNdXV3Pnj1r7B6pVGdlJLN/PwYONCwPGgRxRdCuXr0K4NixYw+Ea3wVQUwM+vSBUB24fXvUqmU42XrtGi5fxuXLKCgQ/yIcQhlUq1btxx9/BDBz5szbt2+XfcUKtyXUalF8B7mFBdRqMY117drVzMwsJSWladOmq0UWCZbKo2XUANSoYZhcIDwcy5Zh2TKIrgcJDqFcevfuHRgYmJubO3HixLKvJUzeZFduF1GM1qpVSdHigwfRtq1pzezevZuI2rVrt2LFCm9v79u3b7///vs9e/aUcCpIEzVtigsXSn68cMFweubrr7FwIRYuxKuvSvAqkg+OYmV0584de3t7AJHFE82XJikpafny5UFBQcVTJnTt2vW5dfIZEhOpQweaM4e+/JJ8fU2bRF6n08XExDz6Y3h4uFDNxdzcfPTo0ffu3ZOux0bS66lnT/r+ezp8mCZNonHjiIj8/am4uE6vXk+rpVM2HEI5LVy4EED9+vUfHTKvVqvj4uJCQ0MDAgL+UYW+du3aLVq0eEZBvucsP58OH6bjx6morHVlBGfPnv3444+f9NvMzMziSjZPqn32nBQV0Zo1NHs2RUUZHtm0iYrrbmzdKr7IBYdQTlqttnXr1gA+++yzEydOhISEBAQE2D8+oqF27doqlWr58uXnz583qvZuxZSdnZ2ZmUlEDx482LZt29Of/Gj9qNatWx96SknDyoxDKLNjx479e2KTZs2affjhh2vXrk1OTpa7gxKbNm3a2LFjjVolMjJSqKEqlJ+5detWOfVNLhxC+Q0ePNjS0rJx48ajR48ODw9/8YKn0+mmT5+en59PRJmZmREREca2kJeX9+WXXwq3KNja2r733nvl0E3ZKEj0ze+MPdOQIUNeffXV4OBgMY0kJyfPmDFjzZo1ALZs2RIYGChR72TGlyjY8xASElI8k4fJXFxcVq9eLdThzcrKkqJfFQLfwM2eh/r169evX1+SplxdXc+dOydcw3gx8JaQVTIk9YzZsuMQskqGQ8iYzCriYAtxXpx3wl4SvCVkTGa8JWRMZrwlZExmvCVkTGa8JWRMZhxCxmTGu6OMyYy3hIzJ7MXbEvJQJlbJnDhxIjs7u23btv+o/VF5cQgZk9mLs01nrJLiEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDImMw4hYzLjEDIms/8H/WxJmJZPuI0AAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SMILES: [H]N(C(=O)[C@H](Cc1cnc[nH]1)N([H])C(=O)[C@@H]1CCCN1C(=O)[C@H](CC(N)=O)N([H])C(=O)[C@H](CCSC)N([H])C(=O)[C@H](CCC(N)=O)N([H])C(C)=O)[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Exact Mass: 918.4494\n", + "Expected mass: 918.4494\n", + "Mass difference: 0.0000\n" + ] + } + ], + "source": [ + "sequence = \"QMNPHIR\"\n", + "mods = \"Acetyl@Any_N-term\"\n", + "mod_sites = \"0\"\n", + "\n", + "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "\n", + "print(f\"Peptide sequence: {sequence}\")\n", + "print(f\"Modifications: {mods}\")\n", + "print(f\"Modification sites: {mod_sites}\")\n", + "visualize_peptide(smiles, \"Peptide with N-terminal modification\")\n", + "\n", + "# Verify mass\n", + "expected_mass = np.sum([CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)])\n", + "actual_mass = Descriptors.ExactMolWt(Chem.MolFromSmiles(smiles)) - MASS_H2O\n", + "print(f\"Expected mass: {expected_mass:.4f}\")\n", + "print(f\"Mass difference: {abs(actual_mass - expected_mass):.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. C-terminal Modification" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[H]N([H])[C@@H](CCC(N)=O)C(=O)N([H])[C@@H](CCSC)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(N)=O)[C@@H](C)CC\n", + "Peptide sequence: QMNPHIR\n", + "Modifications: Amidated@Any_C-term\n", + "Modification sites: -1\n" + ] + }, + { + "data": { + "image/jpeg": "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", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SMILES: [H]N([H])[C@@H](CCC(N)=O)C(=O)N([H])[C@@H](CCSC)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(N)=O)[C@@H](C)CC\n", + "Exact Mass: 875.4548\n", + "Expected mass: 875.4548\n", + "Mass difference: 0.0000\n" + ] + } + ], + "source": [ + "sequence = \"QMNPHIR\"\n", + "mods = \"Amidated@Any_C-term\"\n", + "mod_sites = \"-1\"\n", + "\n", + "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "\n", + "print(f\"Peptide sequence: {sequence}\")\n", + "print(f\"Modifications: {mods}\")\n", + "print(f\"Modification sites: {mod_sites}\")\n", + "visualize_peptide(smiles, \"Peptide with C-terminal modification\")\n", + "\n", + "# Verify mass\n", + "expected_mass = np.sum([CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)])\n", + "actual_mass = Descriptors.ExactMolWt(Chem.MolFromSmiles(smiles)) - MASS_H2O\n", + "print(f\"Expected mass: {expected_mass:.4f}\")\n", + "print(f\"Mass difference: {abs(actual_mass - expected_mass):.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Multiple Modifications" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[H]N(C(=O)[C@H](CCC(N)=O)N([H])C(C)=O)C(CCS(C)=O)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](COP(=O)(O)O)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Peptide sequence: QMNPSIR\n", + "Modifications: Acetyl@Any_N-term;Oxidation@M;Phospho@S\n", + "Modification sites: 0;2;5\n" + ] + }, + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAEsASwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoorM8RtcL4X1ZrMyC6FlMYTFneH2HbtxznOMYqoR55KPcGadFc9b3t7Za3qMurMFtltLcoYVdolO6fcenBwFyfQLmucTUvEejNaubDUtWjJnnWNGO4gvgxsT/d+8ueoOB92uuGClNtJrp87q++2hLkeiUVxtn8UPDEsy22o3UujXjZH2fVIzAwxjqT8vf1ri9W+I8/jXWToHhXz/KySsqnY10AQCwPVYwep4J7d8cdeM6DamtV0OnCUViKsafMop9XsenDWZzIsv2ZPsTXZtA/mHfuDmPdtxjG8Y69OfatisfTNEEEFtLfuJ75P3krI7iIy45YJnbn3x154zWxWcOa2o8R7LmtT6f1/W3ogoooqznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAK19ZJfwCGSSRFDBjsI+bHY5BBHtVmiindtWAp6lpOnaxam21OxtryA/8ALOeIOP1rzz4M6Pp+i2niOxhtY476z1ea1llxl3jXBj5PbB4H1Nen15hbaxpvg/4x+IbbVL2Cxtdatba8gedwiGRQY2G48AnBP4UgPT6KZDNFcQrLDIkkTjKujAgj2Ip9ABRRRQAUUUUAFFFchcrq9t8WLGdJLh9Gu9LkheMyfuo5lfcG256leMgUm0tWB19FFFMAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAqK5uIrS3kuJ32RIMscZ/QdT7VLVbULMX1jJblyhbDK4GdrAhlOO/IHFJ3toVBRckpbdQtL+C8aRI/MWSPG+OWMowB6HB7HB59j6UzUtJ07WLY2+p2FteQn+C4iVx+orl/BviaPxBrOos8bpc+TGBlAgKKzjG3c2CGLc5PUeldpU05qceZHRjMNLDVnTkrNW/FHn0vwn0+xma48K6xqnhy4JzttZjJAx/2onJB+mQKl+HHiHXdTuvEOj+IZobi90a8EAuI4vL81CDhio45xnj1rvK5Czs2034r6jIgIh1fTI52z0MkDiM4/4DIn51ZynX0UUjMEUsxAUDJJPAFAC0VzF1rNteyyTQ6v5Vl9m3WssDDEsu5wwHB3EYTC993Q10Nm80llA9wgSdo1Mi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LPSbKK1h/i2D5nPqzHlj7mslvD9+bxp/tUvlnVVuvs+U2bBjn7u7PHTNdTXfjMTOcVHnunvbq/PzIjGxWv8AT7PVLKSzv7WG5tpRh4pkDKw+hrw7x94Gv9Du9G8P+F9avIdK1u6aE6ZcXD+RGQM8MPm2YJyvt37e9VyPiyKOTxN4VdhETDeO+XK5XK4yM+/HFeZKXKrnTQoutPkTto39yb/Qr+D/AIbad4bs7Y3sg1O/hXCzSoFSP2ROgx68niu2qMTwnbiVDucoPmHLDOR9Rg8exqShRS2FUq1J29o27aK/RBRRRVGQUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFcv498Lv4n8P7bOTyNXspBdadcDgxzLyOfQ9D+fauoooA880v4weGT4et7rXNQjsNTGYrqw2s0scynaw2KCcZHH1+tMn+I2p6zbyQ+GPBWt3pkUqlzdotpDyMbgznkflXbwaJpNtfS30GmWUV5M26S4S3VZHPqWAyTV+gDmfh5oV34a8A6RpF8qJdW8R81UbcFZmLEZ7/e+n1rpqKKACiiigAqvqFqb3Tbq0EhjM8Lxhx/DuBGf1qxRQ1fQcZOLUl0MqztLttRS7uYYLcRWxgCQuWDEkHPQYA28fU9K1aKrXdlFeG3aQyK1vMJoyjlSGAI5x1BBII96SViqlRzd2MS+S7RTZFZkkjdo7hSGi3KcYJBznP8AI1w+qeGfE3hzxLqXibwnPDfrqDpJe6Rd4TzCq4Bik/hbrweOe/Ar0JEWNdqKFGScAY5PJp1Mg5Twx8QNI8R3Dae4l03WYuJtMvV8uZT3wD94e47dhXV1538Z9G067+HupanNaRm/sUR7a6AxJEd69GHOOTx0ru9PZm021Z2LMYUJZjkk4HJoAs0UUUAFYWp6bdzS6isMEUyX9ssAd3x5RAYcjHK/NnjnOfrW7RUyjzI1pVXSldf11/Q5620O7hmtJGuZGEV/NO0ZZduxvMwRxnPzjv3NdDRRRGKjsFatKq7y/rqFFFFUZBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAcP8AGA4+FGv/APXJP/RiV2FgMadaj/pkn8hUzokqFJEV1PVWGQad0GBQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH/9k=", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SMILES: [H]N(C(=O)[C@H](CCC(N)=O)N([H])C(C)=O)C(CCS(C)=O)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](COP(=O)(O)O)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", + "Exact Mass: 964.3838\n", + "Expected mass: 964.3838\n", + "Mass difference: 0.0000\n" + ] + } + ], + "source": [ + "sequence = \"QMNPSIR\"\n", + "mods = \"Acetyl@Any_N-term;Oxidation@M;Phospho@S\"\n", + "mod_sites = \"0;2;5\"\n", + "\n", + "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "\n", + "print(f\"Peptide sequence: {sequence}\")\n", + "print(f\"Modifications: {mods}\")\n", + "print(f\"Modification sites: {mod_sites}\")\n", + "visualize_peptide(smiles, \"Peptide with multiple modifications\")\n", + "\n", + "# Verify mass\n", + "expected_mass = np.sum([CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)])\n", + "actual_mass = Descriptors.ExactMolWt(Chem.MolFromSmiles(smiles)) - MASS_H2O\n", + "print(f\"Expected mass: {expected_mass:.4f}\")\n", + "print(f\"Mass difference: {abs(actual_mass - expected_mass):.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Further usage:\n", + "\n", + "As we stated before, being able to represent a peptide as a SMILES opens some doors for us. Let's take a look at feature extraction using RDKit:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(2048,)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def get_fingerprint(mol, fp_type='morgan', **kwargs):\n", + " \"\"\"\n", + " Generate molecular fingerprints for an RDKit mol object.\n", + " \n", + " Parameters\n", + " ----------\n", + " mol : rdkit.Chem.Mol\n", + " RDKit mol object.\n", + " fp_type : str, optional\n", + " Type of fingerprint. Options are 'morgan', 'topological', 'maccs', 'atompair'.\n", + " Default is 'morgan'.\n", + " **kwargs : dict\n", + " Additional keyword arguments for specific fingerprint parameters.\n", + " \n", + " Returns\n", + " -------\n", + " list\n", + " Fingerprint bit vector.\n", + " \"\"\"\n", + " if fp_type == 'morgan':\n", + " radius = kwargs.get('radius', 2)\n", + " nBits = kwargs.get('nBits', 2048)\n", + " mfpgen = rdFingerprintGenerator.GetMorganGenerator(radius=radius,fpSize=nBits)\n", + " fp = mfpgen.GetFingerprint(mol)\n", + " \n", + " elif fp_type == 'topological':\n", + " minPath = kwargs.get('minPath', 1)\n", + " maxPath = kwargs.get('maxPath', 7)\n", + " fpSize = kwargs.get('fpSize', 2048)\n", + " fp = Chem.RDKFingerprint(mol, minPath=minPath, maxPath=maxPath, fpSize=fpSize)\n", + " \n", + " elif fp_type == 'maccs':\n", + " fp = MACCSkeys.GenMACCSKeys(mol)\n", + " \n", + " elif fp_type == 'atompair':\n", + " nBits = kwargs.get('nBits', 2048)\n", + " fp = Pairs.GetAtomPairFingerprintAsBitVect(mol, nBits=nBits)\n", + " \n", + " else:\n", + " raise ValueError(f\"Unknown fingerprint type: {fp_type}\")\n", + " \n", + " return fp.ToList()\n", + "\n", + "\n", + "sequence = \"QMNPSIR\"\n", + "mods = \"Acetyl@Any_N-term;Oxidation@M;Phospho@S\"\n", + "mod_sites = \"0;2;5\"\n", + "\n", + "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "np.array(get_fingerprint(Chem.MolFromSmiles(smiles), fp_type='topological')).shape" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "alphabase", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/test_peptide_smiles.py b/tests/test_peptide_smiles.py new file mode 100644 index 00000000..5e26b7be --- /dev/null +++ b/tests/test_peptide_smiles.py @@ -0,0 +1,107 @@ +import numpy as np +import pytest +from rdkit import Chem +from rdkit.Chem import Descriptors + +from alphabase.constants.atom import CHEM_MONO_MASS, MASS_H2O +from alphabase.peptide.precursor import get_mod_seq_formula +from alphabase.smiles.peptide import PeptideSmilesEncoder + + +@pytest.fixture +def encoder(): + return PeptideSmilesEncoder() + + +def test_encode_peptide_no_modifications(encoder): + sequence = "QMNPHIR" + smiles = encoder.encode_peptide(sequence) + mol = Chem.MolFromSmiles(smiles) + + assert mol is not None + + expected_mass = np.sum( + [CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, "")] + ) + actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O + assert np.isclose(actual_mass, expected_mass, atol=1e-4) + + +def test_encode_peptide_with_modifications(encoder): + sequence = "QMNPHIR" + mods = "Gln->pyro-Glu@Q^Any_N-term;Oxidation@M" + mod_sites = "1;2" + + smiles = encoder.encode_peptide(sequence, mods, mod_sites) + mol = Chem.MolFromSmiles(smiles) + + assert mol is not None + + expected_mass = np.sum( + [CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)] + ) + actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O + assert np.isclose(actual_mass, expected_mass, atol=1e-4) + + +def test_encode_peptide_invalid_amino_acid(encoder): + with pytest.raises(ValueError, match="Unknown amino acid code"): + encoder.encode_peptide("QMNPHIRX") + + +def test_encode_peptide_invalid_modification(encoder): + with pytest.raises( + ValueError, match="Unknown amino acid code: Q or modification: Invalid_Mod" + ): + encoder.encode_peptide("QMNPHIR", "Invalid_Mod@Q", "1") + + +def test_encode_peptide_n_terminal_modification(encoder): + sequence = "QMNPHIR" + mods = "Acetyl@Any_N-term" + mod_sites = "0" + + smiles = encoder.encode_peptide(sequence, mods, mod_sites) + mol = Chem.MolFromSmiles(smiles) + + assert mol is not None + + expected_mass = np.sum( + [CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)] + ) + actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O + assert np.isclose(actual_mass, expected_mass, atol=1e-4) + + +def test_encode_peptide_c_terminal_modification(encoder): + sequence = "QMNPHIR" + mods = "Amidated@Any_C-term" + mod_sites = "-1" + + smiles = encoder.encode_peptide(sequence, mods, mod_sites) + mol = Chem.MolFromSmiles(smiles) + + assert mol is not None + + expected_mass = np.sum( + [CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)] + ) + actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O + assert np.isclose(actual_mass, expected_mass, atol=1e-4) + + +def test_encode_peptide_multiple_modifications(encoder): + sequence = "QMNPSIR" + mods = "Acetyl@Any_N-term;Oxidation@M;Phospho@S" + mod_sites = "0;2;5" + + smiles = encoder.encode_peptide(sequence, mods, mod_sites) + mol = Chem.MolFromSmiles(smiles) + + assert mol is not None + + expected_mass = np.sum( + [CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)] + ) + actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O + assert np.isclose(actual_mass, expected_mass, atol=1e-4) From b513eb0d3babec98761a3543c50421e45d44560f Mon Sep 17 00:00:00 2001 From: Mikhail Lebedev Date: Mon, 23 Sep 2024 22:13:17 +0200 Subject: [PATCH 05/37] ENH: minor corrections --- alphabase/smiles/peptide.py | 8 ++------ docs/nbs/peptide_smiles.ipynb | 21 ++++++++------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/alphabase/smiles/peptide.py b/alphabase/smiles/peptide.py index ddbe502a..2d11c421 100644 --- a/alphabase/smiles/peptide.py +++ b/alphabase/smiles/peptide.py @@ -33,8 +33,8 @@ def encode_peptide( Returns ------- - Chem.Mol - RDKit molecule object of the peptide. + str + The SMILES string of the peptide molecule. """ if mods and mod_site: mods = {int(m): mod for m, mod in zip(mod_site.split(";"), mods.split(";"))} @@ -81,7 +81,6 @@ def _build_peptide( c_term_mod = mods[-1] # Process each amino acid in the sequence for idx, aa in enumerate(sequence): - # Get the amino acid SMILES if idx + 1 in mods: aa_smiles = self.amino_acid_modifier.ptm_dict.get(mods[idx + 1], None) else: @@ -107,8 +106,6 @@ def _build_peptide( ) for idx in range(1, len(amino_acid_mols)): peptide_mol = self._connect_amino_acids(peptide_mol, amino_acid_mols[idx]) - # if idx == 1: - peptide_mol = self.amino_acid_modifier._apply_n_terminal_modification( peptide_mol, n_term_placeholder_mol=n_term_placeholder_mol, @@ -120,7 +117,6 @@ def _build_peptide( c_term_mod=c_term_mod, ) Chem.SanitizeMol(peptide_mol) - print(Chem.MolToSmiles(peptide_mol)) return peptide_mol def _connect_amino_acids(self, mol1: Chem.Mol, mol2: Chem.Mol) -> Chem.Mol: diff --git a/docs/nbs/peptide_smiles.ipynb b/docs/nbs/peptide_smiles.ipynb index d451ec9b..a4ce70b4 100644 --- a/docs/nbs/peptide_smiles.ipynb +++ b/docs/nbs/peptide_smiles.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -47,14 +47,13 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[H]N([H])[C@@H](CCC(N)=O)C(=O)N([H])[C@@H](CCSC)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", "Peptide sequence: QMNPHIR\n" ] }, @@ -111,14 +110,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[H]N(C(=O)[C@@H]1CCC(=O)N1[H])C(CCS(C)=O)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", "Peptide sequence: QMNPHIR\n", "Modifications: Gln->pyro-Glu@Q^Any_N-term;Oxidation@M\n", "Modification sites: 1;2\n" @@ -174,14 +172,13 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[H]N(C(=O)[C@H](Cc1cnc[nH]1)N([H])C(=O)[C@@H]1CCCN1C(=O)[C@H](CC(N)=O)N([H])C(=O)[C@H](CCSC)N([H])C(=O)[C@H](CCC(N)=O)N([H])C(C)=O)[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", "Peptide sequence: QMNPHIR\n", "Modifications: Acetyl@Any_N-term\n", "Modification sites: 0\n" @@ -237,14 +234,13 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[H]N([H])[C@@H](CCC(N)=O)C(=O)N([H])[C@@H](CCSC)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](Cc1cnc[nH]1)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(N)=O)[C@@H](C)CC\n", "Peptide sequence: QMNPHIR\n", "Modifications: Amidated@Any_C-term\n", "Modification sites: -1\n" @@ -300,14 +296,13 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[H]N(C(=O)[C@H](CCC(N)=O)N([H])C(C)=O)C(CCS(C)=O)C(=O)N([H])[C@@H](CC(N)=O)C(=O)N1CCC[C@H]1C(=O)N([H])[C@@H](COP(=O)(O)O)C(=O)N([H])[C@H](C(=O)N([H])[C@@H](CCCNC(=N)N)C(=O)O)[C@@H](C)CC\n", "Peptide sequence: QMNPSIR\n", "Modifications: Acetyl@Any_N-term;Oxidation@M;Phospho@S\n", "Modification sites: 0;2;5\n" @@ -365,7 +360,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -374,7 +369,7 @@ "(2048,)" ] }, - "execution_count": 29, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } From 46c04cae67ecfe6e2f3cfa5006c6b0b3f3f51ee4 Mon Sep 17 00:00:00 2001 From: Mikhail Lebedev Date: Tue, 24 Sep 2024 11:15:20 +0200 Subject: [PATCH 06/37] ENH: pr comments --- alphabase/smiles/peptide.py | 49 ++++++++--- docs/nbs/peptide_smiles.ipynb | 149 ++++++++++++++++++++++++++++++++-- tests/test_peptide_smiles.py | 57 +++++++++---- 3 files changed, 225 insertions(+), 30 deletions(-) diff --git a/alphabase/smiles/peptide.py b/alphabase/smiles/peptide.py index 2d11c421..6f955540 100644 --- a/alphabase/smiles/peptide.py +++ b/alphabase/smiles/peptide.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Optional from rdkit import Chem @@ -13,7 +13,7 @@ class PeptideSmilesEncoder: def __init__(self): self.amino_acid_modifier = AminoAcidModifier() - def encode_peptide( + def peptide_to_smiles( self, sequence: str, mods: Optional[str] = "", @@ -36,17 +36,39 @@ def encode_peptide( str The SMILES string of the peptide molecule. """ - if mods and mod_site: - mods = {int(m): mod for m, mod in zip(mod_site.split(";"), mods.split(";"))} - else: - mods = {} - peptide_mol = self._build_peptide(sequence, mods) + peptide_mol = self._build_peptide(sequence, mods, mod_site) return Chem.MolToSmiles(peptide_mol) + def peptide_to_mol( + self, + sequence: str, + mods: Optional[str] = "", + mod_site: Optional[str] = "", + ) -> Chem.Mol: + """ + Encode a peptide sequence into an RDKit molecule object. + + Parameters + ---------- + sequence : str + Peptide sequence, e.g., "AFVKMCK". + mods : Optional[str] + Modifications in the format "GG@K;Oxidation@M;Carbamidomethyl@C". + mod_site : Optional[str] + Corresponding modification sites in the format "4;5;6". + + Returns + ------- + Chem.Mol + The peptide molecule. + """ + return self._build_peptide(sequence, mods, mod_site) + def _build_peptide( self, sequence: str, - mods: Dict[int, str], + mods: Optional[str] = "", + mod_site: Optional[str] = "", ) -> Chem.Mol: """ Build the peptide molecule from the sequence and modifications. @@ -55,14 +77,21 @@ def _build_peptide( ---------- sequence : str Peptide sequence. - mods : Dict[int, str] - Modifications dictionary with the site as key and the modification as value. + mods : Optional[str] + Modifications in the format "GG@K;Oxidation@M;Carbamidomethyl@C". + mod_site : Optional[str] + Corresponding modification sites in the format "4;5;6". Returns ------- Chem.Mol The peptide molecule. """ + if mods and mod_site: + mods = {int(m): mod for m, mod in zip(mod_site.split(";"), mods.split(";"))} + else: + mods = {} + # List to hold the amino acid molecules amino_acid_mols = [] diff --git a/docs/nbs/peptide_smiles.ipynb b/docs/nbs/peptide_smiles.ipynb index a4ce70b4..1621af8c 100644 --- a/docs/nbs/peptide_smiles.ipynb +++ b/docs/nbs/peptide_smiles.ipynb @@ -89,7 +89,7 @@ " print(f\"Exact Mass: {Descriptors.ExactMolWt(mol) - MASS_H2O:.4f}\")\n", "\n", "sequence = \"QMNPHIR\"\n", - "smiles = encoder.encode_peptide(sequence)\n", + "smiles = encoder.peptide_to_smiles(sequence)\n", "\n", "print(f\"Peptide sequence: {sequence}\")\n", "visualize_peptide(smiles, \"Peptide without modifications\")\n", @@ -149,7 +149,7 @@ "mods = \"Gln->pyro-Glu@Q^Any_N-term;Oxidation@M\"\n", "mod_sites = \"1;2\"\n", "\n", - "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites)\n", "\n", "print(f\"Peptide sequence: {sequence}\")\n", "print(f\"Modifications: {mods}\")\n", @@ -211,7 +211,7 @@ "mods = \"Acetyl@Any_N-term\"\n", "mod_sites = \"0\"\n", "\n", - "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites)\n", "\n", "print(f\"Peptide sequence: {sequence}\")\n", "print(f\"Modifications: {mods}\")\n", @@ -273,7 +273,7 @@ "mods = \"Amidated@Any_C-term\"\n", "mod_sites = \"-1\"\n", "\n", - "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites)\n", "\n", "print(f\"Peptide sequence: {sequence}\")\n", "print(f\"Modifications: {mods}\")\n", @@ -335,7 +335,7 @@ "mods = \"Acetyl@Any_N-term;Oxidation@M;Phospho@S\"\n", "mod_sites = \"0;2;5\"\n", "\n", - "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites)\n", "\n", "print(f\"Peptide sequence: {sequence}\")\n", "print(f\"Modifications: {mods}\")\n", @@ -355,6 +355,8 @@ "source": [ "# Further usage:\n", "\n", + "### Molecular fingerprints\n", + "\n", "As we stated before, being able to represent a peptide as a SMILES opens some doors for us. Let's take a look at feature extraction using RDKit:" ] }, @@ -423,9 +425,144 @@ "mods = \"Acetyl@Any_N-term;Oxidation@M;Phospho@S\"\n", "mod_sites = \"0;2;5\"\n", "\n", - "smiles = encoder.encode_peptide(sequence, mods, mod_sites)\n", + "smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites)\n", "np.array(get_fingerprint(Chem.MolFromSmiles(smiles), fp_type='topological')).shape" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adjacency matrix\n", + "\n", + "Now we can easily get an adjacency matrix to work with graph algorithms, numbers specify the bond type between the atoms" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(66, 66)\n" + ] + }, + { + "data": { + "text/plain": [ + "array([[0., 1., 0., ..., 0., 0., 0.],\n", + " [1., 0., 2., ..., 0., 0., 0.],\n", + " [0., 2., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 1.],\n", + " [0., 0., 0., ..., 0., 1., 0.]])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mol = Chem.MolFromSmiles(smiles)\n", + "adj_matrix = Chem.GetAdjacencyMatrix(mol, useBO=True)\n", + "print(adj_matrix.shape)\n", + "adj_matrix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "here we can see the types of corresponding atoms themselves:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "N 0 2 1 0 False\n", + "C 1 3 0 0 False\n", + "O 2 1 0 0 False\n", + "C 3 3 1 0 False\n", + "C 4 2 2 0 False\n", + "C 5 2 2 0 False\n", + "C 6 3 0 0 False\n", + "N 7 1 2 0 False\n", + "O 8 1 0 0 False\n", + "N 9 2 1 0 False\n", + "C 10 3 0 0 False\n", + "C 11 1 3 0 False\n", + "O 12 1 0 0 False\n", + "C 13 3 1 0 False\n", + "C 14 2 2 0 False\n", + "C 15 2 2 0 False\n", + "S 16 3 0 0 False\n", + "C 17 1 3 0 False\n", + "O 18 1 0 0 False\n", + "C 19 3 0 0 False\n", + "O 20 1 0 0 False\n", + "N 21 2 1 0 False\n", + "C 22 3 1 0 False\n", + "C 23 2 2 0 False\n", + "C 24 3 0 0 False\n", + "N 25 1 2 0 False\n", + "O 26 1 0 0 False\n", + "C 27 3 0 0 False\n", + "O 28 1 0 0 False\n", + "N 29 3 0 0 False\n", + "C 30 2 2 0 False\n", + "C 31 2 2 0 False\n", + "C 32 2 2 0 False\n", + "C 33 3 1 0 False\n", + "C 34 3 0 0 False\n", + "O 35 1 0 0 False\n", + "N 36 2 1 0 False\n", + "C 37 3 1 0 False\n", + "C 38 2 2 0 False\n", + "O 39 2 0 0 False\n", + "P 40 4 0 0 False\n", + "O 41 1 0 0 False\n", + "O 42 1 1 0 False\n", + "O 43 1 1 0 False\n", + "C 44 3 0 0 False\n", + "O 45 1 0 0 False\n", + "N 46 2 1 0 False\n", + "C 47 3 1 0 False\n", + "C 48 3 0 0 False\n", + "O 49 1 0 0 False\n", + "N 50 2 1 0 False\n", + "C 51 3 1 0 False\n", + "C 52 2 2 0 False\n", + "C 53 2 2 0 False\n", + "C 54 2 2 0 False\n", + "N 55 2 1 0 False\n", + "C 56 3 0 0 False\n", + "N 57 1 1 0 False\n", + "N 58 1 2 0 False\n", + "C 59 3 0 0 False\n", + "O 60 1 0 0 False\n", + "O 61 1 1 0 False\n", + "C 62 3 1 0 False\n", + "C 63 1 3 0 False\n", + "C 64 2 2 0 False\n", + "C 65 1 3 0 False\n" + ] + } + ], + "source": [ + "for atom in mol.GetAtoms():\n", + " print(atom.GetSymbol(), atom.GetIdx(), atom.GetDegree(), atom.GetTotalNumHs(), atom.GetFormalCharge(), atom.GetIsAromatic())" + ] } ], "metadata": { diff --git a/tests/test_peptide_smiles.py b/tests/test_peptide_smiles.py index 5e26b7be..590acfd4 100644 --- a/tests/test_peptide_smiles.py +++ b/tests/test_peptide_smiles.py @@ -13,9 +13,9 @@ def encoder(): return PeptideSmilesEncoder() -def test_encode_peptide_no_modifications(encoder): +def test_peptide_to_smiles_no_modifications(encoder): sequence = "QMNPHIR" - smiles = encoder.encode_peptide(sequence) + smiles = encoder.peptide_to_smiles(sequence) mol = Chem.MolFromSmiles(smiles) assert mol is not None @@ -27,12 +27,12 @@ def test_encode_peptide_no_modifications(encoder): assert np.isclose(actual_mass, expected_mass, atol=1e-4) -def test_encode_peptide_with_modifications(encoder): +def test_peptide_to_smiles_with_modifications(encoder): sequence = "QMNPHIR" mods = "Gln->pyro-Glu@Q^Any_N-term;Oxidation@M" mod_sites = "1;2" - smiles = encoder.encode_peptide(sequence, mods, mod_sites) + smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites) mol = Chem.MolFromSmiles(smiles) assert mol is not None @@ -44,24 +44,24 @@ def test_encode_peptide_with_modifications(encoder): assert np.isclose(actual_mass, expected_mass, atol=1e-4) -def test_encode_peptide_invalid_amino_acid(encoder): +def test_peptide_to_smiles_invalid_amino_acid(encoder): with pytest.raises(ValueError, match="Unknown amino acid code"): - encoder.encode_peptide("QMNPHIRX") + encoder.peptide_to_smiles("QMNPHIRX") -def test_encode_peptide_invalid_modification(encoder): +def test_peptide_to_smiles_invalid_modification(encoder): with pytest.raises( ValueError, match="Unknown amino acid code: Q or modification: Invalid_Mod" ): - encoder.encode_peptide("QMNPHIR", "Invalid_Mod@Q", "1") + encoder.peptide_to_smiles("QMNPHIR", "Invalid_Mod@Q", "1") -def test_encode_peptide_n_terminal_modification(encoder): +def test_peptide_to_smiles_n_terminal_modification(encoder): sequence = "QMNPHIR" mods = "Acetyl@Any_N-term" mod_sites = "0" - smiles = encoder.encode_peptide(sequence, mods, mod_sites) + smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites) mol = Chem.MolFromSmiles(smiles) assert mol is not None @@ -73,12 +73,12 @@ def test_encode_peptide_n_terminal_modification(encoder): assert np.isclose(actual_mass, expected_mass, atol=1e-4) -def test_encode_peptide_c_terminal_modification(encoder): +def test_peptide_to_smiles_c_terminal_modification(encoder): sequence = "QMNPHIR" mods = "Amidated@Any_C-term" mod_sites = "-1" - smiles = encoder.encode_peptide(sequence, mods, mod_sites) + smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites) mol = Chem.MolFromSmiles(smiles) assert mol is not None @@ -90,12 +90,12 @@ def test_encode_peptide_c_terminal_modification(encoder): assert np.isclose(actual_mass, expected_mass, atol=1e-4) -def test_encode_peptide_multiple_modifications(encoder): +def test_peptide_to_smiles_multiple_modifications(encoder): sequence = "QMNPSIR" mods = "Acetyl@Any_N-term;Oxidation@M;Phospho@S" mod_sites = "0;2;5" - smiles = encoder.encode_peptide(sequence, mods, mod_sites) + smiles = encoder.peptide_to_smiles(sequence, mods, mod_sites) mol = Chem.MolFromSmiles(smiles) assert mol is not None @@ -105,3 +105,32 @@ def test_encode_peptide_multiple_modifications(encoder): ) actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O assert np.isclose(actual_mass, expected_mass, atol=1e-4) + + +def test_peptide_to_mol_no_modifications(encoder): + sequence = "QMNPHIR" + mol = encoder.peptide_to_mol(sequence) + + assert mol is not None + + expected_mass = np.sum( + [CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, "")] + ) + actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O + assert np.isclose(actual_mass, expected_mass, atol=1e-4) + + +def test_peptide_to_mol_multiple_modifications(encoder): + sequence = "QMNPSIR" + mods = "Acetyl@Any_N-term;Oxidation@M;Phospho@S" + mod_sites = "0;2;5" + + mol = encoder.peptide_to_mol(sequence, mods, mod_sites) + + assert mol is not None + + expected_mass = np.sum( + [CHEM_MONO_MASS[elem] * n for elem, n in get_mod_seq_formula(sequence, mods)] + ) + actual_mass = Descriptors.ExactMolWt(mol) - MASS_H2O + assert np.isclose(actual_mass, expected_mass, atol=1e-4) From 4968448d4791c19ebbd96c2df220bb26a9c0aca1 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Sat, 28 Sep 2024 19:46:12 +0200 Subject: [PATCH 07/37] enable appending of flat and dense-flat libraries --- alphabase/spectral_library/base.py | 37 +- alphabase/spectral_library/flat.py | 6 - nbs_tests/spectral_library/flat_library.ipynb | 1317 +---------------- 3 files changed, 92 insertions(+), 1268 deletions(-) diff --git a/alphabase/spectral_library/base.py b/alphabase/spectral_library/base.py index 671b333f..eef29f0f 100644 --- a/alphabase/spectral_library/base.py +++ b/alphabase/spectral_library/base.py @@ -193,6 +193,7 @@ def append( other: "SpecLibBase", dfs_to_append: typing.List[str] = [ "_precursor_df", + "_fragment_df", "_fragment_intensity_df", "_fragment_mz_df", "_fragment_intensity_predicted_df", @@ -224,6 +225,7 @@ def append( None """ + if remove_unused_dfs: current_frag_dfs = self.available_dense_fragment_dfs() for attr in current_frag_dfs: @@ -263,15 +265,16 @@ def check_matching_columns(df1, df2): else: matching_columns.append([]) - n_fragments = [] + n_dense_fragments = [] + # get subset of dfs_to_append starting with _fragment for attr in dfs_to_append: - if attr.startswith("_fragment") and hasattr(self, attr): + if attr in self.available_dense_fragment_dfs() and hasattr(self, attr): n_current_fragments = len(getattr(self, attr)) if n_current_fragments > 0: - n_fragments += [n_current_fragments] + n_dense_fragments += [n_current_fragments] - if not np.all(np.array(n_fragments) == n_fragments[0]): + if len(set(n_dense_fragments)) > 1: raise ValueError( "The libraries can't be appended as the number of fragments in the current libraries are not the same." ) @@ -284,13 +287,17 @@ def check_matching_columns(df1, df2): other_df = getattr(other, attr)[column].copy() if attr.startswith("_precursor"): + # increment dense fragment indices frag_idx_increment = 0 - for fragment_df in ["_fragment_intensity_df", "_fragment_mz_df"]: + for fragment_dense_df in [ + "_fragment_intensity_df", + "_fragment_mz_df", + ]: if ( - hasattr(self, fragment_df) - and len(getattr(self, fragment_df)) > 0 + hasattr(self, fragment_dense_df) + and len(getattr(self, fragment_dense_df)) > 0 ): - frag_idx_increment = len(getattr(self, fragment_df)) + frag_idx_increment = len(getattr(self, fragment_dense_df)) if "frag_start_idx" in other_df.columns: other_df["frag_start_idx"] += frag_idx_increment @@ -298,6 +305,20 @@ def check_matching_columns(df1, df2): if "frag_stop_idx" in other_df.columns: other_df["frag_stop_idx"] += frag_idx_increment + # increment flat fragment indices + for fragment_flat_df in ["_fragment_df"]: + if ( + hasattr(self, fragment_flat_df) + and len(getattr(self, fragment_flat_df)) > 0 + ): + frag_idx_increment = len(getattr(self, fragment_flat_df)) + + if "flat_frag_start_idx" in other_df.columns: + other_df["flat_frag_start_idx"] += frag_idx_increment + + if "flat_frag_stop_idx" in other_df.columns: + other_df["flat_frag_stop_idx"] += frag_idx_increment + setattr( self, attr, diff --git a/alphabase/spectral_library/flat.py b/alphabase/spectral_library/flat.py index 62e9db7c..e1944c05 100644 --- a/alphabase/spectral_library/flat.py +++ b/alphabase/spectral_library/flat.py @@ -81,12 +81,6 @@ def protein_df(self) -> pd.DataFrame: """Protein dataframe""" return self._protein_df - def available_dense_fragment_dfs(self): - """Return the available dense fragment dataframes. - This method is inherited from :class:`SpecLibBase` and will return an empty list for a flat library. - """ - return [] - def remove_unused_fragments(self): """Remove unused fragments from fragment_df. This method is inherited from :class:`SpecLibBase` and has not been implemented for a flat library. diff --git a/nbs_tests/spectral_library/flat_library.ipynb b/nbs_tests/spectral_library/flat_library.ipynb index d371863e..6f80b275 100644 --- a/nbs_tests/spectral_library/flat_library.ipynb +++ b/nbs_tests/spectral_library/flat_library.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -11,24 +11,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" - ] - } - ], + "outputs": [], "source": [ "from alphabase.spectral_library.flat import *" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -41,341 +33,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 6/6 [00:00<00:00, 2663.05it/s]\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
mzintensitytype
0609.3007810.450362121
1511.3238531.000000121
2510.2323300.106543121
3412.2554320.374123121
4411.1639400.069116121
5313.1870420.173858121
6321.6947020.515072121
7545.3293460.745664121
8326.1710510.14370398
9432.2452701.000000121
10397.2081600.09488898
11361.2081600.377585121
12496.2765810.05498098
13686.2756350.103734121
14588.2987671.000000121
15349.1717830.092058121
16661.2956540.198974121
17563.3187870.774316121
18494.2973021.000000121
19256.1291810.64971598
20347.2288820.882733121
21403.1976010.35178198
22490.2296140.40047498
23701.2905880.24435098
24603.3136600.63100098
25762.2583010.084908121
26664.2814330.328738121
27497.2830510.284129121
28496.1915280.276969121
29268.1655580.05755498
30398.2146300.262853121
31267.0740360.08743998
32329.1931761.000000121
33435.1639400.06162798
34698.3120730.602346121
35600.3351441.000000121
36611.2800290.141106121
37513.3031010.705295121
38498.1959530.108914121
39400.2190550.279959121
40331.1976010.492018121
\n", - "
" - ], - "text/plain": [ - " mz intensity type\n", - "0 609.300781 0.450362 121\n", - "1 511.323853 1.000000 121\n", - "2 510.232330 0.106543 121\n", - "3 412.255432 0.374123 121\n", - "4 411.163940 0.069116 121\n", - "5 313.187042 0.173858 121\n", - "6 321.694702 0.515072 121\n", - "7 545.329346 0.745664 121\n", - "8 326.171051 0.143703 98\n", - "9 432.245270 1.000000 121\n", - "10 397.208160 0.094888 98\n", - "11 361.208160 0.377585 121\n", - "12 496.276581 0.054980 98\n", - "13 686.275635 0.103734 121\n", - "14 588.298767 1.000000 121\n", - "15 349.171783 0.092058 121\n", - "16 661.295654 0.198974 121\n", - "17 563.318787 0.774316 121\n", - "18 494.297302 1.000000 121\n", - "19 256.129181 0.649715 98\n", - "20 347.228882 0.882733 121\n", - "21 403.197601 0.351781 98\n", - "22 490.229614 0.400474 98\n", - "23 701.290588 0.244350 98\n", - "24 603.313660 0.631000 98\n", - "25 762.258301 0.084908 121\n", - "26 664.281433 0.328738 121\n", - "27 497.283051 0.284129 121\n", - "28 496.191528 0.276969 121\n", - "29 268.165558 0.057554 98\n", - "30 398.214630 0.262853 121\n", - "31 267.074036 0.087439 98\n", - "32 329.193176 1.000000 121\n", - "33 435.163940 0.061627 98\n", - "34 698.312073 0.602346 121\n", - "35 600.335144 1.000000 121\n", - "36 611.280029 0.141106 121\n", - "37 513.303101 0.705295 121\n", - "38 498.195953 0.108914 121\n", - "39 400.219055 0.279959 121\n", - "40 331.197601 0.492018 121" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "#| hide\n", "tsv_str = \"\"\"PrecursorCharge\tModifiedPeptide\tStrippedPeptide\tiRT\tLabeledPeptide\tPrecursorMz\tFragmentLossType\tFragmentNumber\tFragmentType\tFragmentCharge\tFragmentMz\tRelativeIntensity\tIonMobility\n", @@ -439,182 +97,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
nAAsequencechargemobilityprecursor_mzrtmodsmod_sitesfrag_start_idxfrag_stop_idxrt_normccsflat_frag_start_idxflat_frag_stop_idx
07AVVVSPK20.9390.206779-22.849740Phospho@S5060.075327366.85887706
17DPLAVDK20.9379.208161-15.0871006120.199375367.043100613
27MGSLDSK20.9409.161712-27.563500Phospho@S312180.000000366.5644381316
37SVSFSLK10.9847.39611235.014110Phospho@S318241.000000183.1781711625
47VSVSPGR20.9431.167001-23.930850Phospho@S;Phospho@S2;424300.058050366.2548332534
57YSLSPSK20.9431.191326-6.428198Phospho@S430360.337745366.2545093441
\n", - "
" - ], - "text/plain": [ - " nAA sequence charge mobility precursor_mz rt \\\n", - "0 7 AVVVSPK 2 0.9 390.206779 -22.849740 \n", - "1 7 DPLAVDK 2 0.9 379.208161 -15.087100 \n", - "2 7 MGSLDSK 2 0.9 409.161712 -27.563500 \n", - "3 7 SVSFSLK 1 0.9 847.396112 35.014110 \n", - "4 7 VSVSPGR 2 0.9 431.167001 -23.930850 \n", - "5 7 YSLSPSK 2 0.9 431.191326 -6.428198 \n", - "\n", - " mods mod_sites frag_start_idx frag_stop_idx rt_norm \\\n", - "0 Phospho@S 5 0 6 0.075327 \n", - "1 6 12 0.199375 \n", - "2 Phospho@S 3 12 18 0.000000 \n", - "3 Phospho@S 3 18 24 1.000000 \n", - "4 Phospho@S;Phospho@S 2;4 24 30 0.058050 \n", - "5 Phospho@S 4 30 36 0.337745 \n", - "\n", - " ccs flat_frag_start_idx flat_frag_stop_idx \n", - "0 366.858877 0 6 \n", - "1 367.043100 6 13 \n", - "2 366.564438 13 16 \n", - "3 183.178171 16 25 \n", - "4 366.254833 25 34 \n", - "5 366.254509 34 41 " - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "#| hide\n", "flat_lib.precursor_df" @@ -622,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -641,7 +124,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -665,529 +148,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 6/6 [00:00<00:00, 2457.12it/s]\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
b_z1b_z2y_z1y_z2b_modloss_z1b_modloss_z2y_modloss_z1y_modloss_z2
00.0000000.0000000.0000000.0000000.0000000.00.0000000.0
10.0000000.0000000.4503620.0000000.0000000.01.0000000.0
20.0000000.0000000.1065430.0000000.0000000.00.3741230.0
30.0000000.0000000.0691160.0000000.0000000.00.1738580.0
40.0000000.0000000.0000000.0000000.0000000.00.0000000.0
50.0000000.0000000.0000000.0000000.0000000.00.0000000.0
60.0000000.0000000.0000000.5150720.0000000.00.0000000.0
70.0000000.0000000.7456640.0000000.0000000.00.0000000.0
80.1437030.0000001.0000000.0000000.0000000.00.0000000.0
90.0948880.0000000.3775850.0000000.0000000.00.0000000.0
100.0549800.0000000.0000000.0000000.0000000.00.0000000.0
110.0000000.0000000.0000000.0000000.0000000.00.0000000.0
120.0000000.0000000.1037340.0000000.0000000.01.0000000.0
130.0000000.0000000.0000000.0000000.0000000.00.0000000.0
140.0000000.0000000.0000000.0000000.0000000.00.0000000.0
150.0000000.0000000.0920580.0000000.0000000.00.0000000.0
160.0000000.0000000.0000000.0000000.0000000.00.0000000.0
170.0000000.0000000.0000000.0000000.0000000.00.0000000.0
180.0000000.0000000.0000000.0000000.0000000.00.0000000.0
190.0000000.0000000.1989740.0000000.0000000.00.7743160.0
200.0000000.0000001.0000000.0000000.6497150.00.0000000.0
210.0000000.0000000.8827330.0000000.3517810.00.0000000.0
220.0000000.0000000.0000000.0000000.4004740.00.0000000.0
230.2443500.0000000.0000000.0000000.6310000.00.0000000.0
240.0000000.0000000.0849080.0000000.0000000.00.3287380.0
250.0000000.0000000.0000000.0000000.0000000.00.2841290.0
260.0000000.0000000.2769690.0000000.0575540.00.2628530.0
270.0000000.0874391.0000000.0000000.0616270.00.0000000.0
280.0000000.0000000.0000000.0000000.0000000.00.0000000.0
290.0000000.0000000.0000000.0000000.0000000.00.0000000.0
300.0000000.0000000.6023460.0000000.0000000.01.0000000.0
310.0000000.0000000.1411060.0000000.0000000.00.7052950.0
320.0000000.0000000.1089140.0000000.0000000.00.2799590.0
330.0000000.0000000.4920180.0000000.0000000.00.0000000.0
340.0000000.0000000.0000000.0000000.0000000.00.0000000.0
350.0000000.0000000.0000000.0000000.0000000.00.0000000.0
\n", - "
" - ], - "text/plain": [ - " b_z1 b_z2 y_z1 y_z2 b_modloss_z1 b_modloss_z2 \\\n", - "0 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "1 0.000000 0.000000 0.450362 0.000000 0.000000 0.0 \n", - "2 0.000000 0.000000 0.106543 0.000000 0.000000 0.0 \n", - "3 0.000000 0.000000 0.069116 0.000000 0.000000 0.0 \n", - "4 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "5 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "6 0.000000 0.000000 0.000000 0.515072 0.000000 0.0 \n", - "7 0.000000 0.000000 0.745664 0.000000 0.000000 0.0 \n", - "8 0.143703 0.000000 1.000000 0.000000 0.000000 0.0 \n", - "9 0.094888 0.000000 0.377585 0.000000 0.000000 0.0 \n", - "10 0.054980 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "11 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "12 0.000000 0.000000 0.103734 0.000000 0.000000 0.0 \n", - "13 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "14 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "15 0.000000 0.000000 0.092058 0.000000 0.000000 0.0 \n", - "16 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "17 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "18 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "19 0.000000 0.000000 0.198974 0.000000 0.000000 0.0 \n", - "20 0.000000 0.000000 1.000000 0.000000 0.649715 0.0 \n", - "21 0.000000 0.000000 0.882733 0.000000 0.351781 0.0 \n", - "22 0.000000 0.000000 0.000000 0.000000 0.400474 0.0 \n", - "23 0.244350 0.000000 0.000000 0.000000 0.631000 0.0 \n", - "24 0.000000 0.000000 0.084908 0.000000 0.000000 0.0 \n", - "25 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "26 0.000000 0.000000 0.276969 0.000000 0.057554 0.0 \n", - "27 0.000000 0.087439 1.000000 0.000000 0.061627 0.0 \n", - "28 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "29 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "30 0.000000 0.000000 0.602346 0.000000 0.000000 0.0 \n", - "31 0.000000 0.000000 0.141106 0.000000 0.000000 0.0 \n", - "32 0.000000 0.000000 0.108914 0.000000 0.000000 0.0 \n", - "33 0.000000 0.000000 0.492018 0.000000 0.000000 0.0 \n", - "34 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "35 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 \n", - "\n", - " y_modloss_z1 y_modloss_z2 \n", - "0 0.000000 0.0 \n", - "1 1.000000 0.0 \n", - "2 0.374123 0.0 \n", - "3 0.173858 0.0 \n", - "4 0.000000 0.0 \n", - "5 0.000000 0.0 \n", - "6 0.000000 0.0 \n", - "7 0.000000 0.0 \n", - "8 0.000000 0.0 \n", - "9 0.000000 0.0 \n", - "10 0.000000 0.0 \n", - "11 0.000000 0.0 \n", - "12 1.000000 0.0 \n", - "13 0.000000 0.0 \n", - "14 0.000000 0.0 \n", - "15 0.000000 0.0 \n", - "16 0.000000 0.0 \n", - "17 0.000000 0.0 \n", - "18 0.000000 0.0 \n", - "19 0.774316 0.0 \n", - "20 0.000000 0.0 \n", - "21 0.000000 0.0 \n", - "22 0.000000 0.0 \n", - "23 0.000000 0.0 \n", - "24 0.328738 0.0 \n", - "25 0.284129 0.0 \n", - "26 0.262853 0.0 \n", - "27 0.000000 0.0 \n", - "28 0.000000 0.0 \n", - "29 0.000000 0.0 \n", - "30 1.000000 0.0 \n", - "31 0.705295 0.0 \n", - "32 0.279959 0.0 \n", - "33 0.000000 0.0 \n", - "34 0.000000 0.0 \n", - "35 0.000000 0.0 " - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "target = LibraryReaderBase()\n", "target.import_file(StringIO(tsv_str))\n", @@ -1198,100 +159,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
mzintensitytypeloss_typechargeposition
0609.3007810.450362121011
1511.3238531.0000001219811
2510.2323300.106543121012
3412.2554320.3741231219812
4411.1639400.069116121013
\n", - "
" - ], - "text/plain": [ - " mz intensity type loss_type charge position\n", - "0 609.300781 0.450362 121 0 1 1\n", - "1 511.323853 1.000000 121 98 1 1\n", - "2 510.232330 0.106543 121 0 1 2\n", - "3 412.255432 0.374123 121 98 1 2\n", - "4 411.163940 0.069116 121 0 1 3" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Flatten original library to use it as a test input with target the original library\n", "flat_lib = SpecLibFlat(custom_fragment_df_columns=['type','charge','position','loss_type'])\n", @@ -1303,100 +171,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
y_z2b_z2y_modloss_z1b_z1y_z1b_modloss_z1
00.00.00.0000000.00.0000000.0
10.00.01.0000000.00.4503620.0
20.00.00.3741230.00.1065430.0
30.00.00.1738580.00.0691160.0
40.00.00.0000000.00.0000000.0
\n", - "
" - ], - "text/plain": [ - " y_z2 b_z2 y_modloss_z1 b_z1 y_z1 b_modloss_z1\n", - "0 0.0 0.0 0.000000 0.0 0.000000 0.0\n", - "1 0.0 0.0 1.000000 0.0 0.450362 0.0\n", - "2 0.0 0.0 0.374123 0.0 0.106543 0.0\n", - "3 0.0 0.0 0.173858 0.0 0.069116 0.0\n", - "4 0.0 0.0 0.000000 0.0 0.000000 0.0" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "back_to_base = flat_lib.to_SpecLibBase()\n", "back_to_base.fragment_intensity_df.head()" @@ -1406,25 +181,14 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['_fragment_intensity_df', '_fragment_mz_df']" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "back_to_base.available_dense_fragment_dfs()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -1439,7 +203,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -1450,7 +214,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -1461,17 +225,62 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "# enable appending of flat libraries while making sure the flat_frag_start_idx is updated\n", + "\n", + "lib1 = SpecLibBase()\n", + "lib1.precursor_df = pd.DataFrame({\n", + " 'sequence': ['PEPTI', 'SEQUE'],\n", + " 'charge': [2, 3],\n", + " 'mod_sites': ['', ''],\n", + " 'mods': ['', ''],\n", + "})\n", + "lib1.calc_precursor_mz()\n", + "lib1.calc_fragment_mz_df()\n", + "lib1._fragment_intensity_df = lib1.fragment_mz_df.copy()\n", + "lib1._fragment_intensity_df.iloc[:] = 0\n", + "lib2 = lib1.copy()\n", + "lib2._fragment_intensity_df.iloc[:] = 1\n", + "\n", + "flatlib_1 = SpecLibFlat(min_fragment_intensity=0)\n", + "flatlib_1.parse_base_library(lib1, keep_original_frag_dfs=True)\n", + "flatlib_2 = SpecLibFlat(min_fragment_intensity=0)\n", + "flatlib_2.parse_base_library(lib2, keep_original_frag_dfs=True)\n", + "\n", + "flatlib_1.append(flatlib_2)\n", + "\n", + "assert flatlib_1.precursor_df.shape[0] == 4\n", + "assert flatlib_1.fragment_mz_df.shape[0] == 16\n", + "assert flatlib_1.fragment_df.shape[0] == np.prod(flatlib_1.fragment_mz_df.shape)\n", + "\n", + "assert np.all(flatlib_1.precursor_df['frag_start_idx'] == [0, 4, 8, 12])\n", + "assert np.all(flatlib_1.precursor_df['flat_frag_start_idx'] == [0, 16, 32, 48])\n", + "\n", + "assert np.all(flatlib_1.fragment_df['intensity'] == np.repeat([0, 1], 32))\n", + "assert np.all(flatlib_1.fragment_intensity_df.values.flatten() == np.repeat([0, 1], 32))" + ] } ], "metadata": { "kernelspec": { - "display_name": "python3", + "display_name": "metaptcm", "language": "python", "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" } }, "nbformat": 4, From d9d879777b9dbe76a8b235eb4fec162d6d393e7f Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Sat, 28 Sep 2024 20:01:31 +0200 Subject: [PATCH 08/37] 1-based sage scan idx --- alphabase/psm_reader/sage_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alphabase/psm_reader/sage_reader.py b/alphabase/psm_reader/sage_reader.py index 5edb3804..d0687d20 100644 --- a/alphabase/psm_reader/sage_reader.py +++ b/alphabase/psm_reader/sage_reader.py @@ -548,7 +548,7 @@ def _sage_spec_idx_from_scan_nr(scan_nr: str) -> int: The scan_nr field in Sage output. """ - return int(scan_nr.split("=")[-1]) + return int(scan_nr.split("=")[-1]) - 1 class SageReaderBase(PSMReaderBase): From a8f850c61bb3be70041f93effae826a282c0b5b3 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Sat, 28 Sep 2024 20:25:44 +0200 Subject: [PATCH 09/37] fix test --- nbs_tests/psm_reader/sage_reader.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbs_tests/psm_reader/sage_reader.ipynb b/nbs_tests/psm_reader/sage_reader.ipynb index 2646aba0..b7c989a7 100644 --- a/nbs_tests/psm_reader/sage_reader.ipynb +++ b/nbs_tests/psm_reader/sage_reader.ipynb @@ -47,7 +47,7 @@ "outputs": [], "source": [ "#| hide\n", - "assert _sage_spec_idx_from_scan_nr('controllerType=0 controllerNumber=1 scan=7846') == 7846" + "assert _sage_spec_idx_from_scan_nr('controllerType=0 controllerNumber=1 scan=7846') == 7845" ] }, { @@ -195,7 +195,7 @@ ], "metadata": { "kernelspec": { - "display_name": "python3", + "display_name": "alpha", "language": "python", "name": "python3" }, From 24932ad66c4523954b7eef7fc0e5d07228b1b535 Mon Sep 17 00:00:00 2001 From: Mikhail Lebedev Date: Sun, 29 Sep 2024 22:00:44 +0200 Subject: [PATCH 10/37] ENH: hydroxyisobutyryl@K smiles --- .../constants/const_files/modification.tsv | 2 +- docs/nbs/adding_smiles.ipynb | 75 +++++-------------- 2 files changed, 20 insertions(+), 57 deletions(-) diff --git a/alphabase/constants/const_files/modification.tsv b/alphabase/constants/const_files/modification.tsv index 705c5886..47c455ff 100644 --- a/alphabase/constants/const_files/modification.tsv +++ b/alphabase/constants/const_files/modification.tsv @@ -2440,7 +2440,7 @@ PhosphoCytidine@S 305.041287 305.1812 H(12)C(9)N(3)O(7)P(1) 0.0 Post-translatio AzidoF@F 41.001397 41.0122 H(-1)N(3) 0.0 Chemical derivative 1845 0.0 Dimethylaminoethyl@C 71.073499 71.121 H(9)C(4)N(1) 0.0 Chemical derivative 1846 0.0 Gluratylation@K 114.031694 114.0993 H(6)C(5)O(3) 0.0 Post-translational 1848 0.0 -hydroxyisobutyryl@K 86.036779 86.0892 H(6)C(4)O(2) 0.0 Post-translational 1849 0.0 +hydroxyisobutyryl@K 86.036779 86.0892 H(6)C(4)O(2) 0.0 Post-translational 1849 CC(C)(O)C(=O)NCCCCC(N([Xe])[Xe])C([Rn])=O 0.0 MeMePhosphorothioate@S 107.979873 108.0993 H(5)C(2)O(1)P(1)S(1) 0.0 Chemical derivative 1868 0.0 Cation:Fe[III]@D 52.911464 52.8212 H(-3)Fe(1) 0.0 Artefact 1870 0.0 Cation:Fe[III]@E 52.911464 52.8212 H(-3)Fe(1) 0.0 Artefact 1870 0.0 diff --git a/docs/nbs/adding_smiles.ipynb b/docs/nbs/adding_smiles.ipynb index e0899474..66d4c1dd 100644 --- a/docs/nbs/adding_smiles.ipynb +++ b/docs/nbs/adding_smiles.ipynb @@ -49,46 +49,6 @@ "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "defaultdict(int, {'O': -1, 'H': 2, 'C': 1})" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "(ChemicalCompositonFormula(\"C(1)H(4)\") - ChemicalCompositonFormula(\"H(2)O(1)\")).elements" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ChemicalCompositonFormula('C(1)H(2)O(-1)')" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ChemicalCompositonFormula(\"C(1)H(2)O(-1)\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, "outputs": [], "source": [ "n_term_modifications = {'mTRAQ@Any_N-term': 'C(=O)CN1CCN(CC1)C',\n", @@ -153,7 +113,8 @@ " 'Pro->(1S,3S,5S)-2-Azabicyclo[3.1.0]hexane-3-carboxylic_acid@P': '[C@H]12N([Xe])[C@@H](C[C@@H]2C1)C(=O)[Rn]',\n", " 'Pro->(1R,3S,5R)-2-Azabicyclo[3.1.0]hexane-3-carboxylic_acid@P': '[C@@H]12N([Xe])[C@@H](C[C@H]2C1)C(=O)[Rn]',\n", " 'Pro->(2S,3aS,7aS)-Octahydro-1H-indole-2-carboxylic_acid@P': 'N1([Xe])[C@@H](C[C@@H]2CCCC[C@H]12)C(=O)[Rn]',\n", - " 'Pro->(DL)-5-trifluoromethylproline@P': 'FC(C1CCC(N1([Xe]))C(=O)[Rn])(F)F'}" + " 'Pro->(DL)-5-trifluoromethylproline@P': 'FC(C1CCC(N1([Xe]))C(=O)[Rn])(F)F',\n", + " 'hydroxyisobutyryl@K': 'CC(C)(O)C(=O)NCCCCC(N([Xe])[Xe])C([Rn])=O',}\n" ] }, { @@ -170,7 +131,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -195,7 +156,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -222,7 +183,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -299,10 +260,12 @@ " 'Pro->(2S,3aS,7aS)-Octahydro-1H-indole-2-carboxylic_acid@P': {'composition': 'C(4)H(6)',\n", " 'smiles': 'N1([Xe])[C@@H](C[C@@H]2CCCC[C@H]12)C(=O)[Rn]'},\n", " 'Pro->(DL)-5-trifluoromethylproline@P': {'composition': 'C(1)F(3)H(-1)',\n", - " 'smiles': 'FC(C1CCC(N1([Xe]))C(=O)[Rn])(F)F'}}" + " 'smiles': 'FC(C1CCC(N1([Xe]))C(=O)[Rn])(F)F'},\n", + " 'hydroxyisobutyryl@K': {'composition': 'C(4)H(6)O(2)',\n", + " 'smiles': 'CC(C)(O)C(=O)NCCCCC(N([Xe])[Xe])C([Rn])=O'}}" ] }, - "execution_count": 7, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -331,7 +294,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -350,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -411,7 +374,7 @@ " 'smiles': '[13C]([2H])([2H])([1H])'}}" ] }, - "execution_count": 9, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -434,7 +397,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -453,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -478,7 +441,7 @@ " 'Cation:Li@Protein_C-term': {'composition': 'H(-1)Li(1)', 'smiles': 'O[Li]'}}" ] }, - "execution_count": 11, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -502,7 +465,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -521,7 +484,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -832,7 +795,7 @@ "[2796 rows x 13 columns]" ] }, - "execution_count": 13, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -852,7 +815,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ From 56e3d626aab6df1393f65dcc5d2e06a80ea6939b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20M=C3=A4dler?= <15019107+sophiamaedler@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:08:17 +0200 Subject: [PATCH 11/37] Fix missing variable --- alphabase/io/tempmmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alphabase/io/tempmmap.py b/alphabase/io/tempmmap.py index 62b572a4..0b2d1182 100644 --- a/alphabase/io/tempmmap.py +++ b/alphabase/io/tempmmap.py @@ -84,7 +84,7 @@ def _get_file_location(abs_file_path: str, overwrite=False) -> str: ) # ensure that the filename conforms to the naming convention - if not os.path.basename.endswith(".hdf"): + if not os.path.basename(abs_file_path).endswith(".hdf"): raise ValueError("The chosen file name needs to end with .hdf") # ensure that the directory in which the file should be created exists From 8da645f100c4d076a8e34541e770e8c3c1c745a5 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:21:37 +0100 Subject: [PATCH 12/37] open hdf file "r" mode on read operations --- alphabase/io/hdf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/alphabase/io/hdf.py b/alphabase/io/hdf.py index 98650c67..c0fbbf7c 100644 --- a/alphabase/io/hdf.py +++ b/alphabase/io/hdf.py @@ -45,7 +45,7 @@ def editing_mode(self, read_only: bool = False, truncate: bool = True): @property def metadata(self): - with h5py.File(self.file_name, "a") as hdf_file: + with h5py.File(self.file_name) as hdf_file: return dict(hdf_file[self.name].attrs) def __init__( @@ -162,7 +162,7 @@ def components(self): group_names = [] dataset_names = [] datafame_names = [] - with h5py.File(self.file_name, "a") as hdf_file: + with h5py.File(self.file_name) as hdf_file: hdf_object = hdf_file[self.name] for name in sorted(hdf_object): if isinstance(hdf_object[name], h5py.Dataset): @@ -328,12 +328,12 @@ def __len__(self): @property def dtype(self): - with h5py.File(self.file_name, "a") as hdf_file: + with h5py.File(self.file_name) as hdf_file: return hdf_file[self.name].dtype @property def shape(self): - with h5py.File(self.file_name, "a") as hdf_file: + with h5py.File(self.file_name) as hdf_file: return hdf_file[self.name].shape @property @@ -341,7 +341,7 @@ def values(self): return self[...] def __getitem__(self, keys): - with h5py.File(self.file_name, "a") as hdf_file: + with h5py.File(self.file_name) as hdf_file: hdf_object = hdf_file[self.name] if h5py.check_string_dtype(hdf_object.dtype) is not None: hdf_object = hdf_object.asstr() From 91cdb791f5b17ee725fd2e76ab3ee6a486192ace Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Wed, 30 Oct 2024 14:19:46 +0100 Subject: [PATCH 13/37] fixes --- alphabase/psm_reader/sage_reader.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/alphabase/psm_reader/sage_reader.py b/alphabase/psm_reader/sage_reader.py index d0687d20..ee46840b 100644 --- a/alphabase/psm_reader/sage_reader.py +++ b/alphabase/psm_reader/sage_reader.py @@ -540,6 +540,7 @@ def _get_annotated_mod_df() -> pd.DataFrame: def _sage_spec_idx_from_scan_nr(scan_nr: str) -> int: """Extract the spectrum index from the scan_nr field in Sage output. + Sage uses 1-based indexing for spectra, so we need to subtract 1 to convert to 0-based indexing. Parameters ---------- @@ -547,8 +548,20 @@ def _sage_spec_idx_from_scan_nr(scan_nr: str) -> int: scan_nr : str The scan_nr field in Sage output. + Returns + ------- + + int + The 0-based spectrum index. + + Examples + -------- + + >>> _sage_spec_idx_from_scan_nr('controllerType=0 controllerNumber=1 scan=7846') + 7845 + """ - return int(scan_nr.split("=")[-1]) - 1 + return int(re.search(r"scan=(\d+)", scan_nr).group(1)) - 1 class SageReaderBase(PSMReaderBase): From c1b36ff931dd8f04c06e8f1ae29483aef583372b Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Wed, 30 Oct 2024 14:25:27 +0100 Subject: [PATCH 14/37] simplify appending libs --- alphabase/spectral_library/base.py | 43 ++++++++++++------------------ 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/alphabase/spectral_library/base.py b/alphabase/spectral_library/base.py index eef29f0f..b1d3ad2e 100644 --- a/alphabase/spectral_library/base.py +++ b/alphabase/spectral_library/base.py @@ -288,36 +288,27 @@ def check_matching_columns(df1, df2): if attr.startswith("_precursor"): # increment dense fragment indices - frag_idx_increment = 0 - for fragment_dense_df in [ - "_fragment_intensity_df", - "_fragment_mz_df", - ]: + fragment_df_mapping = { + "_fragment_intensity_df": "", + "_fragment_mz_df": "", + "_fragment_df": "flat_", + } + + # Update indices for each fragment dataframe type + for fragment_df, prefix in fragment_df_mapping.items(): if ( - hasattr(self, fragment_dense_df) - and len(getattr(self, fragment_dense_df)) > 0 + hasattr(self, fragment_df) + and len(getattr(self, fragment_df)) > 0 ): - frag_idx_increment = len(getattr(self, fragment_dense_df)) + frag_idx_increment = len(getattr(self, fragment_df)) - if "frag_start_idx" in other_df.columns: - other_df["frag_start_idx"] += frag_idx_increment + start_col = f"{prefix}frag_start_idx" + stop_col = f"{prefix}frag_stop_idx" - if "frag_stop_idx" in other_df.columns: - other_df["frag_stop_idx"] += frag_idx_increment - - # increment flat fragment indices - for fragment_flat_df in ["_fragment_df"]: - if ( - hasattr(self, fragment_flat_df) - and len(getattr(self, fragment_flat_df)) > 0 - ): - frag_idx_increment = len(getattr(self, fragment_flat_df)) - - if "flat_frag_start_idx" in other_df.columns: - other_df["flat_frag_start_idx"] += frag_idx_increment - - if "flat_frag_stop_idx" in other_df.columns: - other_df["flat_frag_stop_idx"] += frag_idx_increment + if start_col in other_df.columns: + other_df[start_col] += frag_idx_increment + if stop_col in other_df.columns: + other_df[stop_col] += frag_idx_increment setattr( self, From 68d4c20de8aa1f88965fc72ea84c846e50f22a24 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Wed, 30 Oct 2024 14:27:29 +0100 Subject: [PATCH 15/37] fix --- alphabase/psm_reader/sage_reader.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/alphabase/psm_reader/sage_reader.py b/alphabase/psm_reader/sage_reader.py index ee46840b..4d237514 100644 --- a/alphabase/psm_reader/sage_reader.py +++ b/alphabase/psm_reader/sage_reader.py @@ -538,15 +538,16 @@ def _get_annotated_mod_df() -> pd.DataFrame: ] -def _sage_spec_idx_from_scan_nr(scan_nr: str) -> int: +def _sage_spec_idx_from_scan_nr(scan_indicator_str: str) -> int: """Extract the spectrum index from the scan_nr field in Sage output. Sage uses 1-based indexing for spectra, so we need to subtract 1 to convert to 0-based indexing. Parameters ---------- - scan_nr : str - The scan_nr field in Sage output. + scan_indicator_str : str + The scan_indicator_str field in Sage output. + e.g. `'controllerType=0 controllerNumber=1 scan=7846'` Returns ------- @@ -561,7 +562,7 @@ def _sage_spec_idx_from_scan_nr(scan_nr: str) -> int: 7845 """ - return int(re.search(r"scan=(\d+)", scan_nr).group(1)) - 1 + return int(re.search(r"scan=(\d+)", scan_indicator_str).group(1)) - 1 class SageReaderBase(PSMReaderBase): From 0992ede8bae5756a02151a30b72fcd90d0b3b3f2 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:25:21 +0100 Subject: [PATCH 16/37] adapt linux release workflow --- .bumpversion.cfg | 6 ++++-- release/linux/build_installer_linux.sh | 18 +++++++++++++++++ release/linux/build_package_linux.sh | 20 +++++++++++++++++++ .../{one_click_linux_gui => linux}/control | 2 +- .../create_installer_linux.sh | 2 +- release/pyinstaller/alphabase.spec | 2 +- 6 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 release/linux/build_installer_linux.sh create mode 100644 release/linux/build_package_linux.sh rename release/{one_click_linux_gui => linux}/control (96%) rename release/{one_click_linux_gui => linux}/create_installer_linux.sh (97%) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index cf2c8ccd..e7e3d262 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -15,9 +15,11 @@ serialize = [bumpversion:file:./docs/conf.py] -[bumpversion:file:./release/one_click_linux_gui/control] +[bumpversion:file:./release/linux/control] -[bumpversion:file:./release/one_click_linux_gui/create_installer_linux.sh] +[bumpversion:file:./release/linux/create_installer_linux.sh] # TODO remove with old release workflow +[bumpversion:file:./release/linux/build_installer_linux.sh] +[bumpversion:file:./release/linux/build_package_linux.sh] [bumpversion:file:./release/one_click_macos_gui/distribution.xml] diff --git a/release/linux/build_installer_linux.sh b/release/linux/build_installer_linux.sh new file mode 100644 index 00000000..c1a279d1 --- /dev/null +++ b/release/linux/build_installer_linux.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e -u + +# Build the installer for Linux. +# This script must be run from the root of the repository. + +rm -rf dist build *.egg-info +rm -rf dist_pyinstaller build_pyinstaller + +# Creating the wheel +python setup.py sdist bdist_wheel + +# Setting up the local package +# Make sure you include the required extra packages and always use the stable or very-stable options! +pip install "dist/alphabase-1.4.0-py3-none-any.whl[stable]" + +# Creating the stand-alone pyinstaller folder +pyinstaller release/pyinstaller/alphabase.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y diff --git a/release/linux/build_package_linux.sh b/release/linux/build_package_linux.sh new file mode 100644 index 00000000..f03e026d --- /dev/null +++ b/release/linux/build_package_linux.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e -u + +# Build the install package for Linux. +# This script must be run from the root of the repository after running build_installer_linux.sh + +PACKAGE_NAME=alphabase + +# BUILD_NAME is taken from environment variables, e.g. 'alphabase-1.2.1-linux-x64' +rm -rf ${BUILD_NAME}.deb + +# Wrapping the pyinstaller folder in a .deb package +mkdir -p dist_pyinstaller/${BUILD_NAME}/usr/local/bin +mv dist_pyinstaller/${PACKAGE_NAME} dist_pyinstaller/${BUILD_NAME}/usr/local/bin/${PACKAGE_NAME} +mkdir dist_pyinstaller/${BUILD_NAME}/DEBIAN +cp release/linux/control dist_pyinstaller/${BUILD_NAME}/DEBIAN +dpkg-deb --build --root-owner-group dist_pyinstaller/${BUILD_NAME} + +# release workflow expects artifact at root of repository +mv dist_pyinstaller/${BUILD_NAME}.deb . diff --git a/release/one_click_linux_gui/control b/release/linux/control similarity index 96% rename from release/one_click_linux_gui/control rename to release/linux/control index c10e77ae..dd9a15fa 100644 --- a/release/one_click_linux_gui/control +++ b/release/linux/control @@ -1,4 +1,4 @@ -Package: AlphaBase +Package: alphabase Version: 1.4.0 Architecture: all Maintainer: Mann Labs diff --git a/release/one_click_linux_gui/create_installer_linux.sh b/release/linux/create_installer_linux.sh similarity index 97% rename from release/one_click_linux_gui/create_installer_linux.sh rename to release/linux/create_installer_linux.sh index d2e0317f..105883db 100644 --- a/release/one_click_linux_gui/create_installer_linux.sh +++ b/release/linux/create_installer_linux.sh @@ -15,7 +15,7 @@ conda activate alphabase_installer python setup.py sdist bdist_wheel # Setting up the local package -cd release/one_click_linux_gui +cd release/linux # Make sure you include the required extra packages and always use the stable or very-stable options! pip install "../../dist/alphabase-1.4.0-py3-none-any.whl[stable]" diff --git a/release/pyinstaller/alphabase.spec b/release/pyinstaller/alphabase.spec index 938d7a35..2b68c135 100644 --- a/release/pyinstaller/alphabase.spec +++ b/release/pyinstaller/alphabase.spec @@ -21,7 +21,7 @@ block_cipher = None location = os.getcwd() project = "alphabase" remove_tests = True -bundle_name = "AlphaBase" +bundle_name = "alphabase" ##################### From 116f6029ce12076fec0811fd38e88c7779db2641 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:31:44 +0100 Subject: [PATCH 17/37] adapt macos release workflow --- .bumpversion.cfg | 8 +++-- misc/checklist.txt | 2 +- release/linux/create_installer_linux.sh | 2 +- .../{one_click_macos_gui => macos}/Info.plist | 2 +- .../Resources/conclusion.html | 0 .../Resources/welcome.html | 0 .../alphabase_terminal | 0 release/macos/build_installer_macos.sh | 15 ++++++++ release/macos/build_package_macos.sh | 34 +++++++++++++++++++ .../create_installer_macos.sh | 4 +-- .../distribution.xml | 4 +-- .../scripts/postinstall | 0 .../scripts/preinstall | 0 13 files changed, 61 insertions(+), 10 deletions(-) rename release/{one_click_macos_gui => macos}/Info.plist (96%) rename release/{one_click_macos_gui => macos}/Resources/conclusion.html (100%) rename release/{one_click_macos_gui => macos}/Resources/welcome.html (100%) rename release/{one_click_macos_gui => macos}/alphabase_terminal (100%) create mode 100644 release/macos/build_installer_macos.sh create mode 100644 release/macos/build_package_macos.sh rename release/{one_click_macos_gui => macos}/create_installer_macos.sh (96%) rename release/{one_click_macos_gui => macos}/distribution.xml (86%) rename release/{one_click_macos_gui => macos}/scripts/postinstall (100%) rename release/{one_click_macos_gui => macos}/scripts/preinstall (100%) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e7e3d262..569f4109 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -21,11 +21,13 @@ serialize = [bumpversion:file:./release/linux/build_installer_linux.sh] [bumpversion:file:./release/linux/build_package_linux.sh] -[bumpversion:file:./release/one_click_macos_gui/distribution.xml] +[bumpversion:file:./release/macos/distribution.xml] -[bumpversion:file:./release/one_click_macos_gui/Info.plist] +[bumpversion:file:./release/macos/Info.plist] -[bumpversion:file:./release/one_click_macos_gui/create_installer_macos.sh] +[bumpversion:file:./release/macos/create_installer_macos.sh] +[bumpversion:file:./release/macos/build_installer_macos.sh] # TODO remove with old release workflow +[bumpversion:file:./release/macos/build_package_macos.sh] # TODO remove with old release workflow [bumpversion:file:./release/one_click_windows_gui/create_installer_windows.sh] diff --git a/misc/checklist.txt b/misc/checklist.txt index fe64d26a..9a7f0f42 100644 --- a/misc/checklist.txt +++ b/misc/checklist.txt @@ -47,7 +47,7 @@ ReadTheDocs up-to-date? Releases for all Windows, MacOS and Linux? - cd misc/one_click_macos + cd misc/macos source create_installer_macos.sh cd misc/one_click_linux diff --git a/release/linux/create_installer_linux.sh b/release/linux/create_installer_linux.sh index 105883db..ef237636 100644 --- a/release/linux/create_installer_linux.sh +++ b/release/linux/create_installer_linux.sh @@ -1,5 +1,5 @@ #!bash - +# TODO remove with old release workflow # Initial cleanup rm -rf dist rm -rf build diff --git a/release/one_click_macos_gui/Info.plist b/release/macos/Info.plist similarity index 96% rename from release/one_click_macos_gui/Info.plist rename to release/macos/Info.plist index d9ee9ad6..15c16d32 100644 --- a/release/one_click_macos_gui/Info.plist +++ b/release/macos/Info.plist @@ -15,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - AlphaBase + alphabase CFBundlePackageType APPL LSBackgroundOnly diff --git a/release/one_click_macos_gui/Resources/conclusion.html b/release/macos/Resources/conclusion.html similarity index 100% rename from release/one_click_macos_gui/Resources/conclusion.html rename to release/macos/Resources/conclusion.html diff --git a/release/one_click_macos_gui/Resources/welcome.html b/release/macos/Resources/welcome.html similarity index 100% rename from release/one_click_macos_gui/Resources/welcome.html rename to release/macos/Resources/welcome.html diff --git a/release/one_click_macos_gui/alphabase_terminal b/release/macos/alphabase_terminal similarity index 100% rename from release/one_click_macos_gui/alphabase_terminal rename to release/macos/alphabase_terminal diff --git a/release/macos/build_installer_macos.sh b/release/macos/build_installer_macos.sh new file mode 100644 index 00000000..10100291 --- /dev/null +++ b/release/macos/build_installer_macos.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e -u + +# Build the installer for MacOS. +# This script must be run from the root of the repository. + +rm -rf dist +rm -rf build + +# Creating the wheel +python setup.py sdist bdist_wheel +pip install "dist/alphabase-1.4.0-py3-none-any.whl[stable]" + +# Creating the stand-alone pyinstaller folder +pyinstaller release/pyinstaller/peptdeep.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y diff --git a/release/macos/build_package_macos.sh b/release/macos/build_package_macos.sh new file mode 100644 index 00000000..7c35dad2 --- /dev/null +++ b/release/macos/build_package_macos.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e -u + +# Build the install package for MacOS. +# This script must be run from the root of the repository after running build_installer_macos.sh + +PACKAGE_NAME=alphabase +# BUILD_NAME is taken from environment variables, e.g. alphabase-1.3.0-macos-darwin-arm64 or alphabase-1.3.0-macos-darwin-x64 +rm -rf ${BUILD_NAME}.pkg + +# If needed, include additional source such as e.g.: +# cp ../../alphabase/data/*.fasta dist/alphabase/data + +# Wrapping the pyinstaller folder in a .pkg package +CONTENTS_FOLDER=dist_pyinstaller/${PACKAGE_NAME}/Contents + +mkdir -p ${CONTENTS_FOLDER}/Resources +cp release/logos/alpha_logo.icns ${CONTENTS_FOLDER}/Resources +mv dist_pyinstaller/alphabase_gui ${CONTENTS_FOLDER}/MacOS +cp release/macos/Info.plist ${CONTENTS_FOLDER} +cp release/macos/alphabase_terminal ${CONTENTS_FOLDER}/MacOS +cp LICENSE.txt ${CONTENTS_FOLDER}/Resources +cp release/logos/alpha_logo.png ${CONTENTS_FOLDER}/Resources + +# link _internal folder containing the python libraries to the Frameworks folder where they are expected +# to avoid e.g. "Failed to load Python shared library '/Applications/AlphaMap.app/Contents/Frameworks/libpython3.8.dylib'" +cd ${CONTENTS_FOLDER} +ln -s ./MacOS/_internal ./Frameworks +cd - + +chmod 777 release/macos/scripts/* + +pkgbuild --root dist_pyinstaller/${PACKAGE_NAME} --identifier de.mpg.biochem.${PACKAGE_NAME}.app --version 1.4.0 --install-location /Applications/${PACKAGE_NAME}.app --scripts release/macos/scripts ${PACKAGE_NAME}.pkg +productbuild --distribution release/macos/distribution.xml --resources release/macos/Resources --package-path ${PACKAGE_NAME}.pkg ${BUILD_NAME}.pkg diff --git a/release/one_click_macos_gui/create_installer_macos.sh b/release/macos/create_installer_macos.sh similarity index 96% rename from release/one_click_macos_gui/create_installer_macos.sh rename to release/macos/create_installer_macos.sh index 2d6bfe85..d1da7f88 100644 --- a/release/one_click_macos_gui/create_installer_macos.sh +++ b/release/macos/create_installer_macos.sh @@ -1,5 +1,5 @@ #!bash - +# TODO remove with old release workflow # Initial cleanup rm -rf dist rm -rf build @@ -19,7 +19,7 @@ conda activate alphabaseinstaller python setup.py sdist bdist_wheel # Setting up the local package -cd release/one_click_macos_gui +cd release/macos pip install "../../dist/alphabase-1.4.0-py3-none-any.whl[stable]" # Creating the stand-alone pyinstaller folder diff --git a/release/one_click_macos_gui/distribution.xml b/release/macos/distribution.xml similarity index 86% rename from release/one_click_macos_gui/distribution.xml rename to release/macos/distribution.xml index 1514159c..9969ef35 100644 --- a/release/one_click_macos_gui/distribution.xml +++ b/release/macos/distribution.xml @@ -11,7 +11,7 @@ - + - AlphaBase.pkg + alphabase.pkg diff --git a/release/one_click_macos_gui/scripts/postinstall b/release/macos/scripts/postinstall similarity index 100% rename from release/one_click_macos_gui/scripts/postinstall rename to release/macos/scripts/postinstall diff --git a/release/one_click_macos_gui/scripts/preinstall b/release/macos/scripts/preinstall similarity index 100% rename from release/one_click_macos_gui/scripts/preinstall rename to release/macos/scripts/preinstall From ccee6136686f9a481fcec19f63a30c18077793fe Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:41:59 +0100 Subject: [PATCH 18/37] adapt windows release workflow --- .bumpversion.cfg | 7 ++- .gitignore | 2 + README.md | 11 ++++ .../alphabase_innoinstaller.iss | 0 .../windows/alphabase_innoinstaller_old.iss | 55 +++++++++++++++++++ release/windows/build_installer_windows.ps1 | 16 ++++++ release/windows/build_package_windows.ps1 | 6 ++ .../create_installer_windows.sh | 6 +- 8 files changed, 98 insertions(+), 5 deletions(-) rename release/{one_click_windows_gui => windows}/alphabase_innoinstaller.iss (100%) create mode 100644 release/windows/alphabase_innoinstaller_old.iss create mode 100644 release/windows/build_installer_windows.ps1 create mode 100644 release/windows/build_package_windows.ps1 rename release/{one_click_windows_gui => windows}/create_installer_windows.sh (92%) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 569f4109..16336822 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -29,8 +29,11 @@ serialize = [bumpversion:file:./release/macos/build_installer_macos.sh] # TODO remove with old release workflow [bumpversion:file:./release/macos/build_package_macos.sh] # TODO remove with old release workflow -[bumpversion:file:./release/one_click_windows_gui/create_installer_windows.sh] +[bumpversion:file:./release/windows/create_installer_windows.sh] # TODO remove with old release workflow +[bumpversion:file:./release/windows/build_installer_windows.ps1] +[bumpversion:file:./release/windows/build_package_windows.sh] -[bumpversion:file:./release/one_click_windows_gui/alphabase_innoinstaller.iss] +[bumpversion:file:./release/windows/alphabase_innoinstaller.iss] +[bumpversion:file:./release/windows/alphabase_innoinstaller_old.iss] # TODO remove with old release workflow search = {current_version} replace = {new_version} diff --git a/.gitignore b/.gitignore index a2252c51..fe79425c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +dist_pyinstaller/ +build_pyinstaller/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/README.md b/README.md index 9170690f..04319a4f 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,17 @@ For an even more interactive participation, check out the [the Contributors License Agreement](misc/CLA.md). ### Notes for developers + +#### Tagging of changes +In order to have release notes automatically generated, changes need to be tagged with labels. +The following labels are used (should be safe-explanatory): +`breaking-change`, `bug`, `enhancement`. + +#### Release a new version +This package uses a shared release process defined in the +[alphashared](https://github.com/MannLabs/alphashared) repository. Please see the instructions +[there](https://github.com/MannLabs/alphashared/blob/reusable-release-workflow/.github/workflows/README.md#release-a-new-version). + #### pre-commit hooks It is highly recommended to use the provided pre-commit hooks, as the CI pipeline enforces all checks therein to pass in order to merge a branch. diff --git a/release/one_click_windows_gui/alphabase_innoinstaller.iss b/release/windows/alphabase_innoinstaller.iss similarity index 100% rename from release/one_click_windows_gui/alphabase_innoinstaller.iss rename to release/windows/alphabase_innoinstaller.iss diff --git a/release/windows/alphabase_innoinstaller_old.iss b/release/windows/alphabase_innoinstaller_old.iss new file mode 100644 index 00000000..f23842fd --- /dev/null +++ b/release/windows/alphabase_innoinstaller_old.iss @@ -0,0 +1,55 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! +; TODO remove with old release workflow +; Note: apparently, ISCC uses the directory of the .iss input file as the working directory, +; so all paths are given relative to the location of this .iss file. + +#define MyAppName "AlphaBase" +#define MyAppVersion "1.4.0" +#define MyAppPublisher "Max Planck Institute of Biochemistry and the University of Copenhagen, Mann Labs" +#define MyAppURL "https://github.com/MannLabs/alphabase" +#define MyAppExeName "alphabase_gui.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{alphabase_Mann_Labs_MPI_CPR} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DisableProgramGroupPage=yes +LicenseFile=..\..\LICENSE.txt +; Uncomment the following line to run in non administrative install mode (install for current user only.) +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +; release workflow expects artifact at root of repository +OutputDir=..\..\ +; example for BUILD_NAME: alphabase-1.2.1-windows-amd64 +OutputBaseFilename={#GetEnv('BUILD_NAME')} +SetupIconFile=..\logos\alpha_logo.ico +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "..\..\dist_pyinstaller\alphabase_gui\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\..\dist_pyinstaller\alphabase_gui\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/release/windows/build_installer_windows.ps1 b/release/windows/build_installer_windows.ps1 new file mode 100644 index 00000000..1e22d9fe --- /dev/null +++ b/release/windows/build_installer_windows.ps1 @@ -0,0 +1,16 @@ +# Build the installer for Windows. +# This script must be run from the root of the repository. + +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./build +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./dist +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./*.egg-info +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./build_pyinstaller +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./dist_pyinstaller + +# Creating the wheel +python setup.py sdist bdist_wheel +# Make sure you include the required extra packages and always use the stable or very-stable options! +pip install "dist/alphabase-1.4.0-py3-none-any.whl[stable]" + +# Creating the stand-alone pyinstaller folder +pyinstaller release/pyinstaller/alphabase.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y diff --git a/release/windows/build_package_windows.ps1 b/release/windows/build_package_windows.ps1 new file mode 100644 index 00000000..6dd0b13e --- /dev/null +++ b/release/windows/build_package_windows.ps1 @@ -0,0 +1,6 @@ +# Build the install package for Windows. +# This script must be run from the root of the repository after running build_installer_windows.ps1 + + +# Wrapping the pyinstaller folder in a .exe package +& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" .\release\windows\alphabase_innoinstaller.iss diff --git a/release/one_click_windows_gui/create_installer_windows.sh b/release/windows/create_installer_windows.sh similarity index 92% rename from release/one_click_windows_gui/create_installer_windows.sh rename to release/windows/create_installer_windows.sh index 806a2f8f..336b80a7 100644 --- a/release/one_click_windows_gui/create_installer_windows.sh +++ b/release/windows/create_installer_windows.sh @@ -1,5 +1,5 @@ #!bash - +# TODO remove with old release workflow # Initial cleanup rm -rf dist rm -rf build @@ -15,7 +15,7 @@ conda activate alphabase_installer python setup.py sdist bdist_wheel # Setting up the local package -cd release/one_click_windows_gui +cd release/windows # Make sure you include the required extra packages and always use the stable or very-stable options! pip install "../../dist/alphabase-1.4.0-py3-none-any.whl[stable]" @@ -28,5 +28,5 @@ conda deactivate # cp ../../alphabase/data/*.fasta dist/alphabase/data # Wrapping the pyinstaller folder in a .exe package -"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" alphabase_innoinstaller.iss +"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" alphabase_innoinstaller_old.iss # WARNING: this assumes a static location for innosetup From 00a70b2d2565f41c3635831f7a903ce10c9192c4 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:42:08 +0100 Subject: [PATCH 19/37] fix --- release/macos/build_installer_macos.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/macos/build_installer_macos.sh b/release/macos/build_installer_macos.sh index 10100291..508a409d 100644 --- a/release/macos/build_installer_macos.sh +++ b/release/macos/build_installer_macos.sh @@ -12,4 +12,4 @@ python setup.py sdist bdist_wheel pip install "dist/alphabase-1.4.0-py3-none-any.whl[stable]" # Creating the stand-alone pyinstaller folder -pyinstaller release/pyinstaller/peptdeep.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y +pyinstaller release/pyinstaller/alphabase.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y From 7ce999511bcc39dde0fb2697d1b5420749fceeb4 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:44:22 +0100 Subject: [PATCH 20/37] remove obsolete instructions from checklist.txt --- misc/checklist.txt | 58 ---------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/misc/checklist.txt b/misc/checklist.txt index 9a7f0f42..0e0f827c 100644 --- a/misc/checklist.txt +++ b/misc/checklist.txt @@ -1,16 +1,8 @@ ######## ALWAYS TODO ######## -Versions updated - conda activate alphabase - cd misc - bumpversion patch --config-file bumpversion.cfg - conda deactivate - # bumpversion patch --new-version VERSION --config-file bumpversion.cfg - No TODOs left? README updated? - - Update CHANGELOG Manuals updated? if [ misc/gui_manual.docx -nt alphabase/docs/gui_manual.pdf ]; then @@ -19,53 +11,3 @@ Manuals updated? if [ misc/cli_manual.docx -nt alphabase/docs/cli_manual.pdf ]; then echo "CLI manual was not updated" fi - -Tutorial and performance? - conda activate alphabase - jupyter nbconvert --execute --inplace --to notebook --NotebookClient.kernel_name="python" nbs/tutorial.ipynb nbs/performance.ipynb - jupyter nbconvert --to html --NotebookClient.kernel_name="python" nbs/tutorial.ipynb nbs/performance.ipynb - conda deactivate - - - - - - -######## AUTOMATED BY NOW ######## - -Merged into master branch? - -Tests passing? - python -m unittest tests.test_bruker - -ReadTheDocs up-to-date? - Docs updated? - cd docs - make html - # https://docs.readthedocs.io/en/stable/ - https://alphabase.readthedocs.io/en/latest/ - -Releases for all Windows, MacOS and Linux? - - cd misc/macos - source create_installer_macos.sh - - cd misc/one_click_linux - source create_installer_linux.sh - - cd misc/one_click_windows - create_installer_windows.bat - -PyPi up-to-date? - # https://realpython.com/pypi-publish-python-package/ - conda activate alphabase_pip - rm -rf dist - rm -rf build - python setup.py sdist bdist_wheel - twine check dist/* - twine upload --repository-url https://test.pypi.org/legacy/ dist/* - conda create -n alphabase_pip_test python=3.9 -y - conda activate alphabase_pip_test - # WARNING!!!!! Test on all OS - pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple "alphabase" - twine upload dist/* From 84a09c92b717ba513ba5ded4b68da05b5c3c4573 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:49:18 +0100 Subject: [PATCH 21/37] fix bumpversion config --- .bumpversion.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 16336822..a107d00f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -19,7 +19,6 @@ serialize = [bumpversion:file:./release/linux/create_installer_linux.sh] # TODO remove with old release workflow [bumpversion:file:./release/linux/build_installer_linux.sh] -[bumpversion:file:./release/linux/build_package_linux.sh] [bumpversion:file:./release/macos/distribution.xml] @@ -31,7 +30,6 @@ serialize = [bumpversion:file:./release/windows/create_installer_windows.sh] # TODO remove with old release workflow [bumpversion:file:./release/windows/build_installer_windows.ps1] -[bumpversion:file:./release/windows/build_package_windows.sh] [bumpversion:file:./release/windows/alphabase_innoinstaller.iss] [bumpversion:file:./release/windows/alphabase_innoinstaller_old.iss] # TODO remove with old release workflow From 3d6adaeba99c29ad8a7f119a1470f20d0e5f9a6f Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:51:28 +0100 Subject: [PATCH 22/37] fix file permissions --- release/linux/build_installer_linux.sh | 0 release/linux/build_package_linux.sh | 0 release/macos/build_installer_macos.sh | 0 release/macos/build_package_macos.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 release/linux/build_installer_linux.sh mode change 100644 => 100755 release/linux/build_package_linux.sh mode change 100644 => 100755 release/macos/build_installer_macos.sh mode change 100644 => 100755 release/macos/build_package_macos.sh diff --git a/release/linux/build_installer_linux.sh b/release/linux/build_installer_linux.sh old mode 100644 new mode 100755 diff --git a/release/linux/build_package_linux.sh b/release/linux/build_package_linux.sh old mode 100644 new mode 100755 diff --git a/release/macos/build_installer_macos.sh b/release/macos/build_installer_macos.sh old mode 100644 new mode 100755 diff --git a/release/macos/build_package_macos.sh b/release/macos/build_package_macos.sh old mode 100644 new mode 100755 From 9bdad60d6840556812b39ab1239410ef2be51920 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:14:32 +0100 Subject: [PATCH 23/37] fix windows installer --- release/windows/alphabase_innoinstaller.iss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/release/windows/alphabase_innoinstaller.iss b/release/windows/alphabase_innoinstaller.iss index 4d0ef041..61496d91 100644 --- a/release/windows/alphabase_innoinstaller.iss +++ b/release/windows/alphabase_innoinstaller.iss @@ -24,7 +24,8 @@ LicenseFile=..\..\LICENSE.txt ; Uncomment the following line to run in non administrative install mode (install for current user only.) PrivilegesRequired=lowest PrivilegesRequiredOverridesAllowed=dialog -OutputDir=dist +; release workflow expects artifact at root of repository +OutputDir=../../ OutputBaseFilename=alphabase_gui_installer_windows SetupIconFile=..\logos\alpha_logo.ico Compression=lzma From 0177c090c6820d8b20562a6a42ca148bf56ff468 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:26:14 +0100 Subject: [PATCH 24/37] delete release folder --- .bumpversion.cfg | 18 --- release/linux/build_installer_linux.sh | 18 --- release/linux/build_package_linux.sh | 20 --- release/linux/control | 7 - release/linux/create_installer_linux.sh | 36 ----- release/logos/alpha_logo.icns | Bin 85999 -> 0 bytes release/logos/alpha_logo.ico | Bin 8843 -> 0 bytes release/logos/alpha_logo.png | Bin 5182 -> 0 bytes release/macos/Info.plist | 24 --- release/macos/Resources/conclusion.html | 13 -- release/macos/Resources/welcome.html | 13 -- release/macos/alphabase_terminal | 3 - release/macos/build_installer_macos.sh | 15 -- release/macos/build_package_macos.sh | 34 ---- release/macos/create_installer_macos.sh | 44 ----- release/macos/distribution.xml | 17 -- release/macos/scripts/postinstall | 6 - release/macos/scripts/preinstall | 5 - release/pyinstaller/alphabase.spec | 152 ------------------ release/pyinstaller/alphabase_pyinstaller.py | 16 -- release/pypi/install_pypi_wheel.sh | 5 - release/pypi/install_test_pypi_wheel.sh | 5 - release/pypi/prepare_pypi_wheel.sh | 9 -- release/windows/alphabase_innoinstaller.iss | 51 ------ .../windows/alphabase_innoinstaller_old.iss | 55 ------- release/windows/build_installer_windows.ps1 | 16 -- release/windows/build_package_windows.ps1 | 6 - release/windows/create_installer_windows.sh | 32 ---- 28 files changed, 620 deletions(-) delete mode 100755 release/linux/build_installer_linux.sh delete mode 100755 release/linux/build_package_linux.sh delete mode 100644 release/linux/control delete mode 100644 release/linux/create_installer_linux.sh delete mode 100644 release/logos/alpha_logo.icns delete mode 100644 release/logos/alpha_logo.ico delete mode 100644 release/logos/alpha_logo.png delete mode 100644 release/macos/Info.plist delete mode 100644 release/macos/Resources/conclusion.html delete mode 100644 release/macos/Resources/welcome.html delete mode 100644 release/macos/alphabase_terminal delete mode 100755 release/macos/build_installer_macos.sh delete mode 100755 release/macos/build_package_macos.sh delete mode 100644 release/macos/create_installer_macos.sh delete mode 100644 release/macos/distribution.xml delete mode 100644 release/macos/scripts/postinstall delete mode 100644 release/macos/scripts/preinstall delete mode 100644 release/pyinstaller/alphabase.spec delete mode 100644 release/pyinstaller/alphabase_pyinstaller.py delete mode 100644 release/pypi/install_pypi_wheel.sh delete mode 100644 release/pypi/install_test_pypi_wheel.sh delete mode 100644 release/pypi/prepare_pypi_wheel.sh delete mode 100644 release/windows/alphabase_innoinstaller.iss delete mode 100644 release/windows/alphabase_innoinstaller_old.iss delete mode 100644 release/windows/build_installer_windows.ps1 delete mode 100644 release/windows/build_package_windows.ps1 delete mode 100644 release/windows/create_installer_windows.sh diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a107d00f..b55048b4 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -15,23 +15,5 @@ serialize = [bumpversion:file:./docs/conf.py] -[bumpversion:file:./release/linux/control] - -[bumpversion:file:./release/linux/create_installer_linux.sh] # TODO remove with old release workflow -[bumpversion:file:./release/linux/build_installer_linux.sh] - -[bumpversion:file:./release/macos/distribution.xml] - -[bumpversion:file:./release/macos/Info.plist] - -[bumpversion:file:./release/macos/create_installer_macos.sh] -[bumpversion:file:./release/macos/build_installer_macos.sh] # TODO remove with old release workflow -[bumpversion:file:./release/macos/build_package_macos.sh] # TODO remove with old release workflow - -[bumpversion:file:./release/windows/create_installer_windows.sh] # TODO remove with old release workflow -[bumpversion:file:./release/windows/build_installer_windows.ps1] - -[bumpversion:file:./release/windows/alphabase_innoinstaller.iss] -[bumpversion:file:./release/windows/alphabase_innoinstaller_old.iss] # TODO remove with old release workflow search = {current_version} replace = {new_version} diff --git a/release/linux/build_installer_linux.sh b/release/linux/build_installer_linux.sh deleted file mode 100755 index c1a279d1..00000000 --- a/release/linux/build_installer_linux.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -e -u - -# Build the installer for Linux. -# This script must be run from the root of the repository. - -rm -rf dist build *.egg-info -rm -rf dist_pyinstaller build_pyinstaller - -# Creating the wheel -python setup.py sdist bdist_wheel - -# Setting up the local package -# Make sure you include the required extra packages and always use the stable or very-stable options! -pip install "dist/alphabase-1.4.0-py3-none-any.whl[stable]" - -# Creating the stand-alone pyinstaller folder -pyinstaller release/pyinstaller/alphabase.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y diff --git a/release/linux/build_package_linux.sh b/release/linux/build_package_linux.sh deleted file mode 100755 index f03e026d..00000000 --- a/release/linux/build_package_linux.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -set -e -u - -# Build the install package for Linux. -# This script must be run from the root of the repository after running build_installer_linux.sh - -PACKAGE_NAME=alphabase - -# BUILD_NAME is taken from environment variables, e.g. 'alphabase-1.2.1-linux-x64' -rm -rf ${BUILD_NAME}.deb - -# Wrapping the pyinstaller folder in a .deb package -mkdir -p dist_pyinstaller/${BUILD_NAME}/usr/local/bin -mv dist_pyinstaller/${PACKAGE_NAME} dist_pyinstaller/${BUILD_NAME}/usr/local/bin/${PACKAGE_NAME} -mkdir dist_pyinstaller/${BUILD_NAME}/DEBIAN -cp release/linux/control dist_pyinstaller/${BUILD_NAME}/DEBIAN -dpkg-deb --build --root-owner-group dist_pyinstaller/${BUILD_NAME} - -# release workflow expects artifact at root of repository -mv dist_pyinstaller/${BUILD_NAME}.deb . diff --git a/release/linux/control b/release/linux/control deleted file mode 100644 index dd9a15fa..00000000 --- a/release/linux/control +++ /dev/null @@ -1,7 +0,0 @@ -Package: alphabase -Version: 1.4.0 -Architecture: all -Maintainer: Mann Labs -Description: AlphaBase - AlphaBase is an open-source Python package in the AlphaX ecosystem. - AlphaBase was developed by the Mann Labs at the Max Planck Institute of Biochemistry and University of Copenhagen and is freely available with an Apache License. Additional third-party licenses are applicable for external Python packages (see https://github.com/MannLabs/AlphaBase for more details.). diff --git a/release/linux/create_installer_linux.sh b/release/linux/create_installer_linux.sh deleted file mode 100644 index ef237636..00000000 --- a/release/linux/create_installer_linux.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!bash -# TODO remove with old release workflow -# Initial cleanup -rm -rf dist -rm -rf build -cd ../.. -rm -rf dist -rm -rf build - -# Creating a conda environment -conda create -n alphabase_installer python=3.9 -y -conda activate alphabase_installer - -# Creating the wheel -python setup.py sdist bdist_wheel - -# Setting up the local package -cd release/linux -# Make sure you include the required extra packages and always use the stable or very-stable options! -pip install "../../dist/alphabase-1.4.0-py3-none-any.whl[stable]" - -# Creating the stand-alone pyinstaller folder -pip install pyinstaller -pyinstaller ../pyinstaller/alphabase.spec -y -conda deactivate - -# If needed, include additional source such as e.g.: -# cp ../../alphabase/data/*.fasta dist/alphabase/data -# WARNING: this probably does not work!!!! - -# Wrapping the pyinstaller folder in a .deb package -mkdir -p dist/AlphaBase_gui_installer_linux/usr/local/bin -mv dist/AlphaBase dist/AlphaBase_gui_installer_linux/usr/local/bin/AlphaBase -mkdir dist/AlphaBase_gui_installer_linux/DEBIAN -cp control dist/AlphaBase_gui_installer_linux/DEBIAN -dpkg-deb --build --root-owner-group dist/AlphaBase_gui_installer_linux/ diff --git a/release/logos/alpha_logo.icns b/release/logos/alpha_logo.icns deleted file mode 100644 index 1a9075342e7fb6f2d590deced84f35e8b772759c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85999 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 N{{11q4nS=lU diff --git a/release/logos/alpha_logo.ico b/release/logos/alpha_logo.ico deleted file mode 100644 index 03a8c4e02ffa0dc6af7fe180544855bcce86c3c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8843 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 diff --git a/release/logos/alpha_logo.png b/release/logos/alpha_logo.png deleted file mode 100644 index 9a4bf8511c98f064b6b826e139d72cd190323807..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5182 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 diff --git a/release/macos/Info.plist b/release/macos/Info.plist deleted file mode 100644 index 15c16d32..00000000 --- a/release/macos/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDisplayName - AlphaBase - CFBundleExecutable - MacOS/alphabase_terminal - CFBundleIconFile - alpha_logo.icns - CFBundleIdentifier - alphabase.1.4.0 - CFBundleShortVersionString - 1.4.0 - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - alphabase - CFBundlePackageType - APPL - LSBackgroundOnly - - - diff --git a/release/macos/Resources/conclusion.html b/release/macos/Resources/conclusion.html deleted file mode 100644 index 28ba71f2..00000000 --- a/release/macos/Resources/conclusion.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - -

-

AlphaBase

-

Thank you for installing AlphaBase.

-
- - diff --git a/release/macos/Resources/welcome.html b/release/macos/Resources/welcome.html deleted file mode 100644 index 47b5e888..00000000 --- a/release/macos/Resources/welcome.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - -
-

AlphaBase

-

AlphaBase is an open-source Python package of the AlphaPept ecosystem.

-

AlphaBase was developed by the Mann Labs at the Max Planck Institute of Biochemistry and the University of Copenhagen and is freely available with an Apache License. Since AlphaBase uses external Python packages, additional third-party licenses are applicable.

-
- - diff --git a/release/macos/alphabase_terminal b/release/macos/alphabase_terminal deleted file mode 100644 index 9aee6719..00000000 --- a/release/macos/alphabase_terminal +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -open -a Terminal "${BASH_SOURCE%/*}/alphabase_gui" diff --git a/release/macos/build_installer_macos.sh b/release/macos/build_installer_macos.sh deleted file mode 100755 index 508a409d..00000000 --- a/release/macos/build_installer_macos.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -e -u - -# Build the installer for MacOS. -# This script must be run from the root of the repository. - -rm -rf dist -rm -rf build - -# Creating the wheel -python setup.py sdist bdist_wheel -pip install "dist/alphabase-1.4.0-py3-none-any.whl[stable]" - -# Creating the stand-alone pyinstaller folder -pyinstaller release/pyinstaller/alphabase.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y diff --git a/release/macos/build_package_macos.sh b/release/macos/build_package_macos.sh deleted file mode 100755 index 7c35dad2..00000000 --- a/release/macos/build_package_macos.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -e -u - -# Build the install package for MacOS. -# This script must be run from the root of the repository after running build_installer_macos.sh - -PACKAGE_NAME=alphabase -# BUILD_NAME is taken from environment variables, e.g. alphabase-1.3.0-macos-darwin-arm64 or alphabase-1.3.0-macos-darwin-x64 -rm -rf ${BUILD_NAME}.pkg - -# If needed, include additional source such as e.g.: -# cp ../../alphabase/data/*.fasta dist/alphabase/data - -# Wrapping the pyinstaller folder in a .pkg package -CONTENTS_FOLDER=dist_pyinstaller/${PACKAGE_NAME}/Contents - -mkdir -p ${CONTENTS_FOLDER}/Resources -cp release/logos/alpha_logo.icns ${CONTENTS_FOLDER}/Resources -mv dist_pyinstaller/alphabase_gui ${CONTENTS_FOLDER}/MacOS -cp release/macos/Info.plist ${CONTENTS_FOLDER} -cp release/macos/alphabase_terminal ${CONTENTS_FOLDER}/MacOS -cp LICENSE.txt ${CONTENTS_FOLDER}/Resources -cp release/logos/alpha_logo.png ${CONTENTS_FOLDER}/Resources - -# link _internal folder containing the python libraries to the Frameworks folder where they are expected -# to avoid e.g. "Failed to load Python shared library '/Applications/AlphaMap.app/Contents/Frameworks/libpython3.8.dylib'" -cd ${CONTENTS_FOLDER} -ln -s ./MacOS/_internal ./Frameworks -cd - - -chmod 777 release/macos/scripts/* - -pkgbuild --root dist_pyinstaller/${PACKAGE_NAME} --identifier de.mpg.biochem.${PACKAGE_NAME}.app --version 1.4.0 --install-location /Applications/${PACKAGE_NAME}.app --scripts release/macos/scripts ${PACKAGE_NAME}.pkg -productbuild --distribution release/macos/distribution.xml --resources release/macos/Resources --package-path ${PACKAGE_NAME}.pkg ${BUILD_NAME}.pkg diff --git a/release/macos/create_installer_macos.sh b/release/macos/create_installer_macos.sh deleted file mode 100644 index d1da7f88..00000000 --- a/release/macos/create_installer_macos.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!bash -# TODO remove with old release workflow -# Initial cleanup -rm -rf dist -rm -rf build -FILE=AlphaBase.pkg -if test -f "$FILE"; then - rm AlphaBase.pkg -fi -cd ../.. -rm -rf dist -rm -rf build - -# Creating a conda environment -conda create -n alphabaseinstaller python=3.9 -y -conda activate alphabaseinstaller - -# Creating the wheel -python setup.py sdist bdist_wheel - -# Setting up the local package -cd release/macos -pip install "../../dist/alphabase-1.4.0-py3-none-any.whl[stable]" - -# Creating the stand-alone pyinstaller folder -pip install pyinstaller -pyinstaller ../pyinstaller/alphabase.spec -y -conda deactivate - -# If needed, include additional source such as e.g.: -# cp ../../alphabase/data/*.fasta dist/alphabase/data - -# Wrapping the pyinstaller folder in a .pkg package -mkdir -p dist/alphabase/Contents/Resources -cp ../logos/alpha_logo.icns dist/alphabase/Contents/Resources -mv dist/alphabase_gui dist/alphabase/Contents/MacOS -cp Info.plist dist/alphabase/Contents -cp alphabase_terminal dist/alphabase/Contents/MacOS -cp ../../LICENSE.txt Resources/LICENSE.txt -cp ../logos/alpha_logo.png Resources/alpha_logo.png -chmod 777 scripts/* - -pkgbuild --root dist/alphabase --identifier de.mpg.biochem.alphabase.app --version 1.4.0 --install-location /Applications/AlphaBase.app --scripts scripts AlphaBase.pkg -productbuild --distribution distribution.xml --resources Resources --package-path AlphaBase.pkg dist/alphabase_gui_installer_macos.pkg diff --git a/release/macos/distribution.xml b/release/macos/distribution.xml deleted file mode 100644 index 9969ef35..00000000 --- a/release/macos/distribution.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - AlphaBase 1.4.0 - - - - - - - - - - - - - alphabase.pkg - diff --git a/release/macos/scripts/postinstall b/release/macos/scripts/postinstall deleted file mode 100644 index 1ef12e27..00000000 --- a/release/macos/scripts/postinstall +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -# make sure this file itself is executable -xattr -dr com.apple.quarantine /Applications/AlphaBase.app -chmod -R 577 /Applications/AlphaBase.app -echo "Postinstall finished" diff --git a/release/macos/scripts/preinstall b/release/macos/scripts/preinstall deleted file mode 100644 index 4d704a6d..00000000 --- a/release/macos/scripts/preinstall +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -# make sure this file itself is executable -rm -rf /Applications/AlphaBase.app -echo "Preinstall finished" diff --git a/release/pyinstaller/alphabase.spec b/release/pyinstaller/alphabase.spec deleted file mode 100644 index 2b68c135..00000000 --- a/release/pyinstaller/alphabase.spec +++ /dev/null @@ -1,152 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -import pkgutil -import os -import sys -from PyInstaller.building.build_main import Analysis, PYZ, EXE, COLLECT, BUNDLE, TOC -import PyInstaller.utils.hooks -import pkg_resources -import importlib.metadata -import alphabase - - -##################### User definitions -exe_name = 'alphabase_gui' -script_name = 'alphabase_pyinstaller.py' -if sys.platform[:6] == "darwin": - icon = '../logos/alpha_logo.icns' -else: - icon = '../logos/alpha_logo.ico' -block_cipher = None -location = os.getcwd() -project = "alphabase" -remove_tests = True -bundle_name = "alphabase" -##################### - - -requirements = { - req.split()[0] for req in importlib.metadata.requires(project) -} -requirements.add(project) -requirements.add("distributed") -hidden_imports = set() -datas = [] -binaries = [] -checked = set() -while requirements: - requirement = requirements.pop() - checked.add(requirement) - if requirement in ["pywin32"]: - continue - try: - module_version = importlib.metadata.version(requirement) - except ( - importlib.metadata.PackageNotFoundError, - ModuleNotFoundError, - ImportError - ): - continue - try: - datas_, binaries_, hidden_imports_ = PyInstaller.utils.hooks.collect_all( - requirement, - include_py_files=True - ) - except ImportError: - continue - datas += datas_ - # binaries += binaries_ - hidden_imports_ = set(hidden_imports_) - if "" in hidden_imports_: - hidden_imports_.remove("") - if None in hidden_imports_: - hidden_imports_.remove(None) - requirements |= hidden_imports_ - checked - hidden_imports |= hidden_imports_ - -if remove_tests: - hidden_imports = sorted( - [h for h in hidden_imports if "tests" not in h.split(".")] - ) -else: - hidden_imports = sorted(hidden_imports) - - -hidden_imports = [h for h in hidden_imports if "__pycache__" not in h] -datas = [d for d in datas if ("__pycache__" not in d[0]) and (d[1] not in [".", "Resources", "scripts"])] - -if sys.platform[:5] == "win32": - base_path = os.path.dirname(sys.executable) - library_path = os.path.join(base_path, "Library", "bin") - dll_path = os.path.join(base_path, "DLLs") - libcrypto_dll_path = os.path.join(dll_path, "libcrypto-1_1-x64.dll") - libssl_dll_path = os.path.join(dll_path, "libssl-1_1-x64.dll") - libcrypto_lib_path = os.path.join(library_path, "libcrypto-1_1-x64.dll") - libssl_lib_path = os.path.join(library_path, "libssl-1_1-x64.dll") - if not os.path.exists(libcrypto_dll_path): - datas.append((libcrypto_lib_path, ".")) - if not os.path.exists(libssl_dll_path): - datas.append((libssl_lib_path, ".")) - -a = Analysis( - [script_name], - pathex=[location], - binaries=binaries, - datas=datas, - hiddenimports=hidden_imports, - hookspath=[], - runtime_hooks=[], - excludes=[h for h in hidden_imports if "datashader" in h], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False -) -pyz = PYZ( - a.pure, - a.zipped_data, - cipher=block_cipher -) - -if sys.platform[:5] == "linux": - exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name=bundle_name, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - upx_exclude=[], - icon=icon - ) -else: - exe = EXE( - pyz, - a.scripts, - # a.binaries, - a.zipfiles, - # a.datas, - exclude_binaries=True, - name=exe_name, - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=True, - icon=icon - ) - coll = COLLECT( - exe, - a.binaries, - # a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name=exe_name - ) diff --git a/release/pyinstaller/alphabase_pyinstaller.py b/release/pyinstaller/alphabase_pyinstaller.py deleted file mode 100644 index 74a4af7d..00000000 --- a/release/pyinstaller/alphabase_pyinstaller.py +++ /dev/null @@ -1,16 +0,0 @@ -if __name__ == "__main__": - try: - import multiprocessing - - import alphabase.gui - - multiprocessing.freeze_support() - alphabase.gui.run() - except Exception: - import sys - import traceback - - exc_info = sys.exc_info() - # Display the *original* exception - traceback.print_exception(*exc_info) - input("Something went wrong, press any key to continue...") diff --git a/release/pypi/install_pypi_wheel.sh b/release/pypi/install_pypi_wheel.sh deleted file mode 100644 index 35ed5385..00000000 --- a/release/pypi/install_pypi_wheel.sh +++ /dev/null @@ -1,5 +0,0 @@ -conda create -n alphabase_pip_test python=3.9 -y -conda activate alphabase_pip_test -pip install "alphabase[stable]" -alphabase -conda deactivate diff --git a/release/pypi/install_test_pypi_wheel.sh b/release/pypi/install_test_pypi_wheel.sh deleted file mode 100644 index 8c0f213d..00000000 --- a/release/pypi/install_test_pypi_wheel.sh +++ /dev/null @@ -1,5 +0,0 @@ -conda create -n alphabase_pip_test python=3.9 -y -conda activate alphabase_pip_test -pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple "alphabase[stable]" -alphabase -conda deactivate diff --git a/release/pypi/prepare_pypi_wheel.sh b/release/pypi/prepare_pypi_wheel.sh deleted file mode 100644 index 78a6e4f8..00000000 --- a/release/pypi/prepare_pypi_wheel.sh +++ /dev/null @@ -1,9 +0,0 @@ -cd ../.. -conda create -n alphabase_pypi_wheel python=3.9 -conda activate alphabase_pypi_wheel -pip install twine -rm -rf dist -rm -rf build -python setup.py sdist bdist_wheel -twine check dist/* -conda deactivate diff --git a/release/windows/alphabase_innoinstaller.iss b/release/windows/alphabase_innoinstaller.iss deleted file mode 100644 index 61496d91..00000000 --- a/release/windows/alphabase_innoinstaller.iss +++ /dev/null @@ -1,51 +0,0 @@ -; Script generated by the Inno Setup Script Wizard. -; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! - -#define MyAppName "AlphaBase" -#define MyAppVersion "1.4.0" -#define MyAppPublisher "Max Planck Institute of Biochemistry and the University of Copenhagen, Mann Labs" -#define MyAppURL "https://github.com/MannLabs/alphabase" -#define MyAppExeName "alphabase_gui.exe" - -[Setup] -; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{alphabase_Mann_Labs_MPI_CPR} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -;AppVerName={#MyAppName} {#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -DefaultDirName={autopf}\{#MyAppName} -DisableProgramGroupPage=yes -LicenseFile=..\..\LICENSE.txt -; Uncomment the following line to run in non administrative install mode (install for current user only.) -PrivilegesRequired=lowest -PrivilegesRequiredOverridesAllowed=dialog -; release workflow expects artifact at root of repository -OutputDir=../../ -OutputBaseFilename=alphabase_gui_installer_windows -SetupIconFile=..\logos\alpha_logo.ico -Compression=lzma -SolidCompression=yes -WizardStyle=modern - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked - -[Files] -Source: "dist\alphabase_gui\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "dist\alphabase_gui\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs -; NOTE: Don't use "Flags: ignoreversion" on any shared system files - -[Icons] -Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - -[Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/release/windows/alphabase_innoinstaller_old.iss b/release/windows/alphabase_innoinstaller_old.iss deleted file mode 100644 index f23842fd..00000000 --- a/release/windows/alphabase_innoinstaller_old.iss +++ /dev/null @@ -1,55 +0,0 @@ -; Script generated by the Inno Setup Script Wizard. -; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! -; TODO remove with old release workflow -; Note: apparently, ISCC uses the directory of the .iss input file as the working directory, -; so all paths are given relative to the location of this .iss file. - -#define MyAppName "AlphaBase" -#define MyAppVersion "1.4.0" -#define MyAppPublisher "Max Planck Institute of Biochemistry and the University of Copenhagen, Mann Labs" -#define MyAppURL "https://github.com/MannLabs/alphabase" -#define MyAppExeName "alphabase_gui.exe" - -[Setup] -; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{alphabase_Mann_Labs_MPI_CPR} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -;AppVerName={#MyAppName} {#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -DefaultDirName={autopf}\{#MyAppName} -DisableProgramGroupPage=yes -LicenseFile=..\..\LICENSE.txt -; Uncomment the following line to run in non administrative install mode (install for current user only.) -PrivilegesRequired=lowest -PrivilegesRequiredOverridesAllowed=dialog -; release workflow expects artifact at root of repository -OutputDir=..\..\ -; example for BUILD_NAME: alphabase-1.2.1-windows-amd64 -OutputBaseFilename={#GetEnv('BUILD_NAME')} -SetupIconFile=..\logos\alpha_logo.ico -Compression=lzma -SolidCompression=yes -WizardStyle=modern - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked - -[Files] -Source: "..\..\dist_pyinstaller\alphabase_gui\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\..\dist_pyinstaller\alphabase_gui\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs -; NOTE: Don't use "Flags: ignoreversion" on any shared system files - -[Icons] -Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - -[Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/release/windows/build_installer_windows.ps1 b/release/windows/build_installer_windows.ps1 deleted file mode 100644 index 1e22d9fe..00000000 --- a/release/windows/build_installer_windows.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -# Build the installer for Windows. -# This script must be run from the root of the repository. - -Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./build -Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./dist -Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./*.egg-info -Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./build_pyinstaller -Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ./dist_pyinstaller - -# Creating the wheel -python setup.py sdist bdist_wheel -# Make sure you include the required extra packages and always use the stable or very-stable options! -pip install "dist/alphabase-1.4.0-py3-none-any.whl[stable]" - -# Creating the stand-alone pyinstaller folder -pyinstaller release/pyinstaller/alphabase.spec --distpath dist_pyinstaller --workpath build_pyinstaller -y diff --git a/release/windows/build_package_windows.ps1 b/release/windows/build_package_windows.ps1 deleted file mode 100644 index 6dd0b13e..00000000 --- a/release/windows/build_package_windows.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -# Build the install package for Windows. -# This script must be run from the root of the repository after running build_installer_windows.ps1 - - -# Wrapping the pyinstaller folder in a .exe package -& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" .\release\windows\alphabase_innoinstaller.iss diff --git a/release/windows/create_installer_windows.sh b/release/windows/create_installer_windows.sh deleted file mode 100644 index 336b80a7..00000000 --- a/release/windows/create_installer_windows.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!bash -# TODO remove with old release workflow -# Initial cleanup -rm -rf dist -rm -rf build -cd ../.. -rm -rf dist -rm -rf build - -# Creating a conda environment -conda create -n alphabase_installer python=3.9 -y -conda activate alphabase_installer - -# Creating the wheel -python setup.py sdist bdist_wheel - -# Setting up the local package -cd release/windows -# Make sure you include the required extra packages and always use the stable or very-stable options! -pip install "../../dist/alphabase-1.4.0-py3-none-any.whl[stable]" - -# Creating the stand-alone pyinstaller folder -pip install pyinstaller -pyinstaller ../pyinstaller/alphabase.spec -y -conda deactivate - -# If needed, include additional source such as e.g.: -# cp ../../alphabase/data/*.fasta dist/alphabase/data - -# Wrapping the pyinstaller folder in a .exe package -"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" alphabase_innoinstaller_old.iss -# WARNING: this assumes a static location for innosetup From e078d42e840ae9e3e2cb12486291c1a9a050f7b8 Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:26:51 +0100 Subject: [PATCH 25/37] delete release workflow folder --- .github/workflows/publish_and_release.yaml | 80 ---------------------- 1 file changed, 80 deletions(-) delete mode 100644 .github/workflows/publish_and_release.yaml diff --git a/.github/workflows/publish_and_release.yaml b/.github/workflows/publish_and_release.yaml deleted file mode 100644 index 031e331f..00000000 --- a/.github/workflows/publish_and_release.yaml +++ /dev/null @@ -1,80 +0,0 @@ -on: - # push: - # branches: [ main ] - workflow_dispatch: - - -name: Publish on PyPi and release on GitHub - -jobs: - Version_Bumped: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.master_version_bumped.outputs.version }} - steps: - - name: Checkout code - uses: actions/checkout@v3 - - uses: conda-incubator/setup-miniconda@v3 - with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - - name: Master version bumped - id: master_version_bumped - shell: bash -l {0} - run: | - cd misc - . ./check_version.sh - echo "version=${current_version}" >> $GITHUB_OUTPUT - Create_PyPi_Release: - runs-on: ubuntu-latest - needs: Version_Bumped - steps: - - name: Checkout code - uses: actions/checkout@v3 - - uses: conda-incubator/setup-miniconda@v3 - with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - - name: Conda info - shell: bash -l {0} - run: conda info - - name: Prepare distribution - shell: bash -l {0} - run: | - cd release/pypi - . ./prepare_pypi_wheel.sh - - name: Publish distribution to Test PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.TEST_PYPI_ALPHABASE_API_TOKEN }} - repository_url: https://test.pypi.org/legacy/ - - name: Test PyPI test release - shell: bash -l {0} - run: | - cd release/pypi - . ./install_test_pypi_wheel.sh - - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_ALPHABASE_API_TOKEN }} - Test_PyPi_Release: - name: Test_PyPi_version_on_${{ matrix.os }} - runs-on: ${{ matrix.os }} - needs: Create_PyPi_Release - strategy: - matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] - steps: - - uses: actions/checkout@v3 - - uses: conda-incubator/setup-miniconda@v3 - with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - - name: Conda info - shell: bash -l {0} - run: conda info - - name: Test pip installation from PyPi - shell: bash -l {0} - run: | - cd release/pypi - . ./install_pypi_wheel.sh From d78728eba4b59720b5e764cd32adfc3a6b9a0b8b Mon Sep 17 00:00:00 2001 From: mschwoerer <82171591+mschwoer@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:29:07 +0100 Subject: [PATCH 26/37] revert gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index fe79425c..a2252c51 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,6 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST -dist_pyinstaller/ -build_pyinstaller/ # PyInstaller # Usually these files are written by a python script from a template From 7ca03da71a80f611fb57ca55c3794814d104af83 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Thu, 31 Oct 2024 08:38:24 +0100 Subject: [PATCH 27/37] fix bug --- alphabase/spectral_library/base.py | 52 +++++++++++++++++++----------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/alphabase/spectral_library/base.py b/alphabase/spectral_library/base.py index b1d3ad2e..5361bd45 100644 --- a/alphabase/spectral_library/base.py +++ b/alphabase/spectral_library/base.py @@ -287,28 +287,44 @@ def check_matching_columns(df1, df2): other_df = getattr(other, attr)[column].copy() if attr.startswith("_precursor"): - # increment dense fragment indices + # we iterate over the types of fragment dataframes fragment_df_mapping = { - "_fragment_intensity_df": "", - "_fragment_mz_df": "", - "_fragment_df": "flat_", + # dense fragment dataframes + "": ["_fragment_intensity_df", "_fragment_mz_df"], + # flat fragment dataframes + "flat_": ["_fragment_df"], } # Update indices for each fragment dataframe type - for fragment_df, prefix in fragment_df_mapping.items(): - if ( - hasattr(self, fragment_df) - and len(getattr(self, fragment_df)) > 0 - ): - frag_idx_increment = len(getattr(self, fragment_df)) - - start_col = f"{prefix}frag_start_idx" - stop_col = f"{prefix}frag_stop_idx" - - if start_col in other_df.columns: - other_df[start_col] += frag_idx_increment - if stop_col in other_df.columns: - other_df[stop_col] += frag_idx_increment + for prefix, fragment_df_list in fragment_df_mapping.items(): + # obtain frag_idx_increment and check if it is the same for all fragment dataframes + # an increment of 0 is allowed, but if not 0, it must be the same for all dense fragment dataframes + frag_idx_increment = 0 + for fragment_df in fragment_df_list: + if ( + hasattr(self, fragment_df) + and len(getattr(self, fragment_df)) > 0 + ): + if ( + frag_idx_increment != 0 + and len(getattr(self, fragment_df)) != 0 + and frag_idx_increment + != len(getattr(self, fragment_df)) + ): + raise ValueError( + f"The number of fragments in the {fragment_df} dataframe must be the same as in all other dense fragment dataframes" + ) + else: + frag_idx_increment = len(getattr(self, fragment_df)) + + # update the indices + start_col = f"{prefix}frag_start_idx" + stop_col = f"{prefix}frag_stop_idx" + + if start_col in other_df.columns: + other_df[start_col] += frag_idx_increment + if stop_col in other_df.columns: + other_df[stop_col] += frag_idx_increment setattr( self, From 56b8c98b1544afd07e9ca9ad7303fd7d7d717382 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Sun, 3 Nov 2024 22:05:00 +0100 Subject: [PATCH 28/37] updated psm-reader settings --- alphabase/constants/const_files/psm_reader.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alphabase/constants/const_files/psm_reader.yaml b/alphabase/constants/const_files/psm_reader.yaml index d2577b93..46b6bedb 100644 --- a/alphabase/constants/const_files/psm_reader.yaml +++ b/alphabase/constants/const_files/psm_reader.yaml @@ -38,6 +38,7 @@ maxquant: 'scan_num': - 'Scan number' - 'MS/MS scan number' + - 'MS/MS Scan Number' - 'Scan index' 'raw_name': 'Raw file' 'precursor_mz': 'm/z' @@ -57,6 +58,8 @@ maxquant: 'Acetyl@Protein_N-term': - '_(Acetyl (Protein_N-term))' - '_(ac)' + 'Acetyl@K': + - 'K(ac)' 'Carbamidomethyl@C': - 'C(Carbamidomethyl (C))' - 'C(Carbamidomethyl)' @@ -84,6 +87,7 @@ maxquant: 'Deamidated@N': ['N(Deamidation (NQ))','N(de)'] 'Deamidated@Q': ['Q(Deamidation (NQ))','Q(de)'] 'GlyGly@K': ['K(GlyGly (K))', 'K(gl)'] + 'hydroxyisobutyryl@K': 'K(2-)' pfind: reader_type: pfind From 0544a3d94cb01ecc02563d10d45c709ae1cfa2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sophia=20M=C3=A4dler?= <15019107+sophiamaedler@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:36:24 +0100 Subject: [PATCH 29/37] replace dtype use np.dtypes.StrDType instead of np.dtypes.ObjectDType --- alphabase/io/tempmmap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alphabase/io/tempmmap.py b/alphabase/io/tempmmap.py index dc2abb7c..20ebf366 100644 --- a/alphabase/io/tempmmap.py +++ b/alphabase/io/tempmmap.py @@ -165,7 +165,7 @@ def array(shape: tuple, dtype: np.dtype, tmp_dir_abs_path: str = None) -> np.nda with h5py.File(temp_file_name, "w") as hdf_file: array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = np.string_("") if isinstance(dtype, np.dtypes.ObjectDType) else 0 + array[0] = np.string_("") if isinstance(dtype, np.dtypes.StrDType) else 0 offset = array.id.get_offset() with open(temp_file_name, "rb+") as raw_hdf_file: @@ -225,7 +225,7 @@ def create_empty_mmap( with h5py.File(temp_file_name, "w") as hdf_file: array = hdf_file.create_dataset("array", shape=shape, dtype=dtype) - array[0] = np.string_("") if isinstance(dtype, np.dtypes.ObjectDType) else 0 + array[0] = np.string_("") if isinstance(dtype, np.dtypes.StrDType) else 0 return temp_file_name From b893aa38a06ed68467ec1a93604c450a80b034fa Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Mon, 4 Nov 2024 20:37:17 +0100 Subject: [PATCH 30/37] remove mbr psms from maxquant on loading --- alphabase/psm_reader/maxquant_reader.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/alphabase/psm_reader/maxquant_reader.py b/alphabase/psm_reader/maxquant_reader.py index 96c5a67c..c88aefb1 100644 --- a/alphabase/psm_reader/maxquant_reader.py +++ b/alphabase/psm_reader/maxquant_reader.py @@ -1,4 +1,5 @@ import copy +import warnings import numba import numpy as np @@ -11,6 +12,8 @@ psm_reader_yaml, ) +warnings.filterwarnings("always") + mod_to_unimod_dict = {} for mod_name, unimod_id in MOD_DF[["mod_name", "unimod_id"]].values: unimod_id = int(unimod_id) @@ -245,6 +248,20 @@ def _load_file(self, filename): self._find_mod_seq_column(df) df = df[~pd.isna(df["Retention time"])] df.fillna("", inplace=True) + + # remove MBR PSMs as they are currently not supported and will crash import + mapped_columns = self._find_mapped_columns(df) + if "scan_num" in mapped_columns: + scan_num_col = mapped_columns["scan_num"] + no_ms2_mask = df[scan_num_col] == "" + if np.sum(no_ms2_mask) > 0: + warnings.warn( + f"Maxquant psm file contains {np.sum(no_ms2_mask)} MBR PSMs without MS2 scan. This is not yet supported and rows containing MBR PSMs will be removed." + ) + df = df[~no_ms2_mask] + df.reset_index(drop=True, inplace=True) + df[scan_num_col] = df[scan_num_col].astype(int) + # if 'K0' in df.columns: # df['Mobility'] = df['K0'] # Bug in MaxQuant? It should be 1/K0 # min_rt = df['Retention time'].min() From 914e613d4a55d85c679075083ac047c22d65f339 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Tue, 5 Nov 2024 09:02:47 +0100 Subject: [PATCH 31/37] added phospho --- alphabase/constants/const_files/psm_reader.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/alphabase/constants/const_files/psm_reader.yaml b/alphabase/constants/const_files/psm_reader.yaml index 46b6bedb..f68892ba 100644 --- a/alphabase/constants/const_files/psm_reader.yaml +++ b/alphabase/constants/const_files/psm_reader.yaml @@ -71,19 +71,27 @@ maxquant: - 'S(Phospho (S))' - 'S(Phospho (ST))' - 'S(Phospho (STY))' + - 'S(Phospho (STYDH))' - 'S(ph)' - 'pS' 'Phospho@T': - 'T(Phospho (T))' - 'T(Phospho (ST))' - 'T(Phospho (STY))' + - 'T(Phospho (STYDH))' - 'T(ph)' - 'pT' 'Phospho@Y': - 'Y(Phospho (Y))' - 'Y(Phospho (STY))' - - 'Y(ph)' + - 'Y(Phospho (STYDH))' - 'pY' + 'Phospho@D': + - 'D(Phospho (STYDH))' + - 'pD' + 'Phospho@H': + - 'H(Phospho (STYDH))' + - 'pH' 'Deamidated@N': ['N(Deamidation (NQ))','N(de)'] 'Deamidated@Q': ['Q(Deamidation (NQ))','Q(de)'] 'GlyGly@K': ['K(GlyGly (K))', 'K(gl)'] From fde8681c41349f5cf2df31f0acb57d5237785a45 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Tue, 5 Nov 2024 20:41:23 +0100 Subject: [PATCH 32/37] update mq --- alphabase/constants/const_files/psm_reader.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alphabase/constants/const_files/psm_reader.yaml b/alphabase/constants/const_files/psm_reader.yaml index f68892ba..8ea3b69d 100644 --- a/alphabase/constants/const_files/psm_reader.yaml +++ b/alphabase/constants/const_files/psm_reader.yaml @@ -92,6 +92,10 @@ maxquant: 'Phospho@H': - 'H(Phospho (STYDH))' - 'pH' + 'Crotonyl@K': + - 'K(cr)' + 'Lactylation@K': + - 'K(la)' 'Deamidated@N': ['N(Deamidation (NQ))','N(de)'] 'Deamidated@Q': ['Q(Deamidation (NQ))','Q(de)'] 'GlyGly@K': ['K(GlyGly (K))', 'K(gl)'] From 9451433b445dc83e8ecdc6b09e96a00296eeefb7 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Tue, 5 Nov 2024 21:01:56 +0100 Subject: [PATCH 33/37] add succinyl --- alphabase/constants/const_files/psm_reader.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/alphabase/constants/const_files/psm_reader.yaml b/alphabase/constants/const_files/psm_reader.yaml index 8ea3b69d..03ce74ee 100644 --- a/alphabase/constants/const_files/psm_reader.yaml +++ b/alphabase/constants/const_files/psm_reader.yaml @@ -96,6 +96,8 @@ maxquant: - 'K(cr)' 'Lactylation@K': - 'K(la)' + 'Succinyl@K': + - 'K(su)' 'Deamidated@N': ['N(Deamidation (NQ))','N(de)'] 'Deamidated@Q': ['Q(Deamidation (NQ))','Q(de)'] 'GlyGly@K': ['K(GlyGly (K))', 'K(gl)'] From b7714b95b7d53c6e55e5a9b2d50bd6db9ff05ba7 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Thu, 7 Nov 2024 09:16:57 +0100 Subject: [PATCH 34/37] fix tests --- nbs_tests/psm_reader/dia_psm_reader.ipynb | 5 ++++- nbs_tests/psm_reader/maxquant_reader.ipynb | 5 ++++- nbs_tests/psm_reader/psm_reader.ipynb | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/nbs_tests/psm_reader/dia_psm_reader.ipynb b/nbs_tests/psm_reader/dia_psm_reader.ipynb index cbb6f0c6..ad48e801 100644 --- a/nbs_tests/psm_reader/dia_psm_reader.ipynb +++ b/nbs_tests/psm_reader/dia_psm_reader.ipynb @@ -986,11 +986,14 @@ " 'S(Phospho (S))',\n", " 'S(Phospho (ST))',\n", " 'S(Phospho (STY))',\n", + " 'S(Phospho (STYDH))',\n", " 'S[ph]',\n", " 'S[UniMod:21]',\n", " 'S[Phospho (S)]',\n", " 'S[Phospho (ST)]',\n", - " 'S[Phospho (STY)]'])\n", + " 'S[Phospho (STY)]',\n", + " 'S[Phospho (STYDH)]'],\n", + " )\n", "_df" ] }, diff --git a/nbs_tests/psm_reader/maxquant_reader.ipynb b/nbs_tests/psm_reader/maxquant_reader.ipynb index 90c9c5f0..16b5f745 100644 --- a/nbs_tests/psm_reader/maxquant_reader.ipynb +++ b/nbs_tests/psm_reader/maxquant_reader.ipynb @@ -174,11 +174,14 @@ " 'S(Phospho (S))',\n", " 'S(Phospho (ST))',\n", " 'S(Phospho (STY))',\n", + " 'S(Phospho (STYDH))',\n", " 'S[ph]',\n", " 'S[UniMod:21]',\n", " 'S[Phospho (S)]',\n", " 'S[Phospho (ST)]',\n", - " 'S[Phospho (STY)]'])" + " 'S[Phospho (STY)]',\n", + " 'S[Phospho (STYDH)]'\n", + "])" ] }, { diff --git a/nbs_tests/psm_reader/psm_reader.ipynb b/nbs_tests/psm_reader/psm_reader.ipynb index 3af8cfb1..bb3481c1 100644 --- a/nbs_tests/psm_reader/psm_reader.ipynb +++ b/nbs_tests/psm_reader/psm_reader.ipynb @@ -211,11 +211,14 @@ " 'S(Phospho (S))',\n", " 'S(Phospho (ST))',\n", " 'S(Phospho (STY))',\n", + " 'S(Phospho (STYDH))',\n", " 'S[ph]',\n", " 'S[UniMod:21]',\n", " 'S[Phospho (S)]',\n", " 'S[Phospho (ST)]',\n", - " 'S[Phospho (STY)]'])\n", + " 'S[Phospho (STY)]',\n", + " 'S[Phospho (STYDH)]'\n", + "])\n", "try:\n", " psm_reader_provider.get_reader_by_yaml(psm_reader_yaml['unknown'])\n", "except Exception as e:\n", From ae84223f74c31a45ef16e6eea8abbe1494e01793 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Thu, 7 Nov 2024 09:23:07 +0100 Subject: [PATCH 35/37] another fix --- nbs_tests/psm_reader/dia_psm_reader.ipynb | 121 ++++++++++++++-------- 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/nbs_tests/psm_reader/dia_psm_reader.ipynb b/nbs_tests/psm_reader/dia_psm_reader.ipynb index ad48e801..2d15c477 100644 --- a/nbs_tests/psm_reader/dia_psm_reader.ipynb +++ b/nbs_tests/psm_reader/dia_psm_reader.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -49,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -76,7 +76,7 @@ " 'genes': ['Genes', 'Gene', 'GeneName', 'GeneNames']}" ] }, - "execution_count": null, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -101,7 +101,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -115,7 +115,7 @@ " 'FullUniModPeptideName']" ] }, - "execution_count": null, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -133,7 +133,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -155,7 +155,7 @@ " 'fdr': 'Q.Value'}" ] }, - "execution_count": null, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -180,7 +180,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -189,7 +189,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -291,7 +291,7 @@ "3 HLLNQAVGEEEVPK 14 1.000000 521.610617 " ] }, - "execution_count": null, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -314,7 +314,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -427,7 +427,7 @@ "2 MAP7 Acetyl@Protein_N-term;Phospho@S 0;4 11 1.0 371.282739 " ] }, - "execution_count": null, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -457,7 +457,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -484,7 +484,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -521,7 +521,7 @@ " scan_num\n", " score\n", " fdr\n", - " spec_idx\n", + " diann_spec_idx\n", " mods\n", " mod_sites\n", " nAA\n", @@ -890,21 +890,21 @@ "12 AAAAAAAPSGGGGGGEEERLEEK 3 7.28562 7.23794 7.33338 1.01500 \n", "13 AAAAAAAPSGGGGGGEEERLEEK 3 7.26825 7.22055 7.31601 1.01208 \n", "\n", - " proteins uniprot_ids genes scan_num score fdr spec_idx \\\n", - "0 P28482 MAPK1 11191 0.843331 0.006937 11190 \n", - "1 P28482 MAPK1 11239 0.951820 0.001225 11238 \n", - "2 Q9UH36 SRRD 30053 0.999997 0.000040 30052 \n", - "3 Q9UH36 SRRD 30029 0.995505 0.000184 30028 \n", - "4 Q9UH36 SRRD 30005 0.997286 0.000185 30004 \n", - "5 Q9UH36 SRRD 29981 0.996593 0.000153 29980 \n", - "6 Q96P70 IPO9 22187 0.999999 0.000040 22186 \n", - "7 Q96P70 IPO9 22091 0.999996 0.000050 22090 \n", - "8 Q96P70 IPO9 22067 0.999999 0.000061 22066 \n", - "9 Q96P70 IPO9 21947 0.999997 0.000044 21946 \n", - "10 P51608-2 MECP2 11077 0.998266 0.000142 11076 \n", - "11 P51608-2 MECP2 11029 0.994097 0.000201 11028 \n", - "12 P51608-2 MECP2 10981 0.999939 0.000070 10980 \n", - "13 P51608-2 MECP2 10957 0.971834 0.000604 10956 \n", + " proteins uniprot_ids genes scan_num score fdr diann_spec_idx \\\n", + "0 P28482 MAPK1 11191 0.843331 0.006937 11190 \n", + "1 P28482 MAPK1 11239 0.951820 0.001225 11238 \n", + "2 Q9UH36 SRRD 30053 0.999997 0.000040 30052 \n", + "3 Q9UH36 SRRD 30029 0.995505 0.000184 30028 \n", + "4 Q9UH36 SRRD 30005 0.997286 0.000185 30004 \n", + "5 Q9UH36 SRRD 29981 0.996593 0.000153 29980 \n", + "6 Q96P70 IPO9 22187 0.999999 0.000040 22186 \n", + "7 Q96P70 IPO9 22091 0.999996 0.000050 22090 \n", + "8 Q96P70 IPO9 22067 0.999999 0.000061 22066 \n", + "9 Q96P70 IPO9 21947 0.999997 0.000044 21946 \n", + "10 P51608-2 MECP2 11077 0.998266 0.000142 11076 \n", + "11 P51608-2 MECP2 11029 0.994097 0.000201 11028 \n", + "12 P51608-2 MECP2 10981 0.999939 0.000070 10980 \n", + "13 P51608-2 MECP2 10957 0.971834 0.000604 10956 \n", "\n", " mods mod_sites nAA rt_norm precursor_mz \\\n", "0 Acetyl@Any_N-term;Oxidation@M 0;12 14 0.372721 650.819344 \n", @@ -939,7 +939,7 @@ "13 612.071553 " ] }, - "execution_count": null, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -999,26 +999,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['S[UniMod:21]',\n", - " 'S[Phospho (S)]',\n", + "['S[ph]',\n", + " 'S[Phospho (STY)]',\n", " 'S(Phospho (S))',\n", + " 'S[Phospho (STYDH)]',\n", + " 'S(ph)',\n", + " 'S(UniMod:21)',\n", + " 'S[Phospho (S)]',\n", + " 'S[Phospho (ST)]',\n", " 'S(Phospho (ST))',\n", - " 'S[Phospho (STY)]',\n", + " 'S[UniMod:21]',\n", " 'pS',\n", - " 'S(UniMod:21)',\n", - " 'S(ph)',\n", - " 'S[ph]',\n", " 'S(Phospho (STY))',\n", - " 'S[Phospho (ST)]']" + " 'S(Phospho (STYDH))']" ] }, - "execution_count": null, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -1029,9 +1031,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "AssertionError", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[13], line 25\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m np\u001b[38;5;241m.\u001b[39msum(\u001b[38;5;241m~\u001b[39mdiann_reader\u001b[38;5;241m.\u001b[39mpsm_df\u001b[38;5;241m.\u001b[39mmods\u001b[38;5;241m.\u001b[39mstr\u001b[38;5;241m.\u001b[39mcontains(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mAcetyl@Any_N-term\u001b[39m\u001b[38;5;124m'\u001b[39m)) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m4\u001b[39m\n\u001b[1;32m 24\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m np\u001b[38;5;241m.\u001b[39msum(diann_reader\u001b[38;5;241m.\u001b[39mpsm_df\u001b[38;5;241m.\u001b[39mmods\u001b[38;5;241m.\u001b[39mstr\u001b[38;5;241m.\u001b[39mcontains(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mOxidation@M\u001b[39m\u001b[38;5;124m'\u001b[39m)) \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m2\u001b[39m\n\u001b[0;32m---> 25\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;28mset\u001b[39m(diann_reader\u001b[38;5;241m.\u001b[39mmodification_mapping[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mPhospho@S\u001b[39m\u001b[38;5;124m'\u001b[39m])\u001b[38;5;241m==\u001b[39m\u001b[38;5;28mset\u001b[39m([\n\u001b[1;32m 26\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mpS\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 27\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS(ph)\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 28\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS(UniMod:21)\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 29\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS(Phospho (S))\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 30\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS(Phospho (ST))\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 31\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS(Phospho (STY))\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 32\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS[ph]\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 33\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS[UniMod:21]\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 34\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS[Phospho (S)]\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 35\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS[Phospho (ST)]\u001b[39m\u001b[38;5;124m'\u001b[39m,\n\u001b[1;32m 36\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mS[Phospho (STY)]\u001b[39m\u001b[38;5;124m'\u001b[39m])\n", + "\u001b[0;31mAssertionError\u001b[0m: " + ] + } + ], "source": [ "tsv = StringIO('''File.Name\tRun\tProtein.Group\tProtein.Ids\tProtein.Names\tGenes\tPG.Quantity\tPG.Normalised\tPG.MaxLFQ\tGenes.Quantity\tGenes.Normalised\tGenes.MaxLFQ\tGenes.MaxLFQ.Unique\tModified.Sequence\tStripped.Sequence\tPrecursor.Id\tPrecursor.Charge\tQ.Value\tGlobal.Q.Value\tProtein.Q.Value\tPG.Q.Value\tGlobal.PG.Q.Value\tGG.Q.Value\tTranslated.Q.Value\tProteotypic\tPrecursor.Quantity\tPrecursor.Normalised\tPrecursor.Translated\tQuantity.Quality\tRT\tRT.Start\tRT.Stop\tiRT\tPredicted.RT\tPredicted.iRT\tLib.Q.Value\tMs1.Profile.Corr\tMs1.Area\tEvidence\tSpectrum.Similarity\tMass.Evidence\tCScore\tDecoy.Evidence\tDecoy.CScore\tFragment.Quant.Raw\tFragment.Quant.Corrected\tFragment.Correlations\tMS2.Scan\tIM\tiIM\tPredicted.IM\tPredicted.iIM\n", "F:\\XXX\\20201218_tims03_Evo03_PS_SA_HeLa_200ng_high_speed_21min_8cm_S2-A2_1_22636.d\t20201218_tims03_Evo03_PS_SA_HeLa_200ng_high_speed_21min_8cm_S2-A2_1_22636\tQ9UH36\tQ9UH36\t\tSRRD\t3296.49\t3428.89\t3428.89\t3296.49\t3428.89\t3428.89\t3428.89\t(UniMod:1)AAAAAAALESWQAAAPR\tAAAAAAALESWQAAAPR\t(UniMod:1)AAAAAAALESWQAAAPR2\t2\t3.99074e-05\t1.96448e-05\t0.000159821\t0.000159821\t0.000146135\t0.000161212\t0\t1\t3296.49\t3428.89\t3296.49\t0.852479\t19.9208\t19.8731\t19.9685\t123.9\t19.8266\t128.292\t0\t0.960106\t5308.05\t1.96902\t0.683134\t0.362287\t0.999997\t1.23691\t3.43242e-05\t1212.01;2178.03;1390.01;1020.01;714.008;778.008;\t1212.01;1351.73;887.591;432.92;216.728;732.751;\t0.956668;0.757581;0.670497;0.592489;0.47072;0.855203;\t30053\t1.19708\t1.19328\t1.19453\t1.19469\n", @@ -1064,16 +1078,19 @@ " 'S(Phospho (S))',\n", " 'S(Phospho (ST))',\n", " 'S(Phospho (STY))',\n", + " 'S(Phospho (STYDH))',\n", " 'S[ph]',\n", " 'S[UniMod:21]',\n", " 'S[Phospho (S)]',\n", " 'S[Phospho (ST)]',\n", - " 'S[Phospho (STY)]'])" + " 'S[Phospho (STY)]',\n", + " 'S[Phospho (STYDH)]'\n", + "])" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -1082,7 +1099,7 @@ "'Acetyl@Any_N-term'" ] }, - "execution_count": null, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -1111,6 +1128,18 @@ "display_name": "python3", "language": "python", "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" } }, "nbformat": 4, From 2e0295bf53ad81a489da921c097c5ccd2d7053b5 Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Tue, 12 Nov 2024 15:03:08 +0100 Subject: [PATCH 36/37] filter MBR rows --- alphabase/psm_reader/maxquant_reader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/alphabase/psm_reader/maxquant_reader.py b/alphabase/psm_reader/maxquant_reader.py index c88aefb1..984ef1cc 100644 --- a/alphabase/psm_reader/maxquant_reader.py +++ b/alphabase/psm_reader/maxquant_reader.py @@ -12,6 +12,7 @@ psm_reader_yaml, ) +# make sure all warnings are shown warnings.filterwarnings("always") mod_to_unimod_dict = {} @@ -254,13 +255,13 @@ def _load_file(self, filename): if "scan_num" in mapped_columns: scan_num_col = mapped_columns["scan_num"] no_ms2_mask = df[scan_num_col] == "" - if np.sum(no_ms2_mask) > 0: + if (num_no_ms2_mask := np.sum(no_ms2_mask)) > 0: warnings.warn( - f"Maxquant psm file contains {np.sum(no_ms2_mask)} MBR PSMs without MS2 scan. This is not yet supported and rows containing MBR PSMs will be removed." + f"Maxquant psm file contains {num_no_ms2_mask} MBR PSMs without MS2 scan. This is not yet supported and rows containing MBR PSMs will be removed." ) df = df[~no_ms2_mask] df.reset_index(drop=True, inplace=True) - df[scan_num_col] = df[scan_num_col].astype(int) + df[scan_num_col] = df[scan_num_col].astype(int) # if 'K0' in df.columns: # df['Mobility'] = df['K0'] # Bug in MaxQuant? It should be 1/K0 From 469d6a6f5935d013444457f29dd425a35a429aac Mon Sep 17 00:00:00 2001 From: GeorgWa Date: Tue, 12 Nov 2024 15:21:16 +0100 Subject: [PATCH 37/37] =?UTF-8?q?Bump=20version:=201.4.0=20=E2=86=92=201.4?= =?UTF-8?q?.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 3 +-- alphabase/__init__.py | 2 +- docs/conf.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b55048b4..4864cd98 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.4.0 +current_version = 1.4.1 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? @@ -14,6 +14,5 @@ serialize = [bumpversion:file:./alphabase/__init__.py] [bumpversion:file:./docs/conf.py] - search = {current_version} replace = {new_version} diff --git a/alphabase/__init__.py b/alphabase/__init__.py index 006941da..86008ce9 100644 --- a/alphabase/__init__.py +++ b/alphabase/__init__.py @@ -2,7 +2,7 @@ __project__ = "alphabase" -__version__ = "1.4.0" +__version__ = "1.4.1" __license__ = "Apache" __description__ = "An infrastructure Python package of the AlphaX ecosystem" __author__ = "Mann Labs" diff --git a/docs/conf.py b/docs/conf.py index 8cd3bdfa..687c35f7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,7 @@ copyright = "2022, Mann Labs, MPIB" author = "Mann Labs, MPIB" -release = "1.4.0" +release = "1.4.1" # -- General configuration ---------------------------------------------------