Skip to content

Commit d295152

Browse files
committed
Resolve merge conflicts
2 parents 34564dc + 7fe2248 commit d295152

4 files changed

Lines changed: 232 additions & 50 deletions

File tree

corems/mass_spectra/input/rawFileReader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from ThermoFisher.CommonCore.Data.Business import Device
5454
from ThermoFisher.CommonCore.Data.Interfaces import IChromatogramSettings
5555
from ThermoFisher.CommonCore.Data.Business import MassOptions, FileHeaderReaderFactory
56+
from ThermoFisher.CommonCore.Data.Business import Device
5657
from ThermoFisher.CommonCore.Data.FilterEnums import MSOrderType
5758

5859

corems/mass_spectrum/input/baseClass.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,21 @@ def get_dataframe(self) -> DataFrame:
254254
)
255255

256256
elif self.data_type == "pks":
257+
# Predator .pks columns are positional: peak location (m/z), relative
258+
# peak height (normalized 0-100), absolute abundance, resolving power,
259+
# frequency, S/N. Use the absolute abundance as the intensity -- the
260+
# relative peak height is per-spectrum normalized and not comparable
261+
# across spectra, so it is named so header_translate drops it.
257262
names = [
258263
"m/z",
259-
"I",
260-
"Scaled Peak Height",
264+
"Relative Abundance",
265+
"Abundance",
261266
"Resolving Power",
262267
"Frequency",
263-
"S/N",
264-
]
268+
"S/N"
269+
]
270+
271+
265272
clean_data = []
266273
with self.file_location.open() as maglabfile:
267274
for i in maglabfile.readlines()[8:-1]:

corems/molecular_id/factory/classification.py

Lines changed: 121 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -699,65 +699,107 @@ def plot_ms_class(self, classe, color="g"):
699699
return ax
700700

701701
def plot_van_krevelen(
702-
self, classe, max_hc=2.5, max_oc=2, ticks_number=5, color="viridis"
702+
self, classe=None, max_hc=2.5, max_oc=2, ticks_number=5, color="viridis",
703+
alpha=0.5, log_abundance=False
703704
):
704-
"""Plot Van Krevelen Diagram
705+
"""Plot Van Krevelen Diagram for a single class or all assigned classes
705706
706707
Parameters
707708
----------
708-
classe : str
709-
Class name
709+
classe : str, optional
710+
Class name or None to plot all assigned classes, by default None
710711
max_hc : float, optional
711712
Max H/C ratio, by default 2.5
712713
max_oc : float, optional
713714
Max O/C ratio, by default 2
714715
ticks_number : int, optional
715716
Number of ticks, by default 5
716717
color : str, optional
717-
Matplotlib color, by default "viridis"
718+
Matplotlib color/colormap, by default "viridis"
719+
alpha : float, optional
720+
Transparency of points, by default 0.5
721+
log_abundance : bool, optional
722+
If True, use log10 scale for abundance values, by default False
718723
719724
Returns
720725
-------
721726
ax : matplotlib.axes
722727
Matplotlib axes object
723-
abun_perc : float
724-
Class percentile of the relative abundance
728+
abun_perc : float or None
729+
Class percentile of the relative abundance (if classe specified)
725730
"""
726-
if classe != Labels.unassigned:
727-
# get data
731+
import numpy as np
732+
ax = plt.gca()
733+
734+
if classe is not None and classe != Labels.unassigned:
735+
# Single class plot
728736
abun_perc = self.abundance_count_percentile(classe)
729737
hc = self.atoms_ratio(classe, "H", "C")
730738
oc = self.atoms_ratio(classe, "O", "C")
731739
abundance = self.abundance(classe)
732740

733-
# plot data
734-
ax = plt.gca()
741+
if log_abundance:
742+
abundance = [np.log10(a + 1e-10) for a in abundance]
743+
colorbar_label = 'log\u2081\u2080(Abundance)'
744+
else:
745+
colorbar_label = 'Abundance'
735746

736-
ax.scatter(oc, hc, c=abundance, alpha=0.5, cmap=color)
747+
# Sort by abundance so higher values are plotted on top
748+
sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
749+
hc = [hc[i] for i in sorted_indices]
750+
oc = [oc[i] for i in sorted_indices]
751+
abundance = [abundance[i] for i in sorted_indices]
737752

738-
# ax.scatter(carbon_number, dbe, c=color, alpha=0.5)
753+
scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
754+
plt.colorbar(scatter, label=colorbar_label)
739755

740756
title = "%s, %.2f %%" % (classe, abun_perc)
741757
ax.set_title(title)
742-
ax.set_xlabel("O/C", fontsize=16)
743-
ax.set_ylabel("H/C", fontsize=16)
744-
ax.tick_params(axis="both", which="major", labelsize=18)
745-
ax.set_xticks(linspace(0, max_oc, ticks_number, endpoint=True))
746-
ax.set_yticks(linspace(0, max_hc, ticks_number, endpoint=True))
747758

748-
# returns matplot axes obj and the class percentile of the relative abundance
759+
return_val = ax, abun_perc
760+
else:
761+
# All assigned classes plot
762+
hc = self.atoms_ratio_all("H", "C")
763+
oc = self.atoms_ratio_all("O", "C")
764+
abundance = self.abundance_assigned()
765+
766+
if log_abundance:
767+
abundance = [np.log10(a + 1e-10) for a in abundance]
768+
colorbar_label = 'log\u2081\u2080(Abundance)'
769+
else:
770+
colorbar_label = 'Abundance'
771+
772+
sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
773+
hc = [hc[i] for i in sorted_indices]
774+
oc = [oc[i] for i in sorted_indices]
775+
abundance = [abundance[i] for i in sorted_indices]
776+
777+
scatter = ax.scatter(oc, hc, c=abundance, alpha=alpha, cmap=color)
778+
plt.colorbar(scatter, label=colorbar_label)
779+
780+
ax.set_title("Van Krevelen Diagram - All Assigned Classes")
781+
782+
return_val = ax
783+
784+
ax.set_xlabel("O/C", fontsize=16)
785+
ax.set_ylabel("H/C", fontsize=16)
786+
ax.tick_params(axis="both", which="major", labelsize=18)
787+
ax.set_xticks(linspace(0, max_oc, ticks_number, endpoint=True))
788+
ax.set_yticks(linspace(0, max_hc, ticks_number, endpoint=True))
789+
ax.grid(alpha=0.3, linestyle='--')
749790

750-
return ax, abun_perc
791+
return return_val
751792

752793
def plot_dbe_vs_carbon_number(
753-
self, classe, max_c=50, max_dbe=40, dbe_incr=5, c_incr=10, color="viridis"
794+
self, classe=None, max_c=50, max_dbe=40, dbe_incr=5, c_incr=10, color="viridis",
795+
alpha=0.5, log_abundance=False
754796
):
755-
"""Plot DBE vs Carbon Number
797+
"""Plot DBE vs Carbon Number for a single class or all assigned classes
756798
757799
Parameters
758800
----------
759-
classe : str
760-
Class name
801+
classe : str, optional
802+
Class name or None to plot all assigned classes, by default None
761803
max_c : int, optional
762804
Max Carbon Number, by default 50
763805
max_dbe : int, optional
@@ -767,37 +809,76 @@ def plot_dbe_vs_carbon_number(
767809
c_incr : int, optional
768810
Carbon Number increment, by default 10
769811
color : str, optional
770-
Matplotlib color, by default "viridis"
812+
Matplotlib color/colormap, by default "viridis"
813+
alpha : float, optional
814+
Transparency of points, by default 0.5
815+
log_abundance : bool, optional
816+
If True, use log10 scale for abundance values, by default False
771817
772818
Returns
773819
-------
774820
ax : matplotlib.axes
775821
Matplotlib axes object
776-
abun_perc : float
777-
Class percentile of the relative abundance
822+
abun_perc : float or None
823+
Class percentile of the relative abundance (if classe specified)
778824
"""
779-
if classe != Labels.unassigned:
780-
# get data
825+
import numpy as np
826+
ax = plt.gca()
827+
828+
if classe is not None and classe != Labels.unassigned:
829+
# Single class plot
781830
abun_perc = self.abundance_count_percentile(classe)
782831
carbon_number = self.carbon_number(classe)
783832
dbe = self.dbe(classe)
784833
abundance = self.abundance(classe)
785834

786-
# plot data
787-
ax = plt.gca()
835+
if log_abundance:
836+
abundance = [np.log10(a + 1e-10) for a in abundance]
837+
colorbar_label = 'log\u2081\u2080(Abundance)'
838+
else:
839+
colorbar_label = 'Abundance'
788840

789-
ax.scatter(carbon_number, dbe, c=abundance, alpha=0.5, cmap=color)
841+
sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
842+
carbon_number = [carbon_number[i] for i in sorted_indices]
843+
dbe = [dbe[i] for i in sorted_indices]
844+
abundance = [abundance[i] for i in sorted_indices]
790845

791-
# ax.scatter(carbon_number, dbe, c=color, alpha=0.5)
846+
scatter = ax.scatter(carbon_number, dbe, c=abundance, alpha=alpha, cmap=color)
847+
plt.colorbar(scatter, label=colorbar_label)
792848

793849
title = "%s, %.2f %%" % (classe, abun_perc)
794850
ax.set_title(title)
795-
ax.set_xlabel("Carbon number", fontsize=16)
796-
ax.set_ylabel("DBE", fontsize=16)
797-
ax.tick_params(axis="both", which="major", labelsize=18)
798-
ax.set_xticks(range(0, max_c, c_incr))
799-
ax.set_yticks(range(0, max_dbe, dbe_incr))
800851

801-
# returns matplot axes obj and the class percentile of the relative abundance
852+
return_val = ax, abun_perc
853+
else:
854+
# All assigned classes plot
855+
carbon_number = self.carbon_number_all()
856+
dbe = self.dbe_all()
857+
abundance = self.abundance_assigned()
858+
859+
if log_abundance:
860+
abundance = [np.log10(a + 1e-10) for a in abundance]
861+
colorbar_label = 'log\u2081\u2080(Abundance)'
862+
else:
863+
colorbar_label = 'Abundance'
864+
865+
sorted_indices = sorted(range(len(abundance)), key=lambda i: abundance[i])
866+
carbon_number = [carbon_number[i] for i in sorted_indices]
867+
dbe = [dbe[i] for i in sorted_indices]
868+
abundance = [abundance[i] for i in sorted_indices]
869+
870+
scatter = ax.scatter(carbon_number, dbe, c=abundance, alpha=alpha, cmap=color)
871+
plt.colorbar(scatter, label=colorbar_label)
872+
873+
ax.set_title("DBE vs Carbon Number - All Assigned Classes")
874+
875+
return_val = ax
876+
877+
ax.set_xlabel("Carbon number", fontsize=16)
878+
ax.set_ylabel("DBE", fontsize=16)
879+
ax.tick_params(axis="both", which="major", labelsize=18)
880+
ax.set_xticks(range(0, max_c, c_incr))
881+
ax.set_yticks(range(0, max_dbe, dbe_incr))
882+
ax.grid(alpha=0.3, linestyle='--')
802883

803-
return ax, abun_perc
884+
return return_val

tests/test_classification.py

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import sys
22

3+
import matplotlib
4+
matplotlib.use("Agg")
5+
import matplotlib.pyplot as plt
36
import pytest
47

58
from corems.molecular_id.factory.classification import HeteroatomsClassification, Labels
69
from corems.molecular_id.search.molecularFormulaSearch import SearchMolecularFormulas
710

811

9-
@pytest.mark.molecular_db
10-
def test_heteroatoms_classification(mass_spectrum_ftms, postgres_database):
12+
@pytest.fixture
13+
def classified_mass_spectrum(mass_spectrum_ftms, postgres_database):
14+
"""Run molecular formula search and return a HeteroatomsClassification object."""
1115
mass_spectrum_ftms.molecular_search_settings.url_database = postgres_database
1216
mass_spectrum_ftms.molecular_search_settings.error_method = 'None'
13-
mass_spectrum_ftms.molecular_search_settings.min_ppm_error = -10
17+
mass_spectrum_ftms.molecular_search_settings.min_ppm_error = -10
1418
mass_spectrum_ftms.molecular_search_settings.max_ppm_error = 10
1519
mass_spectrum_ftms.molecular_search_settings.mz_error_range = 1
1620
mass_spectrum_ftms.molecular_search_settings.isProtonated = True
@@ -27,16 +31,105 @@ def test_heteroatoms_classification(mass_spectrum_ftms, postgres_database):
2731
# Check if search was successful
2832
assert mass_spectrum_ftms.percentage_assigned()[2] > 0
2933

30-
mass_spectrum_by_classes = HeteroatomsClassification(mass_spectrum_ftms)
34+
return HeteroatomsClassification(mass_spectrum_ftms)
35+
36+
37+
@pytest.mark.molecular_db
38+
def test_heteroatoms_classification(classified_mass_spectrum):
39+
mass_spectrum_by_classes = classified_mass_spectrum
3140

3241
# Check that the plot is created
3342
mass_spectrum_by_classes.plot_ms_assigned_unassigned()
3443

3544
# Check that ratios, DBE, carbon number, abundance and mz_exp are calculated
36-
45+
3746
assert len(mass_spectrum_by_classes.atoms_ratio_all("H", "C")) > 0
3847
assert len(mass_spectrum_by_classes.dbe_all()) > 0
3948
assert len(mass_spectrum_by_classes.abundance_assigned()) > 0
4049
assert len(mass_spectrum_by_classes.mz_exp_assigned()) > 0
4150
assert mass_spectrum_by_classes.abundance_count_percentile(Labels.unassigned) > 0
42-
assert mass_spectrum_by_classes.peaks_count_percentile(Labels.unassigned) > 0
51+
assert mass_spectrum_by_classes.peaks_count_percentile(Labels.unassigned) > 0
52+
53+
54+
@pytest.mark.molecular_db
55+
def test_plot_van_krevelen_single_class(classified_mass_spectrum):
56+
"""Test Van Krevelen plot for a single heteroatom class (backward compat)."""
57+
mass_spectrum = classified_mass_spectrum
58+
# Get a valid class name from the assigned classes
59+
assigned_classes = [c for c in mass_spectrum.get_classes() if c != Labels.unassigned]
60+
assert len(assigned_classes) > 0, "No assigned classes found"
61+
62+
plt.figure()
63+
result = mass_spectrum.plot_van_krevelen(assigned_classes[0])
64+
ax, abun_perc = result
65+
assert ax is not None
66+
assert abun_perc > 0
67+
assert assigned_classes[0] in ax.get_title()
68+
assert ax.get_xlabel() == "O/C"
69+
assert ax.get_ylabel() == "H/C"
70+
plt.close()
71+
72+
73+
@pytest.mark.molecular_db
74+
def test_plot_van_krevelen_all_classes(classified_mass_spectrum):
75+
"""Test Van Krevelen plot for all assigned classes (new functionality)."""
76+
plt.figure()
77+
ax = classified_mass_spectrum.plot_van_krevelen()
78+
assert ax is not None
79+
assert "All Assigned Classes" in ax.get_title()
80+
assert ax.get_xlabel() == "O/C"
81+
assert ax.get_ylabel() == "H/C"
82+
plt.close()
83+
84+
85+
@pytest.mark.molecular_db
86+
def test_plot_van_krevelen_log_abundance(classified_mass_spectrum):
87+
"""Test Van Krevelen plot with log10 abundance scaling."""
88+
plt.figure()
89+
ax = classified_mass_spectrum.plot_van_krevelen(log_abundance=True)
90+
assert ax is not None
91+
# Check that colorbar has log label
92+
cbar = ax.figure.axes[-1] # colorbar is the last axes
93+
assert "log" in cbar.get_ylabel().lower() or "log" in cbar.get_ylabel()
94+
plt.close()
95+
96+
97+
@pytest.mark.molecular_db
98+
def test_plot_dbe_vs_carbon_number_single_class(classified_mass_spectrum):
99+
"""Test DBE vs Carbon Number plot for a single class (backward compat)."""
100+
mass_spectrum = classified_mass_spectrum
101+
assigned_classes = [c for c in mass_spectrum.get_classes() if c != Labels.unassigned]
102+
assert len(assigned_classes) > 0
103+
104+
plt.figure()
105+
result = mass_spectrum.plot_dbe_vs_carbon_number(assigned_classes[0])
106+
ax, abun_perc = result
107+
assert ax is not None
108+
assert abun_perc > 0
109+
assert assigned_classes[0] in ax.get_title()
110+
assert ax.get_xlabel() == "Carbon number"
111+
assert ax.get_ylabel() == "DBE"
112+
plt.close()
113+
114+
115+
@pytest.mark.molecular_db
116+
def test_plot_dbe_vs_carbon_number_all_classes(classified_mass_spectrum):
117+
"""Test DBE vs Carbon Number plot for all assigned classes (new functionality)."""
118+
plt.figure()
119+
ax = classified_mass_spectrum.plot_dbe_vs_carbon_number()
120+
assert ax is not None
121+
assert "All Assigned Classes" in ax.get_title()
122+
assert ax.get_xlabel() == "Carbon number"
123+
assert ax.get_ylabel() == "DBE"
124+
plt.close()
125+
126+
127+
@pytest.mark.molecular_db
128+
def test_plot_dbe_vs_carbon_number_log_abundance(classified_mass_spectrum):
129+
"""Test DBE vs Carbon Number plot with log10 abundance scaling."""
130+
plt.figure()
131+
ax = classified_mass_spectrum.plot_dbe_vs_carbon_number(log_abundance=True)
132+
assert ax is not None
133+
cbar = ax.figure.axes[-1]
134+
assert "log" in cbar.get_ylabel().lower() or "log" in cbar.get_ylabel()
135+
plt.close()

0 commit comments

Comments
 (0)