corems.molecular_id.calc.SpectralSimilarity

  1__author__ = "Yuri E. Corilo"
  2__date__ = "Jun 09, 2021"
  3
  4from numpy.fft import rfft
  5from scipy.stats import pearsonr, spearmanr, kendalltau
  6from numpy import (
  7    power,
  8    dot,
  9    absolute,
 10    sqrt,
 11)
 12from numpy import sum as np_sum
 13from numpy.linalg import norm
 14from pandas import DataFrame
 15import numpy as np
 16
 17methods_name = {
 18    # "entropy_distance": "Entropy Distance",
 19    # "weighted_entropy_distance": "Dynamic weighted entropy Distance",
 20    "chebyshev_distance": "Chebyshev Distance",
 21    "squared_euclidean_distance": "Squared Euclidean Distance",
 22    "fidelity_similarity": "Fidelity Similarity",
 23    "matusita_distance": "Matusita Distance",
 24    "squared_chord_distance": "Squared-chord Distance",
 25    # "bhattacharya_1_distance": "Bhattacharya 1 Distance",
 26    # "bhattacharya_2_distance": "Bhattacharya 2 Distance",
 27    "harmonic_mean_similarity": "Harmonic mean Distance",
 28    "Pearson_chi_squared_distance": "Pearson Chi Squared Distance",
 29    "Neyman_chi_squared_distance": "Neyman Chi Squared Distance",
 30    "probabilistic_symmetric_chi_squared_distance": "Probabilistic symmetric X2 Distance",
 31    "topsoe_distance": "Topsoe Distance",
 32    "chernoff_distance": "Chernoff Distance",
 33    "ruzicka_distance": "Ruzicka Distance",
 34    "roberts_distance": "Roberts Distance",
 35    # "intersection_distance": "Intersection Distance",
 36    "motyka_distance": "Motyka Distance",
 37    "canberra_distance": "Canberra Distance",
 38    "canberra_metric": "Canberra Metric",
 39    "kulczynski_1_distance": "Kulczynski 1 Distance",
 40    # "baroni_urbani_buser_distance": "Baroni-Urbani-Buser Distance",
 41    # "penrose_size_distance": "Penrose size Distance",
 42    # "mean_character_distance": "Mean character Distance",
 43    "lorentzian_distance": "Lorentzian Distance",
 44    # "penrose_shape_distance": "Penrose shape Distance",
 45    "clark_distance": "Clark Distance",
 46    "hellinger_distance": "Hellinger Distance",
 47    "whittaker_index_of_association_distance": "Whittaker index of association Distance",
 48    # "similarity_index_distance": "Similarity Index Distance",
 49    # "improved_similarity_distance": "Improved Similarity",
 50    # "absolute_value_distance": "Absolute Value Distance",
 51    "spectral_contrast_angle_distance": "Spectral Contrast Angle",
 52    "wave_hedges_distance": "Wave Hedges Distance",
 53    "dice_similarity": "Dice Similarity",
 54    "inner_product_distance": "Inner Product Distance",
 55    "divergence_distance": "Divergence Distance",
 56    "jensen_difference_distance": "Jensen Differences Distance",
 57    "kumar_johnson_distance": "Kumar Johnson Distance",
 58    "avg_l_distance": "Avg (L1, L8) Distance",
 59    "vicis_wave_hadges_distance": "Vicis Wave Hadges Distance",
 60    "vicis_symmetric_chi_squared_1_distance": "Vicis-Symmetric X2 1 Distance",
 61    "vicis_symmetric_chi_squared_2_distance": "Vicis-Symmetric X2 2 Distance",
 62    "vicis_symmetric_chi_squared_3_distance": "Vicis-Symmetric X2 3 Distance",
 63    "max_symmetric_chi_squared_distance": "Max Symmetric Chi Squared Distance",
 64    "min_symmetric_chi_squared_distance": "Min Symmetric Chi Squared Distance",
 65    # "ms_for_id_v1": "MSforID Distance version 1",
 66    # "ms_for_id": "MSforID Distance",
 67    "additive_sym_chi_sq": "Additive Symmetric Chi Squared",
 68    "bhattacharya_distance": "Battacharya Distance",
 69    "generalized_ochiai_index": "Generalized Ochiai Index",
 70    "gower_distance": "Gower Distance",
 71    "impr_sqrt_cosine_sim": "Improved Square Root Cosine Similarity",
 72    "intersection_sim": "Intersection Similarity",
 73    "j_divergence": "J Divergence",
 74    "jensen_shannon_index": "Jensen Shannon Index",
 75    "k_divergence": "K Divergence",
 76    "VW6": "VW6",
 77    "VW5": "VW5",
 78    "VW4": "VW4",
 79    "VW3": "VW3",
 80    "VW2": "VW2",
 81    "VW1": "VW1",
 82    "taneja_divergence": "Taneja Divergence",
 83    "symmetric_chi_squared_distance": "Symmetric Chi Squared Distance",
 84    "squared_chi_squared_distance": "Squared Chi Squared Distance",
 85    "square_root_cosine_correlation": "Square Root Cosine Correlation",
 86    "sorensen_distance": "Sorensen Distance",
 87    "Minokowski_3": "Minokowski 3 Distance",
 88    "Minokowski_4": "Minokowski 4 Distance",
 89    "kumarjohnson_divergence": "Kumar Johnson Divergence",
 90    "kumarhassebrook_similarity": "Kumar Hassebrook Similarity",
 91    "kullbackleibler_divergence": "Kullback Leibler Divergence",
 92    "soergel_distance": "Soergel Distance",
 93}
 94
 95methods_scale = {
 96    "entropy": [0, np.log(4)],
 97    "weighted_entropy": [0, np.log(4)],
 98    "absolute_value": [0, 2],
 99    "avg_l": [0, 1.5],
100    "bhattacharya_1": [0, np.arccos(0) ** 2],
101    "bhattacharya_2": [0, np.inf],
102    "canberra": [0, np.inf],
103    "clark": [0, np.inf],
104    "divergence": [0, np.inf],
105    "euclidean": [0, np.sqrt(2)],
106    "hellinger": [0, np.inf],
107    "improved_similarity": [0, np.inf],
108    "lorentzian": [0, np.inf],
109    "manhattan": [0, 2],
110    "matusita": [0, np.sqrt(2)],
111    "mean_character": [0, 2],
112    "motyka": [-0.5, 0],
113    "ms_for_id": [-np.inf, 0],
114    "ms_for_id_v1": [0, np.inf],
115    "pearson_correlation": [-1, 1],
116    "penrose_shape": [0, np.sqrt(2)],
117    "penrose_size": [0, np.inf],
118    "probabilistic_symmetric_chi_squared": [0, 1],
119    "similarity_index": [0, np.inf],
120    "squared_chord": [0, 2],
121    "squared_euclidean": [0, 2],
122    "symmetric_chi_squared": [0, 0.5 * np.sqrt(2)],
123    "topsoe": [0, np.sqrt(2)],
124    "vicis_symmetric_chi_squared_3": [0, 2],
125    "wave_hedges": [0, np.inf],
126    "whittaker_index_of_association": [0, np.inf],
127}
128
129
130class SpectralSimilarity:
131    """Class containing methods for calculating spectral similarity between two mass spectra.
132
133    Parameters
134    ----------
135    ms_mz_abun_dict : dict
136        Dictionary of mass to abundance values for the experimental mass spectrum.
137    ref_obj : dict
138        Dictionary of mass to abundance values for the reference mass spectrum.
139    norm_func : function
140        Function to normalize the abundance values.
141
142    Attributes
143    ----------
144    normalize_func : function
145        Function to normalize the abundance values.
146    ms_mz_abun_dict : dict
147        Dictionary of mass to abundance values for the experimental mass spectrum.
148    ref_obj : dict
149        Dictionary of mass to abundance values for the reference mass spectrum.
150    exp_abun : list
151        List of abundance values for the experimental mass spectrum.
152    exp_mz : list
153        List of mass values for the experimental mass spectrum.
154    ref_mz : list
155        List of mass values for the reference mass spectrum.
156    ref_abun : list
157        List of abundance values for the reference mass spectrum.
158    ref_mz_abun_dict : dict
159        Dictionary of mass to abundance values for the reference mass spectrum.
160    df : DataFrame
161        DataFrame containing the experimental and reference mass spectrum data.
162    zero_filled_u_l : tuple
163        Tuple containing the experimental and reference mass spectrum data after zero filling and normalization.
164    common_mz_values : list
165        List of common mass values between the experimental and reference mass spectra.
166    n_x_y : int
167        Number of common mass values between the experimental and reference mass spectra.
168
169    Methods
170    -------
171    * nan_fill(df, fill_with=0).
172        Fill missing mass values with a given value.
173    * normalize(x, y, norm_func=sum).
174        Normalize the abundance values.
175    * weighted_cosine_correlation(a=0.5, b=1.3, nanfill=1e-10).
176        Calculate the weighted cosine correlation between the experimental and reference mass spectra.
177    * cosine_correlation().
178        Calculate the cosine correlation between the experimental and reference mass spectra.
179    * stein_scott().
180        Calculate the Stein-Scott similarity between the experimental and reference mass spectra.
181    * pearson_correlation().
182        Calculate the Pearson correlation between the experimental and reference mass spectra.
183    * spearman_correlation().
184        Calculate the Spearman correlation between the experimental and reference mass spectra.
185
186
187    """
188
189    def __init__(self, ms_mz_abun_dict, ref_obj, norm_func=sum):
190        self.normalize_func = norm_func
191        self.ms_mz_abun_dict = ms_mz_abun_dict
192        self.ref_obj = ref_obj
193
194        self.exp_abun = list(self.ms_mz_abun_dict.values())
195        self.exp_mz = list(self.ms_mz_abun_dict.keys())
196
197        self.ref_mz = self.ref_obj.get("mz")
198        self.ref_abun = self.ref_obj.get("abundance")
199
200        self.ref_mz_abun_dict = dict(zip(self.ref_mz, self.ref_abun))
201
202        # parse to dataframe, easier to zerofill and tranpose
203        self.df = DataFrame([self.ms_mz_abun_dict, self.ref_mz_abun_dict])
204
205        # fill missing mz with abundance 0
206        x, y = self.nan_fill(self.df, fill_with=1e-10)
207
208        self.zero_filled_u_l = self.normalize(x, y, norm_func=self.normalize_func)
209
210        # filter out the mass values that have zero intensities in self.exp_abun
211        exp_mz_filtered = set([k for k in self.exp_mz if self.ms_mz_abun_dict[k] != 0])
212
213        # filter out the mass values that have zero intensities in self.ref_mz
214        ref_mz_filtered = set([k for k in self.ref_mz if self.ref_mz_abun_dict[k] != 0])
215
216        # find the intersection/common mass values of both ref and exp, and sort them
217        self.common_mz_values = sorted(
218            list(exp_mz_filtered.intersection(ref_mz_filtered))
219        )
220
221        # find the number of common mass values (after filtering 0s)
222        self.n_x_y = len(self.common_mz_values)
223        # print(self.n_x_y)
224
225    def nan_fill(self, df, fill_with=0):
226        """Fill missing mass values with a given value.
227
228        Parameters
229        ----------
230        df : DataFrame
231            DataFrame containing the experimental and reference mass spectrum data.
232        fill_with : float
233            Value to fill missing mass values with.
234
235        Returns
236        -------
237        x : list
238            List of abundance values for the experimental mass spectrum.
239        y : list
240            List of abundance values for the reference mass spectrum."""
241        # CoW-safe: avoid inplace fillna; prefer to_numpy over .values.
242        # df is shape (2, n_mz) with row 0 = experimental, row 1 = reference.
243        # Equivalent to the historical `df.T[0].values, df.T[1].values`.
244        df = df.fillna(fill_with)
245        return df.iloc[0].to_numpy(), df.iloc[1].to_numpy()
246
247    def normalize(self, x, y, norm_func=sum):
248        """Normalize the abundance values.
249
250        Parameters
251        ----------
252        x : list
253            List of abundance values for the experimental mass spectrum.
254        y : list
255            List of abundance values for the reference mass spectrum.
256        norm_func : function
257            Function to normalize the abundance values.
258            Default is sum
259
260        Returns
261        -------
262        u_l : tuple
263            Tuple containing the experimental and reference mass spectrum data after zero filling and normalization.
264        """
265        if norm_func is not None:
266            u_l = (x / norm_func(x), y / norm_func(y))
267            return u_l
268        else:
269            return (x, y)
270
271    def weighted_cosine_correlation(self, a=0.5, b=1.3, nanfill=1e-10):
272        """Calculate the weighted cosine correlation between the experimental and reference mass spectra.
273
274        Parameters
275        ----------
276        a : float
277            Weighting factor for the abundance values.
278            Default is 0.5
279        b : float
280            Weighting factor for the mass values.
281            Default is 1.3
282        nanfill : float
283            Value to fill missing mass values with.
284            Default is 1e-10
285
286        Returns
287        -------
288        correlation : float
289            Weighted cosine correlation between the experimental and reference mass spectra.
290        """
291        # create dict['mz'] = abundance, for experimental data
292        # ms_mz_abun_dict = mass_spec.mz_abun_dict
293        # weight exp data
294
295        xc = power(self.exp_abun, a) * power(self.exp_mz, b)
296
297        # track back to individual mz
298        weighted_exp_dict = dict(zip(self.ms_mz_abun_dict.keys(), xc))
299
300        # weight ref data
301        yc = power(self.ref_obj.get("abundance"), a) * power(self.ref_obj.get("mz"), b)
302
303        ref_mz_abun_dict = dict(zip(self.ref_obj.get("mz"), yc))
304
305        # parse to dataframe, easier to zerofill and tranpose
306        df = DataFrame([weighted_exp_dict, ref_mz_abun_dict])
307
308        # fill missing mz with weight {abun**a}{m/z**b} to 0
309        x, y = self.nan_fill(df, fill_with=nanfill)
310
311        # correlation = (1 - cosine(x, y))
312
313        correlation = dot(x, y) / (norm(x) * norm(y))
314
315        return correlation
316
317    def cosine_correlation(self):
318        """Calculate the cosine correlation between the experimental and reference mass spectra.
319
320        Returns
321        -------
322        correlation : float
323            Cosine correlation between the experimental and reference mass spectra.
324
325        """
326        # calculate cosine correlation,
327        x = self.zero_filled_u_l[0]
328        y = self.zero_filled_u_l[1]
329
330        # correlation = (1 - cosine(x, y))
331
332        correlation = dot(x, y) / (norm(x) * norm(y))
333
334        return correlation
335
336    def stein_scott(self):
337        """Calculate the Stein-Scott similarity between the experimental and reference mass spectra.
338
339        Returns
340        -------
341        s_ss_x_y : float
342            Stein-Scott similarity between the experimental and reference mass spectra.
343        s_ss_x_y_nist : float
344            Stein-Scott similarity between the experimental and reference mass spectra.
345        """
346        # TODO check this code
347        if self.n_x_y == 0:
348            return 0, 0
349
350        # count number of non-zero abundance/peak intensity values
351        n_x = sum(a != 0 for a in self.exp_abun)
352
353        s_r_x_y = 0
354
355        a, b = 1, 0
356
357        for i in range(1, self.n_x_y):
358            current_value = self.common_mz_values[i]
359            previous_value = self.common_mz_values[i - 1]
360
361            y_i = self.ref_mz_abun_dict[current_value]
362            y_i_minus1 = self.ref_mz_abun_dict[previous_value]
363
364            lc_current = power(y_i, a) * power(current_value, b)
365            lc_previous = power(y_i_minus1, a) * power(previous_value, b)
366
367            x_i = self.ms_mz_abun_dict[current_value]
368            x_i_minus1 = self.ms_mz_abun_dict[previous_value]
369
370            uc_current = power(x_i, a) * power(current_value, b)
371            uc_previous = power(x_i_minus1, a) * power(previous_value, b)
372
373            T1 = lc_current / lc_previous
374
375            T2 = uc_previous / uc_current
376
377            temp_computation = T1 * T2
378
379            n = 0
380            if temp_computation <= 1:
381                n = 1
382            else:
383                n = -1
384
385            s_r_x_y = s_r_x_y + power(temp_computation, n)
386
387        # finish the calculation of S_R(X,Y)
388
389        s_r_x_y = s_r_x_y / self.n_x_y
390        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
391        s_wc_x_y = self.weighted_cosine_correlation(a=0.5, b=3, nanfill=0)
392
393        s_ss_x_y = ((n_x * s_wc_x_y) + (self.n_x_y * s_r_x_y)) / (n_x + self.n_x_y)
394
395        s_wc_x_y_nist = self.weighted_cosine_correlation(a=0.5, b=1.3, nanfill=0)
396
397        s_ss_x_y_nist = ((n_x * s_wc_x_y_nist) + (self.n_x_y * s_r_x_y)) / (
398            n_x + self.n_x_y
399        )
400        # final step
401
402        return s_ss_x_y, s_ss_x_y_nist
403
404    def pearson_correlation(
405        self,
406    ):
407        """Calculate the Pearson correlation between the experimental and reference mass spectra.
408
409        Returns
410        -------
411        correlation : float
412            Pearson correlation between the experimental and reference mass spectra.
413        """
414        correlation = pearsonr(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
415
416        return correlation[0]
417
418    def spearman_correlation(self):
419        """Calculate the Spearman correlation between the experimental and reference mass spectra.
420
421        Returns
422        -------
423        coorelation : float
424            Spearman correlation between the experimental and reference mass spectra.
425        """
426        # calculate Spearman correlation
427        # ## TODO - Check axis
428        correlation = spearmanr(
429            self.zero_filled_u_l[0], self.zero_filled_u_l[1], axis=0
430        )
431
432        return correlation[0]
433
434    def kendall_tau(self):
435        """Calculate the Kendall's tau correlation between the experimental and reference mass spectra.
436
437        Returns
438        -------
439        correlation : float
440            Kendall's tau correlation between the experimental and reference mass spectra."""
441        # create dict['mz'] = abundance, for experimental data
442        # self.ms_mz_abun_dict = mass_spec.mz_abun_dict
443
444        # create dict['mz'] = abundance, for experimental data
445
446        # calculate Kendall's tau
447        correlation = kendalltau(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
448
449        return correlation[0]
450
451    def dft_correlation(self):
452        """Calculate the DFT correlation between the experimental and reference mass spectra.
453
454        Returns
455        -------
456        correlation : float
457            DFT correlation between the experimental and reference mass spectra.
458        """
459        if self.n_x_y == 0:
460            return 0
461
462        # count number of non-zero abundance/peak intensity values
463        n_x = sum(a != 0 for a in self.exp_abun)
464
465        x, y = self.nan_fill(self.df, fill_with=0)
466
467        x, y = self.normalize(x, y, norm_func=self.normalize_func)
468
469        # get the Fourier transform of x and y
470        x_dft = rfft(x).real
471        y_dft = rfft(y).real
472
473        s_dft_xy = dot(x_dft, y_dft) / (norm(x_dft) * norm(y_dft))
474
475        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
476        s_wc_x_y = self.weighted_cosine_correlation(nanfill=0)
477
478        # final step
479        s_dft = (n_x * s_wc_x_y + self.n_x_y * s_dft_xy) / (n_x + self.n_x_y)
480
481        return s_dft
482
483    def dwt_correlation(self):
484        """Calculate the DWT correlation between the experimental and reference mass spectra.
485
486        Returns
487        -------
488        correlation : float
489            DWT correlation between the experimental and reference mass spectra.
490
491        Notes
492        -----
493        This function requires the PyWavelets library to be installed.
494            This is not a default requirement as this function is not widely used.
495        """
496
497        from pywt import dwt
498
499        if self.n_x_y == 0:
500            return 0
501
502        # count number of non-zero abundance/peak intensity values
503        n_x = sum(a != 0 for a in self.exp_abun)
504
505        # calculate cosine correlation,
506        x, y = self.nan_fill(self.df, fill_with=0)
507
508        x, y = self.normalize(x, y, norm_func=self.normalize_func)
509
510        # Make x and y into an array
511        x_a = list(x)
512        y_a = list(y)
513
514        # get the wavelet transform of x and y (Daubechies with a filter length of 4. Asymmetric. pywavelets function)
515        # Will only use the detail dwt (dwtDd
516        x_dwtD = dwt(x_a, "db2")[1]
517        y_dwtD = dwt(y_a, "db2")[1]
518
519        s_dwt_xy = dot(x_dwtD, y_dwtD) / (norm(x_dwtD) * norm(y_dwtD))
520
521        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
522        s_wc_x_y = self.weighted_cosine_correlation(nanfill=0)
523
524        # final step
525        s_dwt = (n_x * s_wc_x_y + self.n_x_y * s_dwt_xy) / (n_x + self.n_x_y)
526
527        return s_dwt
528
529    def euclidean_distance(self):
530        """Calculate the Euclidean distance between the experimental and reference mass spectra.
531
532        Returns
533        -------
534        correlation : float
535            Euclidean distance between the experimental and reference mass spectra.
536        """
537        # correlation = euclidean_distance_manual(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
538        qlist = self.zero_filled_u_l[0]
539        rlist = self.zero_filled_u_l[1]
540
541        correlation = sqrt(np_sum(power(qlist - rlist, 2)))
542
543        return correlation
544
545    def manhattan_distance(self):
546        """Calculate the Manhattan distance between the experimental and reference mass spectra.
547
548        Returns
549        -------
550        correlation : float
551            Manhattan distance between the experimental and reference mass spectra.
552        """
553        qlist = self.zero_filled_u_l[0]
554        rlist = self.zero_filled_u_l[1]
555
556        return np_sum(absolute(qlist - rlist))
557
558    def jaccard_distance(self):
559        """Calculate the Jaccard distance between the experimental and reference mass spectra.
560
561        Returns
562        -------
563        correlation : float
564            Jaccard distance between the experimental and reference mass spectra.
565        """
566
567        def jaccard_similarity(list1, list2):
568            intersection = len(list(set(list1).intersection(list2)))
569            union = (len(list1) + len(list2)) - intersection
570            return float(intersection) / union
571
572        qlist = self.zero_filled_u_l[0]
573        rlist = self.zero_filled_u_l[1]
574
575        return np_sum(power(qlist - rlist, 2)) / (
576            np_sum(power(qlist, 2)) + np_sum(power(rlist, 2)) - np_sum(qlist * rlist)
577        )
578        # correlation = jaccard_similarity(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
579        # @return correlation
580
581    def extra_distances(self):
582        """Function to calculate distances using additional metrics defined in math_distance.py
583
584        Currently, calculates all distances.
585
586        Returns
587        -------
588        dict_res : dict
589            Dictionary containing the distances between the experimental and reference mass spectra.
590
591        """
592        from corems.molecular_id.calc import math_distance
593
594        # qlist = self.zero_filled_u_l[2]
595        # rlist = self.zero_filled_u_l[3]
596
597        dict_res = {}
598
599        for method in methods_name:
600            # function_name = method + "_distance"
601            function_name = method
602            if hasattr(math_distance, function_name):
603                f = getattr(math_distance, function_name)
604
605                if function_name == "canberra_metric":
606                    x, y = self.nan_fill(self.df, fill_with=0)
607
608                    qlist, rlist = self.normalize(x, y, norm_func=self.normalize_func)
609                    # print("qlist:")
610                    # print(qlist)
611                    # print("rlist:")
612                    # print(rlist)
613
614                else:
615                    qlist = self.zero_filled_u_l[0]
616                    rlist = self.zero_filled_u_l[1]
617
618                dist = f(qlist, rlist)
619                # if method == "Minokowski_3":
620                #    print("qlist:")
621                #    print(qlist)
622                #    print("rlist")
623                #    print(rlist)
624                #    exit()
625                # if dist == np.nan or dis == np.inf:
626                # print(self.exp_abun)
627                # print(self.exp_mz)
628                # print(function_name)
629                # print(len(self.exp_abun))
630                # print(len(self.exp_mz))
631                # print(self.zero_filled_u_l[1])
632                dict_res[method] = dist
633
634        return dict_res
methods_name = {'chebyshev_distance': 'Chebyshev Distance', 'squared_euclidean_distance': 'Squared Euclidean Distance', 'fidelity_similarity': 'Fidelity Similarity', 'matusita_distance': 'Matusita Distance', 'squared_chord_distance': 'Squared-chord Distance', 'harmonic_mean_similarity': 'Harmonic mean Distance', 'Pearson_chi_squared_distance': 'Pearson Chi Squared Distance', 'Neyman_chi_squared_distance': 'Neyman Chi Squared Distance', 'probabilistic_symmetric_chi_squared_distance': 'Probabilistic symmetric X2 Distance', 'topsoe_distance': 'Topsoe Distance', 'chernoff_distance': 'Chernoff Distance', 'ruzicka_distance': 'Ruzicka Distance', 'roberts_distance': 'Roberts Distance', 'motyka_distance': 'Motyka Distance', 'canberra_distance': 'Canberra Distance', 'canberra_metric': 'Canberra Metric', 'kulczynski_1_distance': 'Kulczynski 1 Distance', 'lorentzian_distance': 'Lorentzian Distance', 'clark_distance': 'Clark Distance', 'hellinger_distance': 'Hellinger Distance', 'whittaker_index_of_association_distance': 'Whittaker index of association Distance', 'spectral_contrast_angle_distance': 'Spectral Contrast Angle', 'wave_hedges_distance': 'Wave Hedges Distance', 'dice_similarity': 'Dice Similarity', 'inner_product_distance': 'Inner Product Distance', 'divergence_distance': 'Divergence Distance', 'jensen_difference_distance': 'Jensen Differences Distance', 'kumar_johnson_distance': 'Kumar Johnson Distance', 'avg_l_distance': 'Avg (L1, L8) Distance', 'vicis_wave_hadges_distance': 'Vicis Wave Hadges Distance', 'vicis_symmetric_chi_squared_1_distance': 'Vicis-Symmetric X2 1 Distance', 'vicis_symmetric_chi_squared_2_distance': 'Vicis-Symmetric X2 2 Distance', 'vicis_symmetric_chi_squared_3_distance': 'Vicis-Symmetric X2 3 Distance', 'max_symmetric_chi_squared_distance': 'Max Symmetric Chi Squared Distance', 'min_symmetric_chi_squared_distance': 'Min Symmetric Chi Squared Distance', 'additive_sym_chi_sq': 'Additive Symmetric Chi Squared', 'bhattacharya_distance': 'Battacharya Distance', 'generalized_ochiai_index': 'Generalized Ochiai Index', 'gower_distance': 'Gower Distance', 'impr_sqrt_cosine_sim': 'Improved Square Root Cosine Similarity', 'intersection_sim': 'Intersection Similarity', 'j_divergence': 'J Divergence', 'jensen_shannon_index': 'Jensen Shannon Index', 'k_divergence': 'K Divergence', 'VW6': 'VW6', 'VW5': 'VW5', 'VW4': 'VW4', 'VW3': 'VW3', 'VW2': 'VW2', 'VW1': 'VW1', 'taneja_divergence': 'Taneja Divergence', 'symmetric_chi_squared_distance': 'Symmetric Chi Squared Distance', 'squared_chi_squared_distance': 'Squared Chi Squared Distance', 'square_root_cosine_correlation': 'Square Root Cosine Correlation', 'sorensen_distance': 'Sorensen Distance', 'Minokowski_3': 'Minokowski 3 Distance', 'Minokowski_4': 'Minokowski 4 Distance', 'kumarjohnson_divergence': 'Kumar Johnson Divergence', 'kumarhassebrook_similarity': 'Kumar Hassebrook Similarity', 'kullbackleibler_divergence': 'Kullback Leibler Divergence', 'soergel_distance': 'Soergel Distance'}
methods_scale = {'entropy': [0, np.float64(1.3862943611198906)], 'weighted_entropy': [0, np.float64(1.3862943611198906)], 'absolute_value': [0, 2], 'avg_l': [0, 1.5], 'bhattacharya_1': [0, np.float64(2.4674011002723395)], 'bhattacharya_2': [0, inf], 'canberra': [0, inf], 'clark': [0, inf], 'divergence': [0, inf], 'euclidean': [0, np.float64(1.4142135623730951)], 'hellinger': [0, inf], 'improved_similarity': [0, inf], 'lorentzian': [0, inf], 'manhattan': [0, 2], 'matusita': [0, np.float64(1.4142135623730951)], 'mean_character': [0, 2], 'motyka': [-0.5, 0], 'ms_for_id': [-inf, 0], 'ms_for_id_v1': [0, inf], 'pearson_correlation': [-1, 1], 'penrose_shape': [0, np.float64(1.4142135623730951)], 'penrose_size': [0, inf], 'probabilistic_symmetric_chi_squared': [0, 1], 'similarity_index': [0, inf], 'squared_chord': [0, 2], 'squared_euclidean': [0, 2], 'symmetric_chi_squared': [0, np.float64(0.7071067811865476)], 'topsoe': [0, np.float64(1.4142135623730951)], 'vicis_symmetric_chi_squared_3': [0, 2], 'wave_hedges': [0, inf], 'whittaker_index_of_association': [0, inf]}
class SpectralSimilarity:
131class SpectralSimilarity:
132    """Class containing methods for calculating spectral similarity between two mass spectra.
133
134    Parameters
135    ----------
136    ms_mz_abun_dict : dict
137        Dictionary of mass to abundance values for the experimental mass spectrum.
138    ref_obj : dict
139        Dictionary of mass to abundance values for the reference mass spectrum.
140    norm_func : function
141        Function to normalize the abundance values.
142
143    Attributes
144    ----------
145    normalize_func : function
146        Function to normalize the abundance values.
147    ms_mz_abun_dict : dict
148        Dictionary of mass to abundance values for the experimental mass spectrum.
149    ref_obj : dict
150        Dictionary of mass to abundance values for the reference mass spectrum.
151    exp_abun : list
152        List of abundance values for the experimental mass spectrum.
153    exp_mz : list
154        List of mass values for the experimental mass spectrum.
155    ref_mz : list
156        List of mass values for the reference mass spectrum.
157    ref_abun : list
158        List of abundance values for the reference mass spectrum.
159    ref_mz_abun_dict : dict
160        Dictionary of mass to abundance values for the reference mass spectrum.
161    df : DataFrame
162        DataFrame containing the experimental and reference mass spectrum data.
163    zero_filled_u_l : tuple
164        Tuple containing the experimental and reference mass spectrum data after zero filling and normalization.
165    common_mz_values : list
166        List of common mass values between the experimental and reference mass spectra.
167    n_x_y : int
168        Number of common mass values between the experimental and reference mass spectra.
169
170    Methods
171    -------
172    * nan_fill(df, fill_with=0).
173        Fill missing mass values with a given value.
174    * normalize(x, y, norm_func=sum).
175        Normalize the abundance values.
176    * weighted_cosine_correlation(a=0.5, b=1.3, nanfill=1e-10).
177        Calculate the weighted cosine correlation between the experimental and reference mass spectra.
178    * cosine_correlation().
179        Calculate the cosine correlation between the experimental and reference mass spectra.
180    * stein_scott().
181        Calculate the Stein-Scott similarity between the experimental and reference mass spectra.
182    * pearson_correlation().
183        Calculate the Pearson correlation between the experimental and reference mass spectra.
184    * spearman_correlation().
185        Calculate the Spearman correlation between the experimental and reference mass spectra.
186
187
188    """
189
190    def __init__(self, ms_mz_abun_dict, ref_obj, norm_func=sum):
191        self.normalize_func = norm_func
192        self.ms_mz_abun_dict = ms_mz_abun_dict
193        self.ref_obj = ref_obj
194
195        self.exp_abun = list(self.ms_mz_abun_dict.values())
196        self.exp_mz = list(self.ms_mz_abun_dict.keys())
197
198        self.ref_mz = self.ref_obj.get("mz")
199        self.ref_abun = self.ref_obj.get("abundance")
200
201        self.ref_mz_abun_dict = dict(zip(self.ref_mz, self.ref_abun))
202
203        # parse to dataframe, easier to zerofill and tranpose
204        self.df = DataFrame([self.ms_mz_abun_dict, self.ref_mz_abun_dict])
205
206        # fill missing mz with abundance 0
207        x, y = self.nan_fill(self.df, fill_with=1e-10)
208
209        self.zero_filled_u_l = self.normalize(x, y, norm_func=self.normalize_func)
210
211        # filter out the mass values that have zero intensities in self.exp_abun
212        exp_mz_filtered = set([k for k in self.exp_mz if self.ms_mz_abun_dict[k] != 0])
213
214        # filter out the mass values that have zero intensities in self.ref_mz
215        ref_mz_filtered = set([k for k in self.ref_mz if self.ref_mz_abun_dict[k] != 0])
216
217        # find the intersection/common mass values of both ref and exp, and sort them
218        self.common_mz_values = sorted(
219            list(exp_mz_filtered.intersection(ref_mz_filtered))
220        )
221
222        # find the number of common mass values (after filtering 0s)
223        self.n_x_y = len(self.common_mz_values)
224        # print(self.n_x_y)
225
226    def nan_fill(self, df, fill_with=0):
227        """Fill missing mass values with a given value.
228
229        Parameters
230        ----------
231        df : DataFrame
232            DataFrame containing the experimental and reference mass spectrum data.
233        fill_with : float
234            Value to fill missing mass values with.
235
236        Returns
237        -------
238        x : list
239            List of abundance values for the experimental mass spectrum.
240        y : list
241            List of abundance values for the reference mass spectrum."""
242        # CoW-safe: avoid inplace fillna; prefer to_numpy over .values.
243        # df is shape (2, n_mz) with row 0 = experimental, row 1 = reference.
244        # Equivalent to the historical `df.T[0].values, df.T[1].values`.
245        df = df.fillna(fill_with)
246        return df.iloc[0].to_numpy(), df.iloc[1].to_numpy()
247
248    def normalize(self, x, y, norm_func=sum):
249        """Normalize the abundance values.
250
251        Parameters
252        ----------
253        x : list
254            List of abundance values for the experimental mass spectrum.
255        y : list
256            List of abundance values for the reference mass spectrum.
257        norm_func : function
258            Function to normalize the abundance values.
259            Default is sum
260
261        Returns
262        -------
263        u_l : tuple
264            Tuple containing the experimental and reference mass spectrum data after zero filling and normalization.
265        """
266        if norm_func is not None:
267            u_l = (x / norm_func(x), y / norm_func(y))
268            return u_l
269        else:
270            return (x, y)
271
272    def weighted_cosine_correlation(self, a=0.5, b=1.3, nanfill=1e-10):
273        """Calculate the weighted cosine correlation between the experimental and reference mass spectra.
274
275        Parameters
276        ----------
277        a : float
278            Weighting factor for the abundance values.
279            Default is 0.5
280        b : float
281            Weighting factor for the mass values.
282            Default is 1.3
283        nanfill : float
284            Value to fill missing mass values with.
285            Default is 1e-10
286
287        Returns
288        -------
289        correlation : float
290            Weighted cosine correlation between the experimental and reference mass spectra.
291        """
292        # create dict['mz'] = abundance, for experimental data
293        # ms_mz_abun_dict = mass_spec.mz_abun_dict
294        # weight exp data
295
296        xc = power(self.exp_abun, a) * power(self.exp_mz, b)
297
298        # track back to individual mz
299        weighted_exp_dict = dict(zip(self.ms_mz_abun_dict.keys(), xc))
300
301        # weight ref data
302        yc = power(self.ref_obj.get("abundance"), a) * power(self.ref_obj.get("mz"), b)
303
304        ref_mz_abun_dict = dict(zip(self.ref_obj.get("mz"), yc))
305
306        # parse to dataframe, easier to zerofill and tranpose
307        df = DataFrame([weighted_exp_dict, ref_mz_abun_dict])
308
309        # fill missing mz with weight {abun**a}{m/z**b} to 0
310        x, y = self.nan_fill(df, fill_with=nanfill)
311
312        # correlation = (1 - cosine(x, y))
313
314        correlation = dot(x, y) / (norm(x) * norm(y))
315
316        return correlation
317
318    def cosine_correlation(self):
319        """Calculate the cosine correlation between the experimental and reference mass spectra.
320
321        Returns
322        -------
323        correlation : float
324            Cosine correlation between the experimental and reference mass spectra.
325
326        """
327        # calculate cosine correlation,
328        x = self.zero_filled_u_l[0]
329        y = self.zero_filled_u_l[1]
330
331        # correlation = (1 - cosine(x, y))
332
333        correlation = dot(x, y) / (norm(x) * norm(y))
334
335        return correlation
336
337    def stein_scott(self):
338        """Calculate the Stein-Scott similarity between the experimental and reference mass spectra.
339
340        Returns
341        -------
342        s_ss_x_y : float
343            Stein-Scott similarity between the experimental and reference mass spectra.
344        s_ss_x_y_nist : float
345            Stein-Scott similarity between the experimental and reference mass spectra.
346        """
347        # TODO check this code
348        if self.n_x_y == 0:
349            return 0, 0
350
351        # count number of non-zero abundance/peak intensity values
352        n_x = sum(a != 0 for a in self.exp_abun)
353
354        s_r_x_y = 0
355
356        a, b = 1, 0
357
358        for i in range(1, self.n_x_y):
359            current_value = self.common_mz_values[i]
360            previous_value = self.common_mz_values[i - 1]
361
362            y_i = self.ref_mz_abun_dict[current_value]
363            y_i_minus1 = self.ref_mz_abun_dict[previous_value]
364
365            lc_current = power(y_i, a) * power(current_value, b)
366            lc_previous = power(y_i_minus1, a) * power(previous_value, b)
367
368            x_i = self.ms_mz_abun_dict[current_value]
369            x_i_minus1 = self.ms_mz_abun_dict[previous_value]
370
371            uc_current = power(x_i, a) * power(current_value, b)
372            uc_previous = power(x_i_minus1, a) * power(previous_value, b)
373
374            T1 = lc_current / lc_previous
375
376            T2 = uc_previous / uc_current
377
378            temp_computation = T1 * T2
379
380            n = 0
381            if temp_computation <= 1:
382                n = 1
383            else:
384                n = -1
385
386            s_r_x_y = s_r_x_y + power(temp_computation, n)
387
388        # finish the calculation of S_R(X,Y)
389
390        s_r_x_y = s_r_x_y / self.n_x_y
391        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
392        s_wc_x_y = self.weighted_cosine_correlation(a=0.5, b=3, nanfill=0)
393
394        s_ss_x_y = ((n_x * s_wc_x_y) + (self.n_x_y * s_r_x_y)) / (n_x + self.n_x_y)
395
396        s_wc_x_y_nist = self.weighted_cosine_correlation(a=0.5, b=1.3, nanfill=0)
397
398        s_ss_x_y_nist = ((n_x * s_wc_x_y_nist) + (self.n_x_y * s_r_x_y)) / (
399            n_x + self.n_x_y
400        )
401        # final step
402
403        return s_ss_x_y, s_ss_x_y_nist
404
405    def pearson_correlation(
406        self,
407    ):
408        """Calculate the Pearson correlation between the experimental and reference mass spectra.
409
410        Returns
411        -------
412        correlation : float
413            Pearson correlation between the experimental and reference mass spectra.
414        """
415        correlation = pearsonr(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
416
417        return correlation[0]
418
419    def spearman_correlation(self):
420        """Calculate the Spearman correlation between the experimental and reference mass spectra.
421
422        Returns
423        -------
424        coorelation : float
425            Spearman correlation between the experimental and reference mass spectra.
426        """
427        # calculate Spearman correlation
428        # ## TODO - Check axis
429        correlation = spearmanr(
430            self.zero_filled_u_l[0], self.zero_filled_u_l[1], axis=0
431        )
432
433        return correlation[0]
434
435    def kendall_tau(self):
436        """Calculate the Kendall's tau correlation between the experimental and reference mass spectra.
437
438        Returns
439        -------
440        correlation : float
441            Kendall's tau correlation between the experimental and reference mass spectra."""
442        # create dict['mz'] = abundance, for experimental data
443        # self.ms_mz_abun_dict = mass_spec.mz_abun_dict
444
445        # create dict['mz'] = abundance, for experimental data
446
447        # calculate Kendall's tau
448        correlation = kendalltau(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
449
450        return correlation[0]
451
452    def dft_correlation(self):
453        """Calculate the DFT correlation between the experimental and reference mass spectra.
454
455        Returns
456        -------
457        correlation : float
458            DFT correlation between the experimental and reference mass spectra.
459        """
460        if self.n_x_y == 0:
461            return 0
462
463        # count number of non-zero abundance/peak intensity values
464        n_x = sum(a != 0 for a in self.exp_abun)
465
466        x, y = self.nan_fill(self.df, fill_with=0)
467
468        x, y = self.normalize(x, y, norm_func=self.normalize_func)
469
470        # get the Fourier transform of x and y
471        x_dft = rfft(x).real
472        y_dft = rfft(y).real
473
474        s_dft_xy = dot(x_dft, y_dft) / (norm(x_dft) * norm(y_dft))
475
476        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
477        s_wc_x_y = self.weighted_cosine_correlation(nanfill=0)
478
479        # final step
480        s_dft = (n_x * s_wc_x_y + self.n_x_y * s_dft_xy) / (n_x + self.n_x_y)
481
482        return s_dft
483
484    def dwt_correlation(self):
485        """Calculate the DWT correlation between the experimental and reference mass spectra.
486
487        Returns
488        -------
489        correlation : float
490            DWT correlation between the experimental and reference mass spectra.
491
492        Notes
493        -----
494        This function requires the PyWavelets library to be installed.
495            This is not a default requirement as this function is not widely used.
496        """
497
498        from pywt import dwt
499
500        if self.n_x_y == 0:
501            return 0
502
503        # count number of non-zero abundance/peak intensity values
504        n_x = sum(a != 0 for a in self.exp_abun)
505
506        # calculate cosine correlation,
507        x, y = self.nan_fill(self.df, fill_with=0)
508
509        x, y = self.normalize(x, y, norm_func=self.normalize_func)
510
511        # Make x and y into an array
512        x_a = list(x)
513        y_a = list(y)
514
515        # get the wavelet transform of x and y (Daubechies with a filter length of 4. Asymmetric. pywavelets function)
516        # Will only use the detail dwt (dwtDd
517        x_dwtD = dwt(x_a, "db2")[1]
518        y_dwtD = dwt(y_a, "db2")[1]
519
520        s_dwt_xy = dot(x_dwtD, y_dwtD) / (norm(x_dwtD) * norm(y_dwtD))
521
522        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
523        s_wc_x_y = self.weighted_cosine_correlation(nanfill=0)
524
525        # final step
526        s_dwt = (n_x * s_wc_x_y + self.n_x_y * s_dwt_xy) / (n_x + self.n_x_y)
527
528        return s_dwt
529
530    def euclidean_distance(self):
531        """Calculate the Euclidean distance between the experimental and reference mass spectra.
532
533        Returns
534        -------
535        correlation : float
536            Euclidean distance between the experimental and reference mass spectra.
537        """
538        # correlation = euclidean_distance_manual(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
539        qlist = self.zero_filled_u_l[0]
540        rlist = self.zero_filled_u_l[1]
541
542        correlation = sqrt(np_sum(power(qlist - rlist, 2)))
543
544        return correlation
545
546    def manhattan_distance(self):
547        """Calculate the Manhattan distance between the experimental and reference mass spectra.
548
549        Returns
550        -------
551        correlation : float
552            Manhattan distance between the experimental and reference mass spectra.
553        """
554        qlist = self.zero_filled_u_l[0]
555        rlist = self.zero_filled_u_l[1]
556
557        return np_sum(absolute(qlist - rlist))
558
559    def jaccard_distance(self):
560        """Calculate the Jaccard distance between the experimental and reference mass spectra.
561
562        Returns
563        -------
564        correlation : float
565            Jaccard distance between the experimental and reference mass spectra.
566        """
567
568        def jaccard_similarity(list1, list2):
569            intersection = len(list(set(list1).intersection(list2)))
570            union = (len(list1) + len(list2)) - intersection
571            return float(intersection) / union
572
573        qlist = self.zero_filled_u_l[0]
574        rlist = self.zero_filled_u_l[1]
575
576        return np_sum(power(qlist - rlist, 2)) / (
577            np_sum(power(qlist, 2)) + np_sum(power(rlist, 2)) - np_sum(qlist * rlist)
578        )
579        # correlation = jaccard_similarity(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
580        # @return correlation
581
582    def extra_distances(self):
583        """Function to calculate distances using additional metrics defined in math_distance.py
584
585        Currently, calculates all distances.
586
587        Returns
588        -------
589        dict_res : dict
590            Dictionary containing the distances between the experimental and reference mass spectra.
591
592        """
593        from corems.molecular_id.calc import math_distance
594
595        # qlist = self.zero_filled_u_l[2]
596        # rlist = self.zero_filled_u_l[3]
597
598        dict_res = {}
599
600        for method in methods_name:
601            # function_name = method + "_distance"
602            function_name = method
603            if hasattr(math_distance, function_name):
604                f = getattr(math_distance, function_name)
605
606                if function_name == "canberra_metric":
607                    x, y = self.nan_fill(self.df, fill_with=0)
608
609                    qlist, rlist = self.normalize(x, y, norm_func=self.normalize_func)
610                    # print("qlist:")
611                    # print(qlist)
612                    # print("rlist:")
613                    # print(rlist)
614
615                else:
616                    qlist = self.zero_filled_u_l[0]
617                    rlist = self.zero_filled_u_l[1]
618
619                dist = f(qlist, rlist)
620                # if method == "Minokowski_3":
621                #    print("qlist:")
622                #    print(qlist)
623                #    print("rlist")
624                #    print(rlist)
625                #    exit()
626                # if dist == np.nan or dis == np.inf:
627                # print(self.exp_abun)
628                # print(self.exp_mz)
629                # print(function_name)
630                # print(len(self.exp_abun))
631                # print(len(self.exp_mz))
632                # print(self.zero_filled_u_l[1])
633                dict_res[method] = dist
634
635        return dict_res

Class containing methods for calculating spectral similarity between two mass spectra.

Parameters
  • ms_mz_abun_dict (dict): Dictionary of mass to abundance values for the experimental mass spectrum.
  • ref_obj (dict): Dictionary of mass to abundance values for the reference mass spectrum.
  • norm_func (function): Function to normalize the abundance values.
Attributes
  • normalize_func (function): Function to normalize the abundance values.
  • ms_mz_abun_dict (dict): Dictionary of mass to abundance values for the experimental mass spectrum.
  • ref_obj (dict): Dictionary of mass to abundance values for the reference mass spectrum.
  • exp_abun (list): List of abundance values for the experimental mass spectrum.
  • exp_mz (list): List of mass values for the experimental mass spectrum.
  • ref_mz (list): List of mass values for the reference mass spectrum.
  • ref_abun (list): List of abundance values for the reference mass spectrum.
  • ref_mz_abun_dict (dict): Dictionary of mass to abundance values for the reference mass spectrum.
  • df (DataFrame): DataFrame containing the experimental and reference mass spectrum data.
  • zero_filled_u_l (tuple): Tuple containing the experimental and reference mass spectrum data after zero filling and normalization.
  • common_mz_values (list): List of common mass values between the experimental and reference mass spectra.
  • n_x_y (int): Number of common mass values between the experimental and reference mass spectra.
Methods
  • nan_fill(df, fill_with=0). Fill missing mass values with a given value.
  • normalize(x, y, norm_func=sum). Normalize the abundance values.
  • weighted_cosine_correlation(a=0.5, b=1.3, nanfill=1e-10). Calculate the weighted cosine correlation between the experimental and reference mass spectra.
  • cosine_correlation(). Calculate the cosine correlation between the experimental and reference mass spectra.
  • stein_scott(). Calculate the Stein-Scott similarity between the experimental and reference mass spectra.
  • pearson_correlation(). Calculate the Pearson correlation between the experimental and reference mass spectra.
  • spearman_correlation(). Calculate the Spearman correlation between the experimental and reference mass spectra.
SpectralSimilarity(ms_mz_abun_dict, ref_obj, norm_func=<built-in function sum>)
190    def __init__(self, ms_mz_abun_dict, ref_obj, norm_func=sum):
191        self.normalize_func = norm_func
192        self.ms_mz_abun_dict = ms_mz_abun_dict
193        self.ref_obj = ref_obj
194
195        self.exp_abun = list(self.ms_mz_abun_dict.values())
196        self.exp_mz = list(self.ms_mz_abun_dict.keys())
197
198        self.ref_mz = self.ref_obj.get("mz")
199        self.ref_abun = self.ref_obj.get("abundance")
200
201        self.ref_mz_abun_dict = dict(zip(self.ref_mz, self.ref_abun))
202
203        # parse to dataframe, easier to zerofill and tranpose
204        self.df = DataFrame([self.ms_mz_abun_dict, self.ref_mz_abun_dict])
205
206        # fill missing mz with abundance 0
207        x, y = self.nan_fill(self.df, fill_with=1e-10)
208
209        self.zero_filled_u_l = self.normalize(x, y, norm_func=self.normalize_func)
210
211        # filter out the mass values that have zero intensities in self.exp_abun
212        exp_mz_filtered = set([k for k in self.exp_mz if self.ms_mz_abun_dict[k] != 0])
213
214        # filter out the mass values that have zero intensities in self.ref_mz
215        ref_mz_filtered = set([k for k in self.ref_mz if self.ref_mz_abun_dict[k] != 0])
216
217        # find the intersection/common mass values of both ref and exp, and sort them
218        self.common_mz_values = sorted(
219            list(exp_mz_filtered.intersection(ref_mz_filtered))
220        )
221
222        # find the number of common mass values (after filtering 0s)
223        self.n_x_y = len(self.common_mz_values)
224        # print(self.n_x_y)
normalize_func
ms_mz_abun_dict
ref_obj
exp_abun
exp_mz
ref_mz
ref_abun
ref_mz_abun_dict
df
zero_filled_u_l
common_mz_values
n_x_y
def nan_fill(self, df, fill_with=0):
226    def nan_fill(self, df, fill_with=0):
227        """Fill missing mass values with a given value.
228
229        Parameters
230        ----------
231        df : DataFrame
232            DataFrame containing the experimental and reference mass spectrum data.
233        fill_with : float
234            Value to fill missing mass values with.
235
236        Returns
237        -------
238        x : list
239            List of abundance values for the experimental mass spectrum.
240        y : list
241            List of abundance values for the reference mass spectrum."""
242        # CoW-safe: avoid inplace fillna; prefer to_numpy over .values.
243        # df is shape (2, n_mz) with row 0 = experimental, row 1 = reference.
244        # Equivalent to the historical `df.T[0].values, df.T[1].values`.
245        df = df.fillna(fill_with)
246        return df.iloc[0].to_numpy(), df.iloc[1].to_numpy()

Fill missing mass values with a given value.

Parameters
  • df (DataFrame): DataFrame containing the experimental and reference mass spectrum data.
  • fill_with (float): Value to fill missing mass values with.
Returns
  • x (list): List of abundance values for the experimental mass spectrum.
  • y (list): List of abundance values for the reference mass spectrum.
def normalize(self, x, y, norm_func=<built-in function sum>):
248    def normalize(self, x, y, norm_func=sum):
249        """Normalize the abundance values.
250
251        Parameters
252        ----------
253        x : list
254            List of abundance values for the experimental mass spectrum.
255        y : list
256            List of abundance values for the reference mass spectrum.
257        norm_func : function
258            Function to normalize the abundance values.
259            Default is sum
260
261        Returns
262        -------
263        u_l : tuple
264            Tuple containing the experimental and reference mass spectrum data after zero filling and normalization.
265        """
266        if norm_func is not None:
267            u_l = (x / norm_func(x), y / norm_func(y))
268            return u_l
269        else:
270            return (x, y)

Normalize the abundance values.

Parameters
  • x (list): List of abundance values for the experimental mass spectrum.
  • y (list): List of abundance values for the reference mass spectrum.
  • norm_func (function): Function to normalize the abundance values. Default is sum
Returns
  • u_l (tuple): Tuple containing the experimental and reference mass spectrum data after zero filling and normalization.
def weighted_cosine_correlation(self, a=0.5, b=1.3, nanfill=1e-10):
272    def weighted_cosine_correlation(self, a=0.5, b=1.3, nanfill=1e-10):
273        """Calculate the weighted cosine correlation between the experimental and reference mass spectra.
274
275        Parameters
276        ----------
277        a : float
278            Weighting factor for the abundance values.
279            Default is 0.5
280        b : float
281            Weighting factor for the mass values.
282            Default is 1.3
283        nanfill : float
284            Value to fill missing mass values with.
285            Default is 1e-10
286
287        Returns
288        -------
289        correlation : float
290            Weighted cosine correlation between the experimental and reference mass spectra.
291        """
292        # create dict['mz'] = abundance, for experimental data
293        # ms_mz_abun_dict = mass_spec.mz_abun_dict
294        # weight exp data
295
296        xc = power(self.exp_abun, a) * power(self.exp_mz, b)
297
298        # track back to individual mz
299        weighted_exp_dict = dict(zip(self.ms_mz_abun_dict.keys(), xc))
300
301        # weight ref data
302        yc = power(self.ref_obj.get("abundance"), a) * power(self.ref_obj.get("mz"), b)
303
304        ref_mz_abun_dict = dict(zip(self.ref_obj.get("mz"), yc))
305
306        # parse to dataframe, easier to zerofill and tranpose
307        df = DataFrame([weighted_exp_dict, ref_mz_abun_dict])
308
309        # fill missing mz with weight {abun**a}{m/z**b} to 0
310        x, y = self.nan_fill(df, fill_with=nanfill)
311
312        # correlation = (1 - cosine(x, y))
313
314        correlation = dot(x, y) / (norm(x) * norm(y))
315
316        return correlation

Calculate the weighted cosine correlation between the experimental and reference mass spectra.

Parameters
  • a (float): Weighting factor for the abundance values. Default is 0.5
  • b (float): Weighting factor for the mass values. Default is 1.3
  • nanfill (float): Value to fill missing mass values with. Default is 1e-10
Returns
  • correlation (float): Weighted cosine correlation between the experimental and reference mass spectra.
def cosine_correlation(self):
318    def cosine_correlation(self):
319        """Calculate the cosine correlation between the experimental and reference mass spectra.
320
321        Returns
322        -------
323        correlation : float
324            Cosine correlation between the experimental and reference mass spectra.
325
326        """
327        # calculate cosine correlation,
328        x = self.zero_filled_u_l[0]
329        y = self.zero_filled_u_l[1]
330
331        # correlation = (1 - cosine(x, y))
332
333        correlation = dot(x, y) / (norm(x) * norm(y))
334
335        return correlation

Calculate the cosine correlation between the experimental and reference mass spectra.

Returns
  • correlation (float): Cosine correlation between the experimental and reference mass spectra.
def stein_scott(self):
337    def stein_scott(self):
338        """Calculate the Stein-Scott similarity between the experimental and reference mass spectra.
339
340        Returns
341        -------
342        s_ss_x_y : float
343            Stein-Scott similarity between the experimental and reference mass spectra.
344        s_ss_x_y_nist : float
345            Stein-Scott similarity between the experimental and reference mass spectra.
346        """
347        # TODO check this code
348        if self.n_x_y == 0:
349            return 0, 0
350
351        # count number of non-zero abundance/peak intensity values
352        n_x = sum(a != 0 for a in self.exp_abun)
353
354        s_r_x_y = 0
355
356        a, b = 1, 0
357
358        for i in range(1, self.n_x_y):
359            current_value = self.common_mz_values[i]
360            previous_value = self.common_mz_values[i - 1]
361
362            y_i = self.ref_mz_abun_dict[current_value]
363            y_i_minus1 = self.ref_mz_abun_dict[previous_value]
364
365            lc_current = power(y_i, a) * power(current_value, b)
366            lc_previous = power(y_i_minus1, a) * power(previous_value, b)
367
368            x_i = self.ms_mz_abun_dict[current_value]
369            x_i_minus1 = self.ms_mz_abun_dict[previous_value]
370
371            uc_current = power(x_i, a) * power(current_value, b)
372            uc_previous = power(x_i_minus1, a) * power(previous_value, b)
373
374            T1 = lc_current / lc_previous
375
376            T2 = uc_previous / uc_current
377
378            temp_computation = T1 * T2
379
380            n = 0
381            if temp_computation <= 1:
382                n = 1
383            else:
384                n = -1
385
386            s_r_x_y = s_r_x_y + power(temp_computation, n)
387
388        # finish the calculation of S_R(X,Y)
389
390        s_r_x_y = s_r_x_y / self.n_x_y
391        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
392        s_wc_x_y = self.weighted_cosine_correlation(a=0.5, b=3, nanfill=0)
393
394        s_ss_x_y = ((n_x * s_wc_x_y) + (self.n_x_y * s_r_x_y)) / (n_x + self.n_x_y)
395
396        s_wc_x_y_nist = self.weighted_cosine_correlation(a=0.5, b=1.3, nanfill=0)
397
398        s_ss_x_y_nist = ((n_x * s_wc_x_y_nist) + (self.n_x_y * s_r_x_y)) / (
399            n_x + self.n_x_y
400        )
401        # final step
402
403        return s_ss_x_y, s_ss_x_y_nist

Calculate the Stein-Scott similarity between the experimental and reference mass spectra.

Returns
  • s_ss_x_y (float): Stein-Scott similarity between the experimental and reference mass spectra.
  • s_ss_x_y_nist (float): Stein-Scott similarity between the experimental and reference mass spectra.
def pearson_correlation(self):
405    def pearson_correlation(
406        self,
407    ):
408        """Calculate the Pearson correlation between the experimental and reference mass spectra.
409
410        Returns
411        -------
412        correlation : float
413            Pearson correlation between the experimental and reference mass spectra.
414        """
415        correlation = pearsonr(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
416
417        return correlation[0]

Calculate the Pearson correlation between the experimental and reference mass spectra.

Returns
  • correlation (float): Pearson correlation between the experimental and reference mass spectra.
def spearman_correlation(self):
419    def spearman_correlation(self):
420        """Calculate the Spearman correlation between the experimental and reference mass spectra.
421
422        Returns
423        -------
424        coorelation : float
425            Spearman correlation between the experimental and reference mass spectra.
426        """
427        # calculate Spearman correlation
428        # ## TODO - Check axis
429        correlation = spearmanr(
430            self.zero_filled_u_l[0], self.zero_filled_u_l[1], axis=0
431        )
432
433        return correlation[0]

Calculate the Spearman correlation between the experimental and reference mass spectra.

Returns
  • coorelation (float): Spearman correlation between the experimental and reference mass spectra.
def kendall_tau(self):
435    def kendall_tau(self):
436        """Calculate the Kendall's tau correlation between the experimental and reference mass spectra.
437
438        Returns
439        -------
440        correlation : float
441            Kendall's tau correlation between the experimental and reference mass spectra."""
442        # create dict['mz'] = abundance, for experimental data
443        # self.ms_mz_abun_dict = mass_spec.mz_abun_dict
444
445        # create dict['mz'] = abundance, for experimental data
446
447        # calculate Kendall's tau
448        correlation = kendalltau(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
449
450        return correlation[0]

Calculate the Kendall's tau correlation between the experimental and reference mass spectra.

Returns
  • correlation (float): Kendall's tau correlation between the experimental and reference mass spectra.
def dft_correlation(self):
452    def dft_correlation(self):
453        """Calculate the DFT correlation between the experimental and reference mass spectra.
454
455        Returns
456        -------
457        correlation : float
458            DFT correlation between the experimental and reference mass spectra.
459        """
460        if self.n_x_y == 0:
461            return 0
462
463        # count number of non-zero abundance/peak intensity values
464        n_x = sum(a != 0 for a in self.exp_abun)
465
466        x, y = self.nan_fill(self.df, fill_with=0)
467
468        x, y = self.normalize(x, y, norm_func=self.normalize_func)
469
470        # get the Fourier transform of x and y
471        x_dft = rfft(x).real
472        y_dft = rfft(y).real
473
474        s_dft_xy = dot(x_dft, y_dft) / (norm(x_dft) * norm(y_dft))
475
476        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
477        s_wc_x_y = self.weighted_cosine_correlation(nanfill=0)
478
479        # final step
480        s_dft = (n_x * s_wc_x_y + self.n_x_y * s_dft_xy) / (n_x + self.n_x_y)
481
482        return s_dft

Calculate the DFT correlation between the experimental and reference mass spectra.

Returns
  • correlation (float): DFT correlation between the experimental and reference mass spectra.
def dwt_correlation(self):
484    def dwt_correlation(self):
485        """Calculate the DWT correlation between the experimental and reference mass spectra.
486
487        Returns
488        -------
489        correlation : float
490            DWT correlation between the experimental and reference mass spectra.
491
492        Notes
493        -----
494        This function requires the PyWavelets library to be installed.
495            This is not a default requirement as this function is not widely used.
496        """
497
498        from pywt import dwt
499
500        if self.n_x_y == 0:
501            return 0
502
503        # count number of non-zero abundance/peak intensity values
504        n_x = sum(a != 0 for a in self.exp_abun)
505
506        # calculate cosine correlation,
507        x, y = self.nan_fill(self.df, fill_with=0)
508
509        x, y = self.normalize(x, y, norm_func=self.normalize_func)
510
511        # Make x and y into an array
512        x_a = list(x)
513        y_a = list(y)
514
515        # get the wavelet transform of x and y (Daubechies with a filter length of 4. Asymmetric. pywavelets function)
516        # Will only use the detail dwt (dwtDd
517        x_dwtD = dwt(x_a, "db2")[1]
518        y_dwtD = dwt(y_a, "db2")[1]
519
520        s_dwt_xy = dot(x_dwtD, y_dwtD) / (norm(x_dwtD) * norm(y_dwtD))
521
522        # using the existing weighted_cosine_correlation function to get S_WC(X,Y)
523        s_wc_x_y = self.weighted_cosine_correlation(nanfill=0)
524
525        # final step
526        s_dwt = (n_x * s_wc_x_y + self.n_x_y * s_dwt_xy) / (n_x + self.n_x_y)
527
528        return s_dwt

Calculate the DWT correlation between the experimental and reference mass spectra.

Returns
  • correlation (float): DWT correlation between the experimental and reference mass spectra.
Notes

This function requires the PyWavelets library to be installed. This is not a default requirement as this function is not widely used.

def euclidean_distance(self):
530    def euclidean_distance(self):
531        """Calculate the Euclidean distance between the experimental and reference mass spectra.
532
533        Returns
534        -------
535        correlation : float
536            Euclidean distance between the experimental and reference mass spectra.
537        """
538        # correlation = euclidean_distance_manual(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
539        qlist = self.zero_filled_u_l[0]
540        rlist = self.zero_filled_u_l[1]
541
542        correlation = sqrt(np_sum(power(qlist - rlist, 2)))
543
544        return correlation

Calculate the Euclidean distance between the experimental and reference mass spectra.

Returns
  • correlation (float): Euclidean distance between the experimental and reference mass spectra.
def manhattan_distance(self):
546    def manhattan_distance(self):
547        """Calculate the Manhattan distance between the experimental and reference mass spectra.
548
549        Returns
550        -------
551        correlation : float
552            Manhattan distance between the experimental and reference mass spectra.
553        """
554        qlist = self.zero_filled_u_l[0]
555        rlist = self.zero_filled_u_l[1]
556
557        return np_sum(absolute(qlist - rlist))

Calculate the Manhattan distance between the experimental and reference mass spectra.

Returns
  • correlation (float): Manhattan distance between the experimental and reference mass spectra.
def jaccard_distance(self):
559    def jaccard_distance(self):
560        """Calculate the Jaccard distance between the experimental and reference mass spectra.
561
562        Returns
563        -------
564        correlation : float
565            Jaccard distance between the experimental and reference mass spectra.
566        """
567
568        def jaccard_similarity(list1, list2):
569            intersection = len(list(set(list1).intersection(list2)))
570            union = (len(list1) + len(list2)) - intersection
571            return float(intersection) / union
572
573        qlist = self.zero_filled_u_l[0]
574        rlist = self.zero_filled_u_l[1]
575
576        return np_sum(power(qlist - rlist, 2)) / (
577            np_sum(power(qlist, 2)) + np_sum(power(rlist, 2)) - np_sum(qlist * rlist)
578        )
579        # correlation = jaccard_similarity(self.zero_filled_u_l[0], self.zero_filled_u_l[1])
580        # @return correlation

Calculate the Jaccard distance between the experimental and reference mass spectra.

Returns
  • correlation (float): Jaccard distance between the experimental and reference mass spectra.
def extra_distances(self):
582    def extra_distances(self):
583        """Function to calculate distances using additional metrics defined in math_distance.py
584
585        Currently, calculates all distances.
586
587        Returns
588        -------
589        dict_res : dict
590            Dictionary containing the distances between the experimental and reference mass spectra.
591
592        """
593        from corems.molecular_id.calc import math_distance
594
595        # qlist = self.zero_filled_u_l[2]
596        # rlist = self.zero_filled_u_l[3]
597
598        dict_res = {}
599
600        for method in methods_name:
601            # function_name = method + "_distance"
602            function_name = method
603            if hasattr(math_distance, function_name):
604                f = getattr(math_distance, function_name)
605
606                if function_name == "canberra_metric":
607                    x, y = self.nan_fill(self.df, fill_with=0)
608
609                    qlist, rlist = self.normalize(x, y, norm_func=self.normalize_func)
610                    # print("qlist:")
611                    # print(qlist)
612                    # print("rlist:")
613                    # print(rlist)
614
615                else:
616                    qlist = self.zero_filled_u_l[0]
617                    rlist = self.zero_filled_u_l[1]
618
619                dist = f(qlist, rlist)
620                # if method == "Minokowski_3":
621                #    print("qlist:")
622                #    print(qlist)
623                #    print("rlist")
624                #    print(rlist)
625                #    exit()
626                # if dist == np.nan or dis == np.inf:
627                # print(self.exp_abun)
628                # print(self.exp_mz)
629                # print(function_name)
630                # print(len(self.exp_abun))
631                # print(len(self.exp_mz))
632                # print(self.zero_filled_u_l[1])
633                dict_res[method] = dist
634
635        return dict_res

Function to calculate distances using additional metrics defined in math_distance.py

Currently, calculates all distances.

Returns
  • dict_res (dict): Dictionary containing the distances between the experimental and reference mass spectra.