@@ -1489,12 +1489,14 @@ def clean_ms1_report(self, ms1_summary_full):
14891489 self .get_isotope_type (f ) for f in ms1_summary ["ion_formula" ].tolist ()
14901490 ]
14911491
1492- # Reorder columns
1492+ # Keep MS1 neutral Molecular Formula so it is not confused with library formulas
1493+ # from MS2 spectral metadata.
14931494 ms1_summary = ms1_summary [
14941495 [
14951496 "mf_id" ,
14961497 "ion_formula" ,
14971498 "isotopologue_type" ,
1499+ "Molecular Formula" ,
14981500 "Calculated m/z" ,
14991501 "m/z Error (ppm)" ,
15001502 "m/z Error Score" ,
@@ -1580,16 +1582,20 @@ def clean_ms2_report(self, metabolite_summary):
15801582 The cleaned metabolomics summary DataFrame.
15811583 """
15821584 metabolite_summary = metabolite_summary .reset_index ()
1583- metabolite_summary ["ion_formula" ] = [
1585+ # Library (MS2 metadata) formulas — keep separate from MS1 search fields
1586+ metabolite_summary ["library_ion_formula" ] = [
15841587 self .get_ion_formula (f , a )
15851588 for f , a in zip (metabolite_summary ["formula" ], metabolite_summary ["ref_ion_type" ])
15861589 ]
1590+ metabolite_summary = metabolite_summary .rename (
1591+ columns = {"formula" : "library_formula" }
1592+ )
15871593
15881594 col_order = [
15891595 "mf_id" ,
1590- "ion_formula " ,
1596+ "library_ion_formula " ,
15911597 "ref_ion_type" ,
1592- "formula " ,
1598+ "library_formula " ,
15931599 "inchikey" ,
15941600 "name" ,
15951601 "inchi" ,
@@ -1631,6 +1637,14 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report):
16311637 The MS1 annotation report DataFrame.
16321638 ms2_annot_report : DataFrame
16331639 The MS2 annotation report DataFrame.
1640+
1641+ Notes
1642+ -----
1643+ ``Molecular Formula`` / ``Ion Formula`` come only from MS1 molecular
1644+ formula search. MS2 spectral library formulas are exposed as
1645+ ``Library Molecular Formula`` / ``Library Ion Formula``.
1646+ When MS1 and MS2 match (same ion formula), the library formula columns
1647+ are cleared so they do not repeat the MS1 values on that row.
16341648 """
16351649 # If there is an ms1_annot_report, merge it with the mf_report
16361650 if ms1_annot_report is not None and not ms1_annot_report .empty :
@@ -1642,24 +1656,31 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report):
16421656 on = ["mf_id" , "isotopologue_type" ],
16431657 )
16441658 if ms2_annot_report is not None :
1645- # If both reports contain 'ion_formula', prefer a merge that respects it.
1646- # Otherwise fall back to merging on 'mf_id' only to remain robust when
1647- # MS1 formula assignment wasn't performed or MS2 summary lacks the field.
1648- if "ion_formula" in mf_report .columns and "ion_formula" in ms2_annot_report .columns :
1659+ # Prefer joining MS2 hits to matching MS1 ion formula when both exist.
1660+ # MS2 uses library_ion_formula so MS1 Ion Formula is not overwritten.
1661+ if (
1662+ "ion_formula" in mf_report .columns
1663+ and "library_ion_formula" in ms2_annot_report .columns
1664+ ):
16491665 # pull out the records without ion_formula and merge on mf_id only
16501666 mf_no_ion_formula = mf_report [mf_report ["ion_formula" ].isna ()]
1651- mf_no_ion_formula = mf_no_ion_formula .drop (columns = ["ion_formula" ]) if "ion_formula" in mf_no_ion_formula .columns else mf_no_ion_formula
1667+ mf_no_ion_formula = (
1668+ mf_no_ion_formula .drop (columns = ["ion_formula" ])
1669+ if "ion_formula" in mf_no_ion_formula .columns
1670+ else mf_no_ion_formula
1671+ )
16521672 mf_no_ion_formula = pd .merge (
16531673 mf_no_ion_formula , ms2_annot_report , how = "left" , on = ["mf_id" ]
16541674 )
16551675
1656- # pull out the records with ion_formula and merge on mf_id + ion_formula
1676+ # pull out the records with ion_formula and merge on mf_id + formula match
16571677 mf_with_ion_formula = mf_report [~ mf_report ["ion_formula" ].isna ()]
16581678 mf_with_ion_formula = pd .merge (
16591679 mf_with_ion_formula ,
16601680 ms2_annot_report ,
16611681 how = "left" ,
1662- on = ["mf_id" , "ion_formula" ],
1682+ left_on = ["mf_id" , "ion_formula" ],
1683+ right_on = ["mf_id" , "library_ion_formula" ],
16631684 )
16641685
16651686 # put back together
@@ -1688,8 +1709,9 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report):
16881709 "mass_spectrum_deconvoluted_parent" : "Is Largest Ion after Deconvolution" ,
16891710 "associated_mass_features" : "Associated Mass Features after Deconvolution" ,
16901711 "ion_formula" : "Ion Formula" ,
1691- "formula" : "Molecular Formula" ,
1692- "ref_ion_type" : "Ion Type" ,
1712+ "library_ion_formula" : "Library Ion Formula" ,
1713+ "library_formula" : "Library Molecular Formula" ,
1714+ "ref_ion_type" : "Library Ion Type" ,
16931715 "annot_level" : "Lipid Annotation Level" ,
16941716 "lipid_molecular_species_id" : "Lipid Molecular Species" ,
16951717 "lipid_summed_name" : "Lipid Species" ,
@@ -1701,6 +1723,35 @@ def combine_reports(self, mf_report, ms1_annot_report, ms2_annot_report):
17011723 "n_spectra_contributing" : "Spectra with Annotation (n)" ,
17021724 }
17031725 mf_report = mf_report .rename (columns = rename_dict )
1726+
1727+ # When MS1 and MS2 co-annotate the same ion/molecular formula, library formula
1728+ # columns only repeat MS1 fields — clear them so the row is not redundant.
1729+ # Keep Library * columns populated for MS2-only (or mismatched) hits.
1730+ if (
1731+ "Ion Formula" in mf_report .columns
1732+ and "Library Ion Formula" in mf_report .columns
1733+ ):
1734+ ion_match = (
1735+ mf_report ["Ion Formula" ].notna ()
1736+ & mf_report ["Library Ion Formula" ].notna ()
1737+ & (mf_report ["Ion Formula" ] == mf_report ["Library Ion Formula" ])
1738+ )
1739+ mf_report .loc [ion_match , "Library Ion Formula" ] = pd .NA
1740+ if "Library Molecular Formula" in mf_report .columns :
1741+ mf_report .loc [ion_match , "Library Molecular Formula" ] = pd .NA
1742+ elif (
1743+ "Molecular Formula" in mf_report .columns
1744+ and "Library Molecular Formula" in mf_report .columns
1745+ ):
1746+ mol_match = (
1747+ mf_report ["Molecular Formula" ].notna ()
1748+ & mf_report ["Library Molecular Formula" ].notna ()
1749+ & (mf_report ["Molecular Formula" ] == mf_report ["Library Molecular Formula" ])
1750+ )
1751+ mf_report .loc [mol_match , "Library Molecular Formula" ] = pd .NA
1752+ if "Library Ion Formula" in mf_report .columns :
1753+ mf_report .loc [mol_match , "Library Ion Formula" ] = pd .NA
1754+
17041755 mf_report ["Sample Name" ] = self .mass_spectra .sample_name
17051756 mf_report ["Polarity" ] = self .mass_spectra .polarity
17061757 mf_report = mf_report [
@@ -2065,18 +2116,20 @@ def clean_ms2_report(self, lipid_summary):
20652116 The cleaned lipid summary DataFrame.
20662117 """
20672118 lipid_summary = lipid_summary .reset_index ()
2068- lipid_summary ["ion_formula" ] = [
2119+ # Library (MS2 metadata) formulas — keep separate from MS1 search fields
2120+ lipid_summary ["library_ion_formula" ] = [
20692121 self .get_ion_formula (f , a )
20702122 for f , a in zip (lipid_summary ["formula" ], lipid_summary ["ref_ion_type" ])
20712123 ]
2124+ lipid_summary = lipid_summary .rename (columns = {"formula" : "library_formula" })
20722125
20732126 # Reorder columns
20742127 lipid_summary = lipid_summary [
20752128 [
20762129 "mf_id" ,
2077- "ion_formula " ,
2130+ "library_ion_formula " ,
20782131 "ref_ion_type" ,
2079- "formula " ,
2132+ "library_formula " ,
20802133 "annot_level" ,
20812134 "lipid_molecular_species_id" ,
20822135 "lipid_summed_name" ,
0 commit comments