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:
parent
4414671a30
commit
df94d50954
2757
Manifest.toml
2757
Manifest.toml
File diff suppressed because it is too large
Load Diff
444
app.jl
444
app.jl
@ -8,6 +8,7 @@ using Libz
|
||||
using PlotlyBase
|
||||
using CairoMakie
|
||||
using Colors
|
||||
using Dates
|
||||
using MSI_src # Import the new MSIData library
|
||||
using Statistics
|
||||
using NaturalSort
|
||||
@ -113,7 +114,9 @@ function get_processed_mean_spectrum(spectra::Vector{MutableSpectrum}; num_bins=
|
||||
|
||||
# 3. Bin intensities
|
||||
for s in spectra
|
||||
for i in eachindex(s.mz)
|
||||
# Use minimum length to avoid bounds errors if arrays are mismatched
|
||||
n_points = min(length(s.mz), length(s.intensity))
|
||||
for i in 1:n_points
|
||||
bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0)
|
||||
final_index = clamp(bin_index, 1, num_bins)
|
||||
intensity_sum[final_index] += s.intensity[i]
|
||||
@ -147,7 +150,9 @@ function get_processed_sum_spectrum(spectra::Vector{MutableSpectrum}; num_bins=2
|
||||
inv_bin_step = 1.0 / bin_step
|
||||
|
||||
for s in spectra
|
||||
for i in eachindex(s.mz)
|
||||
# Use minimum length to avoid bounds errors if arrays are mismatched
|
||||
n_points = min(length(s.mz), length(s.intensity))
|
||||
for i in 1:n_points
|
||||
bin_index = trunc(Int, (s.mz[i] - min_mz) * inv_bin_step + 1.0)
|
||||
final_index = clamp(bin_index, 1, num_bins)
|
||||
intensity_sum[final_index] += s.intensity[i]
|
||||
@ -157,11 +162,44 @@ function get_processed_sum_spectrum(spectra::Vector{MutableSpectrum}; num_bins=2
|
||||
return collect(mz_bins), intensity_sum
|
||||
end
|
||||
|
||||
INITIAL_MODEL_STATE = Dict{Symbol,Any}()
|
||||
|
||||
# Function to capture initial state (also outside @app block)
|
||||
function capture_initial_state!(model)
|
||||
empty!(INITIAL_MODEL_STATE)
|
||||
for name in fieldnames(typeof(model))
|
||||
if !startswith(String(name), "_")
|
||||
INITIAL_MODEL_STATE[name] = deepcopy(getfield(model, name))
|
||||
end
|
||||
end
|
||||
println("Captured $(length(INITIAL_MODEL_STATE)) reactive variables")
|
||||
end
|
||||
|
||||
@genietools
|
||||
|
||||
# == Reactive code ==
|
||||
#=
|
||||
macro ui_log(message, level="INFO", log_entries)
|
||||
quote
|
||||
local timestamp = Dates.format(now(), "HH:MM:SS")
|
||||
local new_entry = Dict("time" => timestamp, "message" => string($(esc(message))), "level" => $(esc(level)))
|
||||
println("log entries value: $log_entries")
|
||||
pushfirst!(log_entries, new_entry)
|
||||
if length(log_entries) > 100
|
||||
popfirst!(log_entries)
|
||||
end
|
||||
push!(__model__)
|
||||
end
|
||||
end
|
||||
=#
|
||||
|
||||
# Reactive code to make the UI interactive
|
||||
@app begin
|
||||
# == Notification & Logs ==
|
||||
# @in log_entries = Dict{String,Any}[]
|
||||
# @in show_log_sidebar = false
|
||||
@in showBugModal = false
|
||||
|
||||
# == Loading Screen Variables ==
|
||||
@in is_initializing = true
|
||||
@in initialization_message = "Initializing..."
|
||||
@ -348,6 +386,7 @@ end
|
||||
@in export_params_btn = false # Export parameters to file
|
||||
@in import_params_btn = false # Import parameters from file
|
||||
@in save_feature_matrix_btn = false # Save feature matrix results
|
||||
@in reset_session_btn = false # Deep session reset
|
||||
|
||||
# Preprocessing results
|
||||
@in selected_spectrum_id_for_plot = 1
|
||||
@ -616,12 +655,64 @@ end
|
||||
# Reactive handlers watch a variable and execute a block of code when its value changes
|
||||
# The onbutton handler will set the variable to false after the block is executed
|
||||
|
||||
# This handler correctly uses pick_file and loads the selected file
|
||||
# as the active dataset for the UI.
|
||||
@onbutton reset_session_btn begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
|
||||
try
|
||||
# 1. Clear large data objects explicitly
|
||||
msi_data = nothing
|
||||
feature_matrix_result = nothing
|
||||
bin_info_result = nothing
|
||||
|
||||
# 2. Reset ALL reactive variables using captured initial state
|
||||
if !isempty(INITIAL_MODEL_STATE)
|
||||
for (name, value) in INITIAL_MODEL_STATE
|
||||
setfield!(__model__, name, deepcopy(value))
|
||||
end
|
||||
msg = "Session reset: restored $(length(INITIAL_MODEL_STATE)) variables to initial state."
|
||||
else
|
||||
msg = "Warning: No initial state captured. Using partial reset."
|
||||
end
|
||||
|
||||
# 3. Reset file lists (these will be repopulated by normal operation)
|
||||
msi_bmp = sort(filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir("public")), lt=natural)
|
||||
col_msi_png = sort(filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir("public")), lt=natural)
|
||||
triq_bmp = sort(filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir("public")), lt=natural)
|
||||
col_triq_png = sort(filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir("public")), lt=natural)
|
||||
|
||||
# 4. Clear any cached images/plots
|
||||
imgInt = "/.bmp"
|
||||
imgIntT = "/.bmp"
|
||||
colorbar = "/.png"
|
||||
colorbarT = "/.png"
|
||||
|
||||
# 5. Reset plot data to default traces
|
||||
traceImg = PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())
|
||||
plotdataImg = [traceImg]
|
||||
plotdataImgT = [traceImg]
|
||||
plotdata = [PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(), mode="lines")]
|
||||
|
||||
# 6. Aggressive garbage collection
|
||||
GC.gc(true)
|
||||
if Sys.islinux()
|
||||
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||
end
|
||||
println("Session reset successfully.")
|
||||
catch e
|
||||
println("Error during session reset: $e")
|
||||
msg = "Reset error: $e"
|
||||
finally
|
||||
is_processing = false
|
||||
end
|
||||
end
|
||||
|
||||
@onbutton btnSearch begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml")
|
||||
if isempty(picked_route)
|
||||
|
||||
if isnothing(picked_route) || isempty(picked_route)
|
||||
is_processing = false
|
||||
return
|
||||
end
|
||||
@ -938,6 +1029,7 @@ end
|
||||
|
||||
@onbutton export_params_btn begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
params_to_export = Dict(
|
||||
"pipeline_step_order" => pipeline_step_order,
|
||||
"enable_standards" => enable_standards, # Export global flag
|
||||
@ -1011,6 +1103,7 @@ end
|
||||
|
||||
@onbutton import_params_btn begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
picked_file = pick_file(filterlist="json")
|
||||
if isempty(picked_file)
|
||||
is_processing = false
|
||||
@ -1021,39 +1114,57 @@ end
|
||||
json_string = read(picked_file, String)
|
||||
params = JSON.parse(json_string)
|
||||
|
||||
# This is a list of all known reactive variables that can be imported.
|
||||
# This prevents arbitrary variable assignment.
|
||||
known_params = [
|
||||
"stabilization_method", "smoothing_method", "smoothing_window", "smoothing_order",
|
||||
"baseline_method", "baseline_iterations", "baseline_window", "normalization_method",
|
||||
"alignment_method", "alignment_span", "alignment_tolerance", "alignment_tolerance_unit",
|
||||
"alignment_max_shift_ppm", "alignment_min_matched_peaks", "peak_picking_method",
|
||||
"peak_picking_snr_threshold", "peak_picking_half_window", "peak_picking_min_peak_prominence",
|
||||
"peak_picking_merge_peaks_tolerance", "peak_picking_min_peak_width_ppm",
|
||||
"peak_picking_max_peak_width_ppm", "peak_picking_min_peak_shape_r2", "binning_method",
|
||||
"binning_tolerance", "binning_tolerance_unit", "binning_frequency_threshold",
|
||||
"binning_min_peak_per_bin", "binning_max_bin_width_ppm", "binning_intensity_weighted_centers",
|
||||
"binning_num_uniform_bins", "calibration_fit_order", "calibration_ppm_tolerance",
|
||||
"peak_selection_min_snr", "peak_selection_min_fwhm_ppm", "peak_selection_max_fwhm_ppm",
|
||||
"peak_selection_min_shape_r2", "peak_selection_frequency_threshold", "peak_selection_correlation_threshold"
|
||||
]
|
||||
# Import special variables first
|
||||
if haskey(params, "reference_peaks_list")
|
||||
reference_peaks_list = params["reference_peaks_list"]
|
||||
end
|
||||
if haskey(params, "pipeline_step_order")
|
||||
pipeline_step_order = params["pipeline_step_order"]
|
||||
end
|
||||
if haskey(params, "enable_standards")
|
||||
enable_standards = params["enable_standards"]
|
||||
end
|
||||
|
||||
# Import regular parameters with explicit assignments
|
||||
haskey(params, "stabilization_method") && (stabilization_method = params["stabilization_method"])
|
||||
haskey(params, "smoothing_method") && (smoothing_method = params["smoothing_method"])
|
||||
haskey(params, "smoothing_window") && (smoothing_window = params["smoothing_window"])
|
||||
haskey(params, "smoothing_order") && (smoothing_order = params["smoothing_order"])
|
||||
haskey(params, "baseline_method") && (baseline_method = params["baseline_method"])
|
||||
haskey(params, "baseline_iterations") && (baseline_iterations = params["baseline_iterations"])
|
||||
haskey(params, "baseline_window") && (baseline_window = params["baseline_window"])
|
||||
haskey(params, "normalization_method") && (normalization_method = params["normalization_method"])
|
||||
haskey(params, "alignment_method") && (alignment_method = params["alignment_method"])
|
||||
haskey(params, "alignment_span") && (alignment_span = params["alignment_span"])
|
||||
haskey(params, "alignment_tolerance") && (alignment_tolerance = params["alignment_tolerance"])
|
||||
haskey(params, "alignment_tolerance_unit") && (alignment_tolerance_unit = params["alignment_tolerance_unit"])
|
||||
haskey(params, "alignment_max_shift_ppm") && (alignment_max_shift_ppm = params["alignment_max_shift_ppm"])
|
||||
haskey(params, "alignment_min_matched_peaks") && (alignment_min_matched_peaks = params["alignment_min_matched_peaks"])
|
||||
haskey(params, "peak_picking_method") && (peak_picking_method = params["peak_picking_method"])
|
||||
haskey(params, "peak_picking_snr_threshold") && (peak_picking_snr_threshold = params["peak_picking_snr_threshold"])
|
||||
haskey(params, "peak_picking_half_window") && (peak_picking_half_window = params["peak_picking_half_window"])
|
||||
haskey(params, "peak_picking_min_peak_prominence") && (peak_picking_min_peak_prominence = params["peak_picking_min_peak_prominence"])
|
||||
haskey(params, "peak_picking_merge_peaks_tolerance") && (peak_picking_merge_peaks_tolerance = params["peak_picking_merge_peaks_tolerance"])
|
||||
haskey(params, "peak_picking_min_peak_width_ppm") && (peak_picking_min_peak_width_ppm = params["peak_picking_min_peak_width_ppm"])
|
||||
haskey(params, "peak_picking_max_peak_width_ppm") && (peak_picking_max_peak_width_ppm = params["peak_picking_max_peak_width_ppm"])
|
||||
haskey(params, "peak_picking_min_peak_shape_r2") && (peak_picking_min_peak_shape_r2 = params["peak_picking_min_peak_shape_r2"])
|
||||
haskey(params, "binning_method") && (binning_method = params["binning_method"])
|
||||
haskey(params, "binning_tolerance") && (binning_tolerance = params["binning_tolerance"])
|
||||
haskey(params, "binning_tolerance_unit") && (binning_tolerance_unit = params["binning_tolerance_unit"])
|
||||
haskey(params, "binning_frequency_threshold") && (binning_frequency_threshold = params["binning_frequency_threshold"])
|
||||
haskey(params, "binning_min_peak_per_bin") && (binning_min_peak_per_bin = params["binning_min_peak_per_bin"])
|
||||
haskey(params, "binning_max_bin_width_ppm") && (binning_max_bin_width_ppm = params["binning_max_bin_width_ppm"])
|
||||
haskey(params, "binning_intensity_weighted_centers") && (binning_intensity_weighted_centers = params["binning_intensity_weighted_centers"])
|
||||
haskey(params, "binning_num_uniform_bins") && (binning_num_uniform_bins = params["binning_num_uniform_bins"])
|
||||
haskey(params, "calibration_fit_order") && (calibration_fit_order = params["calibration_fit_order"])
|
||||
haskey(params, "calibration_ppm_tolerance") && (calibration_ppm_tolerance = params["calibration_ppm_tolerance"])
|
||||
haskey(params, "peak_selection_min_snr") && (peak_selection_min_snr = params["peak_selection_min_snr"])
|
||||
haskey(params, "peak_selection_min_fwhm_ppm") && (peak_selection_min_fwhm_ppm = params["peak_selection_min_fwhm_ppm"])
|
||||
haskey(params, "peak_selection_max_fwhm_ppm") && (peak_selection_max_fwhm_ppm = params["peak_selection_max_fwhm_ppm"])
|
||||
haskey(params, "peak_selection_min_shape_r2") && (peak_selection_min_shape_r2 = params["peak_selection_min_shape_r2"])
|
||||
haskey(params, "peak_selection_frequency_threshold") && (peak_selection_frequency_threshold = params["peak_selection_frequency_threshold"])
|
||||
haskey(params, "peak_selection_correlation_threshold") && (peak_selection_correlation_threshold = params["peak_selection_correlation_threshold"])
|
||||
|
||||
for (key, value) in params
|
||||
if key == "reference_peaks_list"
|
||||
reference_peaks_list = value
|
||||
elseif key == "pipeline_step_order"
|
||||
pipeline_step_order = value
|
||||
elseif key == "enable_standards"
|
||||
enable_standards = value
|
||||
elseif key in known_params
|
||||
# Use getfield and setproperty! to update reactive variables by name
|
||||
if hasfield(typeof(@__MODULE__), Symbol(key))
|
||||
getfield(@__MODULE__, Symbol(key))[] = value
|
||||
end
|
||||
else
|
||||
@warn "Unknown parameter '$key' found in JSON file. Skipping."
|
||||
end
|
||||
end
|
||||
msg = "Parameters imported successfully from $(basename(picked_file))."
|
||||
catch e
|
||||
msg = "Failed to import parameters: $e"
|
||||
@ -1110,13 +1221,14 @@ end
|
||||
|
||||
@onbutton run_full_pipeline begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
overall_progress = 0.0
|
||||
local pipeline_msi_data = nothing
|
||||
local current_spectra = Vector{MutableSpectrum}()
|
||||
current_pipeline_step = "Initializing..."
|
||||
println("DEBUG: run_full_pipeline started.")
|
||||
# println("DEBUG: Current pipeline_step_order configuration: $pipeline_step_order")
|
||||
|
||||
try
|
||||
# --- 1. Initial Checks and Data Loading ---
|
||||
println("DEBUG: Performing initial checks and data loading...")
|
||||
if isempty(selected_folder_main)
|
||||
msg = "No dataset loaded. Please load a file using 'Select an imzMl / mzML file'."
|
||||
warning_msg = true
|
||||
@ -1136,26 +1248,31 @@ end
|
||||
target_path = entry["source_path"]
|
||||
|
||||
# Ensure msi_data is for the currently selected file and load if needed
|
||||
if msi_data === nothing || full_route != target_path
|
||||
println("DEBUG: Active file path changed or data not in memory. Reloading MSI data: $(basename(target_path))")
|
||||
if msi_data !== nothing; close(msi_data); end
|
||||
msg = "Reloading $(basename(target_path)) for analysis..."
|
||||
full_route = target_path
|
||||
msi_data = OpenMSIData(target_path)
|
||||
# NOTE: For the pipeline, we will open a DEDICATED instance to avoid race conditions
|
||||
# with the global msi_data used for plotting/interactive exploration.
|
||||
println("DEBUG: Opening isolated MSIData instance for pipeline stability...")
|
||||
pipeline_msi_data = OpenMSIData(target_path)
|
||||
|
||||
# Determine plot mode from loaded data
|
||||
df = msi_data.spectrum_stats_df
|
||||
# Determine plot mode from metadata for correct visualization late
|
||||
metadata = pipeline_msi_data.instrument_metadata
|
||||
acq_mode = metadata !== nothing ? metadata.acquisition_mode : :unknown
|
||||
|
||||
if acq_mode == :centroid
|
||||
last_plot_mode = "stem"
|
||||
elseif acq_mode == :profile
|
||||
last_plot_mode = "lines"
|
||||
else
|
||||
# Fallback to stats if mode is unknown
|
||||
df = pipeline_msi_data.spectrum_stats_df
|
||||
if df !== nothing && "Mode" in names(df)
|
||||
profile_count = count(==(MSI_src.PROFILE), df.Mode)
|
||||
total_count = length(df.Mode)
|
||||
last_plot_mode = profile_count > total_count / 2 ? "lines" : "stem"
|
||||
println("DEBUG: Auto-detected plot mode for pipeline: $(last_plot_mode)")
|
||||
else
|
||||
last_plot_mode = "lines" # Default
|
||||
end
|
||||
else
|
||||
println("DEBUG: Using already loaded MSI data for $(basename(target_path)).")
|
||||
end
|
||||
println("DEBUG: Auto-detected plot mode from metadata: $(last_plot_mode) (acq_mode: $(acq_mode)) [Initial set]")
|
||||
|
||||
# Mask path retrieval from registry
|
||||
local mask_path_for_pipeline::Union{String, Nothing} = nothing
|
||||
@ -1171,6 +1288,7 @@ end
|
||||
warning_msg = true
|
||||
@warn msg
|
||||
println("DEBUG: $msg")
|
||||
close(pipeline_msi_data) # Important cleanup
|
||||
return
|
||||
end
|
||||
else
|
||||
@ -1178,6 +1296,7 @@ end
|
||||
warning_msg = true
|
||||
@warn msg
|
||||
println("DEBUG: $msg")
|
||||
close(pipeline_msi_data) # Important cleanup
|
||||
return
|
||||
end
|
||||
else
|
||||
@ -1185,22 +1304,24 @@ end
|
||||
end
|
||||
|
||||
# Apply mask if enabled to get indices to process
|
||||
spectrum_indices_to_process = collect(1:length(msi_data.spectra_metadata))
|
||||
# Use pipeline_msi_data for consistency
|
||||
spectrum_indices_to_process = collect(1:length(pipeline_msi_data.spectra_metadata))
|
||||
if mask_path_for_pipeline !== nothing
|
||||
current_pipeline_step = "Applying mask..."
|
||||
println("DEBUG: Applying mask matrix to filter spectra...")
|
||||
mask_matrix = load_and_prepare_mask(mask_path_for_pipeline, msi_data.image_dims)
|
||||
masked_indices_set = get_masked_spectrum_indices(msi_data, mask_matrix)
|
||||
mask_matrix = load_and_prepare_mask(mask_path_for_pipeline, pipeline_msi_data.image_dims)
|
||||
masked_indices_set = get_masked_spectrum_indices(pipeline_msi_data, mask_matrix)
|
||||
spectrum_indices_to_process = collect(masked_indices_set)
|
||||
if isempty(spectrum_indices_to_process)
|
||||
msg = "No spectra remaining after applying mask. Aborting pipeline."
|
||||
warning_msg = true
|
||||
println("DEBUG: $msg")
|
||||
close(pipeline_msi_data) # Important cleanup
|
||||
return
|
||||
end
|
||||
println("DEBUG: $(length(spectrum_indices_to_process)) spectra remaining after mask application.")
|
||||
else
|
||||
println("DEBUG: No mask applied. Processing all $(length(msi_data.spectra_metadata)) spectra.")
|
||||
println("DEBUG: No mask applied. Processing all $(length(pipeline_msi_data.spectra_metadata)) spectra.")
|
||||
end
|
||||
|
||||
# Apply subset processing if enabled
|
||||
@ -1211,18 +1332,92 @@ end
|
||||
println("DEBUG: Subset processing enabled. Processing first $(length(spectrum_indices_to_process)) of $n_total spectra.")
|
||||
end
|
||||
|
||||
# Initialize spectra data structure
|
||||
current_pipeline_step = "Loading spectra..."
|
||||
println("DEBUG: Loading $(length(spectrum_indices_to_process)) spectra into MutableSpectrum objects...")
|
||||
current_spectra = Vector{MutableSpectrum}(undef, length(spectrum_indices_to_process))
|
||||
# --- BOUNDS VALIDATION AND DIAGNOSTIC LOGGING ---
|
||||
# Validate all indices are within bounds before attempting to load
|
||||
max_spectra_idx = length(pipeline_msi_data.spectra_metadata)
|
||||
println("DEBUG: Total spectra in dataset: $max_spectra_idx")
|
||||
println("DEBUG: Number of indices to process: $(length(spectrum_indices_to_process))")
|
||||
|
||||
if !isempty(spectrum_indices_to_process)
|
||||
min_idx = minimum(spectrum_indices_to_process)
|
||||
max_idx = maximum(spectrum_indices_to_process)
|
||||
println("DEBUG: Spectrum indices range: $min_idx to $max_idx")
|
||||
|
||||
# Check for invalid indices
|
||||
invalid_indices = filter(idx -> idx < 1 || idx > max_spectra_idx, spectrum_indices_to_process)
|
||||
if !isempty(invalid_indices)
|
||||
n_invalid = length(invalid_indices)
|
||||
sample_invalid = first(sort(invalid_indices), min(10, n_invalid))
|
||||
msg = "Invalid spectrum indices detected: $n_invalid indices out of range [1, $max_spectra_idx]. First few invalid indices: $sample_invalid"
|
||||
warning_msg = true
|
||||
@error msg
|
||||
println("DEBUG: $msg")
|
||||
close(pipeline_msi_data) # Important cleanup
|
||||
return
|
||||
end
|
||||
println("DEBUG: All spectrum indices are valid (within [1, $max_spectra_idx]).")
|
||||
else
|
||||
println("DEBUG: Warning - spectrum_indices_to_process is empty!")
|
||||
end
|
||||
|
||||
# CRITICAL: Verify indices are unique to prevent race conditions during loading
|
||||
if length(Set(spectrum_indices_to_process)) != length(spectrum_indices_to_process)
|
||||
@warn "Non-unique indices detected in spectrum_indices_to_process. This may cause issues during parallel loading."
|
||||
end
|
||||
|
||||
# Use pipeline_msi_data for reading
|
||||
# Split loading into chunks to update progress bar
|
||||
n_spectra = length(spectrum_indices_to_process)
|
||||
println("DEBUG: Loading $n_spectra spectra into MutableSpectrum objects...")
|
||||
current_spectra = Vector{MutableSpectrum}(undef, n_spectra)
|
||||
|
||||
chunk_size = max(1, n_spectra ÷ 10) # Update progress every 10%
|
||||
for chunk_start in 1:chunk_size:n_spectra
|
||||
chunk_end = min(chunk_start + chunk_size - 1, n_spectra)
|
||||
|
||||
Threads.@threads for i in chunk_start:chunk_end
|
||||
local original_idx = spectrum_indices_to_process[i]
|
||||
local mz, intensity # Enforce thread-local scope
|
||||
|
||||
try
|
||||
mz, intensity = GetSpectrum(pipeline_msi_data, original_idx)
|
||||
|
||||
# Diagnostic check for length mismatch and defensive truncation
|
||||
l_mz = length(mz)
|
||||
l_int = length(intensity)
|
||||
if l_mz != l_int
|
||||
new_len = min(l_mz, l_int)
|
||||
@warn "CRITICAL: Mismatch during loading at index $original_idx. mz=$l_mz, int=$l_int. TRUNCATING."
|
||||
mz = mz[1:new_len]
|
||||
intensity = intensity[1:new_len]
|
||||
end
|
||||
|
||||
Threads.@threads for i in 1:length(spectrum_indices_to_process)
|
||||
original_idx = spectrum_indices_to_process[i]
|
||||
mz, intensity = GetSpectrum(msi_data, original_idx) # Fetch mz and intensity for the current spectrum
|
||||
current_spectra[i] = MutableSpectrum(original_idx, copy(Float64.(mz)), copy(Float64.(intensity)), NamedTuple{(:mz, :intensity, :fwhm, :shape_r2, :snr, :prominence), NTuple{6, Float64}}[])
|
||||
catch loop_error
|
||||
rethrow(loop_error)
|
||||
end
|
||||
end
|
||||
|
||||
overall_progress = (chunk_end / n_spectra) * 0.2 # Loading is first 20%
|
||||
push!(__model__)
|
||||
end
|
||||
println("DEBUG: All spectra loaded into temporary structure for processing.")
|
||||
|
||||
# We can now close the local MSI data instance as we have loaded everything into memory
|
||||
# However, if we want to support lazy loading scenarios later, we might keep it open.
|
||||
# For now, let's close it here to free up file handles early,
|
||||
# UNLESS `execute_full_preprocessing` needs it (it doesn't seem to based on signature).
|
||||
close(pipeline_msi_data)
|
||||
pipeline_msi_data = nothing # Prevent accidental use
|
||||
|
||||
# Aggressive memory cleanup to return memory to OS
|
||||
println("DEBUG: Performing aggressive memory cleanup...")
|
||||
GC.gc(true) # Full garbage collection with all generations
|
||||
if Sys.islinux()
|
||||
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||
end
|
||||
println("DEBUG: Closed local pipeline MSIData instance and freed memory.")
|
||||
|
||||
|
||||
# --- 2. Parameter Assembly with Validation ---
|
||||
current_pipeline_step = "Configuring parameters..."
|
||||
@ -1450,8 +1645,16 @@ end
|
||||
mask_path_for_pipeline
|
||||
) do step
|
||||
current_pipeline_step = "Processing: $step"
|
||||
println("DEBUG: Processing step: $step")
|
||||
|
||||
# Update progress based on step index
|
||||
step_idx = findfirst(==(step), pipeline_stp)
|
||||
if step_idx !== nothing
|
||||
# Preprocessing is 20% to 90% (total 70%)
|
||||
overall_progress = 0.2 + (step_idx / length(pipeline_stp)) * 0.7
|
||||
end
|
||||
push!(__model__)
|
||||
end
|
||||
|
||||
println("DEBUG: Pipeline execution finished.")
|
||||
|
||||
# 4. Update Results Display
|
||||
@ -1638,7 +1841,8 @@ end
|
||||
mkpath(output_dir)
|
||||
save_feature_matrix(feature_matrix_result, bin_info_result, output_dir)
|
||||
msg = "Pipeline completed successfully. Feature matrix saved."
|
||||
println("DEBUG: Feature matrix saved to $output_dir")
|
||||
overall_progress = 1.0
|
||||
push!(__model__)
|
||||
else
|
||||
msg = "Pipeline completed successfully. No feature matrix generated (binning step not enabled)."
|
||||
println("DEBUG: $msg")
|
||||
@ -1650,18 +1854,71 @@ end
|
||||
@error "Pipeline failed" exception=(e, catch_backtrace())
|
||||
println("DEBUG: Pipeline caught an exception: $e")
|
||||
finally
|
||||
# Aggressive memory cleanup
|
||||
println("DEBUG: Starting aggressive memory cleanup...")
|
||||
|
||||
# Explicitly clear large data structures
|
||||
try
|
||||
if current_spectra !== nothing && !isempty(current_spectra)
|
||||
# Deep clear individual objects to break references effectively
|
||||
# Use isassigned to prevent UndefRefError if loading failed halfway
|
||||
for i in eachindex(current_spectra)
|
||||
if isassigned(current_spectra, i)
|
||||
s = current_spectra[i]
|
||||
s.mz = Float64[]
|
||||
s.intensity = Float64[]
|
||||
empty!(s.peaks)
|
||||
end
|
||||
end
|
||||
empty!(current_spectra)
|
||||
end
|
||||
current_spectra = nothing
|
||||
|
||||
if feature_matrix_result !== nothing
|
||||
feature_matrix_result = nothing
|
||||
end
|
||||
|
||||
# Close any open pipeline data handles
|
||||
if pipeline_msi_data !== nothing
|
||||
try
|
||||
close(pipeline_msi_data)
|
||||
catch
|
||||
# Already closed, ignore
|
||||
end
|
||||
pipeline_msi_data = nothing
|
||||
end
|
||||
|
||||
println("DEBUG: Data structures cleared. Triggering garbage collection...")
|
||||
catch cleanup_error
|
||||
@warn "Error during data cleanup: $cleanup_error"
|
||||
end
|
||||
|
||||
is_processing = false
|
||||
overall_progress = 0.0
|
||||
current_pipeline_step = ""
|
||||
println("DEBUG: run_full_pipeline finished (finally block).")
|
||||
GC.gc() # Trigger garbage collection
|
||||
|
||||
# Force garbage collection multiple times for thorough cleanup
|
||||
GC.gc()
|
||||
GC.gc() # Second pass to catch any circular references
|
||||
|
||||
# On Linux/Unix, force Julia to return memory to OS
|
||||
if Sys.islinux()
|
||||
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
|
||||
try
|
||||
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||
println("DEBUG: malloc_trim called successfully (Linux).")
|
||||
catch e
|
||||
@warn "malloc_trim failed: $e"
|
||||
end
|
||||
end
|
||||
|
||||
println("DEBUG: Memory cleanup complete.")
|
||||
end
|
||||
end
|
||||
|
||||
@onbutton recalculate_suggestions_btn begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
if msi_data === nothing
|
||||
msg = "Please load a file first."
|
||||
warning_msg = true
|
||||
@ -1903,6 +2160,7 @@ end
|
||||
# This new handler correctly adds the file from full_route to the batch list.
|
||||
@onbutton btnAddBatch begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
if isempty(full_route) || full_route == "unknown (manually added)"
|
||||
msg = "No active file selected to add to batch."
|
||||
warning_msg = true
|
||||
@ -1923,6 +2181,7 @@ end
|
||||
|
||||
@onbutton clear_batch_btn begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
selected_files = String[]
|
||||
batch_file_count = 0
|
||||
msg = "Batch cleared"
|
||||
@ -1974,6 +2233,7 @@ end
|
||||
|
||||
@onchange btnSearchMzml, btnSearchSync begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
if btnSearchMzml
|
||||
picked_route = pick_file(; filterlist="mzML,mzml")
|
||||
if !isempty(picked_route)
|
||||
@ -1997,6 +2257,7 @@ end
|
||||
|
||||
@onbutton convert_process begin
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
if isempty(mzml_full_route) || isempty(sync_full_route)
|
||||
msg_conversion = "Please select both an .mzML file and a .txt sync file."
|
||||
warning_msg = true
|
||||
@ -2029,6 +2290,7 @@ end
|
||||
@error "Conversion failed" exception=(e, catch_backtrace())
|
||||
finally
|
||||
is_processing = false
|
||||
overall_progress = 0.0
|
||||
# Re-enable button if files are still selected
|
||||
btnConvertDisable = isempty(mzml_full_route) || isempty(sync_full_route)
|
||||
end
|
||||
@ -2061,6 +2323,7 @@ end
|
||||
return
|
||||
end
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
|
||||
masses = Float64[]
|
||||
try
|
||||
@ -2087,7 +2350,8 @@ end
|
||||
|
||||
for (file_idx, file_path) in enumerate(current_selected_files)
|
||||
progress_message = "Processing file $(file_idx)/$(num_files): $(basename(file_path))"
|
||||
overall_progress = current_step / total_steps
|
||||
overall_progress = (file_idx - 1) / num_files
|
||||
push!(__model__)
|
||||
|
||||
all_params = (
|
||||
tolerance = current_tol,
|
||||
@ -2218,6 +2482,10 @@ end
|
||||
end
|
||||
|
||||
@onbutton createMeanPlot @time begin
|
||||
# Pre-initialize for safe cleanup in finally block
|
||||
local xSpectraMz = Vector{Float64}()
|
||||
local ySpectraMz = Vector{Float64}()
|
||||
|
||||
if isempty(selected_folder_main)
|
||||
msg = "No dataset selected. Please process a file and select a folder first."
|
||||
warning_msg = true
|
||||
@ -2225,6 +2493,7 @@ end
|
||||
end
|
||||
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
|
||||
try
|
||||
sTime = time()
|
||||
@ -2280,6 +2549,19 @@ end
|
||||
@error "Mean spectrum plotting failed" exception=(e, catch_backtrace())
|
||||
finally
|
||||
is_processing = false
|
||||
try
|
||||
if plotdata_before !== nothing
|
||||
plotdata_before = nothing
|
||||
end
|
||||
if !isempty(xSpectraMz)
|
||||
empty!(xSpectraMz)
|
||||
end
|
||||
if !isempty(ySpectraMz)
|
||||
empty!(ySpectraMz)
|
||||
end
|
||||
catch
|
||||
end
|
||||
|
||||
GC.gc() # Trigger garbage collection
|
||||
if Sys.islinux()
|
||||
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
|
||||
@ -2288,6 +2570,10 @@ end
|
||||
end
|
||||
|
||||
@onbutton createSumPlot @time begin
|
||||
# Pre-initialize for safe cleanup in finally block
|
||||
local xSpectraMz = Vector{Float64}()
|
||||
local ySpectraMz = Vector{Float64}()
|
||||
|
||||
if isempty(selected_folder_main)
|
||||
msg = "No dataset selected. Please process a file and select a folder first."
|
||||
warning_msg = true
|
||||
@ -2295,6 +2581,7 @@ end
|
||||
end
|
||||
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
msg = "Loading total spectrum plot for $(selected_folder_main)..."
|
||||
|
||||
try
|
||||
@ -2350,6 +2637,18 @@ end
|
||||
@error "Total spectrum plotting failed" exception=(e, catch_backtrace())
|
||||
finally
|
||||
is_processing = false
|
||||
try
|
||||
if plotdata_before !== nothing
|
||||
plotdata_before = nothing
|
||||
end
|
||||
if !isempty(xSpectraMz)
|
||||
empty!(xSpectraMz)
|
||||
end
|
||||
if !isempty(ySpectraMz)
|
||||
empty!(ySpectraMz)
|
||||
end
|
||||
catch
|
||||
end
|
||||
GC.gc() # Trigger garbage collection
|
||||
if Sys.islinux()
|
||||
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure Julia returns the freed memory to OS
|
||||
@ -2365,6 +2664,7 @@ end
|
||||
end
|
||||
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
msg = "Loading plot for $(selected_folder_main)..."
|
||||
|
||||
try
|
||||
@ -2479,6 +2779,7 @@ end
|
||||
end
|
||||
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
msg = "Loading plot for $(selected_folder_main)..."
|
||||
|
||||
try
|
||||
@ -3022,6 +3323,7 @@ end
|
||||
return
|
||||
end
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
|
||||
try
|
||||
# --- Get Mask Path ---
|
||||
@ -3079,6 +3381,7 @@ end
|
||||
end
|
||||
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
|
||||
try
|
||||
# --- Get Mask Path ---
|
||||
@ -3137,6 +3440,7 @@ end
|
||||
end
|
||||
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
|
||||
try
|
||||
sTime=time()
|
||||
@ -3175,6 +3479,7 @@ end
|
||||
end
|
||||
|
||||
is_processing = true
|
||||
push!(__model__)
|
||||
|
||||
try
|
||||
sTime=time()
|
||||
@ -3401,6 +3706,10 @@ end
|
||||
@mounted watchplots()
|
||||
|
||||
@onchange isready begin
|
||||
# Capture state on first run only
|
||||
if isempty(INITIAL_MODEL_STATE)
|
||||
capture_initial_state!(__model__)
|
||||
end
|
||||
# is_processing = true
|
||||
if isready && !registry_init_done
|
||||
sTime=time()
|
||||
@ -3471,6 +3780,5 @@ end
|
||||
end
|
||||
end
|
||||
# == Pages ==
|
||||
# Register a new route and the page that will be loaded on access
|
||||
@page("/", "app.jl.html")
|
||||
end
|
||||
386
app.jl.html
386
app.jl.html
@ -3,16 +3,30 @@
|
||||
<div>
|
||||
<h4>JuliaMSI </h4>
|
||||
</div>
|
||||
<q-space />
|
||||
<div class="q-gutter-sm q-pa-sm">
|
||||
<q-btn flat round icon="bug_report" v-on:click="showBugModal = true">
|
||||
<q-tooltip>Report a Bug</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat round icon="refresh" color="negative" v-on:click="reset_session_btn = true">
|
||||
<q-tooltip>Deep Session Reset</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!--
|
||||
<div v-if="is_initializing" class="loading-overlay">
|
||||
<div v-if="is_initializing || is_processing" class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<q-spinner-hourglass color="white" size="4em" />
|
||||
<div class="q-mt-md text-white text-h6">{{ initialization_message }}</div>
|
||||
<div v-if="is_processing" class="q-mt-md"
|
||||
style="width: 250px; background: rgba(255,255,255,0.2); border-radius: 10px; overflow: hidden; height: 12px; border: 1px solid rgba(255,255,255,0.3);">
|
||||
<div
|
||||
:style="{ width: (overall_progress * 100) + '%', height: '100%', background: '#00e676', transition: 'width 0.4s ease-out', boxShadow: '0 0 10px #00e676' }">
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="is_initializing" class="q-mt-md text-white text-h6">{{ initialization_message }}</div>
|
||||
<div v-else class="q-mt-md text-white text-h6">{{ progress_message || 'Processing...' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div id="extDivStyle" class="row col-12 q-pa-xl">
|
||||
<div class="row col-6">
|
||||
@ -37,14 +51,16 @@
|
||||
<q-icon name="search" v-on:click="!is_processing && (btnSearch=true)" class="cursor-pointer" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add" :disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true" :disable="is_processing || batch_file_count === 0"
|
||||
label="Clear"></q-btn>
|
||||
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true"
|
||||
:disable="is_processing || batch_file_count === 0" label="Clear"></q-btn>
|
||||
</div>
|
||||
|
||||
<!-- Mask Configuration -->
|
||||
<div class="row items-center q-mb-md">
|
||||
<q-toggle v-model="maskEnabled" label="Apply Mask During Preprocessing" color="green" class="q-mr-md" :disable="is_processing" />
|
||||
<q-toggle v-model="maskEnabled" label="Apply Mask During Preprocessing" color="green" class="q-mr-md"
|
||||
:disable="is_processing" />
|
||||
</div>
|
||||
|
||||
<!-- Subset Processing -->
|
||||
@ -60,7 +76,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="enable_subset_processing" class="q-mt-sm">
|
||||
<q-input standout="custom-standout" type="number" v-model.number="spectra_subset_size" label="Number of Spectra to Process" :min="1" :readonly="is_processing">
|
||||
<q-input standout="custom-standout" type="number" v-model.number="spectra_subset_size"
|
||||
label="Number of Spectra to Process" :min="1" :readonly="is_processing">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="functions" />
|
||||
</template>
|
||||
@ -73,8 +90,8 @@
|
||||
<!-- Spectrum Selection for Visualization -->
|
||||
<div class="row items-center q-mb-md">
|
||||
<div class="text-subtitle2 q-mr-md">Preview Spectrum:</div>
|
||||
<q-btn-dropdown class="q-ma-sm" :loading="is_processing" :disable="is_processing"
|
||||
label="Generate Spectra" icon="play_arrow">
|
||||
<q-btn-dropdown class="q-ma-sm" :loading="is_processing" :disable="is_processing" label="Generate Spectra"
|
||||
icon="play_arrow">
|
||||
<q-list>
|
||||
<q-item clickable v-close-popup v-on:click="createMeanPlot=true">
|
||||
<q-item-label>Mean spectrum plot</q-item-label>
|
||||
@ -111,43 +128,49 @@
|
||||
</div>
|
||||
|
||||
<!-- Internal Standards (Collapsible) -->
|
||||
<q-expansion-item
|
||||
icon="science"
|
||||
label="Internal Standards"
|
||||
caption="Manage reference peaks for calibration"
|
||||
class="q-mb-md"
|
||||
>
|
||||
<q-expansion-item icon="science" label="Internal Standards" caption="Manage reference peaks for calibration"
|
||||
class="q-mb-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-toggle v-model="enable_standards" v-on:click="enable_standards" label="Use Internal Standards" color="primary" class="q-mb-md" :disable="is_processing" />
|
||||
<q-toggle v-model="enable_standards" v-on:click="enable_standards" label="Use Internal Standards"
|
||||
color="primary" class="q-mb-md" :disable="is_processing" />
|
||||
<!-- Keep your existing reference_peaks_list implementation -->
|
||||
<q-list bordered separator class="q-mt-md">
|
||||
<q-item v-for="(peak, index) in reference_peaks_list" :key="index">
|
||||
<q-item-section avatar>
|
||||
<q-btn flat round icon="delete" color="negative" v-on:click="action_index = index; remove_peak_trigger = true" :disable="is_processing"></q-btn>
|
||||
<q-btn flat round icon="delete" color="negative"
|
||||
v-on:click="action_index = index; remove_peak_trigger = true" :disable="is_processing"></q-btn>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="m/z" type="number" step="0.0001"
|
||||
v-model="peak.mz" :rules="[val => !!val || 'Required', val => val > 0 || 'Must be positive']" :readonly="is_processing"></q-input>
|
||||
<q-input standout="custom-standout" label="m/z" type="number" step="0.0001" v-model="peak.mz"
|
||||
:rules="[val => !!val || 'Required', val => val > 0 || 'Must be positive']"
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Label (optional)" v-model="peak.label" :readonly="is_processing"></q-input>
|
||||
<q-input standout="custom-standout" label="Label (optional)" v-model="peak.label"
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
<q-btn class="q-ma-sm btn-style" icon="add" label="Add Reference Peak" v-on:click="addReferencePeak=true" :disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm btn-style" icon="add" label="Add Reference Peak"
|
||||
v-on:click="addReferencePeak=true" :disable="is_processing"></q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div class="row justify-end q-mt-sm">
|
||||
<q-btn class="q-ma-sm" dense icon="get_app" v-on:click="export_standards_btn=true" label="Export" outline :disable="is_processing" hint="Export standards list to a JSON file." />
|
||||
<q-btn class="q-ma-sm" dense icon="upload_file" v-on:click="import_standards_btn=true" label="Import" outline :disable="is_processing" hint="Import standards list from a JSON file." />
|
||||
<q-btn class="q-ma-sm" icon="functions" v-on:click="recalculate_suggestions_btn=true" label="Recalculate Suggestions" outline hint="Re-run automatic parameter suggestion using the current list of internal standards." :disable="is_processing"/>
|
||||
<q-btn class="q-ma-sm" dense icon="get_app" v-on:click="export_standards_btn=true" label="Export"
|
||||
outline :disable="is_processing" hint="Export standards list to a JSON file." />
|
||||
<q-btn class="q-ma-sm" dense icon="upload_file" v-on:click="import_standards_btn=true" label="Import"
|
||||
outline :disable="is_processing" hint="Import standards list from a JSON file." />
|
||||
<q-btn class="q-ma-sm" icon="functions" v-on:click="recalculate_suggestions_btn=true"
|
||||
label="Recalculate Suggestions" outline
|
||||
hint="Re-run automatic parameter suggestion using the current list of internal standards."
|
||||
:disable="is_processing" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@ -155,18 +178,18 @@
|
||||
<!-- Reorderable Preprocessing Steps -->
|
||||
<div class="text-h6 q-mb-md">Preprocessing Pipeline</div>
|
||||
<q-list bordered :disable="is_processing">
|
||||
<q-expansion-item v-for="(step, index) in pipeline_step_order" :key="step.name"
|
||||
:label="step.label" group="preprocessing-steps"
|
||||
:class="step.enabled ? '' : 'text-grey'" :disable="is_processing">
|
||||
<q-expansion-item v-for="(step, index) in pipeline_step_order" :key="step.name" :label="step.label"
|
||||
group="preprocessing-steps" :class="step.enabled ? '' : 'text-grey'" :disable="is_processing">
|
||||
|
||||
<!-- Header with controls -->
|
||||
<template v-slot:header>
|
||||
<q-item-section avatar>
|
||||
<div class="row no-wrap">
|
||||
<q-btn flat round icon="arrow_upward" size="sm"
|
||||
:disable="is_processing || index === 0" v-on:click.stop="action_index = index; move_step_up_trigger = true"></q-btn>
|
||||
<q-btn flat round icon="arrow_upward" size="sm" :disable="is_processing || index === 0"
|
||||
v-on:click.stop="action_index = index; move_step_up_trigger = true"></q-btn>
|
||||
<q-btn flat round icon="arrow_downward" size="sm"
|
||||
:disable="is_processing || index === pipeline_step_order.length - 1" v-on:click.stop="action_index = index; move_step_down_trigger = true"></q-btn>
|
||||
:disable="is_processing || index === pipeline_step_order.length - 1"
|
||||
v-on:click.stop="action_index = index; move_step_down_trigger = true"></q-btn>
|
||||
</div>
|
||||
</q-item-section>
|
||||
|
||||
@ -175,7 +198,8 @@
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-toggle v-model="step.enabled" color="green" v-on:click.stop="action_index = index; toggle_step_trigger = true" :disable="is_processing" />
|
||||
<q-toggle v-model="step.enabled" color="green"
|
||||
v-on:click.stop="action_index = index; toggle_step_trigger = true" :disable="is_processing" />
|
||||
</q-item-section>
|
||||
</template>
|
||||
|
||||
@ -196,10 +220,12 @@
|
||||
<q-radio v-model="smoothing_method" val="ma" label="Moving Average" :disable="is_processing" />
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Window Size" v-model="smoothing_window" type="number" :readonly="is_processing" />
|
||||
<q-input standout="custom-standout" label="Window Size" v-model="smoothing_window" type="number"
|
||||
:readonly="is_processing" />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Order (Savitzky-Golay)" v-model="smoothing_order" type="number" :readonly="is_processing" />
|
||||
<q-input standout="custom-standout" label="Order (Savitzky-Golay)" v-model="smoothing_order"
|
||||
type="number" :readonly="is_processing" />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
@ -211,9 +237,12 @@
|
||||
<div class="text-caption">The algorithm to use for baseline correction.</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-radio v-model="baseline_method" val="snip" label="SNIP" hint="Sensitive Nonlinear Iterative Peak clipping." :disable="is_processing" /><br>
|
||||
<q-radio v-model="baseline_method" val="convex_hull" label="CONVEX HULL" hint="Finds the lower convex hull of the spectrum." :disable="is_processing" /><br>
|
||||
<q-radio v-model="baseline_method" val="median" label="MEDIAN" hint="Moving median filter." :disable="is_processing" /><br>
|
||||
<q-radio v-model="baseline_method" val="snip" label="SNIP"
|
||||
hint="Sensitive Nonlinear Iterative Peak clipping." :disable="is_processing" /><br>
|
||||
<q-radio v-model="baseline_method" val="convex_hull" label="CONVEX HULL"
|
||||
hint="Finds the lower convex hull of the spectrum." :disable="is_processing" /><br>
|
||||
<q-radio v-model="baseline_method" val="median" label="MEDIAN" hint="Moving median filter."
|
||||
:disable="is_processing" /><br>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Parameters</div>
|
||||
@ -222,11 +251,15 @@
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Iterations (for SNIP)" type="number"
|
||||
:placeholder="suggested_baseline_iterations" v-model="baseline_iterations" hint="The number of iterations for the SNIP algorithm. A higher number results in a more aggressive baseline." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_baseline_iterations" v-model="baseline_iterations"
|
||||
hint="The number of iterations for the SNIP algorithm. A higher number results in a more aggressive baseline."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Window (for Median)" type="number"
|
||||
:placeholder="suggested_baseline_window" v-model="baseline_window" hint="The window size for the median method, determining the local region for median calculation." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_baseline_window" v-model="baseline_window"
|
||||
hint="The window size for the median method, determining the local region for median calculation."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
@ -238,10 +271,15 @@
|
||||
<div class="text-caption">The normalization method to apply.</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-radio v-model="normalization_method" val="tic" label="TIC" hint="Total Ion Current normalization (divides by the sum of intensities)." :disable="is_processing" /><br>
|
||||
<q-radio v-model="normalization_method" val="median" label="MEDIAN" hint="Divides by the median intensity." :disable="is_processing" /><br>
|
||||
<q-radio v-model="normalization_method" val="rms" label="RMS" hint="Root Mean Square normalization." :disable="is_processing" /><br>
|
||||
<q-radio v-model="normalization_method" val="none" label="NONE" hint="No normalization is applied." :disable="is_processing" /><br>
|
||||
<q-radio v-model="normalization_method" val="tic" label="TIC"
|
||||
hint="Total Ion Current normalization (divides by the sum of intensities)."
|
||||
:disable="is_processing" /><br>
|
||||
<q-radio v-model="normalization_method" val="median" label="MEDIAN"
|
||||
hint="Divides by the median intensity." :disable="is_processing" /><br>
|
||||
<q-radio v-model="normalization_method" val="rms" label="RMS" hint="Root Mean Square normalization."
|
||||
:disable="is_processing" /><br>
|
||||
<q-radio v-model="normalization_method" val="none" label="NONE" hint="No normalization is applied."
|
||||
:disable="is_processing" /><br>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@ -251,9 +289,12 @@
|
||||
<div class="text-caption">The alignment algorithm.</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-radio v-model="alignment_method" val="lowess" label="LOWESS" hint="Locally Weighted Scatterplot Smoothing regression." :disable="is_processing" /><br>
|
||||
<q-radio v-model="alignment_method" val="linear" label="LINEAR" hint="Linear regression." :disable="is_processing" /><br>
|
||||
<q-radio v-model="alignment_method" val="ransac" label="RANSAC" hint="Random Sample Consensus algorithm for robust fitting." :disable="is_processing" /><br>
|
||||
<q-radio v-model="alignment_method" val="lowess" label="LOWESS"
|
||||
hint="Locally Weighted Scatterplot Smoothing regression." :disable="is_processing" /><br>
|
||||
<q-radio v-model="alignment_method" val="linear" label="LINEAR" hint="Linear regression."
|
||||
:disable="is_processing" /><br>
|
||||
<q-radio v-model="alignment_method" val="ransac" label="RANSAC"
|
||||
hint="Random Sample Consensus algorithm for robust fitting." :disable="is_processing" /><br>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Parameters</div>
|
||||
@ -262,23 +303,34 @@
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Span (for LOWESS)" type="number" step="0.01"
|
||||
:placeholder="suggested_alignment_span" v-model="alignment_span" :rules="[val => val >= 0.0 && val <= 1.0 || 'Needs to be between 0 and 1']" hint="The span parameter for LOWESS regression, controlling smoothness (0.0 to 1.0)." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_alignment_span" v-model="alignment_span"
|
||||
:rules="[val => val >= 0.0 && val <= 1.0 || 'Needs to be between 0 and 1']"
|
||||
hint="The span parameter for LOWESS regression, controlling smoothness (0.0 to 1.0)."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Tolerance" type="number" step="0.001"
|
||||
:placeholder="suggested_alignment_tolerance" v-model="alignment_tolerance" hint="The tolerance for matching peaks between the target and reference spectrum." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_alignment_tolerance" v-model="alignment_tolerance"
|
||||
hint="The tolerance for matching peaks between the target and reference spectrum."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-select standout="custom-standout" label="Tolerance Unit" v-model="alignment_tolerance_unit"
|
||||
:options="['mz', 'ppm']" class="q-mt-md" hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)." :disable="is_processing"></q-select>
|
||||
:options="['mz', 'ppm']" class="q-mt-md"
|
||||
hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)."
|
||||
:disable="is_processing"></q-select>
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Max Shift PPM" type="number"
|
||||
:placeholder="suggested_alignment_max_shift_ppm" v-model="alignment_max_shift_ppm" hint="The maximum allowed m/z shift in ppm to prevent spurious peak matches." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_alignment_max_shift_ppm" v-model="alignment_max_shift_ppm"
|
||||
hint="The maximum allowed m/z shift in ppm to prevent spurious peak matches."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Min Matched Peaks" type="number"
|
||||
:placeholder="suggested_alignment_min_matched_peaks" v-model="alignment_min_matched_peaks" hint="The minimum number of matching peaks required to perform the alignment." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_alignment_min_matched_peaks" v-model="alignment_min_matched_peaks"
|
||||
hint="The minimum number of matching peaks required to perform the alignment."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
@ -293,12 +345,14 @@
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Fit Order" type="number"
|
||||
:placeholder="suggested_calibration_fit_order" v-model="calibration_fit_order"
|
||||
hint="Polynomial order for the calibration curve (e.g., 1 or 2)." :readonly="is_processing"></q-input>
|
||||
hint="Polynomial order for the calibration curve (e.g., 1 or 2)."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="PPM Tolerance" type="number"
|
||||
:placeholder="suggested_calibration_ppm_tolerance" v-model="calibration_ppm_tolerance"
|
||||
hint="PPM tolerance for matching reference peaks to internal standards." :readonly="is_processing"></q-input>
|
||||
hint="PPM tolerance for matching reference peaks to internal standards."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
@ -310,9 +364,13 @@
|
||||
<div class="text-caption">The peak detection algorithm.</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-radio v-model="peak_picking_method" val="profile" label="PROFILE" hint="For profile-mode data, using local maxima and quality filters." :disable="is_processing" /><br>
|
||||
<q-radio v-model="peak_picking_method" val="wavelet" label="WAVELET" hint="Continuous Wavelet Transform (CWT) based peak detection." :disable="is_processing" /><br>
|
||||
<q-radio v-model="peak_picking_method" val="centroid" label="CENTROID" hint="For centroid-mode data, essentially a filtering step." :disable="is_processing" /><br>
|
||||
<q-radio v-model="peak_picking_method" val="profile" label="PROFILE"
|
||||
hint="For profile-mode data, using local maxima and quality filters."
|
||||
:disable="is_processing" /><br>
|
||||
<q-radio v-model="peak_picking_method" val="wavelet" label="WAVELET"
|
||||
hint="Continuous Wavelet Transform (CWT) based peak detection." :disable="is_processing" /><br>
|
||||
<q-radio v-model="peak_picking_method" val="centroid" label="CENTROID"
|
||||
hint="For centroid-mode data, essentially a filtering step." :disable="is_processing" /><br>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Parameters</div>
|
||||
@ -321,35 +379,52 @@
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Signal to Noise Threshold" type="number" step="0.1"
|
||||
:placeholder="suggested_peak_picking_snr_threshold" v-model="peak_picking_snr_threshold" hint="Signal-to-Noise Ratio threshold. Peaks with SNR below this value are discarded." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_picking_snr_threshold" v-model="peak_picking_snr_threshold"
|
||||
hint="Signal-to-Noise Ratio threshold. Peaks with SNR below this value are discarded."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Half Window Size" type="number"
|
||||
:placeholder="suggested_peak_picking_half_window" v-model="peak_picking_half_window" hint="Number of data points to the left and right of a potential peak to consider for local maximum detection (for Profile method)." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_picking_half_window" v-model="peak_picking_half_window"
|
||||
hint="Number of data points to the left and right of a potential peak to consider for local maximum detection (for Profile method)."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Min Peak Prominence" type="number" step="0.01"
|
||||
:placeholder="suggested_peak_picking_min_peak_prominence" v-model="peak_picking_min_peak_prominence" hint="Minimum required prominence of a peak, expressed as a fraction of its height." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_picking_min_peak_prominence"
|
||||
v-model="peak_picking_min_peak_prominence"
|
||||
hint="Minimum required prominence of a peak, expressed as a fraction of its height."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Merge Peaks Tolerance (m/z)" type="number" step="0.001"
|
||||
:placeholder="suggested_peak_picking_merge_peaks_tolerance" v-model="peak_picking_merge_peaks_tolerance" hint="The m/z tolerance within which to merge adjacent peaks, keeping the more intense one." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_picking_merge_peaks_tolerance"
|
||||
v-model="peak_picking_merge_peaks_tolerance"
|
||||
hint="The m/z tolerance within which to merge adjacent peaks, keeping the more intense one."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Min Peak Width (PPM)" type="number"
|
||||
:placeholder="suggested_peak_picking_min_peak_width_ppm" v-model="peak_picking_min_peak_width_ppm" hint="Minimum acceptable peak width (FWHM) in ppm." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_picking_min_peak_width_ppm"
|
||||
v-model="peak_picking_min_peak_width_ppm" hint="Minimum acceptable peak width (FWHM) in ppm."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Max Peak Width (PPM)" type="number"
|
||||
:placeholder="suggested_peak_picking_max_peak_width_ppm" v-model="peak_picking_max_peak_width_ppm" hint="Maximum acceptable peak width (FWHM) in ppm." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_picking_max_peak_width_ppm"
|
||||
v-model="peak_picking_max_peak_width_ppm" hint="Maximum acceptable peak width (FWHM) in ppm."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01" class="q-mt-md"
|
||||
:placeholder="suggested_peak_picking_min_peak_shape_r2" v-model="peak_picking_min_peak_shape_r2" hint="Minimum R-squared value from a Gaussian fit to the peak, used as a quality measure for peak shape." :readonly="is_processing"></q-input>
|
||||
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01"
|
||||
class="q-mt-md" :placeholder="suggested_peak_picking_min_peak_shape_r2"
|
||||
v-model="peak_picking_min_peak_shape_r2"
|
||||
hint="Minimum R-squared value from a Gaussian fit to the peak, used as a quality measure for peak shape."
|
||||
:readonly="is_processing"></q-input>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@ -361,33 +436,44 @@
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Min SNR" type="number" step="0.1"
|
||||
:placeholder="suggested_peak_selection_min_snr" v-model="peak_selection_min_snr" hint="Minimum Signal-to-Noise Ratio for a peak to be kept." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_selection_min_snr" v-model="peak_selection_min_snr"
|
||||
hint="Minimum Signal-to-Noise Ratio for a peak to be kept." :readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Min FWHM (PPM)" type="number"
|
||||
:placeholder="suggested_peak_selection_min_fwhm_ppm" v-model="peak_selection_min_fwhm_ppm" hint="Minimum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_selection_min_fwhm_ppm" v-model="peak_selection_min_fwhm_ppm"
|
||||
hint="Minimum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Max FWHM (PPM)" type="number"
|
||||
:placeholder="suggested_peak_selection_max_fwhm_ppm" v-model="peak_selection_max_fwhm_ppm" hint="Maximum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_selection_max_fwhm_ppm" v-model="peak_selection_max_fwhm_ppm"
|
||||
hint="Maximum Full Width at Half Maximum (FWHM) in ppm for a peak to be kept."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Min Peak Shape R2" type="number" step="0.01"
|
||||
:placeholder="suggested_peak_selection_min_shape_r2" v-model="peak_selection_min_shape_r2" hint="Minimum R-squared value from a Gaussian fit, filtering for good peak shape." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_selection_min_shape_r2" v-model="peak_selection_min_shape_r2"
|
||||
hint="Minimum R-squared value from a Gaussian fit, filtering for good peak shape."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Frequency Threshold" type="number" step="0.01"
|
||||
:placeholder="suggested_peak_selection_frequency_threshold" v-model="peak_selection_frequency_threshold"
|
||||
hint="The minimum fraction of spectra a peak must be present in to be kept (0.0 to 1.0)." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_selection_frequency_threshold"
|
||||
v-model="peak_selection_frequency_threshold"
|
||||
hint="The minimum fraction of spectra a peak must be present in to be kept (0.0 to 1.0)."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Correlation Threshold" type="number" step="0.01"
|
||||
:placeholder="suggested_peak_selection_correlation_threshold" v-model="peak_selection_correlation_threshold"
|
||||
hint="Minimum correlation with neighboring peaks (not yet implemented)." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_peak_selection_correlation_threshold"
|
||||
v-model="peak_selection_correlation_threshold"
|
||||
hint="Minimum correlation with neighboring peaks (not yet implemented)."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
@ -399,8 +485,11 @@
|
||||
<div class="text-caption">The binning strategy.</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-radio v-model="binning_method" val="adaptive" label="ADAPTIVE" hint="Creates bins based on the density of detected peaks." :disable="is_processing" /><br>
|
||||
<q-radio v-model="binning_method" val="uniform" label="UNIFORM" hint="Creates a fixed number of equally spaced bins over the m/z range." :disable="is_processing" /><br>
|
||||
<q-radio v-model="binning_method" val="adaptive" label="ADAPTIVE"
|
||||
hint="Creates bins based on the density of detected peaks." :disable="is_processing" /><br>
|
||||
<q-radio v-model="binning_method" val="uniform" label="UNIFORM"
|
||||
hint="Creates a fixed number of equally spaced bins over the m/z range."
|
||||
:disable="is_processing" /><br>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Parameters</div>
|
||||
@ -409,35 +498,47 @@
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Tolerance (for Adaptive)" type="number" step="0.001"
|
||||
:placeholder="suggested_binning_tolerance" v-model="binning_tolerance" hint="Tolerance for grouping peaks into a bin in adaptive mode." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_binning_tolerance" v-model="binning_tolerance"
|
||||
hint="Tolerance for grouping peaks into a bin in adaptive mode."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-select standout="custom-standout" label="Tolerance Unit" v-model="binning_tolerance_unit"
|
||||
:options="['mz', 'ppm']" hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)." :disable="is_processing"></q-select>
|
||||
:options="['mz', 'ppm']"
|
||||
hint="The unit for tolerance, either 'mz' (absolute) or 'ppm' (relative)."
|
||||
:disable="is_processing"></q-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Frequency Threshold" type="number" step="0.01"
|
||||
:placeholder="suggested_binning_frequency_threshold" v-model="binning_frequency_threshold" hint="The minimum fraction of spectra a bin must contain a peak in to be kept (0.0 to 1.0)." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_binning_frequency_threshold" v-model="binning_frequency_threshold"
|
||||
hint="The minimum fraction of spectra a bin must contain a peak in to be kept (0.0 to 1.0)."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Min Peaks Per Bin" type="number"
|
||||
:placeholder="suggested_binning_min_peak_per_bin" v-model="binning_min_peak_per_bin" hint="The minimum number of individual peaks required to form a bin in adaptive mode." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_binning_min_peak_per_bin" v-model="binning_min_peak_per_bin"
|
||||
hint="The minimum number of individual peaks required to form a bin in adaptive mode."
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Max Bin Width (PPM)" type="number"
|
||||
:placeholder="suggested_binning_max_bin_width_ppm" v-model="binning_max_bin_width_ppm" hint="Maximum width of a bin in ppm for adaptive mode." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_binning_max_bin_width_ppm" v-model="binning_max_bin_width_ppm"
|
||||
hint="Maximum width of a bin in ppm for adaptive mode." :readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input standout="custom-standout" label="Number of Uniform Bins" type="number"
|
||||
:placeholder="suggested_binning_num_uniform_bins" v-model="binning_num_uniform_bins" hint="The number of bins to create for the uniform method." :readonly="is_processing"></q-input>
|
||||
:placeholder="suggested_binning_num_uniform_bins" v-model="binning_num_uniform_bins"
|
||||
hint="The number of bins to create for the uniform method." :readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-toggle v-model="binning_intensity_weighted_centers" v-on:click="binning_intensity_weighted_centers" label="Intensity Weighted Centers"
|
||||
class="q-mt-md" hint="If enabled, calculates bin centers as an intensity-weighted average of the peaks within it." :disable="is_processing"></q-toggle>
|
||||
<q-toggle v-model="binning_intensity_weighted_centers" v-on:click="binning_intensity_weighted_centers"
|
||||
label="Intensity Weighted Centers" class="q-mt-md"
|
||||
hint="If enabled, calculates bin centers as an intensity-weighted average of the peaks within it."
|
||||
:disable="is_processing"></q-toggle>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@ -446,12 +547,14 @@
|
||||
|
||||
<!-- Pipeline Controls -->
|
||||
<div class="row justify-end items-center q-mt-md">
|
||||
<q-btn class="q-ma-sm" icon="get_app" v-on:click="export_params_btn=true" label="Export Params" outline :disable="is_processing" />
|
||||
<q-btn class="q-ma-sm" icon="upload_file" v-on:click="import_params_btn=true" label="Import Params" outline :disable="is_processing" />
|
||||
<q-btn class="q-ma-sm" icon="get_app" v-on:click="export_params_btn=true" label="Export Params" outline
|
||||
:disable="is_processing" />
|
||||
<q-btn class="q-ma-sm" icon="upload_file" v-on:click="import_params_btn=true" label="Import Params" outline
|
||||
:disable="is_processing" />
|
||||
<q-btn :loading="is_processing" class="q-ma-sm btn-style" icon="play_arrow"
|
||||
v-on:click="run_full_pipeline=true" padding="lg" label="Run Pipeline" :disable="is_processing" />
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="generator">
|
||||
<div class="text-h6">imzML & mzML Data Processor</div>
|
||||
<p>Please make sure the ibd and imzML file are located in the same directory and have the same name.
|
||||
@ -467,9 +570,10 @@
|
||||
<q-icon name="search" v-on:click="btnSearch=true" class="cursor-pointer" :disable="is_processing" />
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add" :disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true" :disable="is_processing || batch_file_count === 0"
|
||||
label="Clear"></q-btn>
|
||||
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true"
|
||||
:disable="is_processing || batch_file_count === 0" label="Clear"></q-btn>
|
||||
</div>
|
||||
<q-list bordered separator v-if="selected_files.length > 0">
|
||||
<q-item v-for="(file, index) in selected_files" :key="index">
|
||||
@ -477,7 +581,8 @@
|
||||
{{ file }}
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn flat round icon="delete" size="sm" v-on:click="selected_files.splice(index, 1)" :disable="is_processing"></q-btn>
|
||||
<q-btn flat round icon="delete" size="sm" v-on:click="selected_files.splice(index, 1)"
|
||||
:disable="is_processing"></q-btn>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
@ -495,12 +600,14 @@
|
||||
<div class="st-col col-4 col-sm q-ma-sm">
|
||||
<q-input standout="custom-standout" id="textTol" step="0.005" v-model="Tol"
|
||||
label="Mass-to-charge ratio tolerance" type="number"
|
||||
:rules="[val => !!val || '* Required', val => val >= 0.0 && 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 && val <= 1.0 || 'Needs to be in range between 0 and 1']"
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
<div class="st-col col-4 col-sm q-ma-sm">
|
||||
<q-input standout="custom-standout" id="textcolorLevel" step="1" v-model="colorLevel" label="Color levels"
|
||||
type="number"
|
||||
:rules="[ val => !!val || '* Required', val => val >= 2 && val <= 256 || 'Needs to be in range between 2 and 256']" :readonly="is_processing"></q-input>
|
||||
:rules="[ val => !!val || '* Required', val => val >= 2 && val <= 256 || 'Needs to be in range between 2 and 256']"
|
||||
:readonly="is_processing"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -519,7 +626,8 @@
|
||||
<q-input standout="custom-standout" id="textTriqProb" step="0.01" v-model="triqProb"
|
||||
label="TrIQ probability" type="number" :rules="[
|
||||
val => triqEnabled ? ( '* Required', val >= 0.8 && val <= 1 || 'Needs to be in range between 0.8 and 1') : true
|
||||
]" :readonly="is_processing || !triqEnabled" :disable="is_processing || !triqEnabled"></q-input>
|
||||
]" :readonly="is_processing || !triqEnabled"
|
||||
:disable="is_processing || !triqEnabled"></q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -585,9 +693,10 @@
|
||||
</q-btn>
|
||||
<q-btn icon="zoom_out_map" class="q-ma-sm on-right btn-style" v-on:click="compareBtn=true" padding="sm"
|
||||
label="Compare" :disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm btn-style" icon="edit" label="Mask Editor" href="/mask" :disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm btn-style" icon="dashboard" v-on:click="showMetadataBtn=true"
|
||||
label="Show Metadata" :disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm btn-style" icon="edit" label="Mask Editor" href="/mask"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn class="q-ma-sm btn-style" icon="dashboard" v-on:click="showMetadataBtn=true" label="Show Metadata"
|
||||
:disable="is_processing"></q-btn>
|
||||
<div class="q-pa-md row items-center" v-show="is_processing">
|
||||
<q-spinner color="primary" size="2em" class="q-mr-sm"></q-spinner>
|
||||
<div class="text-caption">{{ progress_message }}</div>
|
||||
@ -687,7 +796,8 @@
|
||||
<div class="text-subtitle1">Before Preprocessing</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<plotly id="plotSpectraBefore" :data="plotdata_before" :layout="plotlayout_before" class="q-pa-none q-ma-none"></plotly>
|
||||
<plotly id="plotSpectraBefore" :data="plotdata_before" :layout="plotlayout_before"
|
||||
class="q-pa-none q-ma-none"></plotly>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card>
|
||||
@ -710,8 +820,10 @@
|
||||
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset"
|
||||
class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"></q-select>
|
||||
<q-space></q-space>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
</div>
|
||||
<!-- Image manager -->
|
||||
<div id="image-container-normal" class="row st-col col-12">
|
||||
@ -733,8 +845,10 @@
|
||||
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset"
|
||||
class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"></q-select>
|
||||
<q-space></q-space>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
</div>
|
||||
<!-- Triq Image manager -->
|
||||
<div id="image-container-triq" class="row st-col col-12">
|
||||
@ -834,8 +948,8 @@
|
||||
<div class="col-6">
|
||||
<div class="row items-center">
|
||||
<q-select v-model="selected_folder_compare_left" :options="image_available_folders"
|
||||
label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;"
|
||||
v-on:focus="refetch_folders = true" :disable="is_processing"></q-select>
|
||||
label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"
|
||||
:disable="is_processing"></q-select>
|
||||
<q-space></q-space>
|
||||
<st-tabs id="tabHeaderCompareLeft" :ids="CompTabIDsLeft" :labels="CompTabLabelsLeft"
|
||||
v-model="CompSelectedTabLeft"></st-tabs>
|
||||
@ -845,9 +959,10 @@
|
||||
<!-- Content for Tab 0 -->
|
||||
<!-- Btn image changer -->
|
||||
<div>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompLeft=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
|
||||
v-on:click="imgPlusCompLeft=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompLeft=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusCompLeft=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
</div>
|
||||
<!-- Image manager -->
|
||||
<div id="image-container-compare-left-normal" class="row st-col col-12">
|
||||
@ -867,9 +982,10 @@
|
||||
<!-- Content for Tab 1 -->
|
||||
<!-- Triq Btn image changer -->
|
||||
<div>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompLeft=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
|
||||
v-on:click="imgPlusTCompLeft=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompLeft=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusTCompLeft=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
</div>
|
||||
<!-- Triq Image manager -->
|
||||
<div id="image-container-compare-left-triq" class="row st-col col-12">
|
||||
@ -904,8 +1020,8 @@
|
||||
<div class="col-6">
|
||||
<div class="row items-center">
|
||||
<q-select v-model="selected_folder_compare_right" :options="image_available_folders"
|
||||
label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;"
|
||||
v-on:focus="refetch_folders = true" :disable="is_processing"></q-select>
|
||||
label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;" v-on:focus="refetch_folders = true"
|
||||
:disable="is_processing"></q-select>
|
||||
<q-space></q-space>
|
||||
<st-tabs id="tabHeaderCompareRight" :ids="CompTabIDsRight" :labels="CompTabLabelsRight"
|
||||
v-model="CompSelectedTabRight"></st-tabs>
|
||||
@ -915,9 +1031,10 @@
|
||||
<!-- Content for Tab 0 -->
|
||||
<!-- Btn image changer -->
|
||||
<div>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompRight=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
|
||||
v-on:click="imgPlusCompRight=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompRight=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusCompRight=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
</div>
|
||||
<!-- Image manager -->
|
||||
<div id="image-container-compare-right-normal" class="row st-col col-12">
|
||||
@ -937,9 +1054,10 @@
|
||||
<!-- Content for Tab 1 -->
|
||||
<!-- Triq Btn image changer -->
|
||||
<div>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompRight=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
|
||||
v-on:click="imgPlusTCompRight=true" :disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompRight=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusTCompRight=true"
|
||||
:disable="is_processing"></q-btn>
|
||||
</div>
|
||||
<!-- Triq Image manager -->
|
||||
<div id="image-container-compare-right-triq" class="row st-col col-12">
|
||||
@ -1028,3 +1146,37 @@
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showBugModal">
|
||||
<q-card style="min-width: 350px">
|
||||
<q-card-section class="row items-center q-pb-none">
|
||||
<div class="text-h6">Report a Bug</div>
|
||||
<q-space />
|
||||
<q-btn icon="close" flat round dense v-close-popup />
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-md">
|
||||
<p>To help us fix the issue, please send an email to:</p>
|
||||
<div class="bg-grey-2 q-pa-sm text-weight-bold text-center">
|
||||
julian.sierrag@icloud.com
|
||||
</div>
|
||||
|
||||
<p class="q-mt-md"><strong>Subject:</strong> <code class="bg-yellow-2">JuliaMSI Bug</code></p>
|
||||
|
||||
<q-banner rounded class="bg-grey-3 text-body2">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="info" color="primary" />
|
||||
</template>
|
||||
Please include:
|
||||
<ul>
|
||||
<li>The full error message.</li>
|
||||
<li>Steps to reproduce the crash.</li>
|
||||
</ul>
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn flat label="Close" color="primary" v-close-popup />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@ -1,6 +1,5 @@
|
||||
# julia_imzML_visual.jl
|
||||
|
||||
const REGISTRY_LOCK = ReentrantLock()
|
||||
|
||||
"""
|
||||
increment_image(current_image, image_list)
|
||||
|
||||
6
mask.jl
6
mask.jl
@ -9,7 +9,7 @@ using Statistics, NaturalSort, LinearAlgebra, StipplePlotly
|
||||
using Base.Filesystem: mv
|
||||
|
||||
using MSI_src
|
||||
using .MSI_src: MSIData, process_image_pipeline
|
||||
using .MSI_src: MSIData, process_image_pipeline, REGISTRY_LOCK
|
||||
|
||||
# Plot Handling
|
||||
include("./julia_imzML_visual.jl")
|
||||
@ -799,9 +799,11 @@ end
|
||||
end
|
||||
|
||||
# Save the updated registry
|
||||
lock(REGISTRY_LOCK) do
|
||||
open(reg_path, "w") do f
|
||||
JSON.print(f, registry, 4)
|
||||
end
|
||||
end
|
||||
|
||||
@info "Registry updated with mask: $(final_mask_name)"
|
||||
|
||||
@ -874,10 +876,12 @@ end
|
||||
|
||||
if !isempty(new_folders) || !isempty(removed_folders)
|
||||
println("Registry changed, saving...")
|
||||
lock(REGISTRY_LOCK) do
|
||||
open(reg_path, "w") do f
|
||||
JSON.print(f, registry, 4)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
all_folders = sort(collect(keys(registry)), lt=natural)
|
||||
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders)
|
||||
|
||||
@ -82,23 +82,27 @@ A dramatically simplified buffer pool that avoids complex locking.
|
||||
mutable struct SimpleBufferPool
|
||||
buffers::Dict{Int, Vector{Vector{UInt8}}}
|
||||
max_pool_size::Int
|
||||
lock::ReentrantLock # Add lock for thread safety
|
||||
end
|
||||
|
||||
SimpleBufferPool() = SimpleBufferPool(Dict{Int, Vector{Vector{UInt8}}}(), 50)
|
||||
SimpleBufferPool() = SimpleBufferPool(Dict{Int, Vector{Vector{UInt8}}}(), 50, ReentrantLock())
|
||||
|
||||
function get_buffer!(pool::SimpleBufferPool, size::Int)::Vector{UInt8}
|
||||
lock(pool.lock) do
|
||||
# Check for existing buffers of exact size first
|
||||
if haskey(pool.buffers, size) && !isempty(pool.buffers[size])
|
||||
return pop!(pool.buffers[size])
|
||||
end
|
||||
end
|
||||
|
||||
# No suitable buffer found, allocate new one
|
||||
# No suitable buffer found, allocate new one (outside lock to reduce contention)
|
||||
return Vector{UInt8}(undef, size)
|
||||
end
|
||||
|
||||
function release_buffer!(pool::SimpleBufferPool, buffer::Vector{UInt8})
|
||||
size = length(buffer)
|
||||
|
||||
lock(pool.lock) do
|
||||
if !haskey(pool.buffers, size)
|
||||
pool.buffers[size] = Vector{Vector{UInt8}}()
|
||||
end
|
||||
@ -107,6 +111,7 @@ function release_buffer!(pool::SimpleBufferPool, buffer::Vector{UInt8})
|
||||
if length(pool.buffers[size]) < pool.max_pool_size
|
||||
push!(pool.buffers[size], buffer)
|
||||
end
|
||||
end
|
||||
# If pool is full, let buffer get GC'd
|
||||
end
|
||||
|
||||
|
||||
@ -35,7 +35,12 @@ Atomically seeks to a position and reads data into an array. This is the thread-
|
||||
way to read from a specific offset in the file.
|
||||
"""
|
||||
function read_at!(tsfh::ThreadSafeFileHandle, a::AbstractArray, pos::Integer)
|
||||
if pos < 0
|
||||
throw(ArgumentError("Invalid seek position: $pos"))
|
||||
end
|
||||
lock(tsfh.lock) do
|
||||
# Also check against file size if possible, though filesize() might be expensive to call repeatedly
|
||||
# Let's rely on seek throwing if it goes way out of bounds, but catch the negative case which is definitely an error.
|
||||
seek(tsfh.handle, pos)
|
||||
read!(tsfh.handle, a)
|
||||
end
|
||||
@ -510,6 +515,19 @@ function read_spectrum_from_disk(source::ImzMLSource, meta::SpectrumMetadata)
|
||||
mz = Array{source.mz_format}(undef, meta.mz_asset.encoded_length)
|
||||
intensity = Array{source.intensity_format}(undef, meta.int_asset.encoded_length)
|
||||
|
||||
# Validate offsets before reading
|
||||
file_size = filesize(source.ibd_handle)
|
||||
|
||||
mz_end = meta.mz_asset.offset + sizeof(source.mz_format) * meta.mz_asset.encoded_length
|
||||
if meta.mz_asset.offset < 0 || mz_end > file_size
|
||||
throw(FileFormatError("Invalid m/z data offset/length for spectrum $(meta.id): offset=$(meta.mz_asset.offset), end=$mz_end, file_size=$file_size"))
|
||||
end
|
||||
|
||||
int_end = meta.int_asset.offset + sizeof(source.intensity_format) * meta.int_asset.encoded_length
|
||||
if meta.int_asset.offset < 0 || int_end > file_size
|
||||
throw(FileFormatError("Invalid intensity data offset/length for spectrum $(meta.id): offset=$(meta.int_asset.offset), end=$int_end, file_size=$file_size"))
|
||||
end
|
||||
|
||||
# Use the new atomic read_at! method for thread-safety
|
||||
read_at!(source.ibd_handle, mz, meta.mz_asset.offset)
|
||||
read_at!(source.ibd_handle, intensity, meta.int_asset.offset)
|
||||
|
||||
@ -19,7 +19,11 @@ export OpenMSIData,
|
||||
get_global_mz_range,
|
||||
MSIData,
|
||||
_iterate_spectra_fast,
|
||||
validate_spectrum
|
||||
validate_spectrum,
|
||||
REGISTRY_LOCK
|
||||
|
||||
# Define shared registry lock
|
||||
const REGISTRY_LOCK = ReentrantLock()
|
||||
|
||||
# Export the public preprocessing & precalculations API
|
||||
export run_preprocessing_analysis,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using StatsBase # For mean, std, median, quantile, mad
|
||||
using Base.Threads # For atomic counters and locking
|
||||
|
||||
# =============================================================================
|
||||
# 7) Spatial & Advanced Processing (Stubs & New Functions)
|
||||
@ -80,11 +81,12 @@ function analyze_mass_accuracy(
|
||||
println("Analyzing mass accuracy for $(length(spectrum_indices)) spectra...")
|
||||
|
||||
all_ppm_errors = Float64[]
|
||||
total_matched_peaks = 0
|
||||
total_spectra_processed = 0
|
||||
total_matched_peaks = Atomic{Int}(0)
|
||||
total_spectra_processed = Atomic{Int}(0)
|
||||
results_lock = ReentrantLock()
|
||||
|
||||
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
|
||||
total_spectra_processed += 1
|
||||
atomic_add!(total_spectra_processed, 1)
|
||||
if !validate_spectrum(mz, intensity)
|
||||
@warn "Spectrum $idx is invalid, skipping mass accuracy analysis for it."
|
||||
return
|
||||
@ -106,8 +108,10 @@ function analyze_mass_accuracy(
|
||||
end
|
||||
|
||||
if best_matched_peak_mz !== nothing
|
||||
lock(results_lock) do
|
||||
push!(all_ppm_errors, min_ppm_error)
|
||||
total_matched_peaks += 1
|
||||
end
|
||||
atomic_add!(total_matched_peaks, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -140,8 +144,8 @@ function analyze_mass_accuracy(
|
||||
std_ppm_error = std_err,
|
||||
min_ppm_error = min_err,
|
||||
max_ppm_error = max_err,
|
||||
total_matched_peaks = total_matched_peaks,
|
||||
total_spectra_analyzed = total_spectra_processed,
|
||||
total_matched_peaks = total_matched_peaks[],
|
||||
total_spectra_analyzed = total_spectra_processed[],
|
||||
ppm_error_distribution = all_ppm_errors
|
||||
)
|
||||
end
|
||||
@ -318,23 +322,31 @@ function analyze_instrument_characteristics(msi_data::MSIData; sample_indices::A
|
||||
return results
|
||||
end
|
||||
|
||||
results_lock = ReentrantLock()
|
||||
|
||||
_iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity
|
||||
# Record spectrum mode
|
||||
lock(results_lock) do
|
||||
push!(spectrum_modes, msi_data.spectra_metadata[idx].mode)
|
||||
end
|
||||
|
||||
# Calculate m/z step statistics (for profile data)
|
||||
if length(mz) > 1 && msi_data.spectra_metadata[idx].mode == PROFILE
|
||||
steps = diff(mz)
|
||||
if !isempty(steps)
|
||||
lock(results_lock) do
|
||||
push!(mz_step_sizes, mean(steps))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Record intensity range
|
||||
if !isempty(intensity)
|
||||
lock(results_lock) do
|
||||
push!(intensity_ranges, (minimum(intensity), maximum(intensity)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Determine acquisition mode
|
||||
if length(spectrum_modes) == 1
|
||||
@ -442,11 +454,15 @@ function analyze_signal_quality(msi_data::MSIData; sample_indices::AbstractVecto
|
||||
return results
|
||||
end
|
||||
|
||||
results_lock = ReentrantLock()
|
||||
|
||||
_iterate_spectra_fast(msi_data, sample_indices) do idx, mz, intensity
|
||||
if !isempty(intensity)
|
||||
# Noise estimation using MAD
|
||||
noise = mad(intensity, normalize=true)
|
||||
lock(results_lock) do
|
||||
push!(noise_levels, noise)
|
||||
end
|
||||
|
||||
# FIX: More robust SNR calculation
|
||||
valid_intensity = intensity[intensity .> 0] # Remove zeros
|
||||
@ -458,14 +474,18 @@ function analyze_signal_quality(msi_data::MSIData; sample_indices::AbstractVecto
|
||||
if noise_robust > 0 && isfinite(signal_estimate)
|
||||
snr_val = signal_estimate / noise_robust
|
||||
# Cap unrealistic SNR values
|
||||
lock(results_lock) do
|
||||
push!(snr_distribution, min(snr_val, 1e6))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Total ion count
|
||||
lock(results_lock) do
|
||||
push!(tic_values, sum(intensity))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if !isempty(noise_levels)
|
||||
results[:noise_mean] = mean(noise_levels)
|
||||
@ -651,20 +671,23 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
|
||||
peak_counts = Int[]
|
||||
fwhm_values = Float64[]
|
||||
|
||||
spectra_analyzed = 0
|
||||
peaks_analyzed = 0
|
||||
spectra_analyzed = Atomic{Int}(0)
|
||||
peaks_analyzed = Atomic{Int}(0)
|
||||
results_lock = ReentrantLock()
|
||||
|
||||
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
|
||||
if length(mz) < 10 # Skip spectra with too few points
|
||||
return
|
||||
end
|
||||
|
||||
spectra_analyzed += 1
|
||||
atomic_add!(spectra_analyzed, 1)
|
||||
meta = msi_data.spectra_metadata[idx]
|
||||
|
||||
# Detect peaks with lower SNR threshold to find more peaks
|
||||
peaks = detect_peaks_profile_core(mz, intensity; snr_threshold=2.0)
|
||||
lock(results_lock) do
|
||||
push!(peak_counts, length(peaks))
|
||||
end
|
||||
|
||||
if !isempty(peaks)
|
||||
# Analyze the strongest 3 peaks per spectrum
|
||||
@ -679,15 +702,17 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
|
||||
if !isnan(fwhm_delta_m) && fwhm_delta_m > 0.001 && fwhm_delta_m < 0.5 # Reasonable range in Da
|
||||
fwhm_ppm = 1e6 * fwhm_delta_m / peak.mz
|
||||
if 5.0 < fwhm_ppm < 500.0 # Reasonable ppm range
|
||||
lock(results_lock) do
|
||||
push!(peak_widths_ppm, fwhm_ppm)
|
||||
push!(fwhm_values, fwhm_delta_m)
|
||||
|
||||
r2 = _fit_gaussian_and_r2(mz, intensity, peak_idx, 5)
|
||||
push!(r_squared_values, r2)
|
||||
peaks_analyzed += 1
|
||||
end
|
||||
atomic_add!(peaks_analyzed, 1)
|
||||
|
||||
if peaks_analyzed <= 3
|
||||
println("DEBUG: Peak at m/z $(peak.mz), FWHM = $(fwhm_ppm) ppm, R² = $r2")
|
||||
if peaks_analyzed[] <= 3
|
||||
println("DEBUG: Peak at m/z $(peak.mz), FWHM = $(fwhm_ppm) ppm, R² = $(r_squared_values[end])")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -698,7 +723,7 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
|
||||
end
|
||||
end
|
||||
|
||||
println("DEBUG: Analyzed $peaks_analyzed peaks from $spectra_analyzed spectra")
|
||||
println("DEBUG: Analyzed $(peaks_analyzed[]) peaks from $(spectra_analyzed[]) spectra")
|
||||
|
||||
if !isempty(peak_widths_ppm)
|
||||
results[:mean_fwhm_ppm] = mean(peak_widths_ppm)
|
||||
@ -727,11 +752,15 @@ function analyze_peak_characteristics(msi_data::MSIData, instrument_analysis::Di
|
||||
|
||||
peak_counts = Int[]
|
||||
|
||||
results_lock = ReentrantLock()
|
||||
|
||||
_iterate_spectra_fast(msi_data, spectrum_indices) do idx, mz, intensity
|
||||
if !isempty(mz)
|
||||
lock(results_lock) do
|
||||
push!(peak_counts, length(mz))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if !isempty(peak_counts)
|
||||
results[:mean_peaks_per_spectrum] = mean(peak_counts)
|
||||
|
||||
@ -33,7 +33,15 @@ function apply_baseline_correction(spectra::Vector{MutableSpectrum}, params::Dic
|
||||
|
||||
Threads.@threads for s in spectra
|
||||
if validate_spectrum(s.mz, s.intensity)
|
||||
original_length = length(s.intensity)
|
||||
baseline = apply_baseline_correction_core(s.intensity; method=method, iterations=iterations, window=window)
|
||||
|
||||
# CRITICAL: Ensure baseline has the same length as intensity
|
||||
if length(baseline) != original_length
|
||||
@warn "Baseline correction: length mismatch for spectrum $(s.id). baseline=$(length(baseline)), intensity=$original_length. Skipping this spectrum."
|
||||
continue
|
||||
end
|
||||
|
||||
s.intensity = max.(0.0, s.intensity .- baseline)
|
||||
end
|
||||
end
|
||||
@ -53,7 +61,16 @@ function apply_intensity_transformation(spectra::Vector{MutableSpectrum}, params
|
||||
|
||||
Threads.@threads for s in spectra
|
||||
if validate_spectrum(s.mz, s.intensity)
|
||||
s.intensity = transform_intensity_core(s.intensity; method=method)
|
||||
original_length = length(s.intensity)
|
||||
transformed = transform_intensity_core(s.intensity; method=method)
|
||||
|
||||
# CRITICAL: Ensure transformation preserves array length
|
||||
if length(transformed) != original_length
|
||||
@warn "Intensity transformation: length mismatch for spectrum $(s.id). transformed=$(length(transformed)), original=$original_length. Skipping this spectrum."
|
||||
continue
|
||||
end
|
||||
|
||||
s.intensity = transformed
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -76,8 +93,16 @@ function apply_smoothing(spectra::Vector{MutableSpectrum}, params::Dict)
|
||||
|
||||
Threads.@threads for s in spectra
|
||||
if validate_spectrum(s.mz, s.intensity)
|
||||
smoothed_intensity = max.(0.0, smooth_spectrum_core(s.intensity; method=method, window=window, order=order))
|
||||
s.intensity = smoothed_intensity
|
||||
original_length = length(s.intensity)
|
||||
smoothed_intensity = smooth_spectrum_core(s.intensity; method=method, window=window, order=order)
|
||||
|
||||
# CRITICAL: Ensure smoothing preserves array length
|
||||
if length(smoothed_intensity) != original_length
|
||||
@warn "Smoothing: length mismatch for spectrum $(s.id). smoothed=$(length(smoothed_intensity)), original=$original_length, method=$method, window=$window. Skipping this spectrum."
|
||||
continue
|
||||
end
|
||||
|
||||
s.intensity = max.(0.0, smoothed_intensity)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -104,6 +129,7 @@ function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict)
|
||||
|
||||
Threads.@threads for s in spectra
|
||||
if validate_spectrum(s.mz, s.intensity)
|
||||
# old_len = length(s.peaks)
|
||||
if method == :profile
|
||||
s.peaks = detect_peaks_profile_core(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window, min_peak_prominence=min_peak_prominence, merge_peaks_tolerance=merge_peaks_tolerance)
|
||||
elseif method == :wavelet
|
||||
@ -113,6 +139,7 @@ function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict)
|
||||
else
|
||||
s.peaks = detect_peaks_profile_core(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window)
|
||||
end
|
||||
# @info "Spectrum $(s.id): detected $(length(s.peaks)) peaks"
|
||||
else
|
||||
s.peaks = []
|
||||
end
|
||||
@ -182,12 +209,21 @@ function apply_calibration(spectra::Vector{MutableSpectrum}, params::Dict, refer
|
||||
Threads.@threads for i in 1:length(spectra)
|
||||
s = spectra[i]
|
||||
if validate_spectrum(s.mz, s.intensity)
|
||||
original_length = length(s.mz)
|
||||
matched_peaks = find_calibration_peaks_core(s.mz, s.intensity, reference_masses; ppm_tolerance=ppm_tolerance)
|
||||
if length(matched_peaks) >= 2
|
||||
measured = sort(collect(values(matched_peaks)))
|
||||
theoretical = sort(collect(keys(matched_peaks)))
|
||||
itp = linear_interpolation(measured, theoretical, extrapolation_bc=Line())
|
||||
s.mz = itp(s.mz) # Modify mz-axis in-place
|
||||
new_mz = itp(s.mz)
|
||||
|
||||
# CRITICAL: Ensure m/z axis preserves array length
|
||||
if length(new_mz) != original_length
|
||||
@warn "Calibration: length mismatch for spectrum $(s.id). new_mz=$(length(new_mz)), original=$original_length. Skipping this spectrum."
|
||||
continue
|
||||
end
|
||||
|
||||
s.mz = new_mz # Modify mz-axis in-place
|
||||
else
|
||||
@warn "Spectrum $(s.id): insufficient reference peaks ($(length(matched_peaks)) found), skipping calibration."
|
||||
end
|
||||
@ -233,7 +269,16 @@ function apply_peak_alignment(spectra::Vector{MutableSpectrum}, params::Dict)
|
||||
current_peaks_mz = [p.mz for p in s.peaks]
|
||||
alignment_func = align_peaks_lowess_core(ref_peaks_mz, current_peaks_mz; method=method, tolerance=tolerance, tolerance_unit=tolerance_unit)
|
||||
|
||||
s.mz = alignment_func.(s.mz) # Update m/z axis
|
||||
original_length = length(s.mz)
|
||||
new_mz = alignment_func.(s.mz)
|
||||
|
||||
# CRITICAL: Ensure alignment preserves array length
|
||||
if length(new_mz) != original_length
|
||||
@warn "Peak alignment: m/z length mismatch for spectrum $(s.id). new_mz=$(length(new_mz)), original=$original_length. Skipping this spectrum."
|
||||
continue
|
||||
end
|
||||
|
||||
s.mz = new_mz # Update m/z axis
|
||||
|
||||
# Update peak m/z values
|
||||
for i in 1:length(s.peaks)
|
||||
@ -258,7 +303,16 @@ function apply_normalization(spectra::Vector{MutableSpectrum}, params::Dict)
|
||||
|
||||
Threads.@threads for s in spectra
|
||||
if validate_spectrum(s.mz, s.intensity)
|
||||
s.intensity = apply_normalization_core(s.intensity; method=method)
|
||||
original_length = length(s.intensity)
|
||||
normalized = apply_normalization_core(s.intensity; method=method)
|
||||
|
||||
# CRITICAL: Ensure normalization preserves array length
|
||||
if length(normalized) != original_length
|
||||
@warn "Normalization: length mismatch for spectrum $(s.id). normalized=$(length(normalized)), original=$original_length. Skipping this spectrum."
|
||||
continue
|
||||
end
|
||||
|
||||
s.intensity = normalized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -6,17 +6,14 @@ ENV["GENIE_ENV"] = "dev"
|
||||
|
||||
manifest_path = joinpath(@__DIR__, "Manifest.toml")
|
||||
|
||||
# Only instantiate in development mode
|
||||
if get(ENV, "GENIE_ENV", "dev") != "prod" || !isfile(manifest_path)
|
||||
@info "Development environment detected. Instantiating packages..."
|
||||
|
||||
if !isfile(manifest_path)
|
||||
@info "Manifest.toml not found. Generating it based on Project.toml..."
|
||||
end
|
||||
|
||||
# Selective instantiation for faster startup
|
||||
if get(ENV, "GENIE_ENV", "dev") != "prod" && !isfile(manifest_path)
|
||||
@info "Development environment detected and Manifest.toml missing. Instantiating packages..."
|
||||
Pkg.resolve()
|
||||
Pkg.instantiate()
|
||||
Pkg.gc()
|
||||
elseif get(ENV, "GENIE_ENV", "dev") != "prod"
|
||||
@info "Manifest.toml found. Skipping Pkg.instantiate() for faster boot. Delete Manifest.toml if you need to re-instantiate."
|
||||
end
|
||||
|
||||
using Genie
|
||||
|
||||
32
test/test_buffer_pool_concurrency.jl
Normal file
32
test/test_buffer_pool_concurrency.jl
Normal 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
|
||||
146
test/test_pipeline_concurrency.jl
Normal file
146
test/test_pipeline_concurrency.jl
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user