534 lines
19 KiB
Julia
534 lines
19 KiB
Julia
# run_preprocessing.jl
|
|
|
|
using Printf
|
|
import Pkg
|
|
using CairoMakie
|
|
using DataFrames # For creating dataframes
|
|
using CSV
|
|
|
|
# --- Load the MSI_src Module ---
|
|
Pkg.activate(joinpath(@__DIR__, ".."))
|
|
using MSI_src
|
|
|
|
|
|
# ===================================================================
|
|
# CONFIG: PLEASE FILL IN YOUR FILE PATHS HERE
|
|
# ===================================================================
|
|
|
|
const TEST_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/Thricoderma_etc/Imaging_interaccion_trichoderma_vs_streptomyces.imzML"
|
|
# const TEST_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/set de datos MS/Atropina_tuneo_fraq_20ev.mzML"
|
|
# const TEST_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/salida/Stomach_DHB_uncompressed.imzML"
|
|
|
|
# const MASK_ROUTE = "/home/pixel/Documents/Cinvestav_2025/JuliaMSI/public/css/masks/Stomach_DHB_uncompressed.png"
|
|
const MASK_ROUTE = ""
|
|
|
|
const OUTPUT_DIR = "./test/results/preprocessing_results"
|
|
|
|
# Reference peaks for calibration and alignment
|
|
const reference_peaks = Dict(
|
|
137.0244 => "DHB_fragment",
|
|
155.0349 => "DHB_M+H",
|
|
177.0168 => "DHB_M+Na",
|
|
496.3398 => "PC_16:0_16:0",
|
|
520.3398 => "PC_16:0_18:1",
|
|
760.5851 => "PC_16:0_18:1_Na",
|
|
391.2843 => "PDMS",
|
|
413.2662 => "PDMS_Na",
|
|
842.5092 => "Protein_standard",
|
|
1045.532 => "Protein_standard",
|
|
290.1747 => "Atropine [M+H]+",
|
|
304.1903 => "Scopolamine [M+H]+",
|
|
124.0393 => "Tropine [M+H]+",
|
|
)
|
|
|
|
# ===================================================================
|
|
# HELPER FUNCTIONS
|
|
# ===================================================================
|
|
|
|
function ensure_output_dir()
|
|
if !isdir(OUTPUT_DIR)
|
|
mkdir(OUTPUT_DIR)
|
|
println("Created output directory: $OUTPUT_DIR")
|
|
end
|
|
end
|
|
|
|
function plot_spectrum_step(mz, intensity, step_name; peaks=nothing, spectrum_index=1)
|
|
fig = Figure(size=(1200, 600))
|
|
ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Intensity", title="Step: $step_name (Spectrum $spectrum_index)")
|
|
|
|
lines!(ax, mz, intensity, color=:blue, linewidth=1)
|
|
|
|
if peaks !== nothing && !isempty(peaks)
|
|
peak_mz = [p.mz for p in peaks]
|
|
peak_intensity = [p.intensity for p in peaks]
|
|
scatter!(ax, peak_mz, peak_intensity, color=:red, markersize=5)
|
|
end
|
|
|
|
filename = joinpath(OUTPUT_DIR, "spectrum_$(step_name)_index_$spectrum_index.png")
|
|
save(filename, fig)
|
|
println(" - Saved plot: $(basename(filename))")
|
|
end
|
|
|
|
function print_step_header(step_name::String)
|
|
println("\n" * ">"^50)
|
|
println("APPLYING STEP: $step_name")
|
|
println(">"^50)
|
|
end
|
|
|
|
# ===================================================================
|
|
# PREPROCESSING PIPELINE FUNCTIONS (IN-PLACE)
|
|
# ===================================================================
|
|
|
|
function apply_baseline_correction(spectra::Vector{MutableSpectrum}, params::Dict, msi_data::MSIData)
|
|
print_step_header("Baseline Correction")
|
|
|
|
method = get(params, :method, :snip)
|
|
iterations = get(params, :iterations, 100)
|
|
window = get(params, :window, 20)
|
|
println(" Method: $method, Iterations: $iterations, Window: $window")
|
|
|
|
# Safely plot the first spectrum
|
|
if !isempty(spectra)
|
|
s = spectra[1]
|
|
if validate_spectrum(s.mz, s.intensity)
|
|
baseline = MSI_src.apply_baseline_correction(s.intensity; method=method, iterations=iterations, window=window)
|
|
corrected_intensity = max.(0.0, s.intensity .- baseline)
|
|
plot_spectrum_step(s.mz, corrected_intensity, "baseline_correction")
|
|
end
|
|
end
|
|
|
|
Threads.@threads for s in spectra
|
|
if validate_spectrum(s.mz, s.intensity)
|
|
baseline = MSI_src.apply_baseline_correction(s.intensity; method=method, iterations=iterations, window=window)
|
|
s.intensity = max.(0.0, s.intensity .- baseline)
|
|
end
|
|
end
|
|
end
|
|
|
|
function apply_smoothing(spectra::Vector{MutableSpectrum}, params::Dict)
|
|
print_step_header("Smoothing")
|
|
|
|
method = get(params, :method, :savitzky_golay)
|
|
window = get(params, :window, 9)
|
|
order = get(params, :order, 2)
|
|
println(" Method: $method, Window: $window, Order: $order")
|
|
|
|
# Safely plot the first spectrum
|
|
if !isempty(spectra)
|
|
s = spectra[1]
|
|
if validate_spectrum(s.mz, s.intensity)
|
|
smoothed_intensity = max.(0.0, smooth_spectrum(s.intensity; method=method, window=window, order=order))
|
|
plot_spectrum_step(s.mz, smoothed_intensity, "smoothing", spectrum_index=s.id)
|
|
end
|
|
end
|
|
|
|
Threads.@threads for s in spectra
|
|
if validate_spectrum(s.mz, s.intensity)
|
|
smoothed_intensity = max.(0.0, smooth_spectrum(s.intensity; method=method, window=window, order=order))
|
|
s.intensity = smoothed_intensity
|
|
end
|
|
end
|
|
end
|
|
|
|
function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict)
|
|
print_step_header("Peak Picking")
|
|
|
|
method = get(params, :method, :profile)
|
|
snr_threshold = get(params, :snr_threshold, 3.0)
|
|
half_window = get(params, :half_window, 10)
|
|
min_peak_prominence = get(params, :min_peak_prominence, 0.1)
|
|
merge_peaks_tolerance = get(params, :merge_peaks_tolerance, 0.002)
|
|
println(" Method: $method, SNR: $snr_threshold, Half Window: $half_window")
|
|
|
|
found_invalid_spectrum = Threads.Atomic{Bool}(false)
|
|
|
|
Threads.@threads for s in spectra
|
|
is_valid = validate_spectrum(s.mz, s.intensity)
|
|
|
|
if !is_valid && !found_invalid_spectrum[]
|
|
if Threads.atomic_xchg!(found_invalid_spectrum, true) == false
|
|
# Debug printing for the first invalid spectrum found
|
|
end
|
|
end
|
|
|
|
if is_valid
|
|
if method == :profile
|
|
s.peaks = detect_peaks_profile(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
|
|
s.peaks = detect_peaks_wavelet(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window)
|
|
elseif method == :centroid
|
|
s.peaks = detect_peaks_centroid(s.mz, s.intensity; snr_threshold=snr_threshold)
|
|
else
|
|
s.peaks = detect_peaks_profile(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window)
|
|
end
|
|
else
|
|
s.peaks = []
|
|
end
|
|
end
|
|
|
|
# Safely plot the first spectrum
|
|
if !isempty(spectra)
|
|
s1 = findfirst(s -> s.id == 1, spectra)
|
|
if s1 !== nothing
|
|
plot_spectrum_step(spectra[s1].mz, spectra[s1].intensity, "peak_picking", peaks=spectra[s1].peaks)
|
|
println(" - Detected $(length(spectra[s1].peaks)) peaks in spectrum 1")
|
|
end
|
|
end
|
|
end
|
|
|
|
function apply_calibration(spectra::Vector{MutableSpectrum}, params::Dict, reference_peaks::Dict)
|
|
print_step_header("Calibration")
|
|
|
|
method = get(params, :method, :none)
|
|
ppm_tolerance = get(params, :ppm_tolerance, 20.0)
|
|
println(" Method: $method, PPM Tolerance: $ppm_tolerance")
|
|
|
|
if method == :none || isempty(reference_peaks)
|
|
println(" - Skipping calibration (no method or reference peaks)")
|
|
return
|
|
end
|
|
|
|
reference_masses = collect(keys(reference_peaks))
|
|
calibration_info = Vector{String}(undef, length(spectra))
|
|
|
|
Threads.@threads for i in 1:length(spectra)
|
|
s = spectra[i]
|
|
info_message = ""
|
|
if validate_spectrum(s.mz, s.intensity)
|
|
matched_peaks = find_calibration_peaks(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
|
|
info_message = " - Spectrum $(s.id): calibrated using $(length(matched_peaks)) reference peaks"
|
|
else
|
|
info_message = " - Spectrum $(s.id): insufficient reference peaks ($(length(matched_peaks)) found), skipping"
|
|
end
|
|
end
|
|
calibration_info[i] = info_message
|
|
end
|
|
|
|
# Print summary of results
|
|
printed_count = 0
|
|
for info in calibration_info
|
|
if !isempty(info) && printed_count < 5
|
|
println(info)
|
|
printed_count += 1
|
|
end
|
|
end
|
|
end
|
|
|
|
function apply_peak_alignment(spectra::Vector{MutableSpectrum}, params::Dict)
|
|
print_step_header("Peak Alignment")
|
|
|
|
method = get(params, :method, :none)
|
|
tolerance = get(params, :tolerance, 0.002)
|
|
tolerance_unit = get(params, :tolerance_unit, :mz)
|
|
println(" Method: $method, Tolerance: $tolerance $tolerance_unit")
|
|
|
|
if method == :none
|
|
println(" - Skipping peak alignment")
|
|
return
|
|
end
|
|
|
|
ref_find_idx = findfirst(s -> !isempty(s.peaks), spectra)
|
|
if ref_find_idx === nothing
|
|
println(" - Insufficient spectra with peaks for alignment. Skipping.")
|
|
return
|
|
end
|
|
|
|
ref_spectrum = spectra[ref_find_idx]
|
|
ref_peaks_mz = [p.mz for p in ref_spectrum.peaks]
|
|
println(" - Using spectrum $(ref_spectrum.id) as reference with $(length(ref_peaks_mz)) peaks")
|
|
|
|
Threads.@threads for s in spectra
|
|
if s.id == ref_spectrum.id || isempty(s.peaks)
|
|
continue
|
|
end
|
|
|
|
current_peaks_mz = [p.mz for p in s.peaks]
|
|
alignment_func = align_peaks_lowess(ref_peaks_mz, current_peaks_mz; method=method, tolerance=tolerance, tolerance_unit=tolerance_unit)
|
|
|
|
s.mz = alignment_func.(s.mz) # Update m/z axis
|
|
|
|
# Update peak m/z values
|
|
for i in 1:length(s.peaks)
|
|
old_peak = s.peaks[i]
|
|
aligned_peak_mz = alignment_func(old_peak.mz)
|
|
s.peaks[i] = (mz=aligned_peak_mz, intensity=old_peak.intensity, fwhm=old_peak.fwhm, shape_r2=old_peak.shape_r2, snr=old_peak.snr, prominence=old_peak.prominence)
|
|
end
|
|
end
|
|
end
|
|
|
|
function apply_normalization(spectra::Vector{MutableSpectrum}, params::Dict)
|
|
print_step_header("Normalization")
|
|
|
|
method = get(params, :method, :tic)
|
|
println(" Method: $method")
|
|
|
|
# Safely plot the first spectrum
|
|
if !isempty(spectra)
|
|
s1 = findfirst(s -> s.id == 1, spectra)
|
|
if s1 !== nothing
|
|
s = spectra[s1]
|
|
if validate_spectrum(s.mz, s.intensity)
|
|
normalized_intensity = MSI_src.apply_normalization(s.intensity; method=method)
|
|
plot_spectrum_step(s.mz, normalized_intensity, "normalization")
|
|
end
|
|
end
|
|
end
|
|
|
|
Threads.@threads for s in spectra
|
|
if validate_spectrum(s.mz, s.intensity)
|
|
s.intensity = MSI_src.apply_normalization(s.intensity; method=method)
|
|
end
|
|
end
|
|
end
|
|
|
|
function apply_peak_binning(spectra::Vector{MutableSpectrum}, params::Dict)
|
|
print_step_header("Peak Binning")
|
|
|
|
method = get(params, :method, :adaptive)
|
|
tolerance = get(params, :tolerance, 20.0)
|
|
tolerance_unit = get(params, :tolerance_unit, :ppm)
|
|
min_peak_per_bin = get(params, :min_peak_per_bin, 3)
|
|
max_bin_width_ppm = get(params, :max_bin_width_ppm, 150.0)
|
|
intensity_weighted_centers = get(params, :intensity_weighted_centers, true)
|
|
println(" Method: $method, Tolerance: $tolerance $tolerance_unit")
|
|
|
|
if isempty(spectra) || all(s -> isempty(s.peaks), spectra)
|
|
@warn "No peaks found for binning. Returning empty feature matrix."
|
|
return nothing, nothing
|
|
end
|
|
|
|
println(" - Binning peaks from $(length(spectra)) spectra")
|
|
|
|
binning_params = PeakBinningParams(
|
|
method=method,
|
|
tolerance=tolerance,
|
|
tolerance_unit=tolerance_unit,
|
|
min_peak_per_bin=min_peak_per_bin,
|
|
max_bin_width_ppm=max_bin_width_ppm,
|
|
intensity_weighted_centers=intensity_weighted_centers
|
|
)
|
|
|
|
feature_matrix, bin_definitions = bin_peaks(spectra, binning_params)
|
|
|
|
if feature_matrix !== nothing && bin_definitions !== nothing
|
|
println(" - Generated feature matrix: $(size(feature_matrix.matrix))")
|
|
println(" - Number of bins: $(length(bin_definitions))")
|
|
end
|
|
|
|
return feature_matrix, bin_definitions
|
|
end
|
|
|
|
function save_feature_matrix(feature_matrix, bin_definitions)
|
|
print_step_header("Saving Results")
|
|
|
|
# Save feature matrix as CSV
|
|
csv_path = joinpath(OUTPUT_DIR, "feature_matrix.csv")
|
|
|
|
# Create column headers
|
|
bin_headers = ["bin_$(i)_$(round(def[1], digits=4))-$(round(def[2], digits=4))"
|
|
for (i, def) in enumerate(bin_definitions)]
|
|
|
|
# Open file for writing
|
|
open(csv_path, "w") do io
|
|
# Write header row
|
|
write(io, "spectrum_index," * join(bin_headers, ",") * "\n")
|
|
|
|
# Write data rows
|
|
for r_idx in 1:size(feature_matrix.matrix, 1)
|
|
write(io, "$(feature_matrix.sample_ids[r_idx]),")
|
|
for c_idx in 1:size(feature_matrix.matrix, 2)
|
|
write(io, "$(feature_matrix.matrix[r_idx, c_idx])")
|
|
if c_idx < size(feature_matrix.matrix, 2)
|
|
write(io, ",")
|
|
end
|
|
end
|
|
write(io, "\n")
|
|
end
|
|
end
|
|
println(" - Saved feature matrix: $csv_path")
|
|
|
|
# Save bin definitions (still using DataFrame as it's small)
|
|
bins_path = joinpath(OUTPUT_DIR, "bin_definitions.csv")
|
|
bins_df = DataFrame(
|
|
bin_index = 1:length(bin_definitions),
|
|
mz_start = [def[1] for def in bin_definitions],
|
|
mz_end = [def[2] for def in bin_definitions],
|
|
mz_center = [(def[1] + def[2])/2 for def in bin_definitions]
|
|
)
|
|
CSV.write(bins_path, bins_df)
|
|
println(" - Saved bin definitions: $bins_path")
|
|
|
|
return csv_path, bins_path
|
|
end
|
|
|
|
# ===================================================================
|
|
# USER OVERRIDES: Manually specify parameters here
|
|
# ===================================================================
|
|
# This dictionary allows you to override any auto-detected parameters.
|
|
# The structure should match the output of `main_precalculation`.
|
|
# Example: Force a less aggressive peak prominence threshold.
|
|
const USER_OVERRIDES = Dict(
|
|
:PeakPicking => Dict(
|
|
#:snr_threshold => 1.5,
|
|
:half_window => 5,
|
|
:snr_threshold => 15.0,
|
|
#:merge_peaks_tolerance => 2.5,
|
|
#:half_window => 2
|
|
),
|
|
# :Smoothing => Dict(
|
|
# :window => 11
|
|
# )
|
|
#:PeakAlignment => Dict(
|
|
# :tolerance => 0.01
|
|
#)
|
|
:PeakBinningParams => Dict(
|
|
:tolerance => 30.0
|
|
)
|
|
)
|
|
|
|
# ===================================================================
|
|
# MAIN PREPROCESSING PIPELINE
|
|
# ===================================================================
|
|
|
|
function run_preprocessing_pipeline()
|
|
println("="^80)
|
|
println("MSI PREPROCESSING PIPELINE")
|
|
println("="^80)
|
|
|
|
# Ensure output directory exists
|
|
ensure_output_dir()
|
|
|
|
# Load data
|
|
println("\nLoading data: $(basename(TEST_FILE))")
|
|
msi_data = OpenMSIData(TEST_FILE)
|
|
|
|
# Precompute analytics
|
|
println("\nPrecomputing analytics...")
|
|
precompute_analytics(msi_data)
|
|
|
|
# Run precalculation to get automatic parameters
|
|
println("\nRunning precalculation for automatic parameter determination...")
|
|
auto_params = main_precalculation(msi_data, reference_peaks=reference_peaks, mask_path=MASK_ROUTE)
|
|
|
|
# --- Apply User Overrides ---
|
|
if !isempty(USER_OVERRIDES)
|
|
println("\nApplying user overrides...")
|
|
for (step, params) in USER_OVERRIDES
|
|
if haskey(auto_params, step)
|
|
for (param, value) in params
|
|
@info "OVERRIDE: For step `:$step`, setting `:$param` to `$value` (was `$(get(auto_params[step], param, "not set"))`)"
|
|
end
|
|
merge!(auto_params[step], params)
|
|
else
|
|
@warn "User override for non-existent step `:$step` ignored."
|
|
end
|
|
end
|
|
end
|
|
|
|
# --- Final Parameter Summary ---
|
|
println("\n" * "-"^60)
|
|
println("FINAL PARAMETERS FOR PIPELINE RUN")
|
|
println("-"^60)
|
|
for (step, params) in sort(collect(pairs(auto_params)), by=p -> p.first)
|
|
println(" - Step: $step")
|
|
if isempty(params)
|
|
println(" (No parameters)")
|
|
continue
|
|
end
|
|
for (param, value) in sort(collect(pairs(params)), by=p -> p.first)
|
|
println(" - $(rpad(param, 25)): $value")
|
|
end
|
|
end
|
|
|
|
# Define pipeline steps
|
|
pipeline_steps = [
|
|
"baseline_correction",
|
|
"smoothing",
|
|
"peak_picking",
|
|
"calibration",
|
|
"peak_alignment",
|
|
"normalization",
|
|
"peak_binning"
|
|
]
|
|
|
|
println("\nPipeline steps: $(join(pipeline_steps, " -> "))")
|
|
|
|
# Initialize `current_spectra` as a Vector of mutable structs for in-place modification
|
|
println("\nInitializing spectra data structure...")
|
|
num_spectra = length(msi_data.spectra_metadata)
|
|
current_spectra = Vector{MutableSpectrum}(undef, num_spectra)
|
|
_iterate_spectra_fast(msi_data) do idx, mz, intensity
|
|
current_spectra[idx] = MutableSpectrum(idx, mz, intensity, [])
|
|
|
|
# Plot raw spectrum without masks
|
|
if idx == 1
|
|
plot_spectrum_step(mz, intensity, "raw_unmasked_spectrum")
|
|
end
|
|
end
|
|
|
|
# These variables will be populated by the pipeline steps
|
|
feature_matrix = nothing
|
|
bin_definitions = nothing
|
|
|
|
# Apply pipeline steps, modifying `current_spectra` in-place
|
|
for step in pipeline_steps
|
|
println("\n" * "-"^60)
|
|
println("PROCESSING STEP: $step")
|
|
println("-"^60)
|
|
|
|
if step == "baseline_correction"
|
|
@time apply_baseline_correction(current_spectra, auto_params[:BaselineCorrection], msi_data)
|
|
|
|
elseif step == "smoothing"
|
|
apply_smoothing(current_spectra, auto_params[:Smoothing])
|
|
|
|
elseif step == "peak_picking"
|
|
@time apply_peak_picking(current_spectra, auto_params[:PeakPicking])
|
|
|
|
elseif step == "calibration"
|
|
@time apply_calibration(current_spectra, auto_params[:Calibration], reference_peaks)
|
|
|
|
elseif step == "peak_alignment"
|
|
@time apply_peak_alignment(current_spectra, auto_params[:PeakAlignment])
|
|
|
|
elseif step == "normalization"
|
|
@time apply_normalization(current_spectra, auto_params[:Normalization])
|
|
|
|
elseif step == "peak_binning"
|
|
# This step is different as it generates the final matrix, not modifying spectra in-place
|
|
feature_matrix, bin_definitions = @time apply_peak_binning(current_spectra, auto_params[:PeakBinningParams])
|
|
|
|
if feature_matrix !== nothing
|
|
@time save_feature_matrix(feature_matrix, bin_definitions)
|
|
end
|
|
|
|
else
|
|
@warn "Unknown step: $step, skipping"
|
|
end
|
|
|
|
# Print progress
|
|
println("✓ Completed step: $step")
|
|
end
|
|
|
|
# Clean up
|
|
close(msi_data)
|
|
GC.gc()
|
|
|
|
println("\n" * "="^80)
|
|
println("PREPROCESSING PIPELINE COMPLETED SUCCESSFULLY!")
|
|
println("Results saved to: $OUTPUT_DIR")
|
|
println("="^80)
|
|
end
|
|
|
|
# ===================================================================
|
|
# EXECUTE PIPELINE
|
|
# ===================================================================
|
|
|
|
if abspath(PROGRAM_FILE) == @__FILE__
|
|
@time run_preprocessing_pipeline()
|
|
end |