import ipywidgets as widgets
import matplotlib.colors as mcolors
import numpy as np
import pandas as pd
from IPython.display import display, clear_output
from ipywidgets import Output
from pyccapt.calibration.core import ion_selection, mc_plot
from pyccapt.calibration.core.mc_plot_peak_helpers import gaussian_mrp_report
[docs]
def call_ion_selection(variables, colab=False, show_gaussian_controls=False):
out = Output()
output2 = Output()
output3 = Output()
bin_size = widgets.FloatText(value=0.1, description='Bin size:')
prominence = widgets.IntText(value=50, description='Peak prominence:')
distance = widgets.IntText(value=1, description='Peak distance:')
lim_tof = widgets.IntText(value=400, description='Lim tof/mc:')
percent = widgets.IntText(value=50, description='Percent MRP:')
index_fig = widgets.IntText(value=1, description='Fig index:')
plot_peak = widgets.Dropdown(options=[('True', True), ('False', False)], description='Plot peak:')
save_fig = widgets.Dropdown(options=[('False', False), ('True', True)], description='Save fig:')
mrp_left = widgets.FloatText(value=0.0, description='MRP left:')
mrp_right = widgets.FloatText(value=0.0, description='MRP right:')
load_mrp_window_button = widgets.Button(description='Load peak range')
gaussian_mrp_button = widgets.Button(description='MRP')
def _resolve_gaussian_window():
left = float(mrp_left.value)
right = float(mrp_right.value)
if right > left:
return left, right
if getattr(variables, 'selected_x2', 0) > getattr(variables, 'selected_x1', 0):
return float(variables.selected_x1), float(variables.selected_x2)
if getattr(variables, 'h_line_pos', None):
lines = sorted(float(x) for x in variables.h_line_pos)
lower = [x for x in lines if x < peak_val.value]
upper = [x for x in lines if x > peak_val.value]
if lower and upper:
return max(lower), min(upper)
if getattr(variables, 'peak_widths', None) is not None and getattr(variables, 'peak_y', None) is not None:
try:
idx = int(np.argmax(np.asarray(variables.peak_y)))
x_hist = np.asarray(variables.x_hist)
left_idx = int(round(variables.peak_widths[2][idx]))
right_idx = int(round(variables.peak_widths[3][idx]))
if 0 <= left_idx < len(x_hist) and 0 <= right_idx < len(x_hist):
left = float(x_hist[left_idx])
right = float(x_hist[right_idx])
if right > left:
return left, right
except Exception:
pass
return None
def _current_hist_array():
return variables.mc
def _print_gaussian_report(result):
print('=' * 60)
print('PEAK PROFILE MRP REPORT')
print('=' * 60)
print(f'MRP model: {result["recommended_label"]}')
print(f'MRP bin size used: {result["bin_size"]} ({result["num_bins"]} bins)')
print(f'Peak position: {result["peak_position"]:.4f}')
print(f'Ions in range: {result["num_ions"]:,}')
print(f'Recommended FWHM MRP: {result["formatted_recommended_mrp"][0]}')
if result["window_warning"]:
print(result["window_warning"])
print()
print('Gaussian fit MRP:' if result['gaussian_ok'] else 'Gaussian fit FAILED')
if result['gaussian_ok']:
print(f' MRP(0.5) = {result["formatted_gaussian_mrp"][0]}')
print(f' MRP(0.1) = {result["formatted_gaussian_mrp"][1]}')
print(f' MRP(0.01) = {result["formatted_gaussian_mrp"][2]}')
print()
print('Voigt fit MRP:' if result['voigt_ok'] else 'Voigt fit FAILED')
if result['voigt_ok']:
print(f' MRP(0.5) = {result["formatted_voigt_mrp"][0]}')
print(f' MRP(0.1) = {result["formatted_voigt_mrp"][1]}')
print(f' MRP(0.01) = {result["formatted_voigt_mrp"][2]}')
print(f' Voigt FWHM = {result["voigt_fwhm"]:.6f}')
print()
print('Asymmetric (err*expDecay) fit MRP:' if result.get('asymmetric_ok') else 'Asymmetric (err*expDecay) fit FAILED')
if result.get('asymmetric_ok'):
print(f' MRP(0.5) = {result["formatted_asymmetric_mrp"][0]}')
print(f' MRP(0.1) = {result["formatted_asymmetric_mrp"][1]}')
print(f' MRP(0.01) = {result["formatted_asymmetric_mrp"][2]}')
asym_fwhm = result.get('asymmetric_fwhm', float('nan'))
if np.isfinite(asym_fwhm):
print(f' Asymmetric FWHM = {asym_fwhm:.6f}')
print()
print('Histogram-based MRP:')
print(f' MRP(0.5) = {result["formatted_histogram_mrp"][0]}')
print(f' MRP(0.1) = {result["formatted_histogram_mrp"][1]}')
print(f' MRP(0.01) = {result["formatted_histogram_mrp"][2]}')
print('=' * 60)
def load_gaussian_window(b):
window = _resolve_gaussian_window()
with out:
if window is None:
print('No peak window is available yet. Draw range lines or plot/select a peak first.')
else:
mrp_left.value, mrp_right.value = window
print(f'Loaded Gaussian MRP window: ({mrp_left.value:.4f}, {mrp_right.value:.4f})')
def run_gaussian_mrp(b):
gaussian_mrp_button.disabled = True
try:
with out:
window = _resolve_gaussian_window()
if window is None:
print('No valid peak window found. Set MRP left/right or load the current peak range.')
else:
mrp_left.value, mrp_right.value = window
result = gaussian_mrp_report(_current_hist_array(), mrp_left.value, mrp_right.value, bin_size=0.001)
if result is None:
print('Gaussian MRP: insufficient data in selected range')
else:
_print_gaussian_report(result)
finally:
gaussian_mrp_button.disabled = False
load_mrp_window_button.on_click(load_gaussian_window)
gaussian_mrp_button.on_click(run_gaussian_mrp)
def hist_plot_p(variables, out):
with out:
clear_output(True)
# clear the peak_idx
variables.peaks_idx = []
try:
mc_plot.hist_plot(
variables,
bin_size.value,
log=True,
target='mc',
normalize=False,
prominence=prominence.value,
distance=distance.value,
percent=percent.value,
selector='peak',
figname=index_fig.value,
lim=lim_tof.value,
peaks_find_plot=plot_peak.value,
print_info=False,
save_fig=save_fig.value,
compute_mrp=False,
)
except Exception as exc:
print('=============================')
print('Histogram was plotted, but a later step failed.')
print(f'Reason: {type(exc).__name__}: {exc}.')
print('If you intended peak finding, try lowering "Peak prominence" or "Peak distance".')
print('=============================')
def hist_plot_r(variables, out):
with out:
clear_output(True)
print('=============================')
print('Press left click to draw a line')
print('Press right click to remove a line')
print('Press r to remove all the line')
print('Hold shift and use mouse scroll for zooming on x axis')
print('Hold ctrl and left mouse bottom to move a line')
print('=============================')
try:
mc_plot.hist_plot(
variables,
bin_size.value,
log=True,
target='mc',
normalize=False,
prominence=prominence.value,
distance=distance.value,
percent=percent.value,
selector='range',
figname=index_fig.value,
lim=lim_tof.value,
peaks_find_plot=True,
ranging_mode=True,
save_fig=False,
print_info=False,
compute_mrp=False,
)
except Exception as exc:
print('=============================')
print('Histogram was plotted, but a later step failed.')
print(f'Reason: {type(exc).__name__}: {exc}.')
print('If you intended peak finding, try lowering "Peak prominence" or "Peak distance".')
print('=============================')
##############################################
# element calculate
peak_val = widgets.FloatText(value=1.1, description='Peak value:')
mass_difference = widgets.FloatText(value=2, description='Mass range:')
charge = widgets.Dropdown(
options=[('1', 1), ('2', 2), ('3', 3), ('4', 4), ('5', 5), ('6', 6)], value=3, description='Charge:'
)
aboundance_threshold = widgets.FloatText(value=0.0, description='Threshold aboundance:', min=0, max=1, step=0.1)
num_element = widgets.IntText(value=5, description='Num element:')
# formula calculate
formula_m = widgets.Text(
value='{12}C1{16}O2', placeholder='Type a formula {12}C1{16}O2', description='Isotope formula:', disabled=False
)
molecule_charge = widgets.Dropdown(
options=[('1', 1), ('2', 2), ('3', 3), ('4', 4), ('5', 5), ('6', 6)], value=3, description='Charge:'
)
# molecule create
formula_com = widgets.Text(value='', placeholder="H, O", description='Elements:', disabled=False)
complexity = widgets.Dropdown(
options=[('1', 1), ('2', 2), ('3', 3), ('4', 4), ('5', 5), ('6', 6)], value=3, description='Complexity:'
)
charge_com = widgets.Dropdown(
options=[('1', 1), ('2', 2), ('3', 3), ('4', 4), ('5', 5), ('6', 6)], value=3, description='Charge:'
)
##############################################
plot_button_p = widgets.Button(
description='Plot hist',
)
plot_button_r = widgets.Button(
description='Plot hist',
)
plot_button = widgets.Button(
description='Plot hist',
)
find_elem_button = widgets.Button(
description='Find element',
)
plot_element = widgets.Button(
description='Plot element',
)
formula_button = widgets.Button(
description='Manual formula',
)
add_ion_button = widgets.Button(
description='Add ion',
)
romove_ion_button = widgets.Button(
description='Remove ion',
)
show_color = widgets.Button(
description='Show color',
)
change_color = widgets.Button(
description='Change color',
)
change_row = widgets.Button(
description='Change row',
)
del_row_index = widgets.IntText(value=0, description='Del. row:')
delete_row_button = widgets.Button(description='Delete')
color_picker = widgets.ColorPicker(description='Select a color:')
row_index = widgets.IntText(value=0, description='Index row:')
plot_button_p.on_click(lambda b: plot_on_click_p(b, variables, out))
def plot_on_click_p(b, variables, out):
plot_button_p.disabled = True
try:
hist_plot_p(variables, out)
finally:
plot_button_p.disabled = False
plot_button_r.on_click(lambda b: plot_on_click_r(b, variables, out))
def plot_on_click_r(b, variables, out):
plot_button_r.disabled = True
try:
hist_plot_r(variables, out)
finally:
plot_button_r.disabled = False
def plot_found_element(b, variables):
variables.AptHistPlotter.plot_founded_range_loc(variables.ions_list_data, remove_lines=False)
plot_element.on_click(lambda b: plot_found_element(b, variables))
def vol_on_click(b, variables, output2):
with output2:
clear_output(True)
df1 = ion_selection.load_elements(formula_com.value, aboundance_threshold.value, charge.value, variables=variables)
df2 = ion_selection.molecule_create(
formula_com.value, complexity.value, charge.value, aboundance_threshold.value, variables
)
df3 = ion_selection.find_closest_elements(
peak_val.value, num_element.value, aboundance_threshold.value, charge.value, variables=variables
)
df = pd.concat([df1, df2, df3], axis=0)
df = df[(df['abundance'] >= aboundance_threshold.value)]
df = df[abs(df['mass'] - peak_val.value) <= mass_difference.value]
df = ion_selection.rank_candidate_assignments(df, target_mass=peak_val.value, variables=variables)
df = df.iloc[(df['mass'] - peak_val.value).abs().argsort(kind='stable')]
df.reset_index(drop=True, inplace=True)
df = ion_selection.rank_candidate_assignments(df, target_mass=peak_val.value, variables=variables)
df = df.head(num_element.value).reset_index(drop=True)
variables.range_data_backup = df.copy()
variables.ions_list_data = df.copy()
if df.empty:
print('No matching atoms or molecules were found in the requested mass window.')
else:
display(df)
find_elem_button.on_click(lambda b: vol_on_click(b, variables, output2))
formula_button.on_click(lambda b: manual_formula(b, variables, output2))
def manual_formula(b, variables, output2):
with output2:
if formula_m.value == '':
print("Input is empty. Type the formula.")
else:
df = ion_selection.molecule_manual(formula_m.value, molecule_charge.value, latex=True, variables=variables)
clear_output(True)
display(df)
add_ion_button.on_click(lambda b: add_ion_to_range_dataset(b, variables, output3))
def add_ion_to_range_dataset(b, variables, output3):
ion_selection.ranging_dataset_create(variables, row_index.value, peak_val.value)
with output3:
clear_output(True)
display(variables.range_data)
romove_ion_button.on_click(lambda b: remove_ion_to_range_dataset(b, variables, output3))
def remove_ion_to_range_dataset(b, variables, output3):
if len(variables.range_data) >= 1:
variables.range_data = variables.range_data.drop(len(variables.range_data) - 1)
with output3:
clear_output(True)
display(variables.range_data)
show_color.on_click(lambda b: show_color_ions(b, variables, output3))
def show_color_ions(b, variables, output3):
with output3:
clear_output(True)
display(variables.range_data.style.applymap(ion_selection.display_color, subset=['color']))
change_color.on_click(lambda b: change_color_m(b, variables, output3))
def change_color_m(b, variables, output3):
with output3:
selected_color = mcolors.to_hex(color_picker.value)
variables.range_data.at[row_index.value, 'color'] = selected_color
clear_output(True)
display(variables.range_data.style.applymap(ion_selection.display_color, subset=['color']))
# Create "Next" and "Previous" buttons
start_button = widgets.Button(description="Start")
next_button = widgets.Button(description="Next")
prev_button = widgets.Button(description="Previous")
reset_zoom_button = widgets.Button(description="Reset zoom")
all_peaks_button = widgets.Button(description="Add all peaks")
# Define button click events
start_button.on_click(lambda b: start_peak(b, variables))
change_row.on_click(lambda b: move_and_sort_dataframe(b, variables, row_index_source.value, row_index_dest.value, output3))
delete_row_button.on_click(lambda b: delete_row_from_range_dataset(b, variables, del_row_index.value, output3))
row_index_source = widgets.IntText(value=0, description='Target index:')
row_index_dest = widgets.IntText(value=0, description='Destination index:')
def delete_row_from_range_dataset(b, variables, row_to_delete, output3):
with output3:
if variables.range_data.empty:
clear_output(True)
print('No rows to delete.')
return
if row_to_delete < 0 or row_to_delete >= len(variables.range_data):
clear_output(True)
print(f'Invalid Del. row index: {row_to_delete}. Valid range is 0 to {len(variables.range_data) - 1}.')
display(variables.range_data)
return
variables.range_data = variables.range_data.drop(index=row_to_delete).reset_index(drop=True)
clear_output(True)
display(variables.range_data)
def move_and_sort_dataframe(b, variables, row_index, destination_index, output3):
# Check if the indices are valid
with output3:
if (
row_index not in variables.range_data.index
or destination_index < 0
or destination_index >= len(variables.range_data.index)
):
print("Invalid indices provided.")
return variables.range_data
# Move the row to the destination index
row_to_move = variables.range_data.loc[row_index]
variables.range_data = variables.range_data.drop(row_index)
variables.range_data = pd.concat(
[
variables.range_data.iloc[:destination_index],
pd.DataFrame([row_to_move]),
variables.range_data.iloc[destination_index:],
]
)
# Sort the DataFrame based on index
variables.range_data.reset_index(drop=True, inplace=True)
clear_output(True)
display(variables.range_data)
def _ensure_range_plot(variables, out):
"""Make sure the active figure has the range-mode click handler wired up.
Why: clicks only draw the blue left/right lines when ``hist_plot`` was
called with ``selector='range'`` (which attaches ``line_manager``). If the
visible plotter came from the Peak Finder tab, clicks go to the
annotation finder instead and silently do nothing away from peaks.
"""
plotter = getattr(variables, 'AptHistPlotter', None)
if plotter is None or getattr(plotter, 'line_manager', None) is None:
hist_plot_r(variables, out)
def start_peak(b, variables):
_ensure_range_plot(variables, out)
variables.h_line_pos = []
print('=============================')
print('Press left click to draw a line')
print('Press right click to remove a line')
print('Press r to remove all the line')
print('Press a to automatically draw lines')
print('Hold shift and use mouse scroll for zooming on x axis')
print('Hold ctrl and left mouse bottom to move a line')
print('=============================')
variables.peaks_index = 0
peak_val.value = variables.peaks_x_selected[variables.peaks_index]
print('peak idc:', variables.peaks_index, 'Peak location:', peak_val.value)
variables.AptHistPlotter.zoom_to_x_range(x_min=peak_val.value - 5, x_max=peak_val.value + 5, reset=False)
variables.AptHistPlotter.change_peak_color(peak_val.value, dx=0.2)
# reset the range data backup
variables.range_data_backup = pd.DataFrame()
next_button.on_click(lambda b: next_peak(b, variables))
def next_peak(b, variables):
_ensure_range_plot(variables, out)
variables.peaks_index += 1
if variables.peaks_index >= len(variables.peaks_x_selected):
variables.peaks_index = 0
peak_val.value = variables.peaks_x_selected[variables.peaks_index]
print('peak idc:', variables.peaks_index, 'Peak location:', peak_val.value)
variables.AptHistPlotter.zoom_to_x_range(x_min=peak_val.value - 5, x_max=peak_val.value + 5, reset=False)
variables.AptHistPlotter.change_peak_color(peak_val.value, dx=0.2)
if variables.AptHistPlotter.line_manager is not None:
variables.AptHistPlotter.line_manager.remove_all_lines()
# reset the range data backup
variables.range_data_backup = pd.DataFrame()
prev_button.on_click(lambda b: prev_peak(b, variables))
def prev_peak(b, variables):
_ensure_range_plot(variables, out)
variables.peaks_index -= 1
peak_val.value = variables.peaks_x_selected[variables.peaks_index]
print('peak idc:', variables.peaks_index, 'Peak location:', peak_val.value)
variables.AptHistPlotter.zoom_to_x_range(x_min=peak_val.value - 5, x_max=peak_val.value + 5, reset=False)
variables.AptHistPlotter.change_peak_color(peak_val.value, dx=0.2)
if variables.AptHistPlotter.line_manager is not None:
variables.AptHistPlotter.line_manager.remove_all_lines()
reset_zoom_button.on_click(lambda b: rest_h_line(b, variables))
def rest_h_line(b, variables):
variables.AptHistPlotter.zoom_to_x_range(x_min=0, x_max=0, reset=True)
all_peaks_button.on_click(lambda b: select_all_peaks(b, variables))
def select_all_peaks(b, variables):
variables.peaks_x_selected = variables.peak_x
variables.peaks_index_list = [i for i in range(len(variables.peak_x))]
gaussian_controls = widgets.VBox(
[
widgets.HBox([mrp_left, mrp_right]),
widgets.HBox([load_mrp_window_button, gaussian_mrp_button]),
]
)
tab1 = widgets.VBox(
[
bin_size,
index_fig,
prominence,
distance,
lim_tof,
percent,
plot_peak,
save_fig,
widgets.HBox([plot_button_p, all_peaks_button]),
]
)
tab2 = widgets.VBox(
[
bin_size,
index_fig,
prominence,
distance,
lim_tof,
percent,
widgets.HBox([widgets.VBox([plot_button_r, start_button, next_button, prev_button, reset_zoom_button])]),
]
)
tab4 = widgets.VBox(
[
widgets.HBox(
[
widgets.VBox(
[
peak_val,
charge,
aboundance_threshold,
mass_difference,
num_element,
formula_com,
complexity,
find_elem_button,
plot_element,
]
),
widgets.VBox([formula_m, molecule_charge, formula_button]),
widgets.VBox([row_index, color_picker, add_ion_button, romove_ion_button, show_color, change_color]),
widgets.VBox([del_row_index, delete_row_button, row_index_source, row_index_dest, change_row]),
]
)
]
)
if not colab:
tabs1 = widgets.Tab([tab1, tab2])
tabs2 = widgets.Tab([tab4])
tabs1.set_title(0, 'peak finder')
tabs1.set_title(1, 'rangging')
tabs2.set_title(0, 'element finder')
# Create two Output widgets to capture the output of each plot
out = Output()
output2 = Output()
output3 = Output()
# Create an HBox to display the buttons side by side
buttons_layout = widgets.HBox([tabs1, tabs2])
# Create a VBox to display the output widgets below the buttons
output_layout = widgets.HBox([out, widgets.VBox([output3, output2])])
controls_layout = widgets.VBox([buttons_layout, gaussian_controls]) if show_gaussian_controls else buttons_layout
# Display the buttons and the output widgets
display(controls_layout, output_layout)
with output3:
display(variables.range_data)
else:
# Define the content for each tab
tab_contents = {"Peak Finder": tab1, "Rangging": tab2, "Element Finder": tab4}
# Create buttons for each "tab"
buttons = [widgets.Button(description=title) for title in tab_contents.keys()]
# Output widgets to display the corresponding content
out = widgets.Output()
out_tab = widgets.Output()
output2 = widgets.Output()
output3 = widgets.Output()
# Function to handle button clicks
def on_button_click(title):
def handler(change):
with out:
clear_output(wait=True)
with out_tab:
clear_output(wait=True)
display(tab_contents[title])
return handler
# Attach handlers to buttons
for button in buttons:
button.on_click(on_button_click(button.description))
# Layout for buttons and outputs
buttons_layout = widgets.HBox(buttons)
output_layout = widgets.HBox([widgets.VBox([out_tab, out]), widgets.VBox([output3, output2])])
controls_layout = widgets.VBox([buttons_layout, gaussian_controls]) if show_gaussian_controls else buttons_layout
# Display the buttons and output areas
display(controls_layout, output_layout)
# Initial display
with out_tab:
display(tab_contents["Peak Finder"]) # Default to the first "tab" content
with output3:
display(variables.range_data)