Package: ionmob
ionmobA Framework for Predicting Collision Cross Section (CCS) Values of Peptide-Ions with Traditional and Deep Machine Learning Methodsionmobis a Python package designed to predictCCSvalues of peptides. Beyond offering several pre-trained regression models for this task, it provides a comprehensive pipeline that seamlessly integrates data preprocessing, model training, and CCS value inference. Models are built using up-to-date versions of eitherTensorFloworscikit-learn.We encourage you to use, modify, or extend ionmob to suit your needs. It's freely available and open-source under theGNU General Public License v3.0. Feedback is always appreciated! Please let us know if you encounter any missing features, bugs, or if you'd like to contribute in any way.Our ionmob package is now featured in a publication in Bioinformatics. Access the fully open-access paperhere. For those interested in replicating our experiments, training models, or reproducing result plots from the paper, additional data and scripts not included in the ionmob package can be found onZenodo.TLDRWhat is a peptide CCS value?Why do we measure CCS values of ions?Why would I want to predict CCS values of peptides in silico?Can I use ionmob if I am no machine learning expert?What can I do with ionmob if I am an experienced coder?InstallationA simple example of CCS prediction and performance evaluation with pre-trained modelsGetting insight into driving factors of CCSAlignments of in-house and external dataImplementing a custom deep CCS predictorCite ionmobTLDRTo simply get started, load our best performing predictor and infer CCS values on a dataset
provided by this repository:Inference on one of our provided datasetsimporttensorflowastfimportpandasaspdimportnumpyasnpfrommatplotlibimportpyplotaspltfromionmob.utilities.tokenizationimporttokenizer_from_jsonfromionmob.preprocess.dataimportto_tf_dataset_inference# you will need to load the correct tokenizer to translate peptide sequences to tokenstokenizer=tokenizer_from_json('pretrained_models/tokenizers/tokenizer.json')# load the example_datadata=pd.read_parquet('example_data/Tenzer_unimod.parquet')# load the modeldeepGRU=tf.keras.models.load_model('pretrained_models/GRUPredictor/')# create a tensorflow dataset from example_datatf_ds=to_tf_dataset_inference(mz=data['mz'],charge=data['charge'],sequences=[list(s)forsindata['sequence-tokenized']],tokenizer=tokenizer)# do inferenceccs_predicted,deep_residues=deepGRU.predict(tf_ds)data['ccs_predicted']=ccs_predictedInference on your own datasetsInference of CCS values on custom data is already more involved as it requires you to tokenize your sequences before
they can be used by anionmobpredictor. Sequences presented toionmobcan be composed of all 20 amino acids and a
growing number of modifications such as phosphorylation. Have a look at all known symbols:fromionmob.utilities.chemistryimportVARIANT_DICTprint(VARIANT_DICT){'L':['L'],'E':['E'],'S':['S','S[UNIMOD:21]'],'A':['A'],'V':['V'],'D':['D'],'G':['G'],'<END>':['<END>'],'P':['P'],'<START>':['<START>','<START>[UNIMOD:1]'],'T':['T','T[UNIMOD:21]'],'I':['I'],'Q':['Q'],'K':['K','K[UNIMOD:1]'],'N':['N'],'R':['R'],'F':['F'],'H':['H'],'Y':['Y','Y[UNIMOD:21]'],'M':['M','M[UNIMOD:35]'],'W':['W'],'C':['C','C[UNIMOD:312]','C[UNIMOD:4]'],'C[UNIMOD:4]':['C','C[UNIMOD:312]','C[UNIMOD:4]']}Unmodified amino acids are just written as a singe character as capital letters. Modified sequences use theUniMod convention: A modification is noted like[UNIMOD:X], whereXis
the UniMod code for the respective modification. Read more about this conventionhere.
Also, N termini are signified by a<START>token und C termini by an<END>token. This additionally allows for
termini modification tokens as well as indication of read direction of peptide sequences.Translating sequences to tokens from a given output file of PEAKS, DiaNN or MaxQuant is supported out-of-the-box:importpandasaspdfromionmob.utilities.utilityimportpreprocess_max_quant_sequencemq_data=pd.read_table('path/to/mq/evidence.txt',low_memory=False)mq_data['sequence-tokenized']=mq_data.apply(lambdar:preprocess_max_quant_sequence(r['Modified sequence']),axis=1)Depending on the software used for processing the raw-data, precursor mono-isotopic mz values might not be available
in the output files. It is therefore possible to calculate them from a given tokenized sequence and charge state:fromionmob.utilities.chemistryimportcalculate_mzmq_data['mz']=mq_data.apply(lambdar:calculate_mz(r['sequence-tokenized'],r['Charge']),axis=1)Now, a dataset can be created for prediction:fromionmob.preprocess.dataimportto_tf_dataset_inferencefromionmob.utilities.tokenizationimporttokenizer_from_jsontokenizer=tokenizer_from_json('pretrained_models/tokenizers/tokenizer.json')tf_ds=to_tf_dataset_inference(mq_data['mz'],mq_data['Charge'],mq_data['sequence-tokenized'],tokenizer)Calculate experiment specific shifts of CCS valuesA linear shift in calculated CCS values can often be observed between two experiments coming from different sources. You can correct
for this by calculating a shift factor that needs to be added to observed values. Optimally, use a set of high
confidence identifications that contain at least tokenized sequences, charges and CCS values. They can then be used
together with one of the training datasets as reference:importpandasaspdfromionmob.utilities.utilityimportget_ccs_shifttarget=pd.read_table('path/to/my/table.csv')# preprocess, select high confidence identifications, tokenize etc.# read a reference dataset predictor was trained onreference=pd.read_parquet('example_data/reference.parquet')# a shift factor is calculated from charge state 2, which has the lowest varianceshift_factor=get_ccs_shift(target,reference)# optionally, apply shift to target datasettarget['ccs_shifted']=target.apply(lambdar:r['ccs']+shift_factor,axis=1)What is a peptide CCS value?The rotationally-averaged collision cross section -CCS- is a scalar value that describes a physical property of an ion.
It can be directly linked to its ion mobility, meaning its interactive behaviour with respect to a charge-neutral gas.
The ion mobility is used as an additional separating dimension in high throughput mass spectrometry.
It supplements the measurements of retention times and mass-to-charge ratios and ultimately leads to improved peptide identification.Why do we measure CCS values of ions?The CCS value of an ion is a coarse descriptor of its 3D structure.
Since peptides are chains (strings) of amino acids, there exist permutations in nature that have exactly the same mass and chemical properties.
Differences in AA sequence will result in differences in the 3D structure though.
Distinguishing between such peptides with conventional methods like e.g. LC-MS-MS is therefore challenging.
Furthermore, post translational modifications (PTMs) might have only a small impact on an ion's mass but alter the functionality of a protein.
Since both a permutation of sequence as well as PTMs have significant impact on 3D structure, one can use ion mobility separation to distinguish between them.
CCS value calculation then gives us a measure how extensively their rotationally-averaged collision cross section differed.Why would I want to predict CCS values of peptides in silico?First, a predictor might give you insight into factors that drive ion mobility.
This information could then be used to optimize your laboratory workflows or uncover yet unknown relationships.
Second, the high reproducibility of measured CCS values in the lab make it an ideal candidate to increase confidence in peptide identifications from database searches.
We think, the recent triumph of ion mobility enhanced mass spectrometry paves the way for expressive predictors by providing previously unavailable amounts of training data!Can I use ionmob if I am no machine learning expert?Definitely yes!
We implemented and pretrained models of different complexity that allow for in silico prediction of CCS values for peptide ions of different charge states out-of-the-box.
They are easily integratable into your existing proteomics workflows.
All you need is a little bit of python scripting experience.
A short introduction can be found down below.
Additionally, you can have a look at our collection of example notebooks.What can I do with ionmob if I am an experienced coder?We made sure that our framework provides a modular set of tools for more experienced coders that want to implement their own models, training strategies or data preprocessing pipelines.
Have a look at our example notebooks for advanced workflow implementation.
Feel also free to contribute any optimizations, models or ideas that you come up with.
This will ultimately help to push prediction accuracy to a point where it provides a huge benefit for rescoring of peptide identifications!InstallationWe recommend to installionmobinto a separatepython virtual environment. Once activated, you can install the ionmob package into it as follows:git clone https://github.com/theGreatHerrLebert/ionmob.git
cd ionmob
pip install -e .A simple example of CCS prediction and performance evaluation with pre-trained modelsLet us assume that you want to have a look at prediction performance for two differentionmobpredictors on data of peptide identifications that came from some source.
Forionmobmodels, you should at least have the following information per peptide:mz, charge, sequence, ccs.
CCS values are optional in the general case but are required if you want to compare CCS predictions to CCS measurements.
We will demonstrate how to do this with one of our provided example datasets:importpandasaspd# read example_data and a predictordata=pd.read_parquet('example_data/Tenzer.parquet')data.head()This is what the data looks like:mzchargesequence-tokenizedccsrtname2478.782['<START>' 'A' 'A' 'A' 'A' 'A' 'A' 'A' 'L' 'Q' 'A' 'K' '<END>']351.07314.1374Tenzer-tryptic3514.3172['<\START>' 'A' 'A' 'A' 'A' 'A' 'A' 'T' 'V' 'L' 'L' 'R' '<\END>']360.94938.812Tenzer-tryptic4472.2512['<\START>' 'A' 'A' 'A' 'A' 'A' 'D' 'L' 'A' 'N' 'R' '<\END>']320.56214.467Tenzer-trypticLet's compare accuracy for two predictors.
One that only does a zero-information square-root fit on ion mz values and a deep model that also uses information on peptide sequences.
The latter also needs a so-calledtokenizer: a tool that translates sequence symbols into a numerical representation.
It is specific for a pretrained model and therefore needs to be loaded as well:importtensorflowastffrommatplotlibimportpyplotaspltfromionmob.preprocess.dataimportsqrt_model_dataset# read the pretrained predictorssqrtModel=tf.keras.models.load_model('pretrained_models/SqrtModel')gruModel=tf.keras.models.load_model('pretrained_models/GRUPredictor/')# read tokenizer for deep modeltokenizer=tokenizer_from_json('pretrained_models/tokenizer.json')# create dataset for sqrt prediction and predicttensorflow_ds_sqrt=sqrt_model_dataset(data.mz,data.charge,data.ccs).batch(1024)data['ccs_predicted_s']=sqrtModel.predict(tensorflow_ds_sqrt)# create dataset for deep prediction and predicttensorflow_ds_deep=get_tf_dataset(data.mz,data.charge,data.sequence,data.ccs,tokenizer,drop_sequence_ends=False,add_charge=True).batch(1024)ccs_predicted_gru,_=gruModel.predict(tensorflow_ds_deep)data['ccs_predicted_g']=ccs_predicted_gruLet's compare prediction accuracies:importnumpyasnp# define error functionsdefmean_abs_error(ccs,ccs_pred):returnnp.round(np.mean([np.abs(x[0]-x[1])forxinnp.c_[ccs,ccs_pred]]),2)defmean_perc_error(ccs,ccs_pred):returnnp.round(np.mean([np.abs((x[0]-x[1])/x[0])*100forxinnp.c_[ccs,ccs_pred]]),2)# show resultsprint(f"sqrt mean absolute percent error:{mean_perc_error(data.ccs,data.ccs_predicted_s)}")print(f"gru mean absolute percent error :{mean_perc_error(data.ccs,data.ccs_predicted_g)}")print("")print(f"sqrt mean absolute error        :{mean_abs_error(data.ccs,data.ccs_predicted_s)}")print(f"gru mean absolute error         :{mean_abs_error(data.ccs,data.ccs_predicted_g)}")This then gives us CCS accuracies of:sqrtmeanabsolutepercenterror:2.58grumeanabsolutepercenterror:1.84sqrtmeanabsoluteerror:12.69grumeanabsoluteerror:9.04Finally, let's visualize the predictions compared to the CCS measurements:frommatplotlibimportpyplotasplt# visualize the charge states in different colorscolor_dict={2:'red',3:'orange',4:'lightgreen'}# create the plotfig,(ax1,ax2)=plt.subplots(nrows=1,ncols=2,figsize=(12,4),dpi=200,sharey=True,sharex=True)ax1.set_title('sqrt fit prediction')ax1.set_ylabel('CCS')ax1.set_xlabel('MZ')ax2.set_xlabel('MZ')ax2.set_title('deep prediction')ax1.scatter(data.mz,data.ccs,s=10,alpha=.5,label='ground truth')ax1.scatter(data.mz,data.ccs_predicted_s,s=10,alpha=.5,c=[color_dict[x]forxindata.charge],label='prediction')ax2.scatter(data.mz,data.ccs,s=10,alpha=.5,label='ground truth')ax2.scatter(data.mz,data.ccs_predicted_g,s=10,alpha=.2,c=[color_dict[x]forxindata.charge],label='prediction')ax1.legend()ax2.legend()fig.show()This code will result in the following plot:You can also try this yourself by cloning this repository and runningthis notebook.Getting insight into driving factors of CCSRecent papers that worked on CCS prediction such as Chang et al.[^fn2] and Meier et al.[^fn1] identified factors that drive differences in ion mobility.
By using an in silico digest of the human proteome, we can estimate the impact of two of them, namely theGRAVY scoreand helicality of peptides. Our modelling approach will look like this: first an initial CCS value is calculated soley on an ions mass and charge. This is done using the simple formula below (caution, equations might be barely visible if your browser uses darkmode):Where a slopeand an interceptare fit separately for each modeled charge state.
The reason why ion-mobility does add an additional dimension of separation is the fact that an ion's CCS value does not always lie on that line.
If it did, CCS would be perfectly correlated with mz and therefore add no new information.
We can improve our inital CCS prediction modell by also predicting the residues with respect to the square root fit, meaning the vertical difference to our initial value.
These residues could be provided by any predictor but let's use our best performing model: the GRU-based predictor.
It uses deepGRU-unitsthat can take into account sequence specific higher-order information derived from training data.
We will expand our mathematical formulation of the problem as follows:Here, a regressor(GRU-units) with parameter setwas fit to further lower the mean absolut error (MAE) of predicted CCS values compared to the experimentally observed ones.
For convenience, this predictor does not only return the final predicted ccs value but also the residue with respect to the initial fit, giving us an easy way to link specific features of a given sequence to its impact on ion mobility.
An implementation withionmobto derive this could look like this:importpandasaspdimportnumpyasnpimporttensorflowastffromionmob.utilities.utilityimportget_gravy_score,get_helix_scorefromionmob.utilities.tokenizationimporttokenizer_from_jsonfromionmob.preprocess.dataimportget_tf_dataset,sqrt_model_dataset# read in silico digested human proteome to gain insight into predictors behaviourdata=pd.read_hdf('Synthetic.h5').sample(frac=0.25)# read predictors and tokenizergruModel=tf.keras.models.load_model('pretrained_models/GRUPredictor/')sqrtModel=tf.keras.models.load_model('pretrained_models/SqrtModel/')tokenizer=tokenizer_from_json('pretrained_models/tokenizer.json')# generate tensorflow datasets for predictiontensorflow_ds_sqrt=sqrt_model_dataset(data.mz,data.charge,None).batch(1024)tensorflow_ds_deep=get_tf_dataset(data.mz,data.charge,data.sequence,None,tokenizer,drop_sequence_ends=False,add_charge=True).batch(1024)# predict with sqrt-fitccs_predicted_sqrt=sqrtModel.predict(tensorflow_ds_sqrt)# predict with deep fitccs_predicted_gru,deep_part=gruModel.predict(tensorflow_ds_deep)# append predictions to dataframedata['ccs_predicted_gru']=ccs_predicted_grudata['ccs_predicted_sqrt']=ccs_predicted_sqrtdata['ccs_predicted_deep']=deep_part# create normalized value of deep increase or decrease prediction of CCSdata['deep_normalized']=data.ccs_predicted_deep/np.sqrt(data.mz.values)# calculate gravy and helix scores for each sequencegravy=[get_gravy_score(s,normalize=False)forsindata.sequence]helix=[get_helix_score(s)forsindata.sequence]# append calculated values to dataframedata['gravy']=gravydata['helix']=helix# select a single charge state to deconvolce differences between chargescharge_2=data[data['charge']==2]We are now ready to have a look at how both GRAVY score and helix score of a given peptide are correlated with an increase or decrease of the deep predicted CCS with respect to the initial guess. Since the impact is not equal along the mz axis, the deep residue value was normalized by dividing it by the square-root mz value of its ion. We will calculate the pearson correlation to have some objective measure how strong they are correlated:fromscipy.statsimportpearsonr# extract values to correlatex=charge_2.deep_normalized.valuesy_gravy=charge_2.gravy.valuesy_helix=charge_2.helix.valuesprint('Gravy Pearson:',np.round(pearsonr(x,y_gravy),2))print('Helix Pearson:',np.round(pearsonr(x,y_helix),2))This gives us pearson correlation and p values for both gravy and helicality analysis:GravyPearson:[0.490.]HelixPearson:[0.520.]Once again, let's visualize this to get a better feel for what the numbers are telling us:fromsklearn.linear_modelimportLinearRegressionfrommpl_toolkits.axes_grid1importmake_axes_locatabledefline(x,a,b):returnx*a+breg_gravy=LinearRegression().fit(np.expand_dims(x,-1),np.expand_dims(y_gravy,-1))reg_helix=LinearRegression().fit(np.expand_dims(x,-1),np.expand_dims(y_helix,-1))y_line_gravy=[line(x,reg_gravy.coef_,reg_gravy.intercept_)forxincharge_2.deep_normalized.values]y_line_helix=[line(x,reg_helix.coef_,reg_helix.intercept_)forxincharge_2.deep_normalized.values]# create the plotfig,((ax1,ax2),(ax3,ax4))=plt.subplots(nrows=2,ncols=2,figsize=(16,12),dpi=200)ax1.set_title('linear correlation helicality, mobility')ax1.set_ylabel('helix score')ax1.set_xlabel('relative mobility trend')ax2.set_xlabel('MZ')ax2.set_title('Deep vs Sqrt prediction')im1=ax1.scatter(charge_2.deep_normalized,charge_2.helix,c=charge_2.helix,alpha=.3,s=10,label='example_data points')im1=ax1.scatter(charge_2.deep_normalized,y_line_helix,s=10,c='red',label='linear trend')im2=ax2.scatter(charge_2.mz,charge_2.ccs_predicted_gru,s=10,c=charge_2.helix-np.mean(data.gravy),alpha=.3,label='example_data points')im2=ax2.scatter(charge_2.mz,charge_2.ccs_predicted_sqrt,s=2,c='red',alpha=.3,label='sqrt prediction')ax1.legend()ax2.legend()divider=make_axes_locatable(ax2)cax=divider.append_axes('right',size='2%',pad=0.05)cbar=fig.colorbar(im1,cax=cax,orientation='vertical',ticks=[0,0.5,1])cbar.ax.set_yticklabels(['0','0.5','1'])ax3.set_title('linear correlation gravy, mobility')ax3.set_ylabel('gravy score')ax3.set_xlabel('relative mobility trend')ax4.set_xlabel('MZ')ax4.set_title('Deep vs Sqrt prediction')im3=ax3.scatter(charge_2.deep_normalized,charge_2.gravy,c=charge_2.gravy,alpha=.3,s=10,label='example_data points')im3=ax3.scatter(charge_2.deep_normalized,y_line_gravy,s=10,c='red',label='linear trend')im4=ax4.scatter(charge_2.mz,charge_2.ccs_predicted_gru,s=10,c=charge_2.gravy,alpha=.3,label='example_data points')im4=ax4.scatter(charge_2.mz,charge_2.ccs_predicted_sqrt,s=2,c='red',alpha=.3,label='sqrt prediction')ax3.legend()ax4.legend()divider=make_axes_locatable(ax4)cax=divider.append_axes('right',size='2%',pad=0.05)cbar=fig.colorbar(im3,cax=cax,orientation='vertical',ticks=[0,0.5,1])cbar.ax.set_yticklabels(['< -4','0','> 4'])fig.show()This code then creates:As we can observe, our predictor is able to reproduce findings that were already postulated by Chang et al. as well as Meier et al.: Higher GRAVY and helicality values indeed lead to higher CCS values (at least with respect to our trained predictor).
This correlation is by no means perfect, but it lies in the nature of complex interactions that lead to a peptide's 3D structure that they cannot easily be modelled by a simple set of descriptors.
Ultimately, this is why a complex function modelling technique like Deep Learning can add something new!
Implement your own ideas to uncover driving factors like amino acid counts or specific amino acid positions by alteringthis notebook.Alignments of in-house and external dataimportpandasaspdfromionmob.preprocessimportexperimentasexpfromionmob.preprocessimportalignmentasaligdata_dir="example_data/raw_data/"fname="M210115_00[1,2,3]_HeLa_grad110_ramp100__evidence.txt"path=data_dir+fnamedf=pd.read_csv(path,sep="\t")2 ways to construct an Experiment object are provided.
1st method: isolate columns needed from df (see below) as numpy arrays and pass to constructorseq,charge,ccs,intensity,mz,raw_file,evidence_id=df["Modified sequence"].values,df["Charge"].values,df["CCS"].values,df["Intensity"].values,df["m/z"].values,df["Raw file"].values,df["id"].values# give your experiment instance a name. ideally a short uinique version of fnameex_name="HeLa_grad110"ex1=exp.Experiment(ex_name,seq,charge,ccs,intensity,mz,raw_file,evidence_id)or rather like this. be aware of the order of args!args=df["Modified sequence"].values,df["Charge"].values,df["CCS"].values,df["Intensity"].values,df["m/z"].values,df["Raw file"].values,df["id"].valuesex1=exp.Experiment(ex_name,*args)2nd method: if you are sure that the output table contains the columns "Modified sequence",
"Charge", "CCS", "Intensity", "m/z", "Raw file", "id", "Mass", "Number of isotopic peaks",
"Retention time", "Retention length" ( which is usually the case for MaxQuant evidence.txt),
then you can also use this methodex1=exp.Experiment.from_MaxQuant_DataFrame(df,"HeLa_grad110")access the name and data of Experiment like thisprint("name of your experiment: ",ex1.name)print("example_data of your experiment: ",ex1.data)Regardless of the initialization method the provided data is cleaned of NaN values in any of the essential columns and of the singly charged ions.
Furthermore, the entries in the .data attribute are aggregated upon initialization of duplicate features (duplicates of sequence-charge-ccs entries), making those unique.
Additionally, entries that matched with reversed sequences of the decoy database (depicted as '+' entry in 'reverse' column) are removed.Since the .data attribute itself is a pd.DataFrame you can use the pandas library to work on it or isolate information fromex1.data.loc[ex1.data.charge==2]to further aggregate rows and thereby getting rid of possible feature divergence, assign a modality class to each featureex2=ex1.assign_modalities()from this point on you can proceed with the inter-experimental CCS alignment of experiment
data aquired by the same devicedata_dir="example_data/raw_data/"file_names=["M210115_00[1,2,3]_HeLa_grad110_ramp100__evidence.txt","M210115_00[4,5,6]_HeLa_grad47_ramp100__evidence.txt","M210115_00[7,8,9]_HeLa_grad20_ramp100__evidence.txt"]exp_names=["HeLa_grad110","HeLa_grad47","HeLa_grad20"]paths=[data_dir+fnameforfnameinfile_names]dfs=[pd.read_csv(path,sep="\t")forpathinpaths]exs=[exp.Experiment.from_MaxQuant_DataFrame(df,exp_name)forexp_name,dfinzip(exp_names,dfs)]exs=[ex.assign_modalities()forexinexs]# perform the ccs alignment of the experiments to each otheraligned_exs=alig.align_experiments(exs)# merge the alignedaligned_ex=alig.merge_experiments(aligned_exs,"our_experiments")if you want to expand your aquired data you can align a dataset aquired by another lab to the first one
first read and intrinsically align the experiments of the other dataset like you did abovedata_dir2="example_data/mann_data/"file_names2=["Results_evidence_mann_Drosophila.txt","Results_evidence_mann_HeLaTryp.txt","Results_evidence_mann_Celegans.txt"]exp_names2=["mann_Drosophila","mann_HeLaTryp","mann_Celegans"]paths2=[data_dir2+fnameforfnameinfile_names2]dfs2=[pd.read_csv(path,sep="\t")forpathinpaths]exs2=[exp.Experiment.from_MaxQuant_DataFrame(df,exp_name)forexp_name,dfinzip(exp_names2,dfs2)]exs2=[ex.assign_modalities()forexinexs2]aligned_exs2=alig.align_experiments(exs2)aligned_ex2=alig.merge_experiments(aligned_exs2,"mann_experiments")in order to merge experiments from 2 different labs you firstly have to perform a linear shift on the data (in this case a copy of aligned_ex2 is returned with the additional column "shifted_ccs")aligned_ex2_mean_shifted=alig.apply_mean_shift(aligned_ex,aligned_ex2)at this point you can compare the original ccs values to the shifted ones within aligned_ex2_mean_shifted by accessing aligned_ex2_mean_shifted.data.ccs and aligned_ex2_mean_shifted.data.shifted_ccsif you are satisified with the shifted data you can proceed with adopting the shifted ccs values and then merge them with the previous methodaligned_ex2_mean_shifted=alig.adopt_shifted_ccs(aligned_ex2_mean_shifted)exs_of_labs=[aligned_ex,aligned_ex2_mean_shifted]big_dataset=alig.merge_experiments(exs_of_labs,"our_and_mann_dataset")Implementing a custom deep CCS predictorSay you come up with your very own idea for a deep CCS predictor architecture and want to build on top ofionmob.
It is recomended that you have a NVIDIA CUDA enabled GPU with cuDNN bianries available in your working environment,
otherwise training may take quite some time.
We  will assume that a dataset for training was already generated, including all necesarry steps for preprocessing.
For this demonstration, we can useionmobdatasets.
Let's use sets from different sources for training, validation and test.
This way, we make sure that we do not overestimate model performace.
We will start our model implementation by fitting a tokenizer.importpandasaspdimportnumpyasnpimporttensorflowastfimportosfromdatetimeimportdatetimeimportos# suppress CUDA specific logsos.environ['TF_CPP_MIN_LOG_LEVEL']='3'gpus=tf.config.experimental.list_physical_devices('GPU')tf.config.experimental.set_virtual_device_configuration(gpus[0],[tf.config.experimental.VirtualDeviceConfiguration(memory_limit=2048)])frommatplotlibimportpyplotaspltfromionmob.alignment.experimentimportExperimentfromionmob.models.deep_modelsimportProjectToInitialSqrtCCSfromionmob.preprocess.dataimportget_tf_datasetfromionmob.utilities.utilityimportget_sqrt_slopes_and_intercepts,sequence_to_tokens,sequence_with_charge,fit_tokenizerdata_train=pd.read_hdf('example_data/Meier.h5')data_valid=pd.read_hdf('example_data/Tenzer.h5')data_test=pd.read_hdf('example_data/Chang.h5')# tokenize sequencesseq_tokenized=[sequence_to_tokens(s,drop_ends=True)forsindata_train.sequence.values]# fit a tokenizertokenizer=fit_tokenizer(seq_tokenized)# have a look at tokensThe tokenizer now knows 41 tokens, 20 of which are amino acids and 21 are PTMs.It has proven to be a very efficient way to build on top of a simple square-root fit to help a deep predictor reach high accuracy as well as fast convergence.ionmobimplements its own layer that is able to project all charge states at the same time, making it very convenient to add it to your own predictor.
It is done in two steps: First, fit slopes and intercepts for the initial prediction separately.
Second, use the gained values to initialize a first projection layer.ionmobmakes use of charge state one-hot encoding to gate the prediction based on a given charge state.
If you are interested in the intrinsics,have a look at the implementation.slopes,intercepts=get_sqrt_slopes_and_intercepts(data_train.mz,data_train.charge,data_train.ccs)initial_layer=ProjectToInitialSqrtCCS(slopes,intercepts)# just make sure that everything worked by testing the projectioninitial_ccs=initial_layer([np.expand_dims(data_train.mz,1),tf.one_hot(data_train.charge-1,4)]).numpy()# visualize to make sure all went as intendedplt.figure(figsize=(8,4),dpi=120)plt.scatter(data_train.mz,initial_ccs,s=10,label='sqrt projection')plt.xlabel('Mz')plt.ylabel('CCS')plt.legend()plt.show()The most flexible way to implement a new predictor is to subclass atensorflow module or keras model.
We will do the latter, as it is the prominent way to generate new predictors forionmob.
Let's set up a predictor that uses 1D convolutions to extract additional information from the sequence of an ion.
All layers that should be part of the model are defined in the constructor, the execution is defined by specifying the call method.classConvolutionalCCSPredictor(tf.keras.models.Model):def__init__(self,slopes,intercepts,num_tokens=41,seq_len=50):super(ConvolutionalCCSPredictor,self).__init__()# the inital sqrt projectionself.initial=ProjectToInitialSqrtCCS(slopes,intercepts)# the deep sequence processorself.embedding=tf.keras.layers.Embedding(input_dim=num_tokens+1,output_dim=128,input_length=seq_len)self.conv1d=tf.keras.layers.Conv1D(filters=32,kernel_size=8,activation='relu')self.mp1d=tf.keras.layers.MaxPool1D(pool_size=2)self.conv1d_2=tf.keras.layers.Conv1D(filters=64,kernel_size=8,activation='relu')# the deep regression tailself.dense=tf.keras.layers.Dense(128,activation='relu')self.dropout=tf.keras.layers.Dropout(0.5)self.dense_2=tf.keras.layers.Dense(64,activation='relu')self.out=tf.keras.layers.Dense(1,activation=None)defcall(self,inputs):# read inputsmz,charge,sequence,_,_=inputs# calculate sequence partdeep=self.conv1d_2(self.mp1d(self.conv1d(self.embedding(sequence))))# concat with mz and chargeconcat=tf.keras.layers.Concatenate()([tf.keras.layers.Flatten()(deep),tf.sqrt(mz),charge])# deep regressiondense=self.dense_2(self.dropout(self.dense(concat)))# output is sqrt-fit + deep-regressionreturnself.initial([mz,charge])+self.out(dense)Callbacks are a convenient way to further automate your training procedure.
We will use two different callbacks that observe model performance on validation data.
The first one is a learning rate reducer: Should the loss not go down after three consecutive epochs on the validation set, the reducer is going to reduce the learning rate by an order of magnitude.
If there is still no improvement on performance, the early stopper will stop the training procedure after another 2 epochs.early_stopper=tf.keras.callbacks.EarlyStopping(monitor='val_loss',patience=5)reduce_lr=tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss',factor=1e-1,patience=2,monde='auto',min_delta=1e-5,cooldown=0,min_lr=1e-7)cbs=[early_stopper,reduce_lr]We are now ready to instanciate our predictor, build it and then compile it with a desired objective function and optimizer.
The model's summary tells us that it has a total of 178,785 trainable parameters.# create a recurrent predictormodel=ConvolutionalCCSPredictor(slopes,intercepts)# set input shapes: mz, charge_one_hot, max_seq_len, helix_score, gravy_scoremodel.build([(None,1),(None,4),(None,50),(None,1),(None,1)])model.compile(loss=tf.keras.losses.MeanAbsoluteError(),optimizer=tf.keras.optimizers.Adam(1e-2),metrics=['mae'])tf_train=get_tf_dataset(data_train.mz,data_train.charge,data_train.sequence,data_train.ccs,tokenizer,drop_sequence_ends=True,add_charge=False).shuffle(int(1e7)).batch(1024)tf_valid=get_tf_dataset(data_valid.mz,data_valid.charge,data_valid.sequence,data_valid.ccs,tokenizer,drop_sequence_ends=True,add_charge=False).shuffle(int(1e7)).batch(1024)tf_test=get_tf_dataset(data_test.mz,data_test.charge,data_test.sequence,data_test.ccs,tokenizer,drop_sequence_ends=True,add_charge=False).batch(1024)history=model.fit(tf_train,validation_data=tf_valid,epochs=50,verbose=False,callbacks=cbs)# plot training and validation lossplt.figure(figsize=(8,4),dpi=120)plt.plot(history.history['loss'],label='training')plt.plot(history.history['val_loss'],label='validation')plt.xlabel('epoch')plt.ylabel('loss')plt.legend()plt.show()As we can see from the plot above, loss quickly stops to improve on validation data while it is still falling on training data. The reduction of the learning rate is clearly visible after epoch 9. We can now have a look at test performance and report our CCS prediction accuracy.model.evaluate(tf_test)

4/4 [==============================] - 0s 16ms/step - loss: 11.5374 - mae: 11.5374

[11.537385940551758, 11.537385940551758]It is arround 11.5. Not too bad compared to the naive approach which gave us a value of arround 13. Want to try it yourself? Usethis notebook.Cite ionmobIf you useionmobfor your own work, we only ask you to give credit by citing:@article{10.1093/bioinformatics/btad486,
    author = {Teschner, David and Gomez-Zepeda, David and Declercq, Arthur and Łącki, Mateusz K and Avci, Seymen and Bob, Konstantin and Distler, Ute and Michna, Thomas and Martens, Lennart and Tenzer, Stefan and Hildebrandt, Andreas},
    title = "{Ionmob: A Python Package for Prediction of Peptide Collisional Cross-Section Values}",
    journal = {Bioinformatics},
    pages = {btad486},
    year = {2023},
    month = {08},
    issn = {1367-4811},
    doi = {10.1093/bioinformatics/btad486},
    url = {https://doi.org/10.1093/bioinformatics/btad486},
    eprint = {https://academic.oup.com/bioinformatics/advance-article-pdf/doi/10.1093/bioinformatics/btad486/51038853/btad486.pdf},
}[^fn1]: Deep learning the collisional cross-sections of the peptide universe from a million experimental values. Nat Commun, 2021.https://doi.org/10.1038/s41467-021-21352-8[^fn2]: Sequence-Specific Model for Predicting Peptide Collision Cross Section Values in Proteomic Ion Mobility Spectrometry. Journal of Proteome Research, 2021.https://doi.org/10.1021/acs.jproteome.1c00185