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 PlotlyBase
using CairoMakie using CairoMakie
using Colors using Colors
using Dates
using MSI_src # Import the new MSIData library using MSI_src # Import the new MSIData library
using Statistics using Statistics
using NaturalSort using NaturalSort
@ -113,7 +114,9 @@ function get_processed_mean_spectrum(spectra::Vector{MutableSpectrum}; num_bins=
# 3. Bin intensities # 3. Bin intensities
for s in spectra 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) bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0)
final_index = clamp(bin_index, 1, num_bins) final_index = clamp(bin_index, 1, num_bins)
intensity_sum[final_index] += s.intensity[i] 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 inv_bin_step = 1.0 / bin_step
for s in spectra 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) bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0)
final_index = clamp(bin_index, 1, num_bins) final_index = clamp(bin_index, 1, num_bins)
intensity_sum[final_index] += s.intensity[i] 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 return collect(mz_bins), intensity_sum
end 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 @genietools
# == Reactive code == # == 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 # Reactive code to make the UI interactive
@app begin @app begin
# == Notification & Logs ==
# @in log_entries = Dict{String,Any}[]
# @in show_log_sidebar = false
@in showBugModal = false
# == Loading Screen Variables == # == Loading Screen Variables ==
@in is_initializing = true @in is_initializing = true
@in initialization_message = "Initializing..." @in initialization_message = "Initializing..."
@ -348,6 +386,7 @@ end
@in export_params_btn = false # Export parameters to file @in export_params_btn = false # Export parameters to file
@in import_params_btn = false # Import parameters from file @in import_params_btn = false # Import parameters from file
@in save_feature_matrix_btn = false # Save feature matrix results @in save_feature_matrix_btn = false # Save feature matrix results
@in reset_session_btn = false # Deep session reset
# Preprocessing results # Preprocessing results
@in selected_spectrum_id_for_plot = 1 @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 # 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 # 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 @onbutton reset_session_btn begin
# as the active dataset for the UI. 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 @onbutton btnSearch begin
is_processing = true is_processing = true
push!(__model__)
picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml") picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml")
if isempty(picked_route)
if isnothing(picked_route) || isempty(picked_route)
is_processing = false is_processing = false
return return
end end
@ -938,6 +1029,7 @@ end
@onbutton export_params_btn begin @onbutton export_params_btn begin
is_processing = true is_processing = true
push!(__model__)
params_to_export = Dict( params_to_export = Dict(
"pipeline_step_order" => pipeline_step_order, "pipeline_step_order" => pipeline_step_order,
"enable_standards" => enable_standards, # Export global flag "enable_standards" => enable_standards, # Export global flag
@ -1011,6 +1103,7 @@ end
@onbutton import_params_btn begin @onbutton import_params_btn begin
is_processing = true is_processing = true
push!(__model__)
picked_file = pick_file(filterlist="json") picked_file = pick_file(filterlist="json")
if isempty(picked_file) if isempty(picked_file)
is_processing = false is_processing = false
@ -1021,39 +1114,57 @@ end
json_string = read(picked_file, String) json_string = read(picked_file, String)
params = JSON.parse(json_string) params = JSON.parse(json_string)
# This is a list of all known reactive variables that can be imported. # Import special variables first
# This prevents arbitrary variable assignment. if haskey(params, "reference_peaks_list")
known_params = [ reference_peaks_list = params["reference_peaks_list"]
"stabilization_method", "smoothing_method", "smoothing_window", "smoothing_order", end
"baseline_method", "baseline_iterations", "baseline_window", "normalization_method", if haskey(params, "pipeline_step_order")
"alignment_method", "alignment_span", "alignment_tolerance", "alignment_tolerance_unit", pipeline_step_order = params["pipeline_step_order"]
"alignment_max_shift_ppm", "alignment_min_matched_peaks", "peak_picking_method", end
"peak_picking_snr_threshold", "peak_picking_half_window", "peak_picking_min_peak_prominence", if haskey(params, "enable_standards")
"peak_picking_merge_peaks_tolerance", "peak_picking_min_peak_width_ppm", enable_standards = params["enable_standards"]
"peak_picking_max_peak_width_ppm", "peak_picking_min_peak_shape_r2", "binning_method", end
"binning_tolerance", "binning_tolerance_unit", "binning_frequency_threshold",
"binning_min_peak_per_bin", "binning_max_bin_width_ppm", "binning_intensity_weighted_centers", # Import regular parameters with explicit assignments
"binning_num_uniform_bins", "calibration_fit_order", "calibration_ppm_tolerance", haskey(params, "stabilization_method") && (stabilization_method = params["stabilization_method"])
"peak_selection_min_snr", "peak_selection_min_fwhm_ppm", "peak_selection_max_fwhm_ppm", haskey(params, "smoothing_method") && (smoothing_method = params["smoothing_method"])
"peak_selection_min_shape_r2", "peak_selection_frequency_threshold", "peak_selection_correlation_threshold" 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))." msg = "Parameters imported successfully from $(basename(picked_file))."
catch e catch e
msg = "Failed to import parameters: $e" msg = "Failed to import parameters: $e"
@ -1110,13 +1221,14 @@ end
@onbutton run_full_pipeline begin @onbutton run_full_pipeline begin
is_processing = true is_processing = true
push!(__model__)
overall_progress = 0.0
local pipeline_msi_data = nothing
local current_spectra = Vector{MutableSpectrum}()
current_pipeline_step = "Initializing..." current_pipeline_step = "Initializing..."
println("DEBUG: run_full_pipeline started.")
# println("DEBUG: Current pipeline_step_order configuration: $pipeline_step_order")
try try
# --- 1. Initial Checks and Data Loading --- # --- 1. Initial Checks and Data Loading ---
println("DEBUG: Performing initial checks and data loading...")
if isempty(selected_folder_main) if isempty(selected_folder_main)
msg = "No dataset loaded. Please load a file using 'Select an imzMl / mzML file'." msg = "No dataset loaded. Please load a file using 'Select an imzMl / mzML file'."
warning_msg = true warning_msg = true
@ -1136,26 +1248,31 @@ end
target_path = entry["source_path"] target_path = entry["source_path"]
# Ensure msi_data is for the currently selected file and load if needed # Ensure msi_data is for the currently selected file and load if needed
if msi_data === nothing || full_route != target_path # NOTE: For the pipeline, we will open a DEDICATED instance to avoid race conditions
println("DEBUG: Active file path changed or data not in memory. Reloading MSI data: $(basename(target_path))") # with the global msi_data used for plotting/interactive exploration.
if msi_data !== nothing; close(msi_data); end println("DEBUG: Opening isolated MSIData instance for pipeline stability...")
msg = "Reloading $(basename(target_path)) for analysis..." pipeline_msi_data = OpenMSIData(target_path)
full_route = target_path
msi_data = OpenMSIData(target_path)
# Determine plot mode from loaded data # Determine plot mode from metadata for correct visualization late
df = msi_data.spectrum_stats_df 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) if df !== nothing && "Mode" in names(df)
profile_count = count(==(MSI_src.PROFILE), df.Mode) profile_count = count(==(MSI_src.PROFILE), df.Mode)
total_count = length(df.Mode) total_count = length(df.Mode)
last_plot_mode = profile_count > total_count / 2 ? "lines" : "stem" last_plot_mode = profile_count > total_count / 2 ? "lines" : "stem"
println("DEBUG: Auto-detected plot mode for pipeline: $(last_plot_mode)")
else else
last_plot_mode = "lines" # Default last_plot_mode = "lines" # Default
end end
else
println("DEBUG: Using already loaded MSI data for $(basename(target_path)).")
end end
println("DEBUG: Auto-detected plot mode from metadata: $(last_plot_mode) (acq_mode: $(acq_mode)) [Initial set]")
# Mask path retrieval from registry # Mask path retrieval from registry
local mask_path_for_pipeline::Union{String, Nothing} = nothing local mask_path_for_pipeline::Union{String, Nothing} = nothing
@ -1171,6 +1288,7 @@ end
warning_msg = true warning_msg = true
@warn msg @warn msg
println("DEBUG: $msg") println("DEBUG: $msg")
close(pipeline_msi_data) # Important cleanup
return return
end end
else else
@ -1178,6 +1296,7 @@ end
warning_msg = true warning_msg = true
@warn msg @warn msg
println("DEBUG: $msg") println("DEBUG: $msg")
close(pipeline_msi_data) # Important cleanup
return return
end end
else else
@ -1185,22 +1304,24 @@ end
end end
# Apply mask if enabled to get indices to process # 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 if mask_path_for_pipeline !== nothing
current_pipeline_step = "Applying mask..." current_pipeline_step = "Applying mask..."
println("DEBUG: Applying mask matrix to filter spectra...") println("DEBUG: Applying mask matrix to filter spectra...")
mask_matrix = load_and_prepare_mask(mask_path_for_pipeline, msi_data.image_dims) mask_matrix = load_and_prepare_mask(mask_path_for_pipeline, pipeline_msi_data.image_dims)
masked_indices_set = get_masked_spectrum_indices(msi_data, mask_matrix) masked_indices_set = get_masked_spectrum_indices(pipeline_msi_data, mask_matrix)
spectrum_indices_to_process = collect(masked_indices_set) spectrum_indices_to_process = collect(masked_indices_set)
if isempty(spectrum_indices_to_process) if isempty(spectrum_indices_to_process)
msg = "No spectra remaining after applying mask. Aborting pipeline." msg = "No spectra remaining after applying mask. Aborting pipeline."
warning_msg = true warning_msg = true
println("DEBUG: $msg") println("DEBUG: $msg")
close(pipeline_msi_data) # Important cleanup
return return
end end
println("DEBUG: $(length(spectrum_indices_to_process)) spectra remaining after mask application.") println("DEBUG: $(length(spectrum_indices_to_process)) spectra remaining after mask application.")
else 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 end
# Apply subset processing if enabled # 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.") println("DEBUG: Subset processing enabled. Processing first $(length(spectrum_indices_to_process)) of $n_total spectra.")
end end
# Initialize spectra data structure # --- BOUNDS VALIDATION AND DIAGNOSTIC LOGGING ---
current_pipeline_step = "Loading spectra..." # Validate all indices are within bounds before attempting to load
println("DEBUG: Loading $(length(spectrum_indices_to_process)) spectra into MutableSpectrum objects...") max_spectra_idx = length(pipeline_msi_data.spectra_metadata)
current_spectra = Vector{MutableSpectrum}(undef, length(spectrum_indices_to_process)) 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}}[]) 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 end
println("DEBUG: All spectra loaded into temporary structure for processing.") 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 --- # --- 2. Parameter Assembly with Validation ---
current_pipeline_step = "Configuring parameters..." current_pipeline_step = "Configuring parameters..."
@ -1450,8 +1645,16 @@ end
mask_path_for_pipeline mask_path_for_pipeline
) do step ) do step
current_pipeline_step = "Processing: $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 end
push!(__model__)
end
println("DEBUG: Pipeline execution finished.") println("DEBUG: Pipeline execution finished.")
# 4. Update Results Display # 4. Update Results Display
@ -1638,7 +1841,8 @@ end
mkpath(output_dir) mkpath(output_dir)
save_feature_matrix(feature_matrix_result, bin_info_result, output_dir) save_feature_matrix(feature_matrix_result, bin_info_result, output_dir)
msg = "Pipeline completed successfully. Feature matrix saved." msg = "Pipeline completed successfully. Feature matrix saved."
println("DEBUG: Feature matrix saved to $output_dir") overall_progress = 1.0
push!(__model__)
else else
msg = "Pipeline completed successfully. No feature matrix generated (binning step not enabled)." msg = "Pipeline completed successfully. No feature matrix generated (binning step not enabled)."
println("DEBUG: $msg") println("DEBUG: $msg")
@ -1650,18 +1854,71 @@ end
@error "Pipeline failed" exception=(e, catch_backtrace()) @error "Pipeline failed" exception=(e, catch_backtrace())
println("DEBUG: Pipeline caught an exception: $e") println("DEBUG: Pipeline caught an exception: $e")
finally 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 is_processing = false
overall_progress = 0.0
current_pipeline_step = "" current_pipeline_step = ""
println("DEBUG: run_full_pipeline finished (finally block).") 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() 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
end end
println("DEBUG: Memory cleanup complete.")
end
end end
@onbutton recalculate_suggestions_btn begin @onbutton recalculate_suggestions_btn begin
is_processing = true is_processing = true
push!(__model__)
if msi_data === nothing if msi_data === nothing
msg = "Please load a file first." msg = "Please load a file first."
warning_msg = true warning_msg = true
@ -1903,6 +2160,7 @@ end
# This new handler correctly adds the file from full_route to the batch list. # This new handler correctly adds the file from full_route to the batch list.
@onbutton btnAddBatch begin @onbutton btnAddBatch begin
is_processing = true is_processing = true
push!(__model__)
if isempty(full_route) || full_route == "unknown (manually added)" if isempty(full_route) || full_route == "unknown (manually added)"
msg = "No active file selected to add to batch." msg = "No active file selected to add to batch."
warning_msg = true warning_msg = true
@ -1923,6 +2181,7 @@ end
@onbutton clear_batch_btn begin @onbutton clear_batch_btn begin
is_processing = true is_processing = true
push!(__model__)
selected_files = String[] selected_files = String[]
batch_file_count = 0 batch_file_count = 0
msg = "Batch cleared" msg = "Batch cleared"
@ -1974,6 +2233,7 @@ end
@onchange btnSearchMzml, btnSearchSync begin @onchange btnSearchMzml, btnSearchSync begin
is_processing = true is_processing = true
push!(__model__)
if btnSearchMzml if btnSearchMzml
picked_route = pick_file(; filterlist="mzML,mzml") picked_route = pick_file(; filterlist="mzML,mzml")
if !isempty(picked_route) if !isempty(picked_route)
@ -1997,6 +2257,7 @@ end
@onbutton convert_process begin @onbutton convert_process begin
is_processing = true is_processing = true
push!(__model__)
if isempty(mzml_full_route) || isempty(sync_full_route) if isempty(mzml_full_route) || isempty(sync_full_route)
msg_conversion = "Please select both an .mzML file and a .txt sync file." msg_conversion = "Please select both an .mzML file and a .txt sync file."
warning_msg = true warning_msg = true
@ -2029,6 +2290,7 @@ end
@error "Conversion failed" exception=(e, catch_backtrace()) @error "Conversion failed" exception=(e, catch_backtrace())
finally finally
is_processing = false is_processing = false
overall_progress = 0.0
# Re-enable button if files are still selected # Re-enable button if files are still selected
btnConvertDisable = isempty(mzml_full_route) || isempty(sync_full_route) btnConvertDisable = isempty(mzml_full_route) || isempty(sync_full_route)
end end
@ -2061,6 +2323,7 @@ end
return return
end end
is_processing = true is_processing = true
push!(__model__)
masses = Float64[] masses = Float64[]
try try
@ -2087,7 +2350,8 @@ end
for (file_idx, file_path) in enumerate(current_selected_files) for (file_idx, file_path) in enumerate(current_selected_files)
progress_message = "Processing file $(file_idx)/$(num_files): $(basename(file_path))" 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 = ( all_params = (
tolerance = current_tol, tolerance = current_tol,
@ -2218,6 +2482,10 @@ end
end end
@onbutton createMeanPlot @time begin @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) if isempty(selected_folder_main)
msg = "No dataset selected. Please process a file and select a folder first." msg = "No dataset selected. Please process a file and select a folder first."
warning_msg = true warning_msg = true
@ -2225,6 +2493,7 @@ end
end end
is_processing = true is_processing = true
push!(__model__)
try try
sTime = time() sTime = time()
@ -2280,6 +2549,19 @@ end
@error "Mean spectrum plotting failed" exception=(e, catch_backtrace()) @error "Mean spectrum plotting failed" exception=(e, catch_backtrace())
finally finally
is_processing = false 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 GC.gc() # Trigger garbage collection
if Sys.islinux() if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
@ -2288,6 +2570,10 @@ end
end end
@onbutton createSumPlot @time begin @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) if isempty(selected_folder_main)
msg = "No dataset selected. Please process a file and select a folder first." msg = "No dataset selected. Please process a file and select a folder first."
warning_msg = true warning_msg = true
@ -2295,6 +2581,7 @@ end
end end
is_processing = true is_processing = true
push!(__model__)
msg = "Loading total spectrum plot for $(selected_folder_main)..." msg = "Loading total spectrum plot for $(selected_folder_main)..."
try try
@ -2350,6 +2637,18 @@ end
@error "Total spectrum plotting failed" exception=(e, catch_backtrace()) @error "Total spectrum plotting failed" exception=(e, catch_backtrace())
finally finally
is_processing = false 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 GC.gc() # Trigger garbage collection
if Sys.islinux() if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
@ -2365,6 +2664,7 @@ end
end end
is_processing = true is_processing = true
push!(__model__)
msg = "Loading plot for $(selected_folder_main)..." msg = "Loading plot for $(selected_folder_main)..."
try try
@ -2479,6 +2779,7 @@ end
end end
is_processing = true is_processing = true
push!(__model__)
msg = "Loading plot for $(selected_folder_main)..." msg = "Loading plot for $(selected_folder_main)..."
try try
@ -3022,6 +3323,7 @@ end
return return
end end
is_processing = true is_processing = true
push!(__model__)
try try
# --- Get Mask Path --- # --- Get Mask Path ---
@ -3079,6 +3381,7 @@ end
end end
is_processing = true is_processing = true
push!(__model__)
try try
# --- Get Mask Path --- # --- Get Mask Path ---
@ -3137,6 +3440,7 @@ end
end end
is_processing = true is_processing = true
push!(__model__)
try try
sTime=time() sTime=time()
@ -3175,6 +3479,7 @@ end
end end
is_processing = true is_processing = true
push!(__model__)
try try
sTime=time() sTime=time()
@ -3401,6 +3706,10 @@ end
@mounted watchplots() @mounted watchplots()
@onchange isready begin @onchange isready begin
# Capture state on first run only
if isempty(INITIAL_MODEL_STATE)
capture_initial_state!(__model__)
end
# is_processing = true # is_processing = true
if isready && !registry_init_done if isready && !registry_init_done
sTime=time() sTime=time()
@ -3471,6 +3780,5 @@ end
end end
end end
# == Pages == # == Pages ==
# Register a new route and the page that will be loaded on access
@page("/", "app.jl.html") @page("/", "app.jl.html")
end end

View File

@ -3,16 +3,30 @@
<div> <div>
<h4>JuliaMSI&nbsp;</h4> <h4>JuliaMSI&nbsp;</h4>
</div> </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> </header>
<!-- <div v-if="is_initializing || is_processing" class="loading-overlay">
<div v-if="is_initializing" class="loading-overlay">
<div class="loading-content"> <div class="loading-content">
<q-spinner-hourglass color="white" size="4em" /> <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> </div>
-->
<div id="extDivStyle" class="row col-12 q-pa-xl"> <div id="extDivStyle" class="row col-12 q-pa-xl">
<div class="row col-6"> <div class="row col-6">
@ -37,14 +51,16 @@
<q-icon name="search" v-on:click="!is_processing && (btnSearch=true)" class="cursor-pointer" /> <q-icon name="search" v-on:click="!is_processing && (btnSearch=true)" class="cursor-pointer" />
</template> </template>
</q-input> </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="add" v-on:click="btnAddBatch=true" label="Add"
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true" :disable="is_processing || batch_file_count === 0" :disable="is_processing"></q-btn>
label="Clear"></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> </div>
<!-- Mask Configuration --> <!-- Mask Configuration -->
<div class="row items-center q-mb-md"> <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> </div>
<!-- Subset Processing --> <!-- Subset Processing -->
@ -60,7 +76,8 @@
</div> </div>
</div> </div>
<div v-if="enable_subset_processing" class="q-mt-sm"> <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> <template v-slot:prepend>
<q-icon name="functions" /> <q-icon name="functions" />
</template> </template>
@ -73,8 +90,8 @@
<!-- Spectrum Selection for Visualization --> <!-- Spectrum Selection for Visualization -->
<div class="row items-center q-mb-md"> <div class="row items-center q-mb-md">
<div class="text-subtitle2 q-mr-md">Preview Spectrum:</div> <div class="text-subtitle2 q-mr-md">Preview Spectrum:</div>
<q-btn-dropdown class="q-ma-sm" :loading="is_processing" :disable="is_processing" <q-btn-dropdown class="q-ma-sm" :loading="is_processing" :disable="is_processing" label="Generate Spectra"
label="Generate Spectra" icon="play_arrow"> icon="play_arrow">
<q-list> <q-list>
<q-item clickable v-close-popup v-on:click="createMeanPlot=true"> <q-item clickable v-close-popup v-on:click="createMeanPlot=true">
<q-item-label>Mean spectrum plot</q-item-label> <q-item-label>Mean spectrum plot</q-item-label>
@ -111,43 +128,49 @@
</div> </div>
<!-- Internal Standards (Collapsible) --> <!-- Internal Standards (Collapsible) -->
<q-expansion-item <q-expansion-item icon="science" label="Internal Standards" caption="Manage reference peaks for calibration"
icon="science" class="q-mb-md">
label="Internal Standards"
caption="Manage reference peaks for calibration"
class="q-mb-md"
>
<q-card> <q-card>
<q-card-section> <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 --> <!-- Keep your existing reference_peaks_list implementation -->
<q-list bordered separator class="q-mt-md"> <q-list bordered separator class="q-mt-md">
<q-item v-for="(peak, index) in reference_peaks_list" :key="index"> <q-item v-for="(peak, index) in reference_peaks_list" :key="index">
<q-item-section avatar> <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>
<q-item-section> <q-item-section>
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="m/z" type="number" step="0.0001" <q-input standout="custom-standout" label="m/z" type="number" step="0.0001" v-model="peak.mz"
v-model="peak.mz" :rules="[val => !!val || 'Required', val => val > 0 || 'Must be positive']" :readonly="is_processing"></q-input> :rules="[val => !!val || 'Required', val => val > 0 || 'Must be positive']"
:readonly="is_processing"></q-input>
</div> </div>
<div class="col-6"> <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>
</div> </div>
</q-item-section> </q-item-section>
</q-item> </q-item>
<q-item> <q-item>
<q-item-section> <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-section>
</q-item> </q-item>
</q-list> </q-list>
<div class="row justify-end q-mt-sm"> <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="get_app" v-on:click="export_standards_btn=true" label="Export"
<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." /> outline :disable="is_processing" hint="Export standards list to 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="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> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -155,18 +178,18 @@
<!-- Reorderable Preprocessing Steps --> <!-- Reorderable Preprocessing Steps -->
<div class="text-h6 q-mb-md">Preprocessing Pipeline</div> <div class="text-h6 q-mb-md">Preprocessing Pipeline</div>
<q-list bordered :disable="is_processing"> <q-list bordered :disable="is_processing">
<q-expansion-item v-for="(step, index) in pipeline_step_order" :key="step.name" <q-expansion-item v-for="(step, index) in pipeline_step_order" :key="step.name" :label="step.label"
:label="step.label" group="preprocessing-steps" group="preprocessing-steps" :class="step.enabled ? '' : 'text-grey'" :disable="is_processing">
:class="step.enabled ? '' : 'text-grey'" :disable="is_processing">
<!-- Header with controls --> <!-- Header with controls -->
<template v-slot:header> <template v-slot:header>
<q-item-section avatar> <q-item-section avatar>
<div class="row no-wrap"> <div class="row no-wrap">
<q-btn flat round icon="arrow_upward" size="sm" <q-btn flat round icon="arrow_upward" size="sm" :disable="is_processing || index === 0"
:disable="is_processing || index === 0" v-on:click.stop="action_index = index; move_step_up_trigger = true"></q-btn> v-on:click.stop="action_index = index; move_step_up_trigger = true"></q-btn>
<q-btn flat round icon="arrow_downward" size="sm" <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> </div>
</q-item-section> </q-item-section>
@ -175,7 +198,8 @@
</q-item-section> </q-item-section>
<q-item-section side> <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> </q-item-section>
</template> </template>
@ -196,10 +220,12 @@
<q-radio v-model="smoothing_method" val="ma" label="Moving Average" :disable="is_processing" /> <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="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <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>
<div class="col-6"> <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>
</div> </div>
</q-card-section> </q-card-section>
@ -211,9 +237,12 @@
<div class="text-caption">The algorithm to use for baseline correction.</div> <div class="text-caption">The algorithm to use for baseline correction.</div>
</q-card-section> </q-card-section>
<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="snip" label="SNIP"
<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> hint="Sensitive Nonlinear Iterative Peak clipping." :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="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>
<q-card-section> <q-card-section>
<div class="text-h6">Parameters</div> <div class="text-h6">Parameters</div>
@ -222,11 +251,15 @@
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Iterations (for SNIP)" type="number" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Window (for Median)" type="number" <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>
</div> </div>
</q-card-section> </q-card-section>
@ -238,10 +271,15 @@
<div class="text-caption">The normalization method to apply.</div> <div class="text-caption">The normalization method to apply.</div>
</q-card-section> </q-card-section>
<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="tic" label="TIC"
<q-radio v-model="normalization_method" val="median" label="MEDIAN" hint="Divides by the median intensity." :disable="is_processing" /><br> hint="Total Ion Current normalization (divides by the sum of intensities)."
<q-radio v-model="normalization_method" val="rms" label="RMS" hint="Root Mean Square normalization." :disable="is_processing" /><br> :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="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-section>
</q-card> </q-card>
@ -251,9 +289,12 @@
<div class="text-caption">The alignment algorithm.</div> <div class="text-caption">The alignment algorithm.</div>
</q-card-section> </q-card-section>
<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="lowess" label="LOWESS"
<q-radio v-model="alignment_method" val="linear" label="LINEAR" hint="Linear regression." :disable="is_processing" /><br> hint="Locally Weighted Scatterplot Smoothing 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="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>
<q-card-section> <q-card-section>
<div class="text-h6">Parameters</div> <div class="text-h6">Parameters</div>
@ -262,23 +303,34 @@
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Span (for LOWESS)" type="number" step="0.01" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Tolerance" type="number" step="0.001" <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>
</div> </div>
<q-select standout="custom-standout" label="Tolerance Unit" v-model="alignment_tolerance_unit" <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="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Max Shift PPM" type="number" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Min Matched Peaks" type="number" <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>
</div> </div>
</q-card-section> </q-card-section>
@ -293,12 +345,14 @@
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Fit Order" type="number" <q-input standout="custom-standout" label="Fit Order" type="number"
:placeholder="suggested_calibration_fit_order" v-model="calibration_fit_order" :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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="PPM Tolerance" type="number" <q-input standout="custom-standout" label="PPM Tolerance" type="number"
:placeholder="suggested_calibration_ppm_tolerance" v-model="calibration_ppm_tolerance" :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>
</div> </div>
</q-card-section> </q-card-section>
@ -310,9 +364,13 @@
<div class="text-caption">The peak detection algorithm.</div> <div class="text-caption">The peak detection algorithm.</div>
</q-card-section> </q-card-section>
<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="profile" label="PROFILE"
<q-radio v-model="peak_picking_method" val="wavelet" label="WAVELET" hint="Continuous Wavelet Transform (CWT) based peak detection." :disable="is_processing" /><br> hint="For profile-mode data, using local maxima and quality filters."
<q-radio v-model="peak_picking_method" val="centroid" label="CENTROID" hint="For centroid-mode data, essentially a filtering step." :disable="is_processing" /><br> :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>
<q-card-section> <q-card-section>
<div class="text-h6">Parameters</div> <div class="text-h6">Parameters</div>
@ -321,35 +379,52 @@
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Signal to Noise Threshold" type="number" step="0.1" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Half Window Size" type="number" <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> </div>
<div class="row q-col-gutter-sm q-mt-md"> <div class="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Min Peak Prominence" type="number" step="0.01" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Merge Peaks Tolerance (m/z)" type="number" step="0.001" <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> </div>
<div class="row q-col-gutter-sm q-mt-md"> <div class="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Min Peak Width (PPM)" type="number" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Max Peak Width (PPM)" type="number" <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>
</div> </div>
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01" class="q-mt-md" <q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01"
: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> 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-section>
</q-card> </q-card>
@ -361,33 +436,44 @@
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Min SNR" type="number" step="0.1" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Min FWHM (PPM)" type="number" <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> </div>
<div class="row q-col-gutter-sm q-mt-md"> <div class="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Max FWHM (PPM)" type="number" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01" <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> </div>
<div class="row q-col-gutter-sm q-mt-md"> <div class="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Frequency Threshold" type="number" step="0.01" <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" :placeholder="suggested_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> 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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Correlation Threshold" type="number" step="0.01" <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" :placeholder="suggested_peak_selection_correlation_threshold"
hint="Minimum correlation with neighboring peaks (not yet implemented)." :readonly="is_processing"></q-input> v-model="peak_selection_correlation_threshold"
hint="Minimum correlation with neighboring peaks (not yet implemented)."
:readonly="is_processing"></q-input>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
@ -399,8 +485,11 @@
<div class="text-caption">The binning strategy.</div> <div class="text-caption">The binning strategy.</div>
</q-card-section> </q-card-section>
<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="adaptive" label="ADAPTIVE"
<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> 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>
<q-card-section> <q-card-section>
<div class="text-h6">Parameters</div> <div class="text-h6">Parameters</div>
@ -409,35 +498,47 @@
<div class="row q-col-gutter-sm"> <div class="row q-col-gutter-sm">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Tolerance (for Adaptive)" type="number" step="0.001" <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>
<div class="col-6"> <div class="col-6">
<q-select standout="custom-standout" label="Tolerance Unit" v-model="binning_tolerance_unit" <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> </div>
<div class="row q-col-gutter-sm q-mt-md"> <div class="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Frequency Threshold" type="number" step="0.01" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Min Peaks Per Bin" type="number" <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> </div>
<div class="row q-col-gutter-sm q-mt-md"> <div class="row q-col-gutter-sm q-mt-md">
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Max Bin Width (PPM)" type="number" <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>
<div class="col-6"> <div class="col-6">
<q-input standout="custom-standout" label="Number of Uniform Bins" type="number" <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>
</div> </div>
<q-toggle v-model="binning_intensity_weighted_centers" v-on:click="binning_intensity_weighted_centers" label="Intensity Weighted Centers" <q-toggle v-model="binning_intensity_weighted_centers" v-on:click="binning_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> 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-section>
</q-card> </q-card>
@ -446,8 +547,10 @@
<!-- Pipeline Controls --> <!-- Pipeline Controls -->
<div class="row justify-end items-center q-mt-md"> <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="get_app" v-on:click="export_params_btn=true" label="Export Params" outline
<q-btn class="q-ma-sm" icon="upload_file" v-on:click="import_params_btn=true" label="Import Params" outline :disable="is_processing" /> :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" <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" /> v-on:click="run_full_pipeline=true" padding="lg" label="Run Pipeline" :disable="is_processing" />
</div> </div>
@ -467,9 +570,10 @@
<q-icon name="search" v-on:click="btnSearch=true" class="cursor-pointer" :disable="is_processing" /> <q-icon name="search" v-on:click="btnSearch=true" class="cursor-pointer" :disable="is_processing" />
</template> </template>
</q-input> </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="add" v-on:click="btnAddBatch=true" label="Add"
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true" :disable="is_processing || batch_file_count === 0" :disable="is_processing"></q-btn>
label="Clear"></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> </div>
<q-list bordered separator v-if="selected_files.length > 0"> <q-list bordered separator v-if="selected_files.length > 0">
<q-item v-for="(file, index) in selected_files" :key="index"> <q-item v-for="(file, index) in selected_files" :key="index">
@ -477,7 +581,8 @@
{{ file }} {{ file }}
</q-item-section> </q-item-section>
<q-item-section side> <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-section>
</q-item> </q-item>
</q-list> </q-list>
@ -495,12 +600,14 @@
<div class="st-col col-4 col-sm q-ma-sm"> <div class="st-col col-4 col-sm q-ma-sm">
<q-input standout="custom-standout" id="textTol" step="0.005" v-model="Tol" <q-input standout="custom-standout" id="textTol" step="0.005" v-model="Tol"
label="Mass-to-charge ratio tolerance" type="number" 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>
<div class="st-col col-4 col-sm q-ma-sm"> <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" <q-input standout="custom-standout" id="textcolorLevel" step="1" v-model="colorLevel" label="Color levels"
type="number" 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> </div>
<div class="row"> <div class="row">
@ -519,7 +626,8 @@
<q-input standout="custom-standout" id="textTriqProb" step="0.01" v-model="triqProb" <q-input standout="custom-standout" id="textTriqProb" step="0.01" v-model="triqProb"
label="TrIQ probability" type="number" :rules="[ 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 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> </div>
</div> </div>
@ -585,9 +693,10 @@
</q-btn> </q-btn>
<q-btn icon="zoom_out_map" class="q-ma-sm on-right btn-style" v-on:click="compareBtn=true" padding="sm" <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> 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="edit" label="Mask Editor" href="/mask"
<q-btn class="q-ma-sm btn-style" icon="dashboard" v-on:click="showMetadataBtn=true" :disable="is_processing"></q-btn>
label="Show Metadata" :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"> <div class="q-pa-md row items-center" v-show="is_processing">
<q-spinner color="primary" size="2em" class="q-mr-sm"></q-spinner> <q-spinner color="primary" size="2em" class="q-mr-sm"></q-spinner>
<div class="text-caption">{{ progress_message }}</div> <div class="text-caption">{{ progress_message }}</div>
@ -687,7 +796,8 @@
<div class="text-subtitle1">Before Preprocessing</div> <div class="text-subtitle1">Before Preprocessing</div>
</q-card-section> </q-card-section>
<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-section>
</q-card> </q-card>
<q-card> <q-card>
@ -710,8 +820,10 @@
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset" <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> class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"></q-select>
<q-space></q-space> <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_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true" :disable="is_processing"></q-btn> :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> </div>
<!-- Image manager --> <!-- Image manager -->
<div id="image-container-normal" class="row st-col col-12"> <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" <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> class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"></q-select>
<q-space></q-space> <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_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true" :disable="is_processing"></q-btn> :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> </div>
<!-- Triq Image manager --> <!-- Triq Image manager -->
<div id="image-container-triq" class="row st-col col-12"> <div id="image-container-triq" class="row st-col col-12">
@ -834,8 +948,8 @@
<div class="col-6"> <div class="col-6">
<div class="row items-center"> <div class="row items-center">
<q-select v-model="selected_folder_compare_left" :options="image_available_folders" <q-select v-model="selected_folder_compare_left" :options="image_available_folders"
label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;" label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"
v-on:focus="refetch_folders = true" :disable="is_processing"></q-select> :disable="is_processing"></q-select>
<q-space></q-space> <q-space></q-space>
<st-tabs id="tabHeaderCompareLeft" :ids="CompTabIDsLeft" :labels="CompTabLabelsLeft" <st-tabs id="tabHeaderCompareLeft" :ids="CompTabIDsLeft" :labels="CompTabLabelsLeft"
v-model="CompSelectedTabLeft"></st-tabs> v-model="CompSelectedTabLeft"></st-tabs>
@ -845,9 +959,10 @@
<!-- Content for Tab 0 --> <!-- Content for Tab 0 -->
<!-- Btn image changer --> <!-- Btn image changer -->
<div> <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_back" class="q-my-sm btn-style" v-on:click="imgMinusCompLeft=true"
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" :disable="is_processing"></q-btn>
v-on:click="imgPlusCompLeft=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> </div>
<!-- Image manager --> <!-- Image manager -->
<div id="image-container-compare-left-normal" class="row st-col col-12"> <div id="image-container-compare-left-normal" class="row st-col col-12">
@ -867,9 +982,10 @@
<!-- Content for Tab 1 --> <!-- Content for Tab 1 -->
<!-- Triq Btn image changer --> <!-- Triq Btn image changer -->
<div> <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_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompLeft=true"
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" :disable="is_processing"></q-btn>
v-on:click="imgPlusTCompLeft=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> </div>
<!-- Triq Image manager --> <!-- Triq Image manager -->
<div id="image-container-compare-left-triq" class="row st-col col-12"> <div id="image-container-compare-left-triq" class="row st-col col-12">
@ -904,8 +1020,8 @@
<div class="col-6"> <div class="col-6">
<div class="row items-center"> <div class="row items-center">
<q-select v-model="selected_folder_compare_right" :options="image_available_folders" <q-select v-model="selected_folder_compare_right" :options="image_available_folders"
label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;" label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"
v-on:focus="refetch_folders = true" :disable="is_processing"></q-select> :disable="is_processing"></q-select>
<q-space></q-space> <q-space></q-space>
<st-tabs id="tabHeaderCompareRight" :ids="CompTabIDsRight" :labels="CompTabLabelsRight" <st-tabs id="tabHeaderCompareRight" :ids="CompTabIDsRight" :labels="CompTabLabelsRight"
v-model="CompSelectedTabRight"></st-tabs> v-model="CompSelectedTabRight"></st-tabs>
@ -915,9 +1031,10 @@
<!-- Content for Tab 0 --> <!-- Content for Tab 0 -->
<!-- Btn image changer --> <!-- Btn image changer -->
<div> <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_back" class="q-my-sm btn-style" v-on:click="imgMinusCompRight=true"
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" :disable="is_processing"></q-btn>
v-on:click="imgPlusCompRight=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> </div>
<!-- Image manager --> <!-- Image manager -->
<div id="image-container-compare-right-normal" class="row st-col col-12"> <div id="image-container-compare-right-normal" class="row st-col col-12">
@ -937,9 +1054,10 @@
<!-- Content for Tab 1 --> <!-- Content for Tab 1 -->
<!-- Triq Btn image changer --> <!-- Triq Btn image changer -->
<div> <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_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompRight=true"
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" :disable="is_processing"></q-btn>
v-on:click="imgPlusTCompRight=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> </div>
<!-- Triq Image manager --> <!-- Triq Image manager -->
<div id="image-container-compare-right-triq" class="row st-col col-12"> <div id="image-container-compare-right-triq" class="row st-col col-12">
@ -1028,3 +1146,37 @@
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </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 # julia_imzML_visual.jl
const REGISTRY_LOCK = ReentrantLock()
""" """
increment_image(current_image, image_list) increment_image(current_image, image_list)

View File

@ -9,7 +9,7 @@ using Statistics, NaturalSort, LinearAlgebra, StipplePlotly
using Base.Filesystem: mv using Base.Filesystem: mv
using MSI_src using MSI_src
using .MSI_src: MSIData, process_image_pipeline using .MSI_src: MSIData, process_image_pipeline, REGISTRY_LOCK
# Plot Handling # Plot Handling
include("./julia_imzML_visual.jl") include("./julia_imzML_visual.jl")
@ -799,9 +799,11 @@ end
end end
# Save the updated registry # Save the updated registry
lock(REGISTRY_LOCK) do
open(reg_path, "w") do f open(reg_path, "w") do f
JSON.print(f, registry, 4) JSON.print(f, registry, 4)
end end
end
@info "Registry updated with mask: $(final_mask_name)" @info "Registry updated with mask: $(final_mask_name)"
@ -874,10 +876,12 @@ end
if !isempty(new_folders) || !isempty(removed_folders) if !isempty(new_folders) || !isempty(removed_folders)
println("Registry changed, saving...") println("Registry changed, saving...")
lock(REGISTRY_LOCK) do
open(reg_path, "w") do f open(reg_path, "w") do f
JSON.print(f, registry, 4) JSON.print(f, registry, 4)
end end
end end
end
all_folders = sort(collect(keys(registry)), lt=natural) all_folders = sort(collect(keys(registry)), lt=natural)
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders) 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 mutable struct SimpleBufferPool
buffers::Dict{Int, Vector{Vector{UInt8}}} buffers::Dict{Int, Vector{Vector{UInt8}}}
max_pool_size::Int max_pool_size::Int
lock::ReentrantLock # Add lock for thread safety
end 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} function get_buffer!(pool::SimpleBufferPool, size::Int)::Vector{UInt8}
lock(pool.lock) do
# Check for existing buffers of exact size first # Check for existing buffers of exact size first
if haskey(pool.buffers, size) && !isempty(pool.buffers[size]) if haskey(pool.buffers, size) && !isempty(pool.buffers[size])
return pop!(pool.buffers[size]) return pop!(pool.buffers[size])
end 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) return Vector{UInt8}(undef, size)
end end
function release_buffer!(pool::SimpleBufferPool, buffer::Vector{UInt8}) function release_buffer!(pool::SimpleBufferPool, buffer::Vector{UInt8})
size = length(buffer) size = length(buffer)
lock(pool.lock) do
if !haskey(pool.buffers, size) if !haskey(pool.buffers, size)
pool.buffers[size] = Vector{Vector{UInt8}}() pool.buffers[size] = Vector{Vector{UInt8}}()
end end
@ -107,6 +111,7 @@ function release_buffer!(pool::SimpleBufferPool, buffer::Vector{UInt8})
if length(pool.buffers[size]) < pool.max_pool_size if length(pool.buffers[size]) < pool.max_pool_size
push!(pool.buffers[size], buffer) push!(pool.buffers[size], buffer)
end end
end
# If pool is full, let buffer get GC'd # If pool is full, let buffer get GC'd
end 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. way to read from a specific offset in the file.
""" """
function read_at!(tsfh::ThreadSafeFileHandle, a::AbstractArray, pos::Integer) function read_at!(tsfh::ThreadSafeFileHandle, a::AbstractArray, pos::Integer)
if pos < 0
throw(ArgumentError("Invalid seek position: $pos"))
end
lock(tsfh.lock) do 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) seek(tsfh.handle, pos)
read!(tsfh.handle, a) read!(tsfh.handle, a)
end 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) mz = Array{source.mz_format}(undef, meta.mz_asset.encoded_length)
intensity = Array{source.intensity_format}(undef, meta.int_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 # 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, mz, meta.mz_asset.offset)
read_at!(source.ibd_handle, intensity, meta.int_asset.offset) read_at!(source.ibd_handle, intensity, meta.int_asset.offset)

View File

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

View File

@ -1,4 +1,5 @@
using StatsBase # For mean, std, median, quantile, mad using StatsBase # For mean, std, median, quantile, mad
using Base.Threads # For atomic counters and locking
# ============================================================================= # =============================================================================
# 7) Spatial & Advanced Processing (Stubs & New Functions) # 7) Spatial & Advanced Processing (Stubs & New Functions)
@ -80,11 +81,12 @@ function analyze_mass_accuracy(
println("Analyzing mass accuracy for $(length(spectrum_indices)) spectra...") println("Analyzing mass accuracy for $(length(spectrum_indices)) spectra...")
all_ppm_errors = Float64[] all_ppm_errors = Float64[]
total_matched_peaks = 0 total_matched_peaks = Atomic{Int}(0)
total_spectra_processed = 0 total_spectra_processed = Atomic{Int}(0)
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity _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) if !validate_spectrum(mz, intensity)
@warn "Spectrum $idx is invalid, skipping mass accuracy analysis for it." @warn "Spectrum $idx is invalid, skipping mass accuracy analysis for it."
return return
@ -106,8 +108,10 @@ function analyze_mass_accuracy(
end end
if best_matched_peak_mz !== nothing if best_matched_peak_mz !== nothing
lock(results_lock) do
push!(all_ppm_errors, min_ppm_error) push!(all_ppm_errors, min_ppm_error)
total_matched_peaks += 1 end
atomic_add!(total_matched_peaks, 1)
end end
end end
end end
@ -140,8 +144,8 @@ function analyze_mass_accuracy(
std_ppm_error = std_err, std_ppm_error = std_err,
min_ppm_error = min_err, min_ppm_error = min_err,
max_ppm_error = max_err, max_ppm_error = max_err,
total_matched_peaks = total_matched_peaks, total_matched_peaks = total_matched_peaks[],
total_spectra_analyzed = total_spectra_processed, total_spectra_analyzed = total_spectra_processed[],
ppm_error_distribution = all_ppm_errors ppm_error_distribution = all_ppm_errors
) )
end end
@ -318,23 +322,31 @@ function analyze_instrument_characteristics(msi_data::MSIData; sample_indices::A
return results return results
end end
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity _iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity
# Record spectrum mode # Record spectrum mode
lock(results_lock) do
push!(spectrum_modes, msi_data.spectra_metadata[idx].mode) push!(spectrum_modes, msi_data.spectra_metadata[idx].mode)
end
# Calculate m/z step statistics (for profile data) # Calculate m/z step statistics (for profile data)
if length(mz) > 1 && msi_data.spectra_metadata[idx].mode == PROFILE if length(mz) > 1 && msi_data.spectra_metadata[idx].mode == PROFILE
steps = diff(mz) steps = diff(mz)
if !isempty(steps) if !isempty(steps)
lock(results_lock) do
push!(mz_step_sizes, mean(steps)) push!(mz_step_sizes, mean(steps))
end end
end end
end
# Record intensity range # Record intensity range
if !isempty(intensity) if !isempty(intensity)
lock(results_lock) do
push!(intensity_ranges, (minimum(intensity), maximum(intensity))) push!(intensity_ranges, (minimum(intensity), maximum(intensity)))
end end
end end
end
# Determine acquisition mode # Determine acquisition mode
if length(spectrum_modes) == 1 if length(spectrum_modes) == 1
@ -442,11 +454,15 @@ function analyze_signal_quality(msi_data::MSIData; sample_indices::AbstractVecto
return results return results
end end
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity _iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity
if !isempty(intensity) if !isempty(intensity)
# Noise estimation using MAD # Noise estimation using MAD
noise = mad(intensity, normalize=true) noise = mad(intensity, normalize=true)
lock(results_lock) do
push!(noise_levels, noise) push!(noise_levels, noise)
end
# FIX: More robust SNR calculation # FIX: More robust SNR calculation
valid_intensity = intensity[intensity .> 0] # Remove zeros 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) if noise_robust > 0 && isfinite(signal_estimate)
snr_val = signal_estimate / noise_robust snr_val = signal_estimate / noise_robust
# Cap unrealistic SNR values # Cap unrealistic SNR values
lock(results_lock) do
push!(snr_distribution, min(snr_val, 1e6)) push!(snr_distribution, min(snr_val, 1e6))
end end
end end
end
# Total ion count # Total ion count
lock(results_lock) do
push!(tic_values, sum(intensity)) push!(tic_values, sum(intensity))
end end
end end
end
if !isempty(noise_levels) if !isempty(noise_levels)
results[:noise_mean] = mean(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[] peak_counts = Int[]
fwhm_values = Float64[] fwhm_values = Float64[]
spectra_analyzed = 0 spectra_analyzed = Atomic{Int}(0)
peaks_analyzed = 0 peaks_analyzed = Atomic{Int}(0)
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity _iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
if length(mz) < 10 # Skip spectra with too few points if length(mz) < 10 # Skip spectra with too few points
return return
end end
spectra_analyzed += 1 atomic_add!(spectra_analyzed, 1)
meta = msi_data.spectra_metadata[idx] meta = msi_data.spectra_metadata[idx]
# Detect peaks with lower SNR threshold to find more peaks # Detect peaks with lower SNR threshold to find more peaks
peaks = detect_peaks_profile_core(mz, intensity; snr_threshold=2.0) peaks = detect_peaks_profile_core(mz, intensity; snr_threshold=2.0)
lock(results_lock) do
push!(peak_counts, length(peaks)) push!(peak_counts, length(peaks))
end
if !isempty(peaks) if !isempty(peaks)
# Analyze the strongest 3 peaks per spectrum # 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 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 fwhm_ppm = 1e6 * fwhm_delta_m / peak.mz
if 5.0 < fwhm_ppm < 500.0 # Reasonable ppm range if 5.0 < fwhm_ppm < 500.0 # Reasonable ppm range
lock(results_lock) do
push!(peak_widths_ppm, fwhm_ppm) push!(peak_widths_ppm, fwhm_ppm)
push!(fwhm_values, fwhm_delta_m) push!(fwhm_values, fwhm_delta_m)
r2 = _fit_gaussian_and_r2(mz, intensity, peak_idx, 5) r2 = _fit_gaussian_and_r2(mz, intensity, peak_idx, 5)
push!(r_squared_values, r2) push!(r_squared_values, r2)
peaks_analyzed += 1 end
atomic_add!(peaks_analyzed, 1)
if peaks_analyzed <= 3 if peaks_analyzed[] <= 3
println("DEBUG: Peak at m/z $(peak.mz), FWHM = $(fwhm_ppm) ppm, R² = $r2") println("DEBUG: Peak at m/z $(peak.mz), FWHM = $(fwhm_ppm) ppm, R² = $(r_squared_values[end])")
end end
end end
end end
@ -698,7 +723,7 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
end end
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) if !isempty(peak_widths_ppm)
results[:mean_fwhm_ppm] = mean(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[] peak_counts = Int[]
results_lock = ReentrantLock()
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity _iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
if !isempty(mz) if !isempty(mz)
lock(results_lock) do
push!(peak_counts, length(mz)) push!(peak_counts, length(mz))
end end
end end
end
if !isempty(peak_counts) if !isempty(peak_counts)
results[:mean_peaks_per_spectrum] = mean(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 Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity) 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) 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) s.intensity = max.(0.0, s.intensity .- baseline)
end end
end end
@ -53,7 +61,16 @@ function apply_intensity_transformation(spectra::Vector{MutableSpectrum}, params
Threads.@threads for s in spectra Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity) 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 end
end end
@ -76,8 +93,16 @@ function apply_smoothing(spectra::Vector{MutableSpectrum}, params::Dict)
Threads.@threads for s in spectra Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity) if validate_spectrum(s.mz, s.intensity)
smoothed_intensity = max.(0.0, smooth_spectrum_core(s.intensity; method=method, window=window, order=order)) original_length = length(s.intensity)
s.intensity = smoothed_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 end
end end
@ -104,6 +129,7 @@ function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict)
Threads.@threads for s in spectra Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity) if validate_spectrum(s.mz, s.intensity)
# old_len = length(s.peaks)
if method == :profile 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) 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 elseif method == :wavelet
@ -113,6 +139,7 @@ function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict)
else else
s.peaks = detect_peaks_profile_core(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window) s.peaks = detect_peaks_profile_core(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window)
end end
# @info "Spectrum $(s.id): detected $(length(s.peaks)) peaks"
else else
s.peaks = [] s.peaks = []
end end
@ -182,12 +209,21 @@ function apply_calibration(spectra::Vector{MutableSpectrum}, params::Dict, refer
Threads.@threads for i in 1:length(spectra) Threads.@threads for i in 1:length(spectra)
s = spectra[i] s = spectra[i]
if validate_spectrum(s.mz, s.intensity) 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) matched_peaks = find_calibration_peaks_core(s.mz, s.intensity, reference_masses; ppm_tolerance=ppm_tolerance)
if length(matched_peaks) >= 2 if length(matched_peaks) >= 2
measured = sort(collect(values(matched_peaks))) measured = sort(collect(values(matched_peaks)))
theoretical = sort(collect(keys(matched_peaks))) theoretical = sort(collect(keys(matched_peaks)))
itp = linear_interpolation(measured, theoretical, extrapolation_bc=Line()) 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 else
@warn "Spectrum $(s.id): insufficient reference peaks ($(length(matched_peaks)) found), skipping calibration." @warn "Spectrum $(s.id): insufficient reference peaks ($(length(matched_peaks)) found), skipping calibration."
end end
@ -233,7 +269,16 @@ function apply_peak_alignment(spectra::Vector{MutableSpectrum}, params::Dict)
current_peaks_mz = [p.mz for p in s.peaks] 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) 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 # Update peak m/z values
for i in 1:length(s.peaks) 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 Threads.@threads for s in spectra
if validate_spectrum(s.mz, s.intensity) 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 end
end end

View File

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