Implemented more multithreaded precautions in precalculations and preprocessing pipeline, added concurrency tests and validations on several datacentric functions, refractored preprocessing pipeline, added visual upgrades to the UI and a clear session and bug report buttons

This commit is contained in:
Pixelguy14 2026-02-13 13:42:41 -06:00
parent 4414671a30
commit df94d50954
13 changed files with 1328 additions and 3337 deletions

File diff suppressed because it is too large Load Diff

444
app.jl
View File

@ -8,6 +8,7 @@ using Libz
using PlotlyBase
using CairoMakie
using Colors
using Dates
using MSI_src # Import the new MSIData library
using Statistics
using NaturalSort
@ -113,7 +114,9 @@ function get_processed_mean_spectrum(spectra::Vector{MutableSpectrum}; num_bins=
# 3. Bin intensities
for s in spectra
for i in eachindex(s.mz)
# Use minimum length to avoid bounds errors if arrays are mismatched
n_points = min(length(s.mz), length(s.intensity))
for i in 1:n_points
bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0)
final_index = clamp(bin_index, 1, num_bins)
intensity_sum[final_index] += s.intensity[i]
@ -147,7 +150,9 @@ function get_processed_sum_spectrum(spectra::Vector{MutableSpectrum}; num_bins=2
inv_bin_step = 1.0 / bin_step
for s in spectra
for i in eachindex(s.mz)
# Use minimum length to avoid bounds errors if arrays are mismatched
n_points = min(length(s.mz), length(s.intensity))
for i in 1:n_points
bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0)
final_index = clamp(bin_index, 1, num_bins)
intensity_sum[final_index] += s.intensity[i]
@ -157,11 +162,44 @@ function get_processed_sum_spectrum(spectra::Vector{MutableSpectrum}; num_bins=2
return collect(mz_bins), intensity_sum
end
INITIAL_MODEL_STATE = Dict{Symbol,Any}()
# Function to capture initial state (also outside @app block)
function capture_initial_state!(model)
empty!(INITIAL_MODEL_STATE)
for name in fieldnames(typeof(model))
if !startswith(String(name), "_")
INITIAL_MODEL_STATE[name] = deepcopy(getfield(model, name))
end
end
println("Captured $(length(INITIAL_MODEL_STATE)) reactive variables")
end
@genietools
# == Reactive code ==
#=
macro ui_log(message, level="INFO", log_entries)
quote
local timestamp = Dates.format(now(), "HH:MM:SS")
local new_entry = Dict("time" => timestamp, "message" => string($(esc(message))), "level" => $(esc(level)))
println("log entries value: $log_entries")
pushfirst!(log_entries, new_entry)
if length(log_entries) > 100
popfirst!(log_entries)
end
push!(__model__)
end
end
=#
# Reactive code to make the UI interactive
@app begin
# == Notification & Logs ==
# @in log_entries = Dict{String,Any}[]
# @in show_log_sidebar = false
@in showBugModal = false
# == Loading Screen Variables ==
@in is_initializing = true
@in initialization_message = "Initializing..."
@ -348,6 +386,7 @@ end
@in export_params_btn = false # Export parameters to file
@in import_params_btn = false # Import parameters from file
@in save_feature_matrix_btn = false # Save feature matrix results
@in reset_session_btn = false # Deep session reset
# Preprocessing results
@in selected_spectrum_id_for_plot = 1
@ -616,12 +655,64 @@ end
# Reactive handlers watch a variable and execute a block of code when its value changes
# The onbutton handler will set the variable to false after the block is executed
# This handler correctly uses pick_file and loads the selected file
# as the active dataset for the UI.
@onbutton reset_session_btn begin
is_processing = true
push!(__model__)
try
# 1. Clear large data objects explicitly
msi_data = nothing
feature_matrix_result = nothing
bin_info_result = nothing
# 2. Reset ALL reactive variables using captured initial state
if !isempty(INITIAL_MODEL_STATE)
for (name, value) in INITIAL_MODEL_STATE
setfield!(__model__, name, deepcopy(value))
end
msg = "Session reset: restored $(length(INITIAL_MODEL_STATE)) variables to initial state."
else
msg = "Warning: No initial state captured. Using partial reset."
end
# 3. Reset file lists (these will be repopulated by normal operation)
msi_bmp = sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir("public")), lt=natural)
col_msi_png = sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir("public")), lt=natural)
triq_bmp = sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir("public")), lt=natural)
col_triq_png = sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir("public")), lt=natural)
# 4. Clear any cached images/plots
imgInt = "/.bmp"
imgIntT = "/.bmp"
colorbar = "/.png"
colorbarT = "/.png"
# 5. Reset plot data to default traces
traceImg = PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())
plotdataImg = [traceImg]
plotdataImgT = [traceImg]
plotdata = [PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines")]
# 6. Aggressive garbage collection
GC.gc(true)
if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0)
end
println("Session reset successfully.")
catch e
println("Error during session reset: $e")
msg = "Reset error: $e"
finally
is_processing = false
end
end
@onbutton btnSearch begin
is_processing = true
push!(__model__)
picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml")
if isempty(picked_route)
if isnothing(picked_route) || isempty(picked_route)
is_processing = false
return
end
@ -938,6 +1029,7 @@ end
@onbutton export_params_btn begin
is_processing = true
push!(__model__)
params_to_export = Dict(
"pipeline_step_order" => pipeline_step_order,
"enable_standards" => enable_standards, # Export global flag
@ -1011,6 +1103,7 @@ end
@onbutton import_params_btn begin
is_processing = true
push!(__model__)
picked_file = pick_file(filterlist="json")
if isempty(picked_file)
is_processing = false
@ -1021,39 +1114,57 @@ end
json_string = read(picked_file, String)
params = JSON.parse(json_string)
# This is a list of all known reactive variables that can be imported.
# This prevents arbitrary variable assignment.
known_params = [
"stabilization_method", "smoothing_method", "smoothing_window", "smoothing_order",
"baseline_method", "baseline_iterations", "baseline_window", "normalization_method",
"alignment_method", "alignment_span", "alignment_tolerance", "alignment_tolerance_unit",
"alignment_max_shift_ppm", "alignment_min_matched_peaks", "peak_picking_method",
"peak_picking_snr_threshold", "peak_picking_half_window", "peak_picking_min_peak_prominence",
"peak_picking_merge_peaks_tolerance", "peak_picking_min_peak_width_ppm",
"peak_picking_max_peak_width_ppm", "peak_picking_min_peak_shape_r2", "binning_method",
"binning_tolerance", "binning_tolerance_unit", "binning_frequency_threshold",
"binning_min_peak_per_bin", "binning_max_bin_width_ppm", "binning_intensity_weighted_centers",
"binning_num_uniform_bins", "calibration_fit_order", "calibration_ppm_tolerance",
"peak_selection_min_snr", "peak_selection_min_fwhm_ppm", "peak_selection_max_fwhm_ppm",
"peak_selection_min_shape_r2", "peak_selection_frequency_threshold", "peak_selection_correlation_threshold"
]
# Import special variables first
if haskey(params, "reference_peaks_list")
reference_peaks_list = params["reference_peaks_list"]
end
if haskey(params, "pipeline_step_order")
pipeline_step_order = params["pipeline_step_order"]
end
if haskey(params, "enable_standards")
enable_standards = params["enable_standards"]
end
# Import regular parameters with explicit assignments
haskey(params, "stabilization_method") && (stabilization_method = params["stabilization_method"])
haskey(params, "smoothing_method") && (smoothing_method = params["smoothing_method"])
haskey(params, "smoothing_window") && (smoothing_window = params["smoothing_window"])
haskey(params, "smoothing_order") && (smoothing_order = params["smoothing_order"])
haskey(params, "baseline_method") && (baseline_method = params["baseline_method"])
haskey(params, "baseline_iterations") && (baseline_iterations = params["baseline_iterations"])
haskey(params, "baseline_window") && (baseline_window = params["baseline_window"])
haskey(params, "normalization_method") && (normalization_method = params["normalization_method"])
haskey(params, "alignment_method") && (alignment_method = params["alignment_method"])
haskey(params, "alignment_span") && (alignment_span = params["alignment_span"])
haskey(params, "alignment_tolerance") && (alignment_tolerance = params["alignment_tolerance"])
haskey(params, "alignment_tolerance_unit") && (alignment_tolerance_unit = params["alignment_tolerance_unit"])
haskey(params, "alignment_max_shift_ppm") && (alignment_max_shift_ppm = params["alignment_max_shift_ppm"])
haskey(params, "alignment_min_matched_peaks") && (alignment_min_matched_peaks = params["alignment_min_matched_peaks"])
haskey(params, "peak_picking_method") && (peak_picking_method = params["peak_picking_method"])
haskey(params, "peak_picking_snr_threshold") && (peak_picking_snr_threshold = params["peak_picking_snr_threshold"])
haskey(params, "peak_picking_half_window") && (peak_picking_half_window = params["peak_picking_half_window"])
haskey(params, "peak_picking_min_peak_prominence") && (peak_picking_min_peak_prominence = params["peak_picking_min_peak_prominence"])
haskey(params, "peak_picking_merge_peaks_tolerance") && (peak_picking_merge_peaks_tolerance = params["peak_picking_merge_peaks_tolerance"])
haskey(params, "peak_picking_min_peak_width_ppm") && (peak_picking_min_peak_width_ppm = params["peak_picking_min_peak_width_ppm"])
haskey(params, "peak_picking_max_peak_width_ppm") && (peak_picking_max_peak_width_ppm = params["peak_picking_max_peak_width_ppm"])
haskey(params, "peak_picking_min_peak_shape_r2") && (peak_picking_min_peak_shape_r2 = params["peak_picking_min_peak_shape_r2"])
haskey(params, "binning_method") && (binning_method = params["binning_method"])
haskey(params, "binning_tolerance") && (binning_tolerance = params["binning_tolerance"])
haskey(params, "binning_tolerance_unit") && (binning_tolerance_unit = params["binning_tolerance_unit"])
haskey(params, "binning_frequency_threshold") && (binning_frequency_threshold = params["binning_frequency_threshold"])
haskey(params, "binning_min_peak_per_bin") && (binning_min_peak_per_bin = params["binning_min_peak_per_bin"])
haskey(params, "binning_max_bin_width_ppm") && (binning_max_bin_width_ppm = params["binning_max_bin_width_ppm"])
haskey(params, "binning_intensity_weighted_centers") && (binning_intensity_weighted_centers = params["binning_intensity_weighted_centers"])
haskey(params, "binning_num_uniform_bins") && (binning_num_uniform_bins = params["binning_num_uniform_bins"])
haskey(params, "calibration_fit_order") && (calibration_fit_order = params["calibration_fit_order"])
haskey(params, "calibration_ppm_tolerance") && (calibration_ppm_tolerance = params["calibration_ppm_tolerance"])
haskey(params, "peak_selection_min_snr") && (peak_selection_min_snr = params["peak_selection_min_snr"])
haskey(params, "peak_selection_min_fwhm_ppm") && (peak_selection_min_fwhm_ppm = params["peak_selection_min_fwhm_ppm"])
haskey(params, "peak_selection_max_fwhm_ppm") && (peak_selection_max_fwhm_ppm = params["peak_selection_max_fwhm_ppm"])
haskey(params, "peak_selection_min_shape_r2") && (peak_selection_min_shape_r2 = params["peak_selection_min_shape_r2"])
haskey(params, "peak_selection_frequency_threshold") && (peak_selection_frequency_threshold = params["peak_selection_frequency_threshold"])
haskey(params, "peak_selection_correlation_threshold") && (peak_selection_correlation_threshold = params["peak_selection_correlation_threshold"])
for (key, value) in params
if key == "reference_peaks_list"
reference_peaks_list = value
elseif key == "pipeline_step_order"
pipeline_step_order = value
elseif key == "enable_standards"
enable_standards = value
elseif key in known_params
# Use getfield and setproperty! to update reactive variables by name
if hasfield(typeof(@__MODULE__), Symbol(key))
getfield(@__MODULE__, Symbol(key))[] = value
end
else
@warn "Unknown parameter '$key' found in JSON file. Skipping."
end
end
msg = "Parameters imported successfully from $(basename(picked_file))."
catch e
msg = "Failed to import parameters: $e"
@ -1110,13 +1221,14 @@ end
@onbutton run_full_pipeline begin
is_processing = true
push!(__model__)
overall_progress = 0.0
local pipeline_msi_data = nothing
local current_spectra = Vector{MutableSpectrum}()
current_pipeline_step = "Initializing..."
println("DEBUG: run_full_pipeline started.")
# println("DEBUG: Current pipeline_step_order configuration: $pipeline_step_order")
try
# --- 1. Initial Checks and Data Loading ---
println("DEBUG: Performing initial checks and data loading...")
if isempty(selected_folder_main)
msg = "No dataset loaded. Please load a file using 'Select an imzMl / mzML file'."
warning_msg = true
@ -1136,26 +1248,31 @@ end
target_path = entry["source_path"]
# Ensure msi_data is for the currently selected file and load if needed
if msi_data === nothing || full_route != target_path
println("DEBUG: Active file path changed or data not in memory. Reloading MSI data: $(basename(target_path))")
if msi_data !== nothing; close(msi_data); end
msg = "Reloading $(basename(target_path)) for analysis..."
full_route = target_path
msi_data = OpenMSIData(target_path)
# NOTE: For the pipeline, we will open a DEDICATED instance to avoid race conditions
# with the global msi_data used for plotting/interactive exploration.
println("DEBUG: Opening isolated MSIData instance for pipeline stability...")
pipeline_msi_data = OpenMSIData(target_path)
# Determine plot mode from loaded data
df = msi_data.spectrum_stats_df
# Determine plot mode from metadata for correct visualization late
metadata = pipeline_msi_data.instrument_metadata
acq_mode = metadata !== nothing ? metadata.acquisition_mode : :unknown
if acq_mode == :centroid
last_plot_mode = "stem"
elseif acq_mode == :profile
last_plot_mode = "lines"
else
# Fallback to stats if mode is unknown
df = pipeline_msi_data.spectrum_stats_df
if df !== nothing && "Mode" in names(df)
profile_count = count(==(MSI_src.PROFILE), df.Mode)
total_count = length(df.Mode)
last_plot_mode = profile_count > total_count / 2 ? "lines" : "stem"
println("DEBUG: Auto-detected plot mode for pipeline: $(last_plot_mode)")
else
last_plot_mode = "lines" # Default
end
else
println("DEBUG: Using already loaded MSI data for $(basename(target_path)).")
end
println("DEBUG: Auto-detected plot mode from metadata: $(last_plot_mode) (acq_mode: $(acq_mode)) [Initial set]")
# Mask path retrieval from registry
local mask_path_for_pipeline::Union{String, Nothing} = nothing
@ -1171,6 +1288,7 @@ end
warning_msg = true
@warn msg
println("DEBUG: $msg")
close(pipeline_msi_data) # Important cleanup
return
end
else
@ -1178,6 +1296,7 @@ end
warning_msg = true
@warn msg
println("DEBUG: $msg")
close(pipeline_msi_data) # Important cleanup
return
end
else
@ -1185,22 +1304,24 @@ end
end
# Apply mask if enabled to get indices to process
spectrum_indices_to_process = collect(1:length(msi_data.spectra_metadata))
# Use pipeline_msi_data for consistency
spectrum_indices_to_process = collect(1:length(pipeline_msi_data.spectra_metadata))
if mask_path_for_pipeline !== nothing
current_pipeline_step = "Applying mask..."
println("DEBUG: Applying mask matrix to filter spectra...")
mask_matrix = load_and_prepare_mask(mask_path_for_pipeline, msi_data.image_dims)
masked_indices_set = get_masked_spectrum_indices(msi_data, mask_matrix)
mask_matrix = load_and_prepare_mask(mask_path_for_pipeline, pipeline_msi_data.image_dims)
masked_indices_set = get_masked_spectrum_indices(pipeline_msi_data, mask_matrix)
spectrum_indices_to_process = collect(masked_indices_set)
if isempty(spectrum_indices_to_process)
msg = "No spectra remaining after applying mask. Aborting pipeline."
warning_msg = true
println("DEBUG: $msg")
close(pipeline_msi_data) # Important cleanup
return
end
println("DEBUG: $(length(spectrum_indices_to_process)) spectra remaining after mask application.")
else
println("DEBUG: No mask applied. Processing all $(length(msi_data.spectra_metadata)) spectra.")
println("DEBUG: No mask applied. Processing all $(length(pipeline_msi_data.spectra_metadata)) spectra.")
end
# Apply subset processing if enabled
@ -1211,18 +1332,92 @@ end
println("DEBUG: Subset processing enabled. Processing first $(length(spectrum_indices_to_process)) of $n_total spectra.")
end
# Initialize spectra data structure
current_pipeline_step = "Loading spectra..."
println("DEBUG: Loading $(length(spectrum_indices_to_process)) spectra into MutableSpectrum objects...")
current_spectra = Vector{MutableSpectrum}(undef, length(spectrum_indices_to_process))
# --- BOUNDS VALIDATION AND DIAGNOSTIC LOGGING ---
# Validate all indices are within bounds before attempting to load
max_spectra_idx = length(pipeline_msi_data.spectra_metadata)
println("DEBUG: Total spectra in dataset: $max_spectra_idx")
println("DEBUG: Number of indices to process: $(length(spectrum_indices_to_process))")
if !isempty(spectrum_indices_to_process)
min_idx = minimum(spectrum_indices_to_process)
max_idx = maximum(spectrum_indices_to_process)
println("DEBUG: Spectrum indices range: $min_idx to $max_idx")
# Check for invalid indices
invalid_indices = filter(idx -> idx < 1 || idx > max_spectra_idx, spectrum_indices_to_process)
if !isempty(invalid_indices)
n_invalid = length(invalid_indices)
sample_invalid = first(sort(invalid_indices), min(10, n_invalid))
msg = "Invalid spectrum indices detected: $n_invalid indices out of range [1, $max_spectra_idx]. First few invalid indices: $sample_invalid"
warning_msg = true
@error msg
println("DEBUG: $msg")
close(pipeline_msi_data) # Important cleanup
return
end
println("DEBUG: All spectrum indices are valid (within [1, $max_spectra_idx]).")
else
println("DEBUG: Warning - spectrum_indices_to_process is empty!")
end
# CRITICAL: Verify indices are unique to prevent race conditions during loading
if length(Set(spectrum_indices_to_process)) != length(spectrum_indices_to_process)
@warn "Non-unique indices detected in spectrum_indices_to_process. This may cause issues during parallel loading."
end
# Use pipeline_msi_data for reading
# Split loading into chunks to update progress bar
n_spectra = length(spectrum_indices_to_process)
println("DEBUG: Loading $n_spectra spectra into MutableSpectrum objects...")
current_spectra = Vector{MutableSpectrum}(undef, n_spectra)
chunk_size = max(1, n_spectra ÷ 10) # Update progress every 10%
for chunk_start in 1:chunk_size:n_spectra
chunk_end = min(chunk_start + chunk_size - 1, n_spectra)
Threads.@threads for i in chunk_start:chunk_end
local original_idx = spectrum_indices_to_process[i]
local mz, intensity # Enforce thread-local scope
try
mz, intensity = GetSpectrum(pipeline_msi_data, original_idx)
# Diagnostic check for length mismatch and defensive truncation
l_mz = length(mz)
l_int = length(intensity)
if l_mz != l_int
new_len = min(l_mz, l_int)
@warn "CRITICAL: Mismatch during loading at index $original_idx. mz=$l_mz, int=$l_int. TRUNCATING."
mz = mz[1:new_len]
intensity = intensity[1:new_len]
end
Threads.@threads for i in 1:length(spectrum_indices_to_process)
original_idx = spectrum_indices_to_process[i]
mz, intensity = GetSpectrum(msi_data, original_idx) # Fetch mz and intensity for the current spectrum
current_spectra[i] = MutableSpectrum(original_idx, copy(Float64.(mz)), copy(Float64.(intensity)), NamedTuple{(:mz, :intensity, :fwhm, :shape_r2, :snr, :prominence), NTuple{6, Float64}}[])
catch loop_error
rethrow(loop_error)
end
end
overall_progress = (chunk_end / n_spectra) * 0.2 # Loading is first 20%
push!(__model__)
end
println("DEBUG: All spectra loaded into temporary structure for processing.")
# We can now close the local MSI data instance as we have loaded everything into memory
# However, if we want to support lazy loading scenarios later, we might keep it open.
# For now, let's close it here to free up file handles early,
# UNLESS `execute_full_preprocessing` needs it (it doesn't seem to based on signature).
close(pipeline_msi_data)
pipeline_msi_data = nothing # Prevent accidental use
# Aggressive memory cleanup to return memory to OS
println("DEBUG: Performing aggressive memory cleanup...")
GC.gc(true) # Full garbage collection with all generations
if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0)
end
println("DEBUG: Closed local pipeline MSIData instance and freed memory.")
# --- 2. Parameter Assembly with Validation ---
current_pipeline_step = "Configuring parameters..."
@ -1450,8 +1645,16 @@ end
mask_path_for_pipeline
) do step
current_pipeline_step = "Processing: $step"
println("DEBUG: Processing step: $step")
# Update progress based on step index
step_idx = findfirst(==(step), pipeline_stp)
if step_idx !== nothing
# Preprocessing is 20% to 90% (total 70%)
overall_progress = 0.2 + (step_idx / length(pipeline_stp)) * 0.7
end
push!(__model__)
end
println("DEBUG: Pipeline execution finished.")
# 4. Update Results Display
@ -1638,7 +1841,8 @@ end
mkpath(output_dir)
save_feature_matrix(feature_matrix_result, bin_info_result, output_dir)
msg = "Pipeline completed successfully. Feature matrix saved."
println("DEBUG: Feature matrix saved to $output_dir")
overall_progress = 1.0
push!(__model__)
else
msg = "Pipeline completed successfully. No feature matrix generated (binning step not enabled)."
println("DEBUG: $msg")
@ -1650,18 +1854,71 @@ end
@error "Pipeline failed" exception=(e, catch_backtrace())
println("DEBUG: Pipeline caught an exception: $e")
finally
# Aggressive memory cleanup
println("DEBUG: Starting aggressive memory cleanup...")
# Explicitly clear large data structures
try
if current_spectra !== nothing && !isempty(current_spectra)
# Deep clear individual objects to break references effectively
# Use isassigned to prevent UndefRefError if loading failed halfway
for i in eachindex(current_spectra)
if isassigned(current_spectra, i)
s = current_spectra[i]
s.mz = Float64[]
s.intensity = Float64[]
empty!(s.peaks)
end
end
empty!(current_spectra)
end
current_spectra = nothing
if feature_matrix_result !== nothing
feature_matrix_result = nothing
end
# Close any open pipeline data handles
if pipeline_msi_data !== nothing
try
close(pipeline_msi_data)
catch
# Already closed, ignore
end
pipeline_msi_data = nothing
end
println("DEBUG: Data structures cleared. Triggering garbage collection...")
catch cleanup_error
@warn "Error during data cleanup: $cleanup_error"
end
is_processing = false
overall_progress = 0.0
current_pipeline_step = ""
println("DEBUG: run_full_pipeline finished (finally block).")
GC.gc() # Trigger garbage collection
# Force garbage collection multiple times for thorough cleanup
GC.gc()
GC.gc() # Second pass to catch any circular references
# On Linux/Unix, force Julia to return memory to OS
if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
try
ccall(:malloc_trim, Int32, (Int32,), 0)
println("DEBUG: malloc_trim called successfully (Linux).")
catch e
@warn "malloc_trim failed: $e"
end
end
println("DEBUG: Memory cleanup complete.")
end
end
@onbutton recalculate_suggestions_btn begin
is_processing = true
push!(__model__)
if msi_data === nothing
msg = "Please load a file first."
warning_msg = true
@ -1903,6 +2160,7 @@ end
# This new handler correctly adds the file from full_route to the batch list.
@onbutton btnAddBatch begin
is_processing = true
push!(__model__)
if isempty(full_route) || full_route == "unknown (manually added)"
msg = "No active file selected to add to batch."
warning_msg = true
@ -1923,6 +2181,7 @@ end
@onbutton clear_batch_btn begin
is_processing = true
push!(__model__)
selected_files = String[]
batch_file_count = 0
msg = "Batch cleared"
@ -1974,6 +2233,7 @@ end
@onchange btnSearchMzml, btnSearchSync begin
is_processing = true
push!(__model__)
if btnSearchMzml
picked_route = pick_file(; filterlist="mzML,mzml")
if !isempty(picked_route)
@ -1997,6 +2257,7 @@ end
@onbutton convert_process begin
is_processing = true
push!(__model__)
if isempty(mzml_full_route) || isempty(sync_full_route)
msg_conversion = "Please select both an .mzML file and a .txt sync file."
warning_msg = true
@ -2029,6 +2290,7 @@ end
@error "Conversion failed" exception=(e, catch_backtrace())
finally
is_processing = false
overall_progress = 0.0
# Re-enable button if files are still selected
btnConvertDisable = isempty(mzml_full_route) || isempty(sync_full_route)
end
@ -2061,6 +2323,7 @@ end
return
end
is_processing = true
push!(__model__)
masses = Float64[]
try
@ -2087,7 +2350,8 @@ end
for (file_idx, file_path) in enumerate(current_selected_files)
progress_message = "Processing file $(file_idx)/$(num_files): $(basename(file_path))"
overall_progress = current_step / total_steps
overall_progress = (file_idx - 1) / num_files
push!(__model__)
all_params = (
tolerance = current_tol,
@ -2218,6 +2482,10 @@ end
end
@onbutton createMeanPlot @time begin
# Pre-initialize for safe cleanup in finally block
local xSpectraMz = Vector{Float64}()
local ySpectraMz = Vector{Float64}()
if isempty(selected_folder_main)
msg = "No dataset selected. Please process a file and select a folder first."
warning_msg = true
@ -2225,6 +2493,7 @@ end
end
is_processing = true
push!(__model__)
try
sTime = time()
@ -2280,6 +2549,19 @@ end
@error "Mean spectrum plotting failed" exception=(e, catch_backtrace())
finally
is_processing = false
try
if plotdata_before !== nothing
plotdata_before = nothing
end
if !isempty(xSpectraMz)
empty!(xSpectraMz)
end
if !isempty(ySpectraMz)
empty!(ySpectraMz)
end
catch
end
GC.gc() # Trigger garbage collection
if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
@ -2288,6 +2570,10 @@ end
end
@onbutton createSumPlot @time begin
# Pre-initialize for safe cleanup in finally block
local xSpectraMz = Vector{Float64}()
local ySpectraMz = Vector{Float64}()
if isempty(selected_folder_main)
msg = "No dataset selected. Please process a file and select a folder first."
warning_msg = true
@ -2295,6 +2581,7 @@ end
end
is_processing = true
push!(__model__)
msg = "Loading total spectrum plot for $(selected_folder_main)..."
try
@ -2350,6 +2637,18 @@ end
@error "Total spectrum plotting failed" exception=(e, catch_backtrace())
finally
is_processing = false
try
if plotdata_before !== nothing
plotdata_before = nothing
end
if !isempty(xSpectraMz)
empty!(xSpectraMz)
end
if !isempty(ySpectraMz)
empty!(ySpectraMz)
end
catch
end
GC.gc() # Trigger garbage collection
if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
@ -2365,6 +2664,7 @@ end
end
is_processing = true
push!(__model__)
msg = "Loading plot for $(selected_folder_main)..."
try
@ -2479,6 +2779,7 @@ end
end
is_processing = true
push!(__model__)
msg = "Loading plot for $(selected_folder_main)..."
try
@ -3022,6 +3323,7 @@ end
return
end
is_processing = true
push!(__model__)
try
# --- Get Mask Path ---
@ -3079,6 +3381,7 @@ end
end
is_processing = true
push!(__model__)
try
# --- Get Mask Path ---
@ -3137,6 +3440,7 @@ end
end
is_processing = true
push!(__model__)
try
sTime=time()
@ -3175,6 +3479,7 @@ end
end
is_processing = true
push!(__model__)
try
sTime=time()
@ -3401,6 +3706,10 @@ end
@mounted watchplots()
@onchange isready begin
# Capture state on first run only
if isempty(INITIAL_MODEL_STATE)
capture_initial_state!(__model__)
end
# is_processing = true
if isready && !registry_init_done
sTime=time()
@ -3471,6 +3780,5 @@ end
end
end
# == Pages ==
# Register a new route and the page that will be loaded on access
@page("/", "app.jl.html")
end

View File

@ -3,16 +3,30 @@
<div>
<h4>JuliaMSI&nbsp;</h4>
</div>
<q-space />
<div class="q-gutter-sm q-pa-sm">
<q-btn flat round icon="bug_report" v-on:click="showBugModal = true">
<q-tooltip>Report a Bug</q-tooltip>
</q-btn>
<q-btn flat round icon="refresh" color="negative" v-on:click="reset_session_btn = true">
<q-tooltip>Deep Session Reset</q-tooltip>
</q-btn>
</div>
</header>
<!--
<div v-if="is_initializing" class="loading-overlay">
<div v-if="is_initializing || is_processing" class="loading-overlay">
<div class="loading-content">
<q-spinner-hourglass color="white" size="4em" />
<div class="q-mt-md text-white text-h6">{{ initialization_message }}</div>
<div v-if="is_processing" class="q-mt-md"
style="width: 250px; background: rgba(255,255,255,0.2); border-radius: 10px; overflow: hidden; height: 12px; border: 1px solid rgba(255,255,255,0.3);">
<div
:style="{ width: (overall_progress * 100) + '%', height: '100%', background: '#00e676', transition: 'width 0.4s ease-out', boxShadow: '0 0 10px #00e676' }">
</div>
</div>
<div v-if="is_initializing" class="q-mt-md text-white text-h6">{{ initialization_message }}</div>
<div v-else class="q-mt-md text-white text-h6">{{ progress_message || 'Processing...' }}</div>
</div>
</div>
-->
<div id="extDivStyle" class="row col-12 q-pa-xl">
<div class="row col-6">
@ -37,14 +51,16 @@
<q-icon name="search" v-on:click="!is_processing && (btnSearch=true)" class="cursor-pointer" />
</template>
</q-input>
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add" :disable="is_processing"></q-btn>
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true" :disable="is_processing || batch_file_count === 0"
label="Clear"></q-btn>
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add"
:disable="is_processing"></q-btn>
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true"
:disable="is_processing || batch_file_count === 0" label="Clear"></q-btn>
</div>
<!-- Mask Configuration -->
<div class="row items-center q-mb-md">
<q-toggle v-model="maskEnabled" label="Apply Mask During Preprocessing" color="green" class="q-mr-md" :disable="is_processing" />
<q-toggle v-model="maskEnabled" label="Apply Mask During Preprocessing" color="green" class="q-mr-md"
:disable="is_processing" />
</div>
<!-- Subset Processing -->
@ -60,7 +76,8 @@
</div>
</div>
<div v-if="enable_subset_processing" class="q-mt-sm">
<q-input standout="custom-standout" type="number" v-model.number="spectra_subset_size" label="Number of Spectra to Process" :min="1" :readonly="is_processing">
<q-input standout="custom-standout" type="number" v-model.number="spectra_subset_size"
label="Number of Spectra to Process" :min="1" :readonly="is_processing">
<template v-slot:prepend>
<q-icon name="functions" />
</template>
@ -73,8 +90,8 @@
<!-- Spectrum Selection for Visualization -->
<div class="row items-center q-mb-md">
<div class="text-subtitle2 q-mr-md">Preview Spectrum:</div>
<q-btn-dropdown class="q-ma-sm" :loading="is_processing" :disable="is_processing"
label="Generate Spectra" icon="play_arrow">
<q-btn-dropdown class="q-ma-sm" :loading="is_processing" :disable="is_processing" label="Generate Spectra"
icon="play_arrow">
<q-list>
<q-item clickable v-close-popup v-on:click="createMeanPlot=true">
<q-item-label>Mean spectrum plot</q-item-label>
@ -111,43 +128,49 @@
</div>
<!-- Internal Standards (Collapsible) -->
<q-expansion-item
icon="science"
label="Internal Standards"
caption="Manage reference peaks for calibration"
class="q-mb-md"
>
<q-expansion-item icon="science" label="Internal Standards" caption="Manage reference peaks for calibration"
class="q-mb-md">
<q-card>
<q-card-section>
<q-toggle v-model="enable_standards" v-on:click="enable_standards" label="Use Internal Standards" color="primary" class="q-mb-md" :disable="is_processing" />
<q-toggle v-model="enable_standards" v-on:click="enable_standards" label="Use Internal Standards"
color="primary" class="q-mb-md" :disable="is_processing" />
<!-- Keep your existing reference_peaks_list implementation -->
<q-list bordered separator class="q-mt-md">
<q-item v-for="(peak, index) in reference_peaks_list" :key="index">
<q-item-section avatar>
<q-btn flat round icon="delete" color="negative" v-on:click="action_index = index; remove_peak_trigger = true" :disable="is_processing"></q-btn>
<q-btn flat round icon="delete" color="negative"
v-on:click="action_index = index; remove_peak_trigger = true" :disable="is_processing"></q-btn>
</q-item-section>
<q-item-section>
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input standout="custom-standout" label="m/z" type="number" step="0.0001"
v-model="peak.mz" :rules="[val => !!val || 'Required', val => val > 0 || 'Must be positive']" :readonly="is_processing"></q-input>
<q-input standout="custom-standout" label="m/z" type="number" step="0.0001" v-model="peak.mz"
:rules="[val => !!val || 'Required', val => val > 0 || 'Must be positive']"
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Label (optional)" v-model="peak.label" :readonly="is_processing"></q-input>
<q-input standout="custom-standout" label="Label (optional)" v-model="peak.label"
:readonly="is_processing"></q-input>
</div>
</div>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-btn class="q-ma-sm btn-style" icon="add" label="Add Reference Peak" v-on:click="addReferencePeak=true" :disable="is_processing"></q-btn>
<q-btn class="q-ma-sm btn-style" icon="add" label="Add Reference Peak"
v-on:click="addReferencePeak=true" :disable="is_processing"></q-btn>
</q-item-section>
</q-item>
</q-list>
<div class="row justify-end q-mt-sm">
<q-btn class="q-ma-sm" dense icon="get_app" v-on:click="export_standards_btn=true" label="Export" outline :disable="is_processing" hint="Export standards list to a JSON file." />
<q-btn class="q-ma-sm" dense icon="upload_file" v-on:click="import_standards_btn=true" label="Import" outline :disable="is_processing" hint="Import standards list from a JSON file." />
<q-btn class="q-ma-sm" icon="functions" v-on:click="recalculate_suggestions_btn=true" label="Recalculate Suggestions" outline hint="Re-run automatic parameter suggestion using the current list of internal standards." :disable="is_processing"/>
<q-btn class="q-ma-sm" dense icon="get_app" v-on:click="export_standards_btn=true" label="Export"
outline :disable="is_processing" hint="Export standards list to a JSON file." />
<q-btn class="q-ma-sm" dense icon="upload_file" v-on:click="import_standards_btn=true" label="Import"
outline :disable="is_processing" hint="Import standards list from a JSON file." />
<q-btn class="q-ma-sm" icon="functions" v-on:click="recalculate_suggestions_btn=true"
label="Recalculate Suggestions" outline
hint="Re-run automatic parameter suggestion using the current list of internal standards."
:disable="is_processing" />
</div>
</q-card-section>
</q-card>
@ -155,18 +178,18 @@
<!-- Reorderable Preprocessing Steps -->
<div class="text-h6 q-mb-md">Preprocessing Pipeline</div>
<q-list bordered :disable="is_processing">
<q-expansion-item v-for="(step, index) in pipeline_step_order" :key="step.name"
:label="step.label" group="preprocessing-steps"
:class="step.enabled ? '' : 'text-grey'" :disable="is_processing">
<q-expansion-item v-for="(step, index) in pipeline_step_order" :key="step.name" :label="step.label"
group="preprocessing-steps" :class="step.enabled ? '' : 'text-grey'" :disable="is_processing">
<!-- Header with controls -->
<template v-slot:header>
<q-item-section avatar>
<div class="row no-wrap">
<q-btn flat round icon="arrow_upward" size="sm"
:disable="is_processing || index === 0" v-on:click.stop="action_index = index; move_step_up_trigger = true"></q-btn>
<q-btn flat round icon="arrow_upward" size="sm" :disable="is_processing || index === 0"
v-on:click.stop="action_index = index; move_step_up_trigger = true"></q-btn>
<q-btn flat round icon="arrow_downward" size="sm"
:disable="is_processing || index === pipeline_step_order.length - 1" v-on:click.stop="action_index = index; move_step_down_trigger = true"></q-btn>
:disable="is_processing || index === pipeline_step_order.length - 1"
v-on:click.stop="action_index = index; move_step_down_trigger = true"></q-btn>
</div>
</q-item-section>
@ -175,7 +198,8 @@
</q-item-section>
<q-item-section side>
<q-toggle v-model="step.enabled" color="green" v-on:click.stop="action_index = index; toggle_step_trigger = true" :disable="is_processing" />
<q-toggle v-model="step.enabled" color="green"
v-on:click.stop="action_index = index; toggle_step_trigger = true" :disable="is_processing" />
</q-item-section>
</template>
@ -196,10 +220,12 @@
<q-radio v-model="smoothing_method" val="ma" label="Moving Average" :disable="is_processing" />
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Window Size" v-model="smoothing_window" type="number" :readonly="is_processing" />
<q-input standout="custom-standout" label="Window Size" v-model="smoothing_window" type="number"
:readonly="is_processing" />
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Order (Savitzky-Golay)" v-model="smoothing_order" type="number" :readonly="is_processing" />
<q-input standout="custom-standout" label="Order (Savitzky-Golay)" v-model="smoothing_order"
type="number" :readonly="is_processing" />
</div>
</div>
</q-card-section>
@ -211,9 +237,12 @@
<div class="text-caption">The algorithm to use for baseline correction.</div>
</q-card-section>
<q-card-section>
<q-radio v-model="baseline_method" val="snip" label="SNIP" hint="Sensitive Nonlinear Iterative Peak clipping." :disable="is_processing" /><br>
<q-radio v-model="baseline_method" val="convex_hull" label="CONVEX HULL" hint="Finds the lower convex hull of the spectrum." :disable="is_processing" /><br>
<q-radio v-model="baseline_method" val="median" label="MEDIAN" hint="Moving median filter." :disable="is_processing" /><br>
<q-radio v-model="baseline_method" val="snip" label="SNIP"
hint="Sensitive Nonlinear Iterative Peak clipping." :disable="is_processing" /><br>
<q-radio v-model="baseline_method" val="convex_hull" label="CONVEX HULL"
hint="Finds the lower convex hull of the spectrum." :disable="is_processing" /><br>
<q-radio v-model="baseline_method" val="median" label="MEDIAN" hint="Moving median filter."
:disable="is_processing" /><br>
</q-card-section>
<q-card-section>
<div class="text-h6">Parameters</div>
@ -222,11 +251,15 @@
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input standout="custom-standout" label="Iterations (for SNIP)" type="number"
:placeholder="suggested_baseline_iterations" v-model="baseline_iterations" hint="The number of iterations for the SNIP algorithm. A higher number results in a more aggressive baseline." :readonly="is_processing"></q-input>
:placeholder="suggested_baseline_iterations" v-model="baseline_iterations"
hint="The number of iterations for the SNIP algorithm. A higher number results in a more aggressive baseline."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Window (for Median)" type="number"
:placeholder="suggested_baseline_window" v-model="baseline_window" hint="The window size for the median method, determining the local region for median calculation." :readonly="is_processing"></q-input>
:placeholder="suggested_baseline_window" v-model="baseline_window"
hint="The window size for the median method, determining the local region for median calculation."
:readonly="is_processing"></q-input>
</div>
</div>
</q-card-section>
@ -238,10 +271,15 @@
<div class="text-caption">The normalization method to apply.</div>
</q-card-section>
<q-card-section>
<q-radio v-model="normalization_method" val="tic" label="TIC" hint="Total Ion Current normalization (divides by the sum of intensities)." :disable="is_processing" /><br>
<q-radio v-model="normalization_method" val="median" label="MEDIAN" hint="Divides by the median intensity." :disable="is_processing" /><br>
<q-radio v-model="normalization_method" val="rms" label="RMS" hint="Root Mean Square normalization." :disable="is_processing" /><br>
<q-radio v-model="normalization_method" val="none" label="NONE" hint="No normalization is applied." :disable="is_processing" /><br>
<q-radio v-model="normalization_method" val="tic" label="TIC"
hint="Total Ion Current normalization (divides by the sum of intensities)."
:disable="is_processing" /><br>
<q-radio v-model="normalization_method" val="median" label="MEDIAN"
hint="Divides by the median intensity." :disable="is_processing" /><br>
<q-radio v-model="normalization_method" val="rms" label="RMS" hint="Root Mean Square normalization."
:disable="is_processing" /><br>
<q-radio v-model="normalization_method" val="none" label="NONE" hint="No normalization is applied."
:disable="is_processing" /><br>
</q-card-section>
</q-card>
@ -251,9 +289,12 @@
<div class="text-caption">The alignment algorithm.</div>
</q-card-section>
<q-card-section>
<q-radio v-model="alignment_method" val="lowess" label="LOWESS" hint="Locally Weighted Scatterplot Smoothing regression." :disable="is_processing" /><br>
<q-radio v-model="alignment_method" val="linear" label="LINEAR" hint="Linear regression." :disable="is_processing" /><br>
<q-radio v-model="alignment_method" val="ransac" label="RANSAC" hint="Random Sample Consensus algorithm for robust fitting." :disable="is_processing" /><br>
<q-radio v-model="alignment_method" val="lowess" label="LOWESS"
hint="Locally Weighted Scatterplot Smoothing regression." :disable="is_processing" /><br>
<q-radio v-model="alignment_method" val="linear" label="LINEAR" hint="Linear regression."
:disable="is_processing" /><br>
<q-radio v-model="alignment_method" val="ransac" label="RANSAC"
hint="Random Sample Consensus algorithm for robust fitting." :disable="is_processing" /><br>
</q-card-section>
<q-card-section>
<div class="text-h6">Parameters</div>
@ -262,23 +303,34 @@
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input standout="custom-standout" label="Span (for LOWESS)" type="number" step="0.01"
:placeholder="suggested_alignment_span" v-model="alignment_span" :rules="[val => val >= 0.0 && val <= 1.0 || 'Needs to be between 0 and 1']" hint="The span parameter for LOWESS regression, controlling smoothness (0.0 to 1.0)." :readonly="is_processing"></q-input>
:placeholder="suggested_alignment_span" v-model="alignment_span"
:rules="[val => val >= 0.0 && val <= 1.0 || 'Needs to be between 0 and 1']"
hint="The span parameter for LOWESS regression, controlling smoothness (0.0 to 1.0)."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Tolerance" type="number" step="0.001"
:placeholder="suggested_alignment_tolerance" v-model="alignment_tolerance" hint="The tolerance for matching peaks between the target and reference spectrum." :readonly="is_processing"></q-input>
:placeholder="suggested_alignment_tolerance" v-model="alignment_tolerance"
hint="The tolerance for matching peaks between the target and reference spectrum."
:readonly="is_processing"></q-input>
</div>
</div>
<q-select standout="custom-standout" label="Tolerance Unit" v-model="alignment_tolerance_unit"
:options="['mz', 'ppm']" class="q-mt-md" hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)." :disable="is_processing"></q-select>
:options="['mz', 'ppm']" class="q-mt-md"
hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)."
:disable="is_processing"></q-select>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Max Shift PPM" type="number"
:placeholder="suggested_alignment_max_shift_ppm" v-model="alignment_max_shift_ppm" hint="The maximum allowed m/z shift in ppm to prevent spurious peak matches." :readonly="is_processing"></q-input>
:placeholder="suggested_alignment_max_shift_ppm" v-model="alignment_max_shift_ppm"
hint="The maximum allowed m/z shift in ppm to prevent spurious peak matches."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Min Matched Peaks" type="number"
:placeholder="suggested_alignment_min_matched_peaks" v-model="alignment_min_matched_peaks" hint="The minimum number of matching peaks required to perform the alignment." :readonly="is_processing"></q-input>
:placeholder="suggested_alignment_min_matched_peaks" v-model="alignment_min_matched_peaks"
hint="The minimum number of matching peaks required to perform the alignment."
:readonly="is_processing"></q-input>
</div>
</div>
</q-card-section>
@ -293,12 +345,14 @@
<div class="col-6">
<q-input standout="custom-standout" label="Fit Order" type="number"
:placeholder="suggested_calibration_fit_order" v-model="calibration_fit_order"
hint="Polynomial order for the calibration curve (e.g., 1 or 2)." :readonly="is_processing"></q-input>
hint="Polynomial order for the calibration curve (e.g., 1 or 2)."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="PPM Tolerance" type="number"
:placeholder="suggested_calibration_ppm_tolerance" v-model="calibration_ppm_tolerance"
hint="PPM tolerance for matching reference peaks to internal standards." :readonly="is_processing"></q-input>
hint="PPM tolerance for matching reference peaks to internal standards."
:readonly="is_processing"></q-input>
</div>
</div>
</q-card-section>
@ -310,9 +364,13 @@
<div class="text-caption">The peak detection algorithm.</div>
</q-card-section>
<q-card-section>
<q-radio v-model="peak_picking_method" val="profile" label="PROFILE" hint="For profile-mode data, using local maxima and quality filters." :disable="is_processing" /><br>
<q-radio v-model="peak_picking_method" val="wavelet" label="WAVELET" hint="Continuous Wavelet Transform (CWT) based peak detection." :disable="is_processing" /><br>
<q-radio v-model="peak_picking_method" val="centroid" label="CENTROID" hint="For centroid-mode data, essentially a filtering step." :disable="is_processing" /><br>
<q-radio v-model="peak_picking_method" val="profile" label="PROFILE"
hint="For profile-mode data, using local maxima and quality filters."
:disable="is_processing" /><br>
<q-radio v-model="peak_picking_method" val="wavelet" label="WAVELET"
hint="Continuous Wavelet Transform (CWT) based peak detection." :disable="is_processing" /><br>
<q-radio v-model="peak_picking_method" val="centroid" label="CENTROID"
hint="For centroid-mode data, essentially a filtering step." :disable="is_processing" /><br>
</q-card-section>
<q-card-section>
<div class="text-h6">Parameters</div>
@ -321,35 +379,52 @@
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input standout="custom-standout" label="Signal to Noise Threshold" type="number" step="0.1"
:placeholder="suggested_peak_picking_snr_threshold" v-model="peak_picking_snr_threshold" hint="Signal-to-Noise Ratio threshold. Peaks with SNR below this value are discarded." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_picking_snr_threshold" v-model="peak_picking_snr_threshold"
hint="Signal-to-Noise Ratio threshold. Peaks with SNR below this value are discarded."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Half Window Size" type="number"
:placeholder="suggested_peak_picking_half_window" v-model="peak_picking_half_window" hint="Number of data points to the left and right of a potential peak to consider for local maximum detection (for Profile method)." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_picking_half_window" v-model="peak_picking_half_window"
hint="Number of data points to the left and right of a potential peak to consider for local maximum detection (for Profile method)."
:readonly="is_processing"></q-input>
</div>
</div>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Min Peak Prominence" type="number" step="0.01"
:placeholder="suggested_peak_picking_min_peak_prominence" v-model="peak_picking_min_peak_prominence" hint="Minimum required prominence of a peak, expressed as a fraction of its height." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_picking_min_peak_prominence"
v-model="peak_picking_min_peak_prominence"
hint="Minimum required prominence of a peak, expressed as a fraction of its height."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Merge Peaks Tolerance (m/z)" type="number" step="0.001"
:placeholder="suggested_peak_picking_merge_peaks_tolerance" v-model="peak_picking_merge_peaks_tolerance" hint="The m/z tolerance within which to merge adjacent peaks, keeping the more intense one." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_picking_merge_peaks_tolerance"
v-model="peak_picking_merge_peaks_tolerance"
hint="The m/z tolerance within which to merge adjacent peaks, keeping the more intense one."
:readonly="is_processing"></q-input>
</div>
</div>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Min Peak Width (PPM)" type="number"
:placeholder="suggested_peak_picking_min_peak_width_ppm" v-model="peak_picking_min_peak_width_ppm" hint="Minimum acceptable peak width (FWHM) in ppm." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_picking_min_peak_width_ppm"
v-model="peak_picking_min_peak_width_ppm" hint="Minimum acceptable peak width (FWHM) in ppm."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Max Peak Width (PPM)" type="number"
:placeholder="suggested_peak_picking_max_peak_width_ppm" v-model="peak_picking_max_peak_width_ppm" hint="Maximum acceptable peak width (FWHM) in ppm." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_picking_max_peak_width_ppm"
v-model="peak_picking_max_peak_width_ppm" hint="Maximum acceptable peak width (FWHM) in ppm."
:readonly="is_processing"></q-input>
</div>
</div>
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01" class="q-mt-md"
:placeholder="suggested_peak_picking_min_peak_shape_r2" v-model="peak_picking_min_peak_shape_r2" hint="Minimum R-squared value from a Gaussian fit to the peak, used as a quality measure for peak shape." :readonly="is_processing"></q-input>
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01"
class="q-mt-md" :placeholder="suggested_peak_picking_min_peak_shape_r2"
v-model="peak_picking_min_peak_shape_r2"
hint="Minimum R-squared value from a Gaussian fit to the peak, used as a quality measure for peak shape."
:readonly="is_processing"></q-input>
</q-card-section>
</q-card>
@ -361,33 +436,44 @@
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input standout="custom-standout" label="Min SNR" type="number" step="0.1"
:placeholder="suggested_peak_selection_min_snr" v-model="peak_selection_min_snr" hint="Minimum Signal-to-Noise Ratio for a peak to be kept." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_selection_min_snr" v-model="peak_selection_min_snr"
hint="Minimum Signal-to-Noise Ratio for a peak to be kept." :readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Min FWHM (PPM)" type="number"
:placeholder="suggested_peak_selection_min_fwhm_ppm" v-model="peak_selection_min_fwhm_ppm" hint="Minimum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_selection_min_fwhm_ppm" v-model="peak_selection_min_fwhm_ppm"
hint="Minimum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept."
:readonly="is_processing"></q-input>
</div>
</div>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Max FWHM (PPM)" type="number"
:placeholder="suggested_peak_selection_max_fwhm_ppm" v-model="peak_selection_max_fwhm_ppm" hint="Maximum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_selection_max_fwhm_ppm" v-model="peak_selection_max_fwhm_ppm"
hint="Maximum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01"
:placeholder="suggested_peak_selection_min_shape_r2" v-model="peak_selection_min_shape_r2" hint="Minimum R-squared value from a Gaussian fit, filtering for good peak shape." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_selection_min_shape_r2" v-model="peak_selection_min_shape_r2"
hint="Minimum R-squared value from a Gaussian fit, filtering for good peak shape."
:readonly="is_processing"></q-input>
</div>
</div>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Frequency Threshold" type="number" step="0.01"
:placeholder="suggested_peak_selection_frequency_threshold" v-model="peak_selection_frequency_threshold"
hint="The minimum fraction of spectra a peak must be present in to be kept (0.0 to 1.0)." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_selection_frequency_threshold"
v-model="peak_selection_frequency_threshold"
hint="The minimum fraction of spectra a peak must be present in to be kept (0.0 to 1.0)."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Correlation Threshold" type="number" step="0.01"
:placeholder="suggested_peak_selection_correlation_threshold" v-model="peak_selection_correlation_threshold"
hint="Minimum correlation with neighboring peaks (not yet implemented)." :readonly="is_processing"></q-input>
:placeholder="suggested_peak_selection_correlation_threshold"
v-model="peak_selection_correlation_threshold"
hint="Minimum correlation with neighboring peaks (not yet implemented)."
:readonly="is_processing"></q-input>
</div>
</div>
</q-card-section>
@ -399,8 +485,11 @@
<div class="text-caption">The binning strategy.</div>
</q-card-section>
<q-card-section>
<q-radio v-model="binning_method" val="adaptive" label="ADAPTIVE" hint="Creates bins based on the density of detected peaks." :disable="is_processing" /><br>
<q-radio v-model="binning_method" val="uniform" label="UNIFORM" hint="Creates a fixed number of equally spaced bins over the m/z range." :disable="is_processing" /><br>
<q-radio v-model="binning_method" val="adaptive" label="ADAPTIVE"
hint="Creates bins based on the density of detected peaks." :disable="is_processing" /><br>
<q-radio v-model="binning_method" val="uniform" label="UNIFORM"
hint="Creates a fixed number of equally spaced bins over the m/z range."
:disable="is_processing" /><br>
</q-card-section>
<q-card-section>
<div class="text-h6">Parameters</div>
@ -409,35 +498,47 @@
<div class="row q-col-gutter-sm">
<div class="col-6">
<q-input standout="custom-standout" label="Tolerance (for Adaptive)" type="number" step="0.001"
:placeholder="suggested_binning_tolerance" v-model="binning_tolerance" hint="Tolerance for grouping peaks into a bin in adaptive mode." :readonly="is_processing"></q-input>
:placeholder="suggested_binning_tolerance" v-model="binning_tolerance"
hint="Tolerance for grouping peaks into a bin in adaptive mode."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-select standout="custom-standout" label="Tolerance Unit" v-model="binning_tolerance_unit"
:options="['mz', 'ppm']" hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)." :disable="is_processing"></q-select>
:options="['mz', 'ppm']"
hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)."
:disable="is_processing"></q-select>
</div>
</div>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Frequency Threshold" type="number" step="0.01"
:placeholder="suggested_binning_frequency_threshold" v-model="binning_frequency_threshold" hint="The minimum fraction of spectra a bin must contain a peak in to be kept (0.0 to 1.0)." :readonly="is_processing"></q-input>
:placeholder="suggested_binning_frequency_threshold" v-model="binning_frequency_threshold"
hint="The minimum fraction of spectra a bin must contain a peak in to be kept (0.0 to 1.0)."
:readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Min Peaks Per Bin" type="number"
:placeholder="suggested_binning_min_peak_per_bin" v-model="binning_min_peak_per_bin" hint="The minimum number of individual peaks required to form a bin in adaptive mode." :readonly="is_processing"></q-input>
:placeholder="suggested_binning_min_peak_per_bin" v-model="binning_min_peak_per_bin"
hint="The minimum number of individual peaks required to form a bin in adaptive mode."
:readonly="is_processing"></q-input>
</div>
</div>
<div class="row q-col-gutter-sm q-mt-md">
<div class="col-6">
<q-input standout="custom-standout" label="Max Bin Width (PPM)" type="number"
:placeholder="suggested_binning_max_bin_width_ppm" v-model="binning_max_bin_width_ppm" hint="Maximum width of a bin in ppm for adaptive mode." :readonly="is_processing"></q-input>
:placeholder="suggested_binning_max_bin_width_ppm" v-model="binning_max_bin_width_ppm"
hint="Maximum width of a bin in ppm for adaptive mode." :readonly="is_processing"></q-input>
</div>
<div class="col-6">
<q-input standout="custom-standout" label="Number of Uniform Bins" type="number"
:placeholder="suggested_binning_num_uniform_bins" v-model="binning_num_uniform_bins" hint="The number of bins to create for the uniform method." :readonly="is_processing"></q-input>
:placeholder="suggested_binning_num_uniform_bins" v-model="binning_num_uniform_bins"
hint="The number of bins to create for the uniform method." :readonly="is_processing"></q-input>
</div>
</div>
<q-toggle v-model="binning_intensity_weighted_centers" v-on:click="binning_intensity_weighted_centers" label="Intensity Weighted Centers"
class="q-mt-md" hint="If enabled, calculates bin centers as an intensity-weighted average of the peaks within it." :disable="is_processing"></q-toggle>
<q-toggle v-model="binning_intensity_weighted_centers" v-on:click="binning_intensity_weighted_centers"
label="Intensity Weighted Centers" class="q-mt-md"
hint="If enabled, calculates bin centers as an intensity-weighted average of the peaks within it."
:disable="is_processing"></q-toggle>
</q-card-section>
</q-card>
@ -446,12 +547,14 @@
<!-- Pipeline Controls -->
<div class="row justify-end items-center q-mt-md">
<q-btn class="q-ma-sm" icon="get_app" v-on:click="export_params_btn=true" label="Export Params" outline :disable="is_processing" />
<q-btn class="q-ma-sm" icon="upload_file" v-on:click="import_params_btn=true" label="Import Params" outline :disable="is_processing" />
<q-btn class="q-ma-sm" icon="get_app" v-on:click="export_params_btn=true" label="Export Params" outline
:disable="is_processing" />
<q-btn class="q-ma-sm" icon="upload_file" v-on:click="import_params_btn=true" label="Import Params" outline
:disable="is_processing" />
<q-btn :loading="is_processing" class="q-ma-sm btn-style" icon="play_arrow"
v-on:click="run_full_pipeline=true" padding="lg" label="Run Pipeline" :disable="is_processing" />
</div>
</q-tab-panel>
</q-tab-panel>
<q-tab-panel name="generator">
<div class="text-h6">imzML & mzML Data Processor</div>
<p>Please make sure the ibd and imzML file are located in the same directory and have the same name.
@ -467,9 +570,10 @@
<q-icon name="search" v-on:click="btnSearch=true" class="cursor-pointer" :disable="is_processing" />
</template>
</q-input>
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add" :disable="is_processing"></q-btn>
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true" :disable="is_processing || batch_file_count === 0"
label="Clear"></q-btn>
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add"
:disable="is_processing"></q-btn>
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true"
:disable="is_processing || batch_file_count === 0" label="Clear"></q-btn>
</div>
<q-list bordered separator v-if="selected_files.length > 0">
<q-item v-for="(file, index) in selected_files" :key="index">
@ -477,7 +581,8 @@
{{ file }}
</q-item-section>
<q-item-section side>
<q-btn flat round icon="delete" size="sm" v-on:click="selected_files.splice(index, 1)" :disable="is_processing"></q-btn>
<q-btn flat round icon="delete" size="sm" v-on:click="selected_files.splice(index, 1)"
:disable="is_processing"></q-btn>
</q-item-section>
</q-item>
</q-list>
@ -495,12 +600,14 @@
<div class="st-col col-4 col-sm q-ma-sm">
<q-input standout="custom-standout" id="textTol" step="0.005" v-model="Tol"
label="Mass-to-charge ratio tolerance" type="number"
:rules="[val => !!val || '* Required', val => val >= 0.0 &amp;&amp; val <= 1.0 || 'Needs to be in range between 0 and 1']" :readonly="is_processing"></q-input>
:rules="[val => !!val || '* Required', val => val >= 0.0 &amp;&amp; val <= 1.0 || 'Needs to be in range between 0 and 1']"
:readonly="is_processing"></q-input>
</div>
<div class="st-col col-4 col-sm q-ma-sm">
<q-input standout="custom-standout" id="textcolorLevel" step="1" v-model="colorLevel" label="Color levels"
type="number"
:rules="[ val => !!val || '* Required', val => val >= 2 &amp;&amp; val <= 256 || 'Needs to be in range between 2 and 256']" :readonly="is_processing"></q-input>
:rules="[ val => !!val || '* Required', val => val >= 2 &amp;&amp; val <= 256 || 'Needs to be in range between 2 and 256']"
:readonly="is_processing"></q-input>
</div>
</div>
<div class="row">
@ -519,7 +626,8 @@
<q-input standout="custom-standout" id="textTriqProb" step="0.01" v-model="triqProb"
label="TrIQ probability" type="number" :rules="[
val => triqEnabled ? ( '* Required', val >= 0.8 &amp;&amp; val <= 1 || 'Needs to be in range between 0.8 and 1') : true
]" :readonly="is_processing || !triqEnabled" :disable="is_processing || !triqEnabled"></q-input>
]" :readonly="is_processing || !triqEnabled"
:disable="is_processing || !triqEnabled"></q-input>
</div>
</div>
</div>
@ -585,9 +693,10 @@
</q-btn>
<q-btn icon="zoom_out_map" class="q-ma-sm on-right btn-style" v-on:click="compareBtn=true" padding="sm"
label="Compare" :disable="is_processing"></q-btn>
<q-btn class="q-ma-sm btn-style" icon="edit" label="Mask Editor" href="/mask" :disable="is_processing"></q-btn>
<q-btn class="q-ma-sm btn-style" icon="dashboard" v-on:click="showMetadataBtn=true"
label="Show Metadata" :disable="is_processing"></q-btn>
<q-btn class="q-ma-sm btn-style" icon="edit" label="Mask Editor" href="/mask"
:disable="is_processing"></q-btn>
<q-btn class="q-ma-sm btn-style" icon="dashboard" v-on:click="showMetadataBtn=true" label="Show Metadata"
:disable="is_processing"></q-btn>
<div class="q-pa-md row items-center" v-show="is_processing">
<q-spinner color="primary" size="2em" class="q-mr-sm"></q-spinner>
<div class="text-caption">{{ progress_message }}</div>
@ -687,7 +796,8 @@
<div class="text-subtitle1">Before Preprocessing</div>
</q-card-section>
<q-card-section>
<plotly id="plotSpectraBefore" :data="plotdata_before" :layout="plotlayout_before" class="q-pa-none q-ma-none"></plotly>
<plotly id="plotSpectraBefore" :data="plotdata_before" :layout="plotlayout_before"
class="q-pa-none q-ma-none"></plotly>
</q-card-section>
</q-card>
<q-card>
@ -710,8 +820,10 @@
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset"
class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"></q-select>
<q-space></q-space>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"
:disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true"
:disable="is_processing"></q-btn>
</div>
<!-- Image manager -->
<div id="image-container-normal" class="row st-col col-12">
@ -733,8 +845,10 @@
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset"
class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"></q-select>
<q-space></q-space>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"
:disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true"
:disable="is_processing"></q-btn>
</div>
<!-- Triq Image manager -->
<div id="image-container-triq" class="row st-col col-12">
@ -834,8 +948,8 @@
<div class="col-6">
<div class="row items-center">
<q-select v-model="selected_folder_compare_left" :options="image_available_folders"
label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;"
v-on:focus="refetch_folders = true" :disable="is_processing"></q-select>
label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"
:disable="is_processing"></q-select>
<q-space></q-space>
<st-tabs id="tabHeaderCompareLeft" :ids="CompTabIDsLeft" :labels="CompTabLabelsLeft"
v-model="CompSelectedTabLeft"></st-tabs>
@ -845,9 +959,10 @@
<!-- Content for Tab 0 -->
<!-- Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompLeft=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusCompLeft=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompLeft=true"
:disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusCompLeft=true"
:disable="is_processing"></q-btn>
</div>
<!-- Image manager -->
<div id="image-container-compare-left-normal" class="row st-col col-12">
@ -867,9 +982,10 @@
<!-- Content for Tab 1 -->
<!-- Triq Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompLeft=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusTCompLeft=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompLeft=true"
:disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusTCompLeft=true"
:disable="is_processing"></q-btn>
</div>
<!-- Triq Image manager -->
<div id="image-container-compare-left-triq" class="row st-col col-12">
@ -904,8 +1020,8 @@
<div class="col-6">
<div class="row items-center">
<q-select v-model="selected_folder_compare_right" :options="image_available_folders"
label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;"
v-on:focus="refetch_folders = true" :disable="is_processing"></q-select>
label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"
:disable="is_processing"></q-select>
<q-space></q-space>
<st-tabs id="tabHeaderCompareRight" :ids="CompTabIDsRight" :labels="CompTabLabelsRight"
v-model="CompSelectedTabRight"></st-tabs>
@ -915,9 +1031,10 @@
<!-- Content for Tab 0 -->
<!-- Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompRight=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusCompRight=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompRight=true"
:disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusCompRight=true"
:disable="is_processing"></q-btn>
</div>
<!-- Image manager -->
<div id="image-container-compare-right-normal" class="row st-col col-12">
@ -937,9 +1054,10 @@
<!-- Content for Tab 1 -->
<!-- Triq Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompRight=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusTCompRight=true" :disable="is_processing"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompRight=true"
:disable="is_processing"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusTCompRight=true"
:disable="is_processing"></q-btn>
</div>
<!-- Triq Image manager -->
<div id="image-container-compare-right-triq" class="row st-col col-12">
@ -1028,3 +1146,37 @@
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showBugModal">
<q-card style="min-width: 350px">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Report a Bug</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>
<q-card-section class="q-pt-md">
<p>To help us fix the issue, please send an email to:</p>
<div class="bg-grey-2 q-pa-sm text-weight-bold text-center">
julian.sierrag@icloud.com
</div>
<p class="q-mt-md"><strong>Subject:</strong> <code class="bg-yellow-2">JuliaMSI Bug</code></p>
<q-banner rounded class="bg-grey-3 text-body2">
<template v-slot:avatar>
<q-icon name="info" color="primary" />
</template>
Please include:
<ul>
<li>The full error message.</li>
<li>Steps to reproduce the crash.</li>
</ul>
</q-banner>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Close" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>

View File

@ -1,6 +1,5 @@
# julia_imzML_visual.jl
const REGISTRY_LOCK = ReentrantLock()
"""
increment_image(current_image, image_list)

View File

@ -9,7 +9,7 @@ using Statistics, NaturalSort, LinearAlgebra, StipplePlotly
using Base.Filesystem: mv
using MSI_src
using .MSI_src: MSIData, process_image_pipeline
using .MSI_src: MSIData, process_image_pipeline, REGISTRY_LOCK
# Plot Handling
include("./julia_imzML_visual.jl")
@ -799,9 +799,11 @@ end
end
# Save the updated registry
lock(REGISTRY_LOCK) do
open(reg_path, "w") do f
JSON.print(f, registry, 4)
end
end
@info "Registry updated with mask: $(final_mask_name)"
@ -874,10 +876,12 @@ end
if !isempty(new_folders) || !isempty(removed_folders)
println("Registry changed, saving...")
lock(REGISTRY_LOCK) do
open(reg_path, "w") do f
JSON.print(f, registry, 4)
end
end
end
all_folders = sort(collect(keys(registry)), lt=natural)
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders)

View File

@ -82,23 +82,27 @@ A dramatically simplified buffer pool that avoids complex locking.
mutable struct SimpleBufferPool
buffers::Dict{Int, Vector{Vector{UInt8}}}
max_pool_size::Int
lock::ReentrantLock # Add lock for thread safety
end
SimpleBufferPool() = SimpleBufferPool(Dict{Int, Vector{Vector{UInt8}}}(), 50)
SimpleBufferPool() = SimpleBufferPool(Dict{Int, Vector{Vector{UInt8}}}(), 50, ReentrantLock())
function get_buffer!(pool::SimpleBufferPool, size::Int)::Vector{UInt8}
lock(pool.lock) do
# Check for existing buffers of exact size first
if haskey(pool.buffers, size) && !isempty(pool.buffers[size])
return pop!(pool.buffers[size])
end
end
# No suitable buffer found, allocate new one
# No suitable buffer found, allocate new one (outside lock to reduce contention)
return Vector{UInt8}(undef, size)
end
function release_buffer!(pool::SimpleBufferPool, buffer::Vector{UInt8})
size = length(buffer)
lock(pool.lock) do
if !haskey(pool.buffers, size)
pool.buffers[size] = Vector{Vector{UInt8}}()
end
@ -107,6 +111,7 @@ function release_buffer!(pool::SimpleBufferPool, buffer::Vector{UInt8})
if length(pool.buffers[size]) < pool.max_pool_size
push!(pool.buffers[size], buffer)
end
end
# If pool is full, let buffer get GC'd
end

View File

@ -35,7 +35,12 @@ Atomically seeks to a position and reads data into an array. This is the thread-
way to read from a specific offset in the file.
"""
function read_at!(tsfh::ThreadSafeFileHandle, a::AbstractArray, pos::Integer)
if pos < 0
throw(ArgumentError("Invalid seek position: $pos"))
end
lock(tsfh.lock) do
# Also check against file size if possible, though filesize() might be expensive to call repeatedly
# Let's rely on seek throwing if it goes way out of bounds, but catch the negative case which is definitely an error.
seek(tsfh.handle, pos)
read!(tsfh.handle, a)
end
@ -510,6 +515,19 @@ function read_spectrum_from_disk(source::ImzMLSource, meta::SpectrumMetadata)
mz = Array{source.mz_format}(undef, meta.mz_asset.encoded_length)
intensity = Array{source.intensity_format}(undef, meta.int_asset.encoded_length)
# Validate offsets before reading
file_size = filesize(source.ibd_handle)
mz_end = meta.mz_asset.offset + sizeof(source.mz_format) * meta.mz_asset.encoded_length
if meta.mz_asset.offset < 0 || mz_end > file_size
throw(FileFormatError("Invalid m/z data offset/length for spectrum $(meta.id): offset=$(meta.mz_asset.offset), end=$mz_end, file_size=$file_size"))
end
int_end = meta.int_asset.offset + sizeof(source.intensity_format) * meta.int_asset.encoded_length
if meta.int_asset.offset < 0 || int_end > file_size
throw(FileFormatError("Invalid intensity data offset/length for spectrum $(meta.id): offset=$(meta.int_asset.offset), end=$int_end, file_size=$file_size"))
end
# Use the new atomic read_at! method for thread-safety
read_at!(source.ibd_handle, mz, meta.mz_asset.offset)
read_at!(source.ibd_handle, intensity, meta.int_asset.offset)

View File

@ -19,7 +19,11 @@ export OpenMSIData,
get_global_mz_range,
MSIData,
_iterate_spectra_fast,
validate_spectrum
validate_spectrum,
REGISTRY_LOCK
# Define shared registry lock
const REGISTRY_LOCK = ReentrantLock()
# Export the public preprocessing & precalculations API
export run_preprocessing_analysis,

View File

@ -1,4 +1,5 @@
using StatsBase # For mean, std, median, quantile, mad
using Base.Threads # For atomic counters and locking
# =============================================================================
# 7) Spatial & Advanced Processing (Stubs & New Functions)
@ -80,11 +81,12 @@ function analyze_mass_accuracy(
println("Analyzing mass accuracy for $(length(spectrum_indices)) spectra...")
all_ppm_errors = Float64[]
total_matched_peaks = 0
total_spectra_processed = 0
total_matched_peaks = Atomic{Int}(0)
total_spectra_processed = Atomic{Int}(0)
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
total_spectra_processed += 1
atomic_add!(total_spectra_processed, 1)
if !validate_spectrum(mz, intensity)
@warn "Spectrum $idx is invalid, skipping mass accuracy analysis for it."
return
@ -106,8 +108,10 @@ function analyze_mass_accuracy(
end
if best_matched_peak_mz !== nothing
lock(results_lock) do
push!(all_ppm_errors, min_ppm_error)
total_matched_peaks += 1
end
atomic_add!(total_matched_peaks, 1)
end
end
end
@ -140,8 +144,8 @@ function analyze_mass_accuracy(
std_ppm_error = std_err,
min_ppm_error = min_err,
max_ppm_error = max_err,
total_matched_peaks = total_matched_peaks,
total_spectra_analyzed = total_spectra_processed,
total_matched_peaks = total_matched_peaks[],
total_spectra_analyzed = total_spectra_processed[],
ppm_error_distribution = all_ppm_errors
)
end
@ -318,23 +322,31 @@ function analyze_instrument_characteristics(msi_data::MSIData; sample_indices::A
return results
end
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity
# Record spectrum mode
lock(results_lock) do
push!(spectrum_modes, msi_data.spectra_metadata[idx].mode)
end
# Calculate m/z step statistics (for profile data)
if length(mz) > 1 && msi_data.spectra_metadata[idx].mode == PROFILE
steps = diff(mz)
if !isempty(steps)
lock(results_lock) do
push!(mz_step_sizes, mean(steps))
end
end
end
# Record intensity range
if !isempty(intensity)
lock(results_lock) do
push!(intensity_ranges, (minimum(intensity), maximum(intensity)))
end
end
end
# Determine acquisition mode
if length(spectrum_modes) == 1
@ -442,11 +454,15 @@ function analyze_signal_quality(msi_data::MSIData; sample_indices::AbstractVecto
return results
end
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity
if !isempty(intensity)
# Noise estimation using MAD
noise = mad(intensity, normalize=true)
lock(results_lock) do
push!(noise_levels, noise)
end
# FIX: More robust SNR calculation
valid_intensity = intensity[intensity .> 0] # Remove zeros
@ -458,14 +474,18 @@ function analyze_signal_quality(msi_data::MSIData; sample_indices::AbstractVecto
if noise_robust > 0 && isfinite(signal_estimate)
snr_val = signal_estimate / noise_robust
# Cap unrealistic SNR values
lock(results_lock) do
push!(snr_distribution, min(snr_val, 1e6))
end
end
end
# Total ion count
lock(results_lock) do
push!(tic_values, sum(intensity))
end
end
end
if !isempty(noise_levels)
results[:noise_mean] = mean(noise_levels)
@ -651,20 +671,23 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
peak_counts = Int[]
fwhm_values = Float64[]
spectra_analyzed = 0
peaks_analyzed = 0
spectra_analyzed = Atomic{Int}(0)
peaks_analyzed = Atomic{Int}(0)
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
if length(mz) < 10 # Skip spectra with too few points
return
end
spectra_analyzed += 1
atomic_add!(spectra_analyzed, 1)
meta = msi_data.spectra_metadata[idx]
# Detect peaks with lower SNR threshold to find more peaks
peaks = detect_peaks_profile_core(mz, intensity; snr_threshold=2.0)
lock(results_lock) do
push!(peak_counts, length(peaks))
end
if !isempty(peaks)
# Analyze the strongest 3 peaks per spectrum
@ -679,15 +702,17 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
if !isnan(fwhm_delta_m) && fwhm_delta_m > 0.001 && fwhm_delta_m < 0.5 # Reasonable range in Da
fwhm_ppm = 1e6 * fwhm_delta_m / peak.mz
if 5.0 < fwhm_ppm < 500.0 # Reasonable ppm range
lock(results_lock) do
push!(peak_widths_ppm, fwhm_ppm)
push!(fwhm_values, fwhm_delta_m)
r2 = _fit_gaussian_and_r2(mz, intensity, peak_idx, 5)
push!(r_squared_values, r2)
peaks_analyzed += 1
end
atomic_add!(peaks_analyzed, 1)
if peaks_analyzed <= 3
println("DEBUG: Peak at m/z $(peak.mz), FWHM = $(fwhm_ppm) ppm, R² = $r2")
if peaks_analyzed[] <= 3
println("DEBUG: Peak at m/z $(peak.mz), FWHM = $(fwhm_ppm) ppm, R² = $(r_squared_values[end])")
end
end
end
@ -698,7 +723,7 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
end
end
println("DEBUG: Analyzed $peaks_analyzed peaks from $spectra_analyzed spectra")
println("DEBUG: Analyzed $(peaks_analyzed[]) peaks from $(spectra_analyzed[]) spectra")
if !isempty(peak_widths_ppm)
results[:mean_fwhm_ppm] = mean(peak_widths_ppm)
@ -727,11 +752,15 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
peak_counts = Int[]
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
if !isempty(mz)
lock(results_lock) do
push!(peak_counts, length(mz))
end
end
end
if !isempty(peak_counts)
results[:mean_peaks_per_spectrum] = mean(peak_counts)

View File

@ -33,7 +33,15 @@ function apply_baseline_correction(spectra::Vector{MutableSpectrum}, params::Dic
Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity)
original_length = length(s.intensity)
baseline = apply_baseline_correction_core(s.intensity; method=method, iterations=iterations, window=window)
# CRITICAL: Ensure baseline has the same length as intensity
if length(baseline) != original_length
@warn "Baseline correction: length mismatch for spectrum $(s.id). baseline=$(length(baseline)), intensity=$original_length. Skipping this spectrum."
continue
end
s.intensity = max.(0.0, s.intensity .- baseline)
end
end
@ -53,7 +61,16 @@ function apply_intensity_transformation(spectra::Vector{MutableSpectrum}, params
Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity)
s.intensity = transform_intensity_core(s.intensity; method=method)
original_length = length(s.intensity)
transformed = transform_intensity_core(s.intensity; method=method)
# CRITICAL: Ensure transformation preserves array length
if length(transformed) != original_length
@warn "Intensity transformation: length mismatch for spectrum $(s.id). transformed=$(length(transformed)), original=$original_length. Skipping this spectrum."
continue
end
s.intensity = transformed
end
end
end
@ -76,8 +93,16 @@ function apply_smoothing(spectra::Vector{MutableSpectrum}, params::Dict)
Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity)
smoothed_intensity = max.(0.0, smooth_spectrum_core(s.intensity; method=method, window=window, order=order))
s.intensity = smoothed_intensity
original_length = length(s.intensity)
smoothed_intensity = smooth_spectrum_core(s.intensity; method=method, window=window, order=order)
# CRITICAL: Ensure smoothing preserves array length
if length(smoothed_intensity) != original_length
@warn "Smoothing: length mismatch for spectrum $(s.id). smoothed=$(length(smoothed_intensity)), original=$original_length, method=$method, window=$window. Skipping this spectrum."
continue
end
s.intensity = max.(0.0, smoothed_intensity)
end
end
end
@ -104,6 +129,7 @@ function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict)
Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity)
# old_len = length(s.peaks)
if method == :profile
s.peaks = detect_peaks_profile_core(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window, min_peak_prominence=min_peak_prominence, merge_peaks_tolerance=merge_peaks_tolerance)
elseif method == :wavelet
@ -113,6 +139,7 @@ function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict)
else
s.peaks = detect_peaks_profile_core(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window)
end
# @info "Spectrum $(s.id): detected $(length(s.peaks)) peaks"
else
s.peaks = []
end
@ -182,12 +209,21 @@ function apply_calibration(spectra::Vector{MutableSpectrum}, params::Dict, refer
Threads.@threads for i in 1:length(spectra)
s = spectra[i]
if validate_spectrum(s.mz, s.intensity)
original_length = length(s.mz)
matched_peaks = find_calibration_peaks_core(s.mz, s.intensity, reference_masses; ppm_tolerance=ppm_tolerance)
if length(matched_peaks) >= 2
measured = sort(collect(values(matched_peaks)))
theoretical = sort(collect(keys(matched_peaks)))
itp = linear_interpolation(measured, theoretical, extrapolation_bc=Line())
s.mz = itp(s.mz) # Modify mz-axis in-place
new_mz = itp(s.mz)
# CRITICAL: Ensure m/z axis preserves array length
if length(new_mz) != original_length
@warn "Calibration: length mismatch for spectrum $(s.id). new_mz=$(length(new_mz)), original=$original_length. Skipping this spectrum."
continue
end
s.mz = new_mz # Modify mz-axis in-place
else
@warn "Spectrum $(s.id): insufficient reference peaks ($(length(matched_peaks)) found), skipping calibration."
end
@ -233,7 +269,16 @@ function apply_peak_alignment(spectra::Vector{MutableSpectrum}, params::Dict)
current_peaks_mz = [p.mz for p in s.peaks]
alignment_func = align_peaks_lowess_core(ref_peaks_mz, current_peaks_mz; method=method, tolerance=tolerance, tolerance_unit=tolerance_unit)
s.mz = alignment_func.(s.mz) # Update m/z axis
original_length = length(s.mz)
new_mz = alignment_func.(s.mz)
# CRITICAL: Ensure alignment preserves array length
if length(new_mz) != original_length
@warn "Peak alignment: m/z length mismatch for spectrum $(s.id). new_mz=$(length(new_mz)), original=$original_length. Skipping this spectrum."
continue
end
s.mz = new_mz # Update m/z axis
# Update peak m/z values
for i in 1:length(s.peaks)
@ -258,7 +303,16 @@ function apply_normalization(spectra::Vector{MutableSpectrum}, params::Dict)
Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity)
s.intensity = apply_normalization_core(s.intensity; method=method)
original_length = length(s.intensity)
normalized = apply_normalization_core(s.intensity; method=method)
# CRITICAL: Ensure normalization preserves array length
if length(normalized) != original_length
@warn "Normalization: length mismatch for spectrum $(s.id). normalized=$(length(normalized)), original=$original_length. Skipping this spectrum."
continue
end
s.intensity = normalized
end
end
end

View File

@ -6,17 +6,14 @@ ENV["GENIE_ENV"] = "dev"
manifest_path = joinpath(@__DIR__, "Manifest.toml")
# Only instantiate in development mode
if get(ENV, "GENIE_ENV", "dev") != "prod" || !isfile(manifest_path)
@info "Development environment detected. Instantiating packages..."
if !isfile(manifest_path)
@info "Manifest.toml not found. Generating it based on Project.toml..."
end
# Selective instantiation for faster startup
if get(ENV, "GENIE_ENV", "dev") != "prod" && !isfile(manifest_path)
@info "Development environment detected and Manifest.toml missing. Instantiating packages..."
Pkg.resolve()
Pkg.instantiate()
Pkg.gc()
elseif get(ENV, "GENIE_ENV", "dev") != "prod"
@info "Manifest.toml found. Skipping Pkg.instantiate() for faster boot. Delete Manifest.toml if you need to re-instantiate."
end
using Genie

View File

@ -0,0 +1,32 @@
using MSI_src
using Test
using Base.Threads
@testset "SimpleBufferPool Concurrency Stress Test" begin
pool = MSI_src.SimpleBufferPool()
n_threads = Threads.nthreads()
n_iterations = 1000
buffer_size = 1024
println("Running buffer pool stress test with $n_threads threads...")
# Parallel stress test
Threads.@threads for i in 1:(n_threads * n_iterations)
# get_buffer! and release_buffer! are now thread-safe
buf = MSI_src.get_buffer!(pool, buffer_size)
# Simulate some work
fill!(buf, UInt8(i % 256))
MSI_src.release_buffer!(pool, buf)
end
# After stress test, the dictionary should be coherent
sizes = collect(keys(pool.buffers))
if !isempty(sizes)
@test buffer_size sizes
@test length(pool.buffers[buffer_size]) <= pool.max_pool_size
end
println("Buffer pool stress test PASSED.")
end

View File

@ -0,0 +1,146 @@
using Pkg
Pkg.activate(joinpath(@__DIR__, ".."))
using MSI_src
using Test
using Base.Threads
# --- Test Configuration ---
const TEST_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/Stomach/Stomach_DHB_uncompressed.imzML"
println("Starting concurrency test with file: $TEST_FILE")
if !isfile(TEST_FILE)
error("Test file not found: $TEST_FILE")
end
# --- Simulation of the Concurrency Issue ---
# Global "UI State" variable simulates the app's msi_data
global_msi_data = nothing
const GLOBAL_LOCK = ReentrantLock()
function ui_load_file(path)
global global_msi_data
lock(GLOBAL_LOCK) do
if global_msi_data !== nothing
close(global_msi_data)
end
println("UI: Loading file...")
global_msi_data = OpenMSIData(path)
println("UI: File loaded.")
end
end
function ui_close_file()
global global_msi_data
lock(GLOBAL_LOCK) do
if global_msi_data !== nothing
println("UI: Closing file...")
close(global_msi_data)
global_msi_data = nothing
println("UI: File closed.")
end
end
end
# Mock pipeline function that mimics `run_full_pipeline` in app.jl
# Crucially, it uses its own local `pipeline_msi_data` as per the fix
function run_mock_pipeline_isolated(path)
println("Pipeline: Starting isolated pipeline...")
# 1. Open ISOLATED instance
pipeline_msi_data = OpenMSIData(path)
println("Pipeline: Opened isolated MSIData instance.")
try
# 2. Simulate reading spectra
indices = 1:min(100, length(pipeline_msi_data.spectra_metadata)) # Read first 100 spectra
# Artificial delay to allow "user" interaction
sleep(0.5)
println("Pipeline: Reading spectra...")
Threads.@threads for i in indices
# Use local instance
mz, int = GetSpectrum(pipeline_msi_data, i)
# Simulate processing work
sum(int)
end
println("Pipeline: Finished reading spectra successfully.")
return true
catch e
println("Pipeline: CRASHED with error: $e")
return false
finally
close(pipeline_msi_data)
println("Pipeline: Closed isolated MSIData instance.")
end
end
# Mock pipeline that uses GLOBAL instance (The BUGGY version)
function run_mock_pipeline_buggy()
println("Buggy Pipeline: Starting...")
# Uses global_msi_data directly
try
global global_msi_data
if global_msi_data === nothing
println("Buggy Pipeline: No data loaded!")
return false
end
local_ref = global_msi_data # Still points to same object
indices = 1:min(100, length(local_ref.spectra_metadata))
sleep(0.5)
println("Buggy Pipeline: Reading spectra from shared object...")
Threads.@threads for i in indices
# This will fail if ui_close_file() happens concurrently
mz, int = GetSpectrum(local_ref, i)
sum(int)
end
println("Buggy Pipeline: Success (Unexpected if concurrency worked)")
return true
catch e
println("Buggy Pipeline: CRASHED as expected: $e")
return false
end
end
# --- execute checks ---
@testset "Concurrency Crash Fix Verification" begin
# 1. Setup: Load file initially
ui_load_file(TEST_FILE)
# 2. Test the FIX: Isolated Pipeline
println("\n--- Testing Fixed (Isolated) Pipeline ---")
t_pipeline = Threads.@spawn run_mock_pipeline_isolated(TEST_FILE)
# Simulate user closing/reloading file while pipeline runs
sleep(0.2)
ui_close_file()
# Wait for pipeline
success = fetch(t_pipeline)
@test success == true
println("Fixed pipeline result: ", success ? "PASSED" : "FAILED")
# 3. Test the BUG: Global Pipeline (Optional, to prove it crashes without fix)
# Uncomment to verify the bug exists if needed, but we assume it does based on user report.
# println("\n--- Testing Buggy (Shared) Pipeline ---")
# ui_load_file(TEST_FILE)
# t_buggy = Threads.@spawn run_mock_pipeline_buggy()
# sleep(0.2)
# ui_close_file()
# buggy_success = fetch(t_buggy)
# println("Buggy pipeline result: ", buggy_success ? "PASSED (No crash?)" : "FAILED (Crashed as expected)")
end