Implementing foundations for multithreading, standarized mzml converter, profile/centroid detection per pixel, memory-efficient buffer reuse, added preparations to bloom filter alongside buffer pulling, removed memory leaks on linux due to @async macro used on @app module, better error management
This commit is contained in:
parent
3439a8a4dd
commit
a8c2dfcc91
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
julia_version = "1.11.7"
|
julia_version = "1.11.7"
|
||||||
manifest_format = "2.0"
|
manifest_format = "2.0"
|
||||||
project_hash = "9b36a3561cfc9927071202de68baf8c2b0f02a5c"
|
project_hash = "f5ab935406d0b7b7e657445ecfa45944ea71f247"
|
||||||
|
|
||||||
[[deps.ATK_jll]]
|
[[deps.ATK_jll]]
|
||||||
deps = ["Artifacts", "Glib_jll", "JLLWrappers", "Libdl"]
|
deps = ["Artifacts", "Glib_jll", "JLLWrappers", "Libdl"]
|
||||||
|
|||||||
@ -32,8 +32,10 @@ NativeFileDialog = "e1fe445b-aa65-4df4-81c1-2041507f0fd4"
|
|||||||
NaturalSort = "c020b1a1-e9b0-503a-9c33-f039bfc54a85"
|
NaturalSort = "c020b1a1-e9b0-503a-9c33-f039bfc54a85"
|
||||||
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
|
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
|
||||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||||
|
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
|
||||||
SavitzkyGolay = "c4bf5708-b6a6-4fbe-bcd0-6850ed671584"
|
SavitzkyGolay = "c4bf5708-b6a6-4fbe-bcd0-6850ed671584"
|
||||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||||
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
||||||
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
|
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
|
||||||
StipplePlotly = "ec984513-233d-481d-95b0-a3b58b97af2b"
|
StipplePlotly = "ec984513-233d-481d-95b0-a3b58b97af2b"
|
||||||
|
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||||
|
|||||||
@ -22,7 +22,7 @@ https://codeberg.org/LabABI/JuliaMSI
|
|||||||
~/Downloads/JuliaMSI-main/juliamsi
|
~/Downloads/JuliaMSI-main/juliamsi
|
||||||
2. Without entering the Julia environment, launch the project in your terminal with the following command (which works for all operating systems):
|
2. Without entering the Julia environment, launch the project in your terminal with the following command (which works for all operating systems):
|
||||||
```
|
```
|
||||||
julia --project=. start_MSI_GUI.jl
|
julia --threads auto --project=. start_MSI_GUI.jl
|
||||||
```
|
```
|
||||||
3. After the script has finished loading, you can open a [page](http://127.0.0.1:1481/) in your browser with the web app running.
|
3. After the script has finished loading, you can open a [page](http://127.0.0.1:1481/) in your browser with the web app running.
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# julia_imzML_visual.jl
|
# julia_imzML_visual.jl
|
||||||
|
|
||||||
|
const REGISTRY_LOCK = ReentrantLock()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
increment_image(current_image, image_list)
|
increment_image(current_image, image_list)
|
||||||
|
|
||||||
@ -541,7 +543,17 @@ function meanSpectrumPlot(data::MSIData, dataset_name::String=""; mask_path::Uni
|
|||||||
# Update title to indicate empty spectrum
|
# Update title to indicate empty spectrum
|
||||||
layout.title.text = "Empty " * layout.title.text
|
layout.title.text = "Empty " * layout.title.text
|
||||||
else
|
else
|
||||||
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
df = data.spectrum_stats_df
|
||||||
|
profile_count = 0
|
||||||
|
if df !== nothing && hasproperty(df, :Mode)
|
||||||
|
profile_count = count(==(MSI_src.PROFILE), df.Mode)
|
||||||
|
end
|
||||||
|
|
||||||
|
if profile_count > 0
|
||||||
|
trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, mode="lines", marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
||||||
|
else
|
||||||
|
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
plotdata = [trace]
|
plotdata = [trace]
|
||||||
@ -571,6 +583,7 @@ Generates a plot for the spectrum at a specific coordinate (for imaging data) or
|
|||||||
function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, imgHeight::Int, dataset_name::String=""; mask_path::Union{String, Nothing}=nothing)
|
function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, imgHeight::Int, dataset_name::String=""; mask_path::Union{String, Nothing}=nothing)
|
||||||
local mz::AbstractVector, intensity::AbstractVector
|
local mz::AbstractVector, intensity::AbstractVector
|
||||||
local plot_title::String
|
local plot_title::String
|
||||||
|
local spectrum_mode = MSI_src.CENTROID # Default to centroid
|
||||||
|
|
||||||
is_imaging = data.source isa ImzMLSource
|
is_imaging = data.source isa ImzMLSource
|
||||||
|
|
||||||
@ -578,6 +591,17 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
|
|||||||
x = clamp(xCoord, 1, imgWidth)
|
x = clamp(xCoord, 1, imgWidth)
|
||||||
y = clamp(yCoord, 1, imgHeight)
|
y = clamp(yCoord, 1, imgHeight)
|
||||||
|
|
||||||
|
# Get spectrum mode
|
||||||
|
if data.spectrum_stats_df !== nothing && hasproperty(data.spectrum_stats_df, :Mode)
|
||||||
|
w, h = data.image_dims
|
||||||
|
if y > 0 && x > 0 && y <= h && x <= w
|
||||||
|
idx = (y - 1) * w + x
|
||||||
|
if idx <= length(data.spectrum_stats_df.Mode)
|
||||||
|
spectrum_mode = data.spectrum_stats_df.Mode[idx]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if mask_path !== nothing
|
if mask_path !== nothing
|
||||||
mask_matrix = load_and_prepare_mask(mask_path, (imgWidth, imgHeight))
|
mask_matrix = load_and_prepare_mask(mask_path, (imgWidth, imgHeight))
|
||||||
if !mask_matrix[y, x]
|
if !mask_matrix[y, x]
|
||||||
@ -602,6 +626,12 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
|
|||||||
# For non-imaging data, treat xCoord as the spectrum index
|
# For non-imaging data, treat xCoord as the spectrum index
|
||||||
index = clamp(xCoord, 1, length(data.spectra_metadata))
|
index = clamp(xCoord, 1, length(data.spectra_metadata))
|
||||||
|
|
||||||
|
if data.spectrum_stats_df !== nothing && hasproperty(data.spectrum_stats_df, :Mode)
|
||||||
|
if index <= length(data.spectrum_stats_df.Mode)
|
||||||
|
spectrum_mode = data.spectrum_stats_df.Mode[index]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
process_spectrum(data, index) do recieved_mz, recieved_intensity
|
process_spectrum(data, index) do recieved_mz, recieved_intensity
|
||||||
mz = recieved_mz
|
mz = recieved_mz
|
||||||
intensity = recieved_intensity
|
intensity = recieved_intensity
|
||||||
@ -636,7 +666,11 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
|
|||||||
# Downsample for plotting performance
|
# Downsample for plotting performance
|
||||||
mz_down, int_down = MSI_src.downsample_spectrum(mz, intensity)
|
mz_down, int_down = MSI_src.downsample_spectrum(mz, intensity)
|
||||||
|
|
||||||
trace = PlotlyBase.stem(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
trace = if spectrum_mode == MSI_src.PROFILE
|
||||||
|
PlotlyBase.scatter(x=mz_down, y=int_down, mode="lines", marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
||||||
|
else
|
||||||
|
PlotlyBase.stem(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
||||||
|
end
|
||||||
|
|
||||||
plotdata = [trace]
|
plotdata = [trace]
|
||||||
plotlayout = layout
|
plotlayout = layout
|
||||||
@ -700,7 +734,17 @@ function sumSpectrumPlot(data::MSIData, dataset_name::String=""; mask_path::Unio
|
|||||||
# Update title to indicate empty spectrum
|
# Update title to indicate empty spectrum
|
||||||
layout.title.text = "Empty " * layout.title.text
|
layout.title.text = "Empty " * layout.title.text
|
||||||
else
|
else
|
||||||
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
df = data.spectrum_stats_df
|
||||||
|
profile_count = 0
|
||||||
|
if df !== nothing && hasproperty(df, :Mode)
|
||||||
|
profile_count = count(==(MSI_src.PROFILE), df.Mode)
|
||||||
|
end
|
||||||
|
|
||||||
|
if profile_count > 0
|
||||||
|
trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, mode="lines", marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
||||||
|
else
|
||||||
|
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
plotdata = [trace]
|
plotdata = [trace]
|
||||||
@ -733,7 +777,7 @@ function warmup_init()
|
|||||||
try
|
try
|
||||||
save_bitmap(dummy_bmp_path, zeros(UInt8, 10, 10), ViridisPalette)
|
save_bitmap(dummy_bmp_path, zeros(UInt8, 10, 10), ViridisPalette)
|
||||||
loadImgPlot("/dummy.bmp")
|
loadImgPlot("/dummy.bmp")
|
||||||
generate_colorbar_image(zeros(10, 10), 256, dummy_png_path)
|
generate_colorbar_image(zeros(10, 10), 256, dummy_png_path, (0.0, 1.0))
|
||||||
catch e
|
catch e
|
||||||
@warn "Pre-compilation step failed (this is expected if dummy files can't be created/read)"
|
@warn "Pre-compilation step failed (this is expected if dummy files can't be created/read)"
|
||||||
finally
|
finally
|
||||||
@ -757,15 +801,18 @@ Loads the dataset registry from a JSON file.
|
|||||||
- A dictionary containing the registry data. Returns an empty dictionary if the file doesn't exist or fails to parse.
|
- A dictionary containing the registry data. Returns an empty dictionary if the file doesn't exist or fails to parse.
|
||||||
"""
|
"""
|
||||||
function load_registry(registry_path)
|
function load_registry(registry_path)
|
||||||
if isfile(registry_path)
|
return lock(REGISTRY_LOCK) do
|
||||||
try
|
if isfile(registry_path)
|
||||||
return JSON.parsefile(registry_path, dicttype=Dict{String,Any})
|
try
|
||||||
catch e
|
JSON.parsefile(registry_path, dicttype=Dict{String,Any})
|
||||||
@error "Failed to parse registry.json: $e"
|
catch e
|
||||||
return Dict{String,Any}()
|
@error "Failed to parse registry.json: $e"
|
||||||
|
Dict{String,Any}()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Dict{String,Any}()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return Dict{String,Any}()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -839,32 +886,43 @@ Adds or updates an entry in the dataset registry JSON file.
|
|||||||
- `is_imzML`: Boolean indicating if the source is an imzML file.
|
- `is_imzML`: Boolean indicating if the source is an imzML file.
|
||||||
"""
|
"""
|
||||||
function update_registry(registry_path, dataset_name, source_path, metadata=nothing, is_imzML=false)
|
function update_registry(registry_path, dataset_name, source_path, metadata=nothing, is_imzML=false)
|
||||||
registry = load_registry(registry_path)
|
lock(REGISTRY_LOCK) do
|
||||||
|
registry = if isfile(registry_path)
|
||||||
# Get existing entry if it exists, otherwise create new one
|
try
|
||||||
existing_entry = get(registry, dataset_name, Dict{String,Any}())
|
JSON.parsefile(registry_path, dicttype=Dict{String,Any})
|
||||||
|
catch e
|
||||||
# Start with existing data and update only the basic fields
|
@error "Failed to parse registry.json while updating: $e"
|
||||||
entry = copy(existing_entry)
|
Dict{String,Any}() # Start with empty if parsing fails
|
||||||
entry["source_path"] = source_path
|
end
|
||||||
entry["processed_date"] = string(now())
|
else
|
||||||
entry["is_imzML"] = is_imzML
|
Dict{String,Any}()
|
||||||
|
end
|
||||||
# Only update metadata if provided
|
|
||||||
if metadata !== nothing
|
# Get existing entry if it exists, otherwise create new one
|
||||||
entry["metadata"] = metadata
|
existing_entry = get(registry, dataset_name, Dict{String,Any}())
|
||||||
end
|
|
||||||
|
# Start with existing data and update only the basic fields
|
||||||
# Note: has_mask and mask_path are preserved from existing_entry if they exist
|
entry = copy(existing_entry)
|
||||||
|
entry["source_path"] = source_path
|
||||||
registry[dataset_name] = entry
|
entry["processed_date"] = string(now())
|
||||||
|
entry["is_imzML"] = is_imzML
|
||||||
try
|
|
||||||
open(registry_path, "w") do f
|
# Only update metadata if provided
|
||||||
JSON.print(f, registry, 4)
|
if metadata !== nothing
|
||||||
|
entry["metadata"] = metadata
|
||||||
|
end
|
||||||
|
|
||||||
|
# Note: has_mask and mask_path are preserved from existing_entry if they exist
|
||||||
|
|
||||||
|
registry[dataset_name] = entry
|
||||||
|
|
||||||
|
try
|
||||||
|
open(registry_path, "w") do f
|
||||||
|
JSON.print(f, registry, 4)
|
||||||
|
end
|
||||||
|
catch e
|
||||||
|
@error "Failed to write to registry.json: $e"
|
||||||
end
|
end
|
||||||
catch e
|
|
||||||
@error "Failed to write to registry.json: $e"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -945,18 +1003,27 @@ function process_file_safely(file_path, masses, params, progress_message_ref, ov
|
|||||||
|
|
||||||
if all(iszero, slice)
|
if all(iszero, slice)
|
||||||
sliceQuant = zeros(UInt8, size(slice))
|
sliceQuant = zeros(UInt8, size(slice))
|
||||||
|
bounds = (0.0, 1.0) # Default bounds for empty slice
|
||||||
@warn "No intensity data for m/z = $mass in $(dataset_name)"
|
@warn "No intensity data for m/z = $mass in $(dataset_name)"
|
||||||
else
|
else
|
||||||
sliceQuant = params.triqE ? TrIQ(slice, params.colorL, params.triqP, mask_matrix=mask_matrix_for_triq) : quantize_intensity(slice, params.colorL, mask_matrix=mask_matrix_for_triq)
|
# Get both quantized data AND bounds in one call
|
||||||
|
if params.triqE
|
||||||
|
sliceQuant, bounds = TrIQ(slice, params.colorL, params.triqP, mask_matrix=mask_matrix_for_triq)
|
||||||
|
else
|
||||||
|
sliceQuant, bounds = quantize_intensity(slice, params.colorL, mask_matrix=mask_matrix_for_triq)
|
||||||
|
end
|
||||||
|
|
||||||
if params.medianF
|
if params.medianF
|
||||||
sliceQuant = round.(UInt8, median_filter(sliceQuant))
|
sliceQuant = round.(UInt8, median_filter(sliceQuant))
|
||||||
|
# Note: bounds remain the same after median filter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
save_bitmap(joinpath(output_dir, bitmap_filename), sliceQuant, ViridisPalette)
|
save_bitmap(joinpath(output_dir, bitmap_filename), sliceQuant, ViridisPalette)
|
||||||
if !all(iszero, slice)
|
if !all(iszero, slice)
|
||||||
generate_colorbar_image(slice, params.colorL, joinpath(output_dir, colorbar_filename);
|
# Now pass the precomputed bounds to colorbar generation
|
||||||
use_triq=params.triqE, triq_prob=params.triqP, mask_path=mask_path)
|
generate_colorbar_image(slice, params.colorL, joinpath(output_dir, colorbar_filename),
|
||||||
|
bounds; use_triq=params.triqE, triq_prob=params.triqP, mask_path=mask_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -980,28 +1047,39 @@ function process_file_safely(file_path, masses, params, progress_message_ref, ov
|
|||||||
end
|
end
|
||||||
|
|
||||||
function update_registry_mask_fields(registry_path, dataset_name, has_mask, mask_path)
|
function update_registry_mask_fields(registry_path, dataset_name, has_mask, mask_path)
|
||||||
registry = load_registry(registry_path)
|
lock(REGISTRY_LOCK) do
|
||||||
|
registry = if isfile(registry_path)
|
||||||
if haskey(registry, dataset_name)
|
try
|
||||||
registry[dataset_name]["has_mask"] = has_mask
|
JSON.parsefile(registry_path, dicttype=Dict{String,Any})
|
||||||
registry[dataset_name]["mask_path"] = mask_path
|
catch e
|
||||||
else
|
@error "Failed to parse registry.json while updating mask fields: $e"
|
||||||
# Create new entry if it doesn't exist
|
Dict{String,Any}()
|
||||||
registry[dataset_name] = Dict{String,Any}(
|
end
|
||||||
"has_mask" => has_mask,
|
else
|
||||||
"mask_path" => mask_path,
|
Dict{String,Any}()
|
||||||
"source_path" => "",
|
end
|
||||||
"processed_date" => string(now()),
|
|
||||||
"is_imzML" => false
|
if haskey(registry, dataset_name)
|
||||||
)
|
registry[dataset_name]["has_mask"] = has_mask
|
||||||
end
|
registry[dataset_name]["mask_path"] = mask_path
|
||||||
|
else
|
||||||
try
|
# Create new entry if it doesn't exist
|
||||||
open(registry_path, "w") do f
|
registry[dataset_name] = Dict{String,Any}(
|
||||||
JSON.print(f, registry, 4)
|
"has_mask" => has_mask,
|
||||||
|
"mask_path" => mask_path,
|
||||||
|
"source_path" => "",
|
||||||
|
"processed_date" => string(now()),
|
||||||
|
"is_imzML" => false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
try
|
||||||
|
open(registry_path, "w") do f
|
||||||
|
JSON.print(f, registry, 4)
|
||||||
|
end
|
||||||
|
catch e
|
||||||
|
@error "Failed to write to registry.json: $e"
|
||||||
end
|
end
|
||||||
catch e
|
|
||||||
@error "Failed to write to registry.json: $e"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
98
mask.jl
98
mask.jl
@ -713,59 +713,57 @@ end
|
|||||||
|
|
||||||
@onchange isready begin
|
@onchange isready begin
|
||||||
if isready && !registry_init_done
|
if isready && !registry_init_done
|
||||||
@async begin # Run asynchronously to not block startup
|
sleep(1.0) # Give frontend time to initialize
|
||||||
sleep(1.0) # Give frontend time to initialize
|
try
|
||||||
try
|
println("Synchronizing registry for mask editor...")
|
||||||
println("Synchronizing registry for mask editor...")
|
reg_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
|
||||||
reg_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
|
# Assuming load_registry is available from MSI_src or a similar utility file
|
||||||
# Assuming load_registry is available from MSI_src or a similar utility file
|
# For now, handle its absence gracefully if it's not explicitly defined here.
|
||||||
# For now, handle its absence gracefully if it's not explicitly defined here.
|
registry = isfile(reg_path) ? load_registry(reg_path) : Dict{String, Any}()
|
||||||
registry = isfile(reg_path) ? load_registry(reg_path) : Dict{String, Any}()
|
|
||||||
|
|
||||||
public_dirs = isdir("public") ? readdir("public") : []
|
public_dirs = isdir("public") ? readdir("public") : []
|
||||||
ignored_dirs = ["css", "masks"]
|
ignored_dirs = ["css", "masks"]
|
||||||
|
|
||||||
dataset_dirs = filter(d -> isdir(joinpath("public", d)) && !(d in ignored_dirs), public_dirs)
|
dataset_dirs = filter(d -> isdir(joinpath("public", d)) && !(d in ignored_dirs), public_dirs)
|
||||||
|
|
||||||
registry_keys = Set(keys(registry))
|
registry_keys = Set(keys(registry))
|
||||||
folder_set = Set(dataset_dirs)
|
folder_set = Set(dataset_dirs)
|
||||||
|
|
||||||
new_folders = setdiff(folder_set, registry_keys)
|
new_folders = setdiff(folder_set, registry_keys)
|
||||||
for folder in new_folders
|
for folder in new_folders
|
||||||
println("Found new folder: $folder")
|
println("Found new folder: $folder")
|
||||||
registry[folder] = Dict(
|
registry[folder] = Dict(
|
||||||
"source_path" => "unknown (manually added)",
|
"source_path" => "unknown (manually added)",
|
||||||
"processed_date" => "unknown",
|
"processed_date" => "unknown",
|
||||||
"metadata" => Dict(),
|
"metadata" => Dict(),
|
||||||
"is_imzML" => true # Assume folder contains images if found this way
|
"is_imzML" => true # Assume folder contains images if found this way
|
||||||
)
|
)
|
||||||
end
|
|
||||||
|
|
||||||
removed_folders = setdiff(registry_keys, folder_set)
|
|
||||||
for folder in removed_folders
|
|
||||||
delete!(registry, folder)
|
|
||||||
end
|
|
||||||
|
|
||||||
if !isempty(new_folders) || !isempty(removed_folders)
|
|
||||||
println("Registry changed, saving...")
|
|
||||||
open(reg_path, "w") do f
|
|
||||||
JSON.print(f, registry, 4)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
all_folders = sort(collect(keys(registry)), lt=natural)
|
|
||||||
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders)
|
|
||||||
|
|
||||||
available_folders = deepcopy(all_folders)
|
|
||||||
image_available_folders = deepcopy(img_folders)
|
|
||||||
println("Mask editor UI lists updated. All: $(length(available_folders)), Images: $(length(image_available_folders))")
|
|
||||||
catch e
|
|
||||||
@warn "Mask editor registry synchronization failed: $e"
|
|
||||||
available_folders = []
|
|
||||||
image_available_folders = []
|
|
||||||
finally
|
|
||||||
registry_init_done = true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
removed_folders = setdiff(registry_keys, folder_set)
|
||||||
|
for folder in removed_folders
|
||||||
|
delete!(registry, folder)
|
||||||
|
end
|
||||||
|
|
||||||
|
if !isempty(new_folders) || !isempty(removed_folders)
|
||||||
|
println("Registry changed, saving...")
|
||||||
|
open(reg_path, "w") do f
|
||||||
|
JSON.print(f, registry, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
all_folders = sort(collect(keys(registry)), lt=natural)
|
||||||
|
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders)
|
||||||
|
|
||||||
|
available_folders = deepcopy(all_folders)
|
||||||
|
image_available_folders = deepcopy(img_folders)
|
||||||
|
println("Mask editor UI lists updated. All: $(length(available_folders)), Images: $(length(image_available_folders))")
|
||||||
|
catch e
|
||||||
|
@warn "Mask editor registry synchronization failed: $e"
|
||||||
|
available_folders = []
|
||||||
|
image_available_folders = []
|
||||||
|
finally
|
||||||
|
registry_init_done = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
50
src/Common.jl
Normal file
50
src/Common.jl
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# src/Common.jl
|
||||||
|
|
||||||
|
# --- Unified Error Types ---
|
||||||
|
abstract type MSIError <: Exception end
|
||||||
|
|
||||||
|
struct FileFormatError <: MSIError
|
||||||
|
msg::String
|
||||||
|
end
|
||||||
|
|
||||||
|
struct SpectrumNotFoundError <: MSIError
|
||||||
|
id
|
||||||
|
end
|
||||||
|
|
||||||
|
struct InvalidSpectrumError <: MSIError
|
||||||
|
id
|
||||||
|
reason::String
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Shared Constants ---
|
||||||
|
const DEFAULT_CACHE_SIZE = 100
|
||||||
|
const DEFAULT_NUM_BINS = 2000
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
validate_spectrum_data(mz, intensity, id)
|
||||||
|
|
||||||
|
Checks a spectrum for common data integrity issues.
|
||||||
|
|
||||||
|
- Checks for `NaN` or `Inf` values in intensity and m/z arrays.
|
||||||
|
- Verifies that the m/z array is sorted in ascending order.
|
||||||
|
- Ensures that m/z and intensity arrays have the same length.
|
||||||
|
|
||||||
|
# Throws
|
||||||
|
- `InvalidSpectrumError` if any of the checks fail.
|
||||||
|
"""
|
||||||
|
function validate_spectrum_data(mz::AbstractVector, intensity::AbstractVector, id)
|
||||||
|
if length(mz) != length(intensity)
|
||||||
|
throw(InvalidSpectrumError(id, "m/z and intensity arrays have different lengths"))
|
||||||
|
end
|
||||||
|
|
||||||
|
if !all(isfinite, mz) || !all(isfinite, intensity)
|
||||||
|
throw(InvalidSpectrumError(id, "Spectrum contains NaN or Inf values"))
|
||||||
|
end
|
||||||
|
|
||||||
|
if !issorted(mz)
|
||||||
|
throw(InvalidSpectrumError(id, "m/z array is not sorted"))
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
231
src/MSIData.jl
231
src/MSIData.jl
@ -46,9 +46,10 @@ Contains metadata for a single binary data array (m/z or intensity) within a spe
|
|||||||
- `format`: The data type of the elements (e.g., `Float32`, `Int64`).
|
- `format`: The data type of the elements (e.g., `Float32`, `Int64`).
|
||||||
- `is_compressed`: A boolean flag indicating if the data is compressed (e.g., with zlib).
|
- `is_compressed`: A boolean flag indicating if the data is compressed (e.g., with zlib).
|
||||||
- `offset`: The byte offset of the data within the file (`.ibd` for imzML, `.mzML` for mzML).
|
- `offset`: The byte offset of the data within the file (`.ibd` for imzML, `.mzML` for mzML).
|
||||||
- `encoded_length`: The length of the data. For uncompressed imzML, this is the number of
|
- `encoded_length`: The length of the data, which has different meanings depending on the format:
|
||||||
elements in the array. For compressed imzML, it is the number of bytes of the compressed
|
- **Uncompressed imzML**: The number of elements (points) in the array.
|
||||||
data. For mzML, this is the length of the Base64 encoded string.
|
- **Compressed imzML**: The number of bytes of the compressed data chunk in the `.ibd` file.
|
||||||
|
- **mzML**: The number of characters in the Base64 encoded string within the `.mzML` file.
|
||||||
- `axis_type`: A symbol (`:mz` or `:intensity`) indicating the type of data.
|
- `axis_type`: A symbol (`:mz` or `:intensity`) indicating the type of data.
|
||||||
"""
|
"""
|
||||||
struct SpectrumAsset
|
struct SpectrumAsset
|
||||||
@ -61,6 +62,15 @@ struct SpectrumAsset
|
|||||||
axis_type::Symbol
|
axis_type::Symbol
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
SpectrumMode
|
||||||
|
|
||||||
|
An enum representing the type of a spectrum's data.
|
||||||
|
|
||||||
|
- `CENTROID`: The spectrum contains centroided data (i.e., processed peaks).
|
||||||
|
- `PROFILE`: The spectrum contains raw profile data.
|
||||||
|
- `UNKNOWN`: The spectrum type is not specified.
|
||||||
|
"""
|
||||||
@enum SpectrumMode CENTROID=1 PROFILE=2 UNKNOWN=3
|
@enum SpectrumMode CENTROID=1 PROFILE=2 UNKNOWN=3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -116,7 +126,7 @@ mutable struct MSIData
|
|||||||
coordinate_map::Union{Matrix{Int}, Nothing} # Maps (x,y) to linear index for imzML
|
coordinate_map::Union{Matrix{Int}, Nothing} # Maps (x,y) to linear index for imzML
|
||||||
|
|
||||||
# LRU Cache for GetSpectrum
|
# LRU Cache for GetSpectrum
|
||||||
cache::Dict{Int, Tuple{Vector, Vector}}
|
cache::Dict{Int, Tuple{Vector{Float64}, Vector{Float64}}}
|
||||||
cache_order::Vector{Int} # Stores indices, with most recently used at the end
|
cache_order::Vector{Int} # Stores indices, with most recently used at the end
|
||||||
cache_size::Int # Max number of spectra in cache
|
cache_size::Int # Max number of spectra in cache
|
||||||
cache_lock::ReentrantLock # To make cache access thread-safe
|
cache_lock::ReentrantLock # To make cache access thread-safe
|
||||||
@ -143,6 +153,74 @@ mutable struct MSIData
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base.close(data::MSIData)
|
||||||
|
|
||||||
|
Explicitly closes the file handles associated with the `MSIData` object and clears the spectrum cache.
|
||||||
|
It is good practice to call this method when you are finished with an `MSIData` object to release resources immediately.
|
||||||
|
"""
|
||||||
|
function Base.close(data::MSIData)
|
||||||
|
if data.source isa ImzMLSource && isopen(data.source.ibd_handle)
|
||||||
|
close(data.source.ibd_handle)
|
||||||
|
elseif data.source isa MzMLSource && isopen(data.source.file_handle)
|
||||||
|
close(data.source.file_handle)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clear cache
|
||||||
|
empty!(data.cache)
|
||||||
|
empty!(data.cache_order)
|
||||||
|
|
||||||
|
println("MSIData object closed and resources released.")
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
warm_cache(data::MSIData, indices::AbstractVector{Int})
|
||||||
|
|
||||||
|
Pre-loads a specified list of spectra into the cache. This is useful for warming up
|
||||||
|
the cache with frequently accessed spectra to improve performance for subsequent
|
||||||
|
interactive analysis.
|
||||||
|
"""
|
||||||
|
function warm_cache(data::MSIData, indices::AbstractVector{Int})
|
||||||
|
println("Warming cache with $(length(indices)) spectra...")
|
||||||
|
for idx in indices
|
||||||
|
# Calling GetSpectrum will load the data and place it in the cache
|
||||||
|
GetSpectrum(data, idx)
|
||||||
|
end
|
||||||
|
println("Cache warming complete.")
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
get_memory_usage(data::MSIData)
|
||||||
|
|
||||||
|
Calculates and returns a summary of the memory currently used by the MSIData object,
|
||||||
|
including the spectrum cache and metadata.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A `NamedTuple` with memory usage in bytes for different components.
|
||||||
|
"""
|
||||||
|
function get_memory_usage(data::MSIData)
|
||||||
|
cache_bytes = 0
|
||||||
|
lock(data.cache_lock) do
|
||||||
|
# This is an approximation; `sizeof` on a dictionary is not fully representative,
|
||||||
|
# but it's much better to sum the size of the actual vectors.
|
||||||
|
for (mz, intensity) in values(data.cache)
|
||||||
|
cache_bytes += sizeof(mz) + sizeof(intensity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
metadata_bytes = sizeof(data.spectra_metadata)
|
||||||
|
coordinate_map_bytes = data.coordinate_map === nothing ? 0 : sizeof(data.coordinate_map)
|
||||||
|
|
||||||
|
total_bytes = cache_bytes + metadata_bytes + coordinate_map_bytes
|
||||||
|
|
||||||
|
return (
|
||||||
|
total_mb = total_bytes / 1024^2,
|
||||||
|
cache_mb = cache_bytes / 1024^2,
|
||||||
|
metadata_mb = metadata_bytes / 1024^2,
|
||||||
|
coordinate_map_mb = coordinate_map_bytes / 1024^2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
get_masked_spectrum_indices(data::MSIData, mask_matrix::BitMatrix) -> Set{Int}
|
get_masked_spectrum_indices(data::MSIData, mask_matrix::BitMatrix) -> Set{Int}
|
||||||
|
|
||||||
@ -158,14 +236,14 @@ spectrum indices that fall within the `true` regions of the mask.
|
|||||||
"""
|
"""
|
||||||
function get_masked_spectrum_indices(data::MSIData, mask_matrix::BitMatrix)
|
function get_masked_spectrum_indices(data::MSIData, mask_matrix::BitMatrix)
|
||||||
if data.coordinate_map === nothing
|
if data.coordinate_map === nothing
|
||||||
error("Coordinate map not available. Cannot apply mask to non-imaging data.")
|
throw(ArgumentError("Coordinate map not available. Cannot apply mask to non-imaging data."))
|
||||||
end
|
end
|
||||||
|
|
||||||
width, height = data.image_dims
|
width, height = data.image_dims
|
||||||
mask_height, mask_width = size(mask_matrix)
|
mask_height, mask_width = size(mask_matrix)
|
||||||
|
|
||||||
if mask_width != width || mask_height != height
|
if mask_width != width || mask_height != height
|
||||||
error("Mask dimensions ($(mask_width)x$(mask_height)) do not match image dimensions ($(width)x$(height)).")
|
throw(ArgumentError("Mask dimensions ($(mask_width)x$(mask_height)) do not match image dimensions ($(width)x$(height))."))
|
||||||
end
|
end
|
||||||
|
|
||||||
masked_indices = Set{Int}()
|
masked_indices = Set{Int}()
|
||||||
@ -209,6 +287,9 @@ by this function and is assumed to be handled by the caller if necessary.
|
|||||||
- A `Vector` of the appropriate type containing the decoded data.
|
- A `Vector` of the appropriate type containing the decoded data.
|
||||||
"""
|
"""
|
||||||
function read_binary_vector(io::IO, asset::SpectrumAsset)
|
function read_binary_vector(io::IO, asset::SpectrumAsset)
|
||||||
|
if asset.offset < 0 || asset.offset >= filesize(io)
|
||||||
|
throw(FileFormatError("Invalid asset offset: $(asset.offset) for file size $(filesize(io))"))
|
||||||
|
end
|
||||||
seek(io, asset.offset)
|
seek(io, asset.offset)
|
||||||
raw_b64 = read(io, asset.encoded_length)
|
raw_b64 = read(io, asset.encoded_length)
|
||||||
decoded_bytes = base64decode(strip(String(raw_b64)))
|
decoded_bytes = base64decode(strip(String(raw_b64)))
|
||||||
@ -217,6 +298,10 @@ function read_binary_vector(io::IO, asset::SpectrumAsset)
|
|||||||
n_elements = Int(bytes_io.size / sizeof(asset.format))
|
n_elements = Int(bytes_io.size / sizeof(asset.format))
|
||||||
out_array = Array{asset.format}(undef, n_elements)
|
out_array = Array{asset.format}(undef, n_elements)
|
||||||
read!(bytes_io, out_array)
|
read!(bytes_io, out_array)
|
||||||
|
|
||||||
|
# FIX: little-endian to host byte order for mzML data
|
||||||
|
out_array .= ltoh.(out_array)
|
||||||
|
|
||||||
return out_array
|
return out_array
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -241,17 +326,29 @@ function read_spectrum_from_disk(source::ImzMLSource, meta::SpectrumMetadata)
|
|||||||
mz = Array{source.mz_format}(undef, meta.mz_asset.encoded_length)
|
mz = Array{source.mz_format}(undef, meta.mz_asset.encoded_length)
|
||||||
intensity = Array{source.intensity_format}(undef, meta.int_asset.encoded_length)
|
intensity = Array{source.intensity_format}(undef, meta.int_asset.encoded_length)
|
||||||
|
|
||||||
seek(source.ibd_handle, meta.mz_asset.offset)
|
# FIX: Read arrays in the order they appear in the file for efficiency,
|
||||||
read!(source.ibd_handle, mz)
|
# but ensure we seek to the correct offset for both.
|
||||||
|
if meta.mz_asset.offset < meta.int_asset.offset
|
||||||
seek(source.ibd_handle, meta.int_asset.offset)
|
seek(source.ibd_handle, meta.mz_asset.offset)
|
||||||
read!(source.ibd_handle, intensity)
|
read!(source.ibd_handle, mz)
|
||||||
|
seek(source.ibd_handle, meta.int_asset.offset)
|
||||||
|
read!(source.ibd_handle, intensity)
|
||||||
|
else
|
||||||
|
seek(source.ibd_handle, meta.int_asset.offset)
|
||||||
|
read!(source.ibd_handle, intensity)
|
||||||
|
seek(source.ibd_handle, meta.mz_asset.offset)
|
||||||
|
read!(source.ibd_handle, mz)
|
||||||
|
end
|
||||||
|
|
||||||
# imzML data is little-endian. Convert to host byte order.
|
# imzML data is little-endian. Convert to host byte order.
|
||||||
mz .= ltoh.(mz)
|
mz .= ltoh.(mz)
|
||||||
intensity .= ltoh.(intensity)
|
intensity .= ltoh.(intensity) # FIX: Added missing conversion for intensity
|
||||||
|
|
||||||
return mz, intensity
|
mz_f64, intensity_f64 = Float64.(mz), Float64.(intensity)
|
||||||
|
|
||||||
|
validate_spectrum_data(mz_f64, intensity_f64, meta.id)
|
||||||
|
|
||||||
|
return mz_f64, intensity_f64
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -272,7 +369,12 @@ function read_spectrum_from_disk(source::MzMLSource, meta::SpectrumMetadata)
|
|||||||
# For mzML, data is Base64 encoded within the XML
|
# For mzML, data is Base64 encoded within the XML
|
||||||
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
||||||
intensity = read_binary_vector(source.file_handle, meta.int_asset)
|
intensity = read_binary_vector(source.file_handle, meta.int_asset)
|
||||||
return mz, intensity
|
|
||||||
|
mz_f64, intensity_f64 = Float64.(mz), Float64.(intensity)
|
||||||
|
|
||||||
|
validate_spectrum_data(mz_f64, intensity_f64, meta.id)
|
||||||
|
|
||||||
|
return mz_f64, intensity_f64
|
||||||
end
|
end
|
||||||
|
|
||||||
# --- Public API --- #
|
# --- Public API --- #
|
||||||
@ -294,7 +396,7 @@ This function is the core of the "Indexed" and "Cache" access patterns.
|
|||||||
"""
|
"""
|
||||||
function GetSpectrum(data::MSIData, index::Int)
|
function GetSpectrum(data::MSIData, index::Int)
|
||||||
if index < 1 || index > length(data.spectra_metadata)
|
if index < 1 || index > length(data.spectra_metadata)
|
||||||
error("Spectrum index $index out of bounds.")
|
throw(SpectrumNotFoundError(index))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Phase 1: Check the cache (with lock)
|
# Phase 1: Check the cache (with lock)
|
||||||
@ -352,16 +454,16 @@ the indexed `GetSpectrum` method, benefiting from caching.
|
|||||||
"""
|
"""
|
||||||
function GetSpectrum(data::MSIData, x::Int, y::Int)
|
function GetSpectrum(data::MSIData, x::Int, y::Int)
|
||||||
if data.coordinate_map === nothing
|
if data.coordinate_map === nothing
|
||||||
error("Coordinate map not available. This method is only for imaging data loaded from .imzML files.")
|
throw(ArgumentError("Coordinate map not available. This method is only for imaging data loaded from .imzML files."))
|
||||||
end
|
end
|
||||||
width, height = data.image_dims
|
width, height = data.image_dims
|
||||||
if x < 1 || x > width || y < 1 || y > height
|
if x < 1 || x > width || y < 1 || y > height
|
||||||
error("Coordinates ($x, $y) out of bounds for image dimensions ($width, $height).")
|
throw(ArgumentError("Coordinates ($x, $y) out of bounds for image dimensions ($width, $height)."))
|
||||||
end
|
end
|
||||||
|
|
||||||
index = data.coordinate_map[x, y]
|
index = data.coordinate_map[x, y]
|
||||||
if index == 0
|
if index == 0
|
||||||
error("No spectrum found at coordinates ($x, $y).")
|
throw(SpectrumNotFoundError((x, y)))
|
||||||
end
|
end
|
||||||
|
|
||||||
return GetSpectrum(data, index) # Call the existing method
|
return GetSpectrum(data, index) # Call the existing method
|
||||||
@ -373,13 +475,14 @@ end
|
|||||||
A "function barrier" helper for safely processing a single spectrum.
|
A "function barrier" helper for safely processing a single spectrum.
|
||||||
|
|
||||||
This function retrieves a spectrum and then immediately calls the provided function `f`
|
This function retrieves a spectrum and then immediately calls the provided function `f`
|
||||||
with the resulting `(mz, intensity)` arrays. This pattern is crucial for performance.
|
with the resulting `(mz, intensity)` arrays. While the `GetSpectrum` API is now type-stable,
|
||||||
While `GetSpectrum` itself is type-unstable (because the array data types are not
|
this pattern remains good practice for separating data access from data processing.
|
||||||
known at compile time), calling `f` with the result allows Julia's compiler to
|
|
||||||
generate specialized, fast code for `f` based on the *concrete* types it receives.
|
|
||||||
|
|
||||||
Use this function when you need to perform performance-critical operations on a
|
Use this function when you need to perform performance-critical operations on a
|
||||||
single spectrum. Your logic should be inside the function passed to this helper.
|
single spectrum. Your logic should be inside the function passed to this helper.
|
||||||
|
|
||||||
|
For bulk processing of all spectra, consider using the `_iterate_spectra_fast`
|
||||||
|
function, which is optimized for sequential access and avoids cache overhead.
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function process_spectrum(f::Function, data::MSIData, index::Int)
|
function process_spectrum(f::Function, data::MSIData, index::Int)
|
||||||
@ -611,19 +714,14 @@ function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000, masked_
|
|||||||
intensity_view = intensity
|
intensity_view = intensity
|
||||||
|
|
||||||
# Manual binning without clamp/round for SIMD
|
# Manual binning without clamp/round for SIMD
|
||||||
@inbounds for i in eachindex(mz_view)
|
@inbounds @simd for i in eachindex(mz_view)
|
||||||
# Calculate raw bin index (much faster than round+clamp)
|
# Calculate raw bin index (much faster than round+clamp)
|
||||||
raw_index = (mz_view[i] - min_mz) * inv_bin_step + 1.0
|
raw_index = (mz_view[i] - min_mz) * inv_bin_step + 1.0
|
||||||
bin_index = trunc(Int, raw_index)
|
bin_index = trunc(Int, raw_index)
|
||||||
|
|
||||||
# Manual bounds checking (faster than clamp)
|
# Branchless version using clamp
|
||||||
if 1 <= bin_index <= num_bins
|
final_index = clamp(bin_index, 1, num_bins)
|
||||||
intensity_sum[bin_index] += intensity_view[i]
|
intensity_sum[final_index] += intensity_view[i]
|
||||||
elseif bin_index < 1
|
|
||||||
intensity_sum[1] += intensity_view[i]
|
|
||||||
else # bin_index > num_bins
|
|
||||||
intensity_sum[num_bins] += intensity_view[i]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
pass2_duration = (time_ns() - pass2_start_time) / 1e9
|
pass2_duration = (time_ns() - pass2_start_time) / 1e9
|
||||||
@ -706,19 +804,14 @@ function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000, masked_i
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@inbounds for i in eachindex(mz)
|
@inbounds @simd for i in eachindex(mz)
|
||||||
# Calculate raw bin index
|
# Calculate raw bin index
|
||||||
raw_index = (mz[i] - min_mz) * inv_bin_step + 1.0
|
raw_index = (mz[i] - min_mz) * inv_bin_step + 1.0
|
||||||
bin_index = trunc(Int, raw_index)
|
bin_index = trunc(Int, raw_index)
|
||||||
|
|
||||||
# Manual bounds checking
|
# Branchless version using clamp
|
||||||
if 1 <= bin_index <= num_bins
|
final_index = clamp(bin_index, 1, num_bins)
|
||||||
intensity_sum[bin_index] += intensity[i]
|
intensity_sum[final_index] += intensity[i]
|
||||||
elseif bin_index < 1
|
|
||||||
intensity_sum[1] += intensity[i]
|
|
||||||
else # bin_index > num_bins
|
|
||||||
intensity_sum[num_bins] += intensity[i]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
pass2_duration = (time_ns() - pass2_start_time) / 1e9
|
pass2_duration = (time_ns() - pass2_start_time) / 1e9
|
||||||
@ -842,9 +935,14 @@ end
|
|||||||
Reads a single data array (m/z or intensity) from an `.ibd` file stream,
|
Reads a single data array (m/z or intensity) from an `.ibd` file stream,
|
||||||
handling both compressed and uncompressed data.
|
handling both compressed and uncompressed data.
|
||||||
|
|
||||||
- If `asset.is_compressed` is true, it reads the compressed bytes, inflates
|
This is an internal function designed for high-performance iteration. It assumes
|
||||||
them using zlib, and reinterprets the result as a vector of the given `format`.
|
the file stream `io` is already positioned at the correct offset.
|
||||||
- If false, it reads the uncompressed data directly into a vector.
|
|
||||||
|
- If `asset.is_compressed` is true, it reads `asset.encoded_length` bytes of
|
||||||
|
compressed data, inflates them using zlib, and reinterprets the result as a
|
||||||
|
vector of the given `format`.
|
||||||
|
- If false, it reads `asset.encoded_length` *elements* of uncompressed data
|
||||||
|
directly into a vector.
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
- `io`: The IO stream of the `.ibd` file.
|
- `io`: The IO stream of the `.ibd` file.
|
||||||
@ -853,8 +951,16 @@ handling both compressed and uncompressed data.
|
|||||||
|
|
||||||
# Returns
|
# Returns
|
||||||
- A `Vector` containing the data.
|
- A `Vector` containing the data.
|
||||||
|
|
||||||
|
# Throws
|
||||||
|
- An error if zlib decompression fails, which can indicate corrupt data or
|
||||||
|
an incorrect offset in the `.imzML` metadata.
|
||||||
"""
|
"""
|
||||||
function read_compressed_array(io::IO, asset::SpectrumAsset, format::Type)
|
function read_compressed_array(io::IO, asset::SpectrumAsset, format::Type)
|
||||||
|
# Add validation before seeking
|
||||||
|
if asset.offset < 0 || asset.offset >= filesize(io)
|
||||||
|
throw(FileFormatError("Invalid asset offset: $(asset.offset) for file size $(filesize(io))"))
|
||||||
|
end
|
||||||
seek(io, asset.offset)
|
seek(io, asset.offset)
|
||||||
|
|
||||||
if asset.is_compressed
|
if asset.is_compressed
|
||||||
@ -930,10 +1036,12 @@ function _iterate_uncompressed_fast(f::Function, data::MSIData, source::ImzMLSou
|
|||||||
if meta.mz_asset.offset < meta.int_asset.offset
|
if meta.mz_asset.offset < meta.int_asset.offset
|
||||||
seek(source.ibd_handle, meta.mz_asset.offset)
|
seek(source.ibd_handle, meta.mz_asset.offset)
|
||||||
read!(source.ibd_handle, mz_view)
|
read!(source.ibd_handle, mz_view)
|
||||||
|
seek(source.ibd_handle, meta.int_asset.offset) # FIX: Added missing seek
|
||||||
read!(source.ibd_handle, int_view)
|
read!(source.ibd_handle, int_view)
|
||||||
else
|
else
|
||||||
seek(source.ibd_handle, meta.int_asset.offset)
|
seek(source.ibd_handle, meta.int_asset.offset)
|
||||||
read!(source.ibd_handle, int_view)
|
read!(source.ibd_handle, int_view)
|
||||||
|
seek(source.ibd_handle, meta.mz_asset.offset) # FIX: Added missing seek
|
||||||
read!(source.ibd_handle, mz_view)
|
read!(source.ibd_handle, mz_view)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1020,15 +1128,19 @@ provided function. It bypasses the `GetSpectrum` cache to avoid storing
|
|||||||
all decoded spectra in memory.
|
all decoded spectra in memory.
|
||||||
"""
|
"""
|
||||||
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::MzMLSource, indices_to_iterate::Union{AbstractVector{Int}, Nothing})
|
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::MzMLSource, indices_to_iterate::Union{AbstractVector{Int}, Nothing})
|
||||||
# This implementation is for mzML. It iterates through each spectrum,
|
# This implementation is for mzML. To improve disk I/O, we can reorder the read
|
||||||
# decodes it, and then calls the function `f`. It's less performant than
|
# operations to be as sequential as possible based on their offset in the file.
|
||||||
# the imzML version because we cannot pre-allocate buffers of a known size,
|
|
||||||
# but it is correct and still faster than using GetSpectrum due to bypassing the cache.
|
|
||||||
|
|
||||||
# Determine which indices to iterate over
|
# Determine which indices to iterate over
|
||||||
spectrum_indices = (indices_to_iterate === nothing) ? (1:length(data.spectra_metadata)) : indices_to_iterate
|
spectrum_indices = (indices_to_iterate === nothing) ? (1:length(data.spectra_metadata)) : indices_to_iterate
|
||||||
|
|
||||||
for i in spectrum_indices
|
# Create a vector of (index, offset) tuples to be sorted
|
||||||
|
indices_with_offsets = [(i, data.spectra_metadata[i].mz_asset.offset) for i in spectrum_indices]
|
||||||
|
|
||||||
|
# Sort by offset to make disk access more sequential
|
||||||
|
sort!(indices_with_offsets, by = x -> x[2])
|
||||||
|
|
||||||
|
for (i, _) in indices_with_offsets
|
||||||
meta = data.spectra_metadata[i]
|
meta = data.spectra_metadata[i]
|
||||||
|
|
||||||
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
||||||
@ -1039,14 +1151,27 @@ function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::MzMLSour
|
|||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
_iterate_spectra_fast(f::Function, data::MSIData)
|
_iterate_spectra_fast(f::Function, data::MSIData, indices_to_iterate=nothing)
|
||||||
|
|
||||||
An internal, high-performance iterator for bulk processing that bypasses the cache.
|
An internal, high-performance iterator for bulk processing that bypasses the `GetSpectrum` cache.
|
||||||
It dispatches to a specialized implementation based on the data source type
|
It provides direct, sequential access to spectral data, making it the most efficient
|
||||||
(`ImzMLSource` or `MzMLSource`).
|
method for operations that need to process many or all spectra (e.g., calculating
|
||||||
|
a total spectrum, pre-computing analytics, or exporting data).
|
||||||
|
|
||||||
- `f`: A function to call for each spectrum, with signature `f(index, mz, intensity)`.
|
This function dispatches to a specialized implementation based on the data source type
|
||||||
|
(`ImzMLSource` or `MzMLSource`) and data characteristics (e.g., compressed vs.
|
||||||
|
uncompressed) to maximize performance.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `f`: A function to call for each spectrum, with the signature `f(index, mz, intensity)`.
|
||||||
- `data`: The `MSIData` object.
|
- `data`: The `MSIData` object.
|
||||||
|
- `indices_to_iterate`: An optional `AbstractVector{Int}` specifying a subset of
|
||||||
|
spectrum indices to iterate over. If `nothing`, it iterates over all spectra.
|
||||||
|
|
||||||
|
# Warning
|
||||||
|
This is a low-level function that bypasses the public `GetSpectrum` API. It does not
|
||||||
|
use the cache and is not guaranteed to be thread-safe if the same `MSIData` object
|
||||||
|
is accessed from multiple threads concurrently.
|
||||||
"""
|
"""
|
||||||
function _iterate_spectra_fast(f::Function, data::MSIData, indices_to_iterate::Union{AbstractVector{Int}, Nothing}=nothing)
|
function _iterate_spectra_fast(f::Function, data::MSIData, indices_to_iterate::Union{AbstractVector{Int}, Nothing}=nothing)
|
||||||
# Dispatch to the correct implementation based on the source type
|
# Dispatch to the correct implementation based on the source type
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export FeatureMatrix,
|
|||||||
get_common_calibration_standards
|
get_common_calibration_standards
|
||||||
|
|
||||||
# Include all source files directly into the main module
|
# Include all source files directly into the main module
|
||||||
|
include("Common.jl")
|
||||||
include("MSIData.jl")
|
include("MSIData.jl")
|
||||||
include("ParserHelpers.jl")
|
include("ParserHelpers.jl")
|
||||||
include("mzML.jl")
|
include("mzML.jl")
|
||||||
|
|||||||
@ -5,7 +5,7 @@ into a proper .imzML/.ibd file pair, using a separate synchronization file.
|
|||||||
It replicates the functionality of the original R scripts that use MALDIquant.
|
It replicates the functionality of the original R scripts that use MALDIquant.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
using DataFrames, Printf, CSV
|
using DataFrames, Printf, CSV, UUIDs, ProgressMeter
|
||||||
|
|
||||||
# This file assumes that the main application file (e.g., app.jl) has already included
|
# This file assumes that the main application file (e.g., app.jl) has already included
|
||||||
# the necessary source files: MSIData.jl, mzML.jl, imzML.jl
|
# the necessary source files: MSIData.jl, mzML.jl, imzML.jl
|
||||||
@ -396,6 +396,7 @@ This function handles three cases:
|
|||||||
- A tuple `(mz_array, intensity_array)` for the rendered pixel spectrum.
|
- A tuple `(mz_array, intensity_array)` for the rendered pixel spectrum.
|
||||||
"""
|
"""
|
||||||
function RenderPixel(
|
function RenderPixel(
|
||||||
|
intensity_buffer::Vector{Float32},
|
||||||
pixel_info::AbstractVector{Int64},
|
pixel_info::AbstractVector{Int64},
|
||||||
scans::AbstractMatrix{Int64},
|
scans::AbstractMatrix{Int64},
|
||||||
msi_data::MSIData,
|
msi_data::MSIData,
|
||||||
@ -408,19 +409,32 @@ function RenderPixel(
|
|||||||
num_actions = last_scan - first_scan
|
num_actions = last_scan - first_scan
|
||||||
|
|
||||||
# Get reference m/z array from the first scan involved.
|
# Get reference m/z array from the first scan involved.
|
||||||
# This call remains type-unstable, but its impact is now isolated and only paid once.
|
|
||||||
mz_array, _ = GetSpectrum(msi_data, first_scan)
|
mz_array, _ = GetSpectrum(msi_data, first_scan)
|
||||||
|
|
||||||
# If the first spectrum was empty, we can't do anything else.
|
# If the first spectrum was empty, we can't do anything else.
|
||||||
if isempty(mz_array)
|
if isempty(mz_array)
|
||||||
return (mz_array, Float32[])
|
return (mz_array, view(intensity_buffer, 0:-1), UNKNOWN)
|
||||||
end
|
end
|
||||||
|
|
||||||
new_intensity = zeros(Float32, length(mz_array))
|
# Determine the mode for the output spectrum. If any contributing scan is profile, the result is profile.
|
||||||
|
final_mode = CENTROID
|
||||||
|
for i in first_scan:last_scan
|
||||||
|
if msi_data.spectra_metadata[i].mode == PROFILE
|
||||||
|
final_mode = PROFILE
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use the provided buffer instead of allocating a new one.
|
||||||
|
if length(intensity_buffer) < length(mz_array)
|
||||||
|
error("Provided intensity buffer is too small for spectrum of length $(length(mz_array))")
|
||||||
|
end
|
||||||
|
new_intensity = view(intensity_buffer, 1:length(mz_array))
|
||||||
|
fill!(new_intensity, 0.0f0)
|
||||||
|
|
||||||
# SAFETY: Ensure we have valid scan indices
|
# SAFETY: Ensure we have valid scan indices
|
||||||
if first_scan < 1 || last_scan > size(scans, 1) || first_scan > last_scan
|
if first_scan < 1 || last_scan > size(scans, 1) || first_scan > last_scan
|
||||||
return (mz_array, new_intensity) # Return zero intensity for invalid ranges
|
return (mz_array, new_intensity, final_mode) # Return zero intensity for invalid ranges
|
||||||
end
|
end
|
||||||
|
|
||||||
if num_actions == 0 # Single scan contributes to the pixel
|
if num_actions == 0 # Single scan contributes to the pixel
|
||||||
@ -469,7 +483,7 @@ function RenderPixel(
|
|||||||
# FINAL SAFETY: Clamp any negative values to zero
|
# FINAL SAFETY: Clamp any negative values to zero
|
||||||
new_intensity = max.(new_intensity, 0.0f0)
|
new_intensity = max.(new_intensity, 0.0f0)
|
||||||
|
|
||||||
return (mz_array, new_intensity)
|
return (mz_array, new_intensity, final_mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -498,84 +512,96 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
|
|||||||
open(target_ibd_file, "w") do ibd_stream
|
open(target_ibd_file, "w") do ibd_stream
|
||||||
write(ibd_stream, zeros(UInt8, 16)) # UUID placeholder
|
write(ibd_stream, zeros(UInt8, 16)) # UUID placeholder
|
||||||
end
|
end
|
||||||
return BinaryMetadata[], Tuple{Int, Int}[], (0, 0), UNKNOWN
|
return BinaryMetadata[], Tuple{Int, Int}[], (0, 0), SpectrumMode[], uuid4()
|
||||||
end
|
end
|
||||||
|
|
||||||
width = maximum(timing_matrix[:, 1])
|
width = maximum(timing_matrix[:, 1])
|
||||||
height = maximum(timing_matrix[:, 2])
|
height = maximum(timing_matrix[:, 2])
|
||||||
|
|
||||||
msi_data = OpenMSIData(source_file)
|
msi_data = OpenMSIData(source_file)
|
||||||
|
|
||||||
|
local binary_meta_vec, coords_vec, pixel_modes, ibd_uuid
|
||||||
|
|
||||||
|
try
|
||||||
|
precompute_analytics(msi_data)
|
||||||
|
max_points = isempty(msi_data.spectrum_stats_df.NumPoints) ? 0 : maximum(msi_data.spectrum_stats_df.NumPoints)
|
||||||
|
intensity_buffer = zeros(Float32, max_points)
|
||||||
|
|
||||||
source_mode = UNKNOWN
|
scan_time_deltas = zeros(Int64, size(scans, 1))
|
||||||
if !isempty(msi_data.spectra_metadata)
|
if size(scans, 1) > 1
|
||||||
source_mode = msi_data.spectra_metadata[1].mode
|
for i in 1:(size(scans, 1) - 1)
|
||||||
end
|
delta = scans[i+1, 2] - scans[i, 2]
|
||||||
|
scan_time_deltas[i] = max(1, delta)
|
||||||
scan_time_deltas = zeros(Int64, size(scans, 1))
|
|
||||||
if size(scans, 1) > 1
|
|
||||||
for i in 1:(size(scans, 1) - 1)
|
|
||||||
delta = scans[i+1, 2] - scans[i, 2]
|
|
||||||
scan_time_deltas[i] = max(1, delta)
|
|
||||||
end
|
|
||||||
scan_time_deltas[end] = max(1, scan_time_deltas[end-1])
|
|
||||||
end
|
|
||||||
|
|
||||||
pixel_time_deltas = zeros(Int64, size(timing_matrix, 1))
|
|
||||||
if size(timing_matrix, 1) > 1
|
|
||||||
for i in 1:(size(timing_matrix, 1) - 1)
|
|
||||||
delta = timing_matrix[i+1, 3] - timing_matrix[i, 3]
|
|
||||||
pixel_time_deltas[i] = max(1, delta)
|
|
||||||
end
|
|
||||||
pixel_time_deltas[end] = max(1, pixel_time_deltas[end-1])
|
|
||||||
end
|
|
||||||
|
|
||||||
binary_meta_vec = BinaryMetadata[]
|
|
||||||
sizehint!(binary_meta_vec, size(timing_matrix, 1))
|
|
||||||
coords_vec = Tuple{Int, Int}[]
|
|
||||||
sizehint!(coords_vec, size(timing_matrix, 1))
|
|
||||||
empty_pixel_count = 0
|
|
||||||
|
|
||||||
open(target_ibd_file, "w") do ibd_stream
|
|
||||||
write(ibd_stream, zeros(UInt8, 16)) # UUID placeholder
|
|
||||||
|
|
||||||
for i in 1:size(timing_matrix, 1)
|
|
||||||
pixel_info = timing_matrix[i, :]
|
|
||||||
x, y = pixel_info[1], pixel_info[2]
|
|
||||||
push!(coords_vec, (x, y))
|
|
||||||
|
|
||||||
first_scan = pixel_info[4]
|
|
||||||
last_scan = pixel_info[5]
|
|
||||||
|
|
||||||
if first_scan > last_scan || first_scan < 1 || last_scan > size(scans, 1)
|
|
||||||
empty_pixel_count += 1
|
|
||||||
# For empty pixels, offsets point to the current end of file, with zero length
|
|
||||||
current_pos = position(ibd_stream)
|
|
||||||
push!(binary_meta_vec, BinaryMetadata(current_pos, 0, current_pos, 0))
|
|
||||||
continue
|
|
||||||
end
|
end
|
||||||
|
scan_time_deltas[end] = max(1, scan_time_deltas[end-1])
|
||||||
mz, intensity = RenderPixel(pixel_info, scans, msi_data, scan_time_deltas, pixel_time_deltas)
|
|
||||||
|
|
||||||
# Write m/z array
|
|
||||||
mz_offset = position(ibd_stream)
|
|
||||||
for val in mz
|
|
||||||
write(ibd_stream, htol(Float32(val)))
|
|
||||||
end
|
|
||||||
mz_length = position(ibd_stream) - mz_offset
|
|
||||||
|
|
||||||
# Write intensity array
|
|
||||||
int_offset = position(ibd_stream)
|
|
||||||
for val in intensity
|
|
||||||
write(ibd_stream, htol(Float32(val)))
|
|
||||||
end
|
|
||||||
int_length = position(ibd_stream) - int_offset
|
|
||||||
|
|
||||||
push!(binary_meta_vec, BinaryMetadata(mz_offset, mz_length, int_offset, int_length))
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
@info "Found and processed $empty_pixel_count empty pixels out of $(size(timing_matrix, 1)) total."
|
pixel_time_deltas = zeros(Int64, size(timing_matrix, 1))
|
||||||
return binary_meta_vec, coords_vec, (width, height), source_mode
|
if size(timing_matrix, 1) > 1
|
||||||
|
for i in 1:(size(timing_matrix, 1) - 1)
|
||||||
|
delta = timing_matrix[i+1, 3] - timing_matrix[i, 3]
|
||||||
|
pixel_time_deltas[i] = max(1, delta)
|
||||||
|
end
|
||||||
|
pixel_time_deltas[end] = max(1, pixel_time_deltas[end-1])
|
||||||
|
end
|
||||||
|
|
||||||
|
binary_meta_vec = BinaryMetadata[]
|
||||||
|
sizehint!(binary_meta_vec, size(timing_matrix, 1))
|
||||||
|
coords_vec = Tuple{Int, Int}[]
|
||||||
|
sizehint!(coords_vec, size(timing_matrix, 1))
|
||||||
|
pixel_modes = SpectrumMode[]
|
||||||
|
sizehint!(pixel_modes, size(timing_matrix, 1))
|
||||||
|
empty_pixel_count = 0
|
||||||
|
|
||||||
|
open(target_ibd_file, "w") do ibd_stream
|
||||||
|
# Generate and write a valid UUID
|
||||||
|
ibd_uuid = uuid4()
|
||||||
|
write(ibd_stream, htol(ibd_uuid.value))
|
||||||
|
|
||||||
|
p = Progress(size(timing_matrix, 1), 1, "Converting pixels... ")
|
||||||
|
|
||||||
|
for i in 1:size(timing_matrix, 1)
|
||||||
|
pixel_info = timing_matrix[i, :]
|
||||||
|
x, y = pixel_info[1], pixel_info[2]
|
||||||
|
push!(coords_vec, (x, y))
|
||||||
|
|
||||||
|
first_scan = pixel_info[4]
|
||||||
|
last_scan = pixel_info[5]
|
||||||
|
|
||||||
|
if first_scan > last_scan || first_scan < 1 || last_scan > size(scans, 1)
|
||||||
|
empty_pixel_count += 1
|
||||||
|
current_pos = position(ibd_stream)
|
||||||
|
push!(binary_meta_vec, BinaryMetadata(current_pos, 0, current_pos, 0))
|
||||||
|
push!(pixel_modes, UNKNOWN) # Mode for empty pixel
|
||||||
|
next!(p)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
mz, intensity, pixel_mode = RenderPixel(intensity_buffer, pixel_info, scans, msi_data, scan_time_deltas, pixel_time_deltas)
|
||||||
|
push!(pixel_modes, pixel_mode)
|
||||||
|
|
||||||
|
# Write m/z array (as Float64)
|
||||||
|
mz_offset = position(ibd_stream)
|
||||||
|
write(ibd_stream, htol.(mz)) # mz is Vector{Float64}
|
||||||
|
mz_length = position(ibd_stream) - mz_offset
|
||||||
|
|
||||||
|
# Write intensity array (as Float32)
|
||||||
|
int_offset = position(ibd_stream)
|
||||||
|
write(ibd_stream, htol.(intensity)) # intensity is Vector{Float32}
|
||||||
|
int_length = position(ibd_stream) - int_offset
|
||||||
|
|
||||||
|
push!(binary_meta_vec, BinaryMetadata(mz_offset, mz_length, int_offset, int_length))
|
||||||
|
next!(p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@info "Found and processed $empty_pixel_count empty pixels out of $(size(timing_matrix, 1)) total."
|
||||||
|
|
||||||
|
finally
|
||||||
|
close(msi_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
return binary_meta_vec, coords_vec, (width, height), pixel_modes, ibd_uuid
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -595,7 +621,7 @@ experiment and data format.
|
|||||||
# Returns
|
# Returns
|
||||||
- `true` on success, `false` on failure.
|
- `true` on success, `false` on failure.
|
||||||
"""
|
"""
|
||||||
function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, coords::Vector{Tuple{Int, Int}}, dims::Tuple{Int, Int}, mode::SpectrumMode)
|
function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, coords::Vector{Tuple{Int, Int}}, dims::Tuple{Int, Int}, modes::Vector{SpectrumMode}, ibd_uuid::UUID)
|
||||||
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
|
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
|
||||||
|
|
||||||
if isempty(binary_meta)
|
if isempty(binary_meta)
|
||||||
@ -625,6 +651,7 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
|
|||||||
<cvParam cvRef="MS" accession="MS:1000579" name="MS1 spectrum"/>
|
<cvParam cvRef="MS" accession="MS:1000579" name="MS1 spectrum"/>
|
||||||
<cvParam cvRef="IMS" accession="IMS:1000080" name="mass spectrum"/>
|
<cvParam cvRef="IMS" accession="IMS:1000080" name="mass spectrum"/>
|
||||||
<cvParam cvRef="IMS" accession="IMS:1000031" name="processed"/>
|
<cvParam cvRef="IMS" accession="IMS:1000031" name="processed"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000081" name="ibd uuid" value="$(ibd_uuid)"/>
|
||||||
</fileContent>
|
</fileContent>
|
||||||
</fileDescription>
|
</fileDescription>
|
||||||
""")
|
""")
|
||||||
@ -632,7 +659,7 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
|
|||||||
<referenceableParamGroup id="mzArray">
|
<referenceableParamGroup id="mzArray">
|
||||||
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
|
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
|
||||||
<cvParam cvRef="MS" accession="MS:1000514" name="m/z array" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>
|
<cvParam cvRef="MS" accession="MS:1000514" name="m/z array" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>
|
||||||
<cvParam cvRef="MS" accession="MS:1000521" name="32-bit float"/>
|
<cvParam cvRef="MS" accession="MS:1000523" name="64-bit float"/>
|
||||||
</referenceableParamGroup>
|
</referenceableParamGroup>
|
||||||
<referenceableParamGroup id="intensityArray">
|
<referenceableParamGroup id="intensityArray">
|
||||||
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
|
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
|
||||||
@ -659,6 +686,8 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
|
|||||||
<cvParam cvRef="IMS" accession="IMS:1000043" name="max count of pixel y" value="$(dims[2])"/>
|
<cvParam cvRef="IMS" accession="IMS:1000043" name="max count of pixel y" value="$(dims[2])"/>
|
||||||
<cvParam cvRef="IMS" accession="IMS:1000046" name="pixel size x" value="1" unitCvRef="UO" unitAccession="UO:0000017" unitName="micrometer"/>
|
<cvParam cvRef="IMS" accession="IMS:1000046" name="pixel size x" value="1" unitCvRef="UO" unitAccession="UO:0000017" unitName="micrometer"/>
|
||||||
<cvParam cvRef="IMS" accession="IMS:1000047" name="pixel size y" value="1" unitCvRef="UO" unitAccession="UO:0000017" unitName="micrometer"/>
|
<cvParam cvRef="IMS" accession="IMS:1000047" name="pixel size y" value="1" unitCvRef="UO" unitAccession="UO:0000017" unitName="micrometer"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000092" name="line scan sequence" value="line scan"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000093" name="spotsize" value="1"/>
|
||||||
</scanSettings>
|
</scanSettings>
|
||||||
</scanSettingsList>
|
</scanSettingsList>
|
||||||
""")
|
""")
|
||||||
@ -702,16 +731,16 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
|
|||||||
push!(spectrum_offsets, spectrum_start)
|
push!(spectrum_offsets, spectrum_start)
|
||||||
|
|
||||||
# Calculate number of points from byte length
|
# Calculate number of points from byte length
|
||||||
mz_points = meta.mz_length ÷ sizeof(Float32)
|
mz_points = meta.mz_length ÷ sizeof(Float64)
|
||||||
int_points = meta.int_length ÷ sizeof(Float32)
|
int_points = meta.int_length ÷ sizeof(Float32)
|
||||||
|
|
||||||
write(imzml_stream, """ <spectrum id="Scan=$(i)" defaultArrayLength="$(mz_points)" index="$(i-1)">
|
write(imzml_stream, """ <spectrum id="Scan=$(i)" defaultArrayLength="$(mz_points)" index="$(i-1)">
|
||||||
<cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="1"/>
|
<cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="1"/>
|
||||||
""")
|
""")
|
||||||
if mode == CENTROID
|
if modes[i] == CENTROID
|
||||||
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000127" name="centroid spectrum"/>
|
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000127" name="centroid spectrum"/>
|
||||||
""")
|
""")
|
||||||
else
|
elseif modes[i] == PROFILE
|
||||||
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum"/>
|
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum"/>
|
||||||
""")
|
""")
|
||||||
end
|
end
|
||||||
@ -771,6 +800,10 @@ Main workflow function to convert a .mzML file to an .imzML file.
|
|||||||
* `img_height`: height dimention for the creation of the y axis
|
* `img_height`: height dimention for the creation of the y axis
|
||||||
"""
|
"""
|
||||||
function ImportMzmlFile(source_file::String, sync_file::String, target_file::String; img_width::Int=0, img_height::Int=0)
|
function ImportMzmlFile(source_file::String, sync_file::String, target_file::String; img_width::Int=0, img_height::Int=0)
|
||||||
|
if !isfile(source_file)
|
||||||
|
throw(ArgumentError("Source mzML file not found: $source_file"))
|
||||||
|
end
|
||||||
|
|
||||||
println("Step 1: Getting scan times from .mzML file...")
|
println("Step 1: Getting scan times from .mzML file...")
|
||||||
scans = GetMzmlScanTime(source_file)
|
scans = GetMzmlScanTime(source_file)
|
||||||
|
|
||||||
@ -779,13 +812,13 @@ function ImportMzmlFile(source_file::String, sync_file::String, target_file::Str
|
|||||||
|
|
||||||
println("Step 3: Converting spectra and writing .ibd file...")
|
println("Step 3: Converting spectra and writing .ibd file...")
|
||||||
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
|
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
|
||||||
binary_meta, coords, (width, height), source_mode = ConvertMzmlToImzml(source_file, ibd_file, timing_matrix, scans)
|
binary_meta, coords, (width, height), pixel_modes, ibd_uuid = ConvertMzmlToImzml(source_file, ibd_file, timing_matrix, scans)
|
||||||
|
|
||||||
# Flip image vertically to match R script output
|
# Flip image vertically to match R script output
|
||||||
flipped_coords = [(x, height - y + 1) for (x, y) in coords]
|
flipped_coords = [(x, height - y + 1) for (x, y) in coords]
|
||||||
|
|
||||||
println("Step 4: Exporting .imzML metadata file...")
|
println("Step 4: Exporting .imzML metadata file...")
|
||||||
success = ExportImzml(target_file, binary_meta, flipped_coords, (width, height), source_mode)
|
success = ExportImzml(target_file, binary_meta, flipped_coords, (width, height), pixel_modes, ibd_uuid)
|
||||||
|
|
||||||
if success
|
if success
|
||||||
println("Conversion successful: $target_file")
|
println("Conversion successful: $target_file")
|
||||||
|
|||||||
275
src/imzML.jl
275
src/imzML.jl
@ -1,11 +1,8 @@
|
|||||||
# src/imzML.jl
|
# src/imzML.jl
|
||||||
using Images, Statistics, CairoMakie, DataFrames, Printf, ColorSchemes, StatsBase
|
using Images, Statistics, CairoMakie, DataFrames, Printf, ColorSchemes, StatsBase
|
||||||
|
|
||||||
# --- Extracted from imzML.jl ---
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This file provides a library for parsing `.imzML` and `.ibd` files in pure Julia.
|
This file provides a library for parsing `.imzML` and `.ibd` files in pure Julia.
|
||||||
It is intended to be included by a parent script.
|
|
||||||
|
|
||||||
Core Functions:
|
Core Functions:
|
||||||
- `load_imzml_lazy`: The main function that orchestrates the parsing.
|
- `load_imzml_lazy`: The main function that orchestrates the parsing.
|
||||||
@ -146,6 +143,37 @@ function get_spectrum_attributes(stream::IO, hIbd::IO)
|
|||||||
return skip
|
return skip
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function get_spectrum_attributes_v2(stream::IO, hIbd::IO)
|
||||||
|
# Look for position x
|
||||||
|
readuntil(stream, "IMS:1000050")
|
||||||
|
x_skip = 0
|
||||||
|
|
||||||
|
# Look for position y
|
||||||
|
readuntil(stream, "IMS:1000051")
|
||||||
|
y_skip = 0
|
||||||
|
|
||||||
|
# Determine order of mz/intensity arrays
|
||||||
|
pos_before = position(stream)
|
||||||
|
|
||||||
|
# Look for first external offset (could be mz or intensity)
|
||||||
|
readuntil(stream, "external offset")
|
||||||
|
current_line = readline(stream)
|
||||||
|
|
||||||
|
# Check which array comes first by looking at the param group reference
|
||||||
|
mz_first = occursin("mzArray", current_line) ? 3 : 4
|
||||||
|
|
||||||
|
# Find array length - look for the first external array length after coordinates
|
||||||
|
seek(stream, pos_before)
|
||||||
|
readuntil(stream, "IMS:1000103")
|
||||||
|
array_len_skip = 0
|
||||||
|
|
||||||
|
# Find spectrum end
|
||||||
|
readuntil(stream, "</spectrum>")
|
||||||
|
spectrum_end_skip = 0
|
||||||
|
|
||||||
|
return [x_skip, y_skip, mz_first, array_len_skip, spectrum_end_skip]
|
||||||
|
end
|
||||||
|
|
||||||
function determine_parser(stream::IO, mz_is_compressed::Bool, int_is_compressed::Bool)
|
function determine_parser(stream::IO, mz_is_compressed::Bool, int_is_compressed::Bool)
|
||||||
start_pos = position(stream)
|
start_pos = position(stream)
|
||||||
spectrum_xml = ""
|
spectrum_xml = ""
|
||||||
@ -200,13 +228,13 @@ end
|
|||||||
function load_imzml_lazy(file_path::String; cache_size::Int=100)
|
function load_imzml_lazy(file_path::String; cache_size::Int=100)
|
||||||
println("DEBUG: Checking for .imzML file at $file_path")
|
println("DEBUG: Checking for .imzML file at $file_path")
|
||||||
if !isfile(file_path)
|
if !isfile(file_path)
|
||||||
error("Provided path is not a file: $(file_path)")
|
throw(FileFormatError("Provided path is not a file: $(file_path)"))
|
||||||
end
|
end
|
||||||
|
|
||||||
ibd_path = replace(file_path, r"\.(imzML|mzML)"i => ".ibd")
|
ibd_path = replace(file_path, r"\.(imzML|mzML)"i => ".ibd")
|
||||||
println("DEBUG: Checking for .ibd file at $ibd_path")
|
println("DEBUG: Checking for .ibd file at $ibd_path")
|
||||||
if !isfile(ibd_path)
|
if !isfile(ibd_path)
|
||||||
error("Corresponding .ibd file not found for: $(file_path)")
|
throw(FileFormatError("Corresponding .ibd file not found for: $(file_path)"))
|
||||||
end
|
end
|
||||||
|
|
||||||
println("DEBUG: Opening file streams for .imzML and .ibd")
|
println("DEBUG: Opening file streams for .imzML and .ibd")
|
||||||
@ -262,6 +290,7 @@ function load_imzml_lazy(file_path::String; cache_size::Int=100)
|
|||||||
spectra_metadata = parse_neofx(stream, hIbd, param_groups, width, height, num_spectra,
|
spectra_metadata = parse_neofx(stream, hIbd, param_groups, width, height, num_spectra,
|
||||||
default_mz_format, default_intensity_format,
|
default_mz_format, default_intensity_format,
|
||||||
mz_is_compressed, int_is_compressed, global_mode)
|
mz_is_compressed, int_is_compressed, global_mode)
|
||||||
|
#=
|
||||||
elseif parser_type == :compressed
|
elseif parser_type == :compressed
|
||||||
println("DEBUG: Using compressed parser.")
|
println("DEBUG: Using compressed parser.")
|
||||||
spectra_metadata = parse_compressed(stream, hIbd, param_groups, width, height, num_spectra,
|
spectra_metadata = parse_compressed(stream, hIbd, param_groups, width, height, num_spectra,
|
||||||
@ -273,6 +302,13 @@ function load_imzml_lazy(file_path::String; cache_size::Int=100)
|
|||||||
default_mz_format, default_intensity_format,
|
default_mz_format, default_intensity_format,
|
||||||
mz_is_compressed, int_is_compressed, global_mode)
|
mz_is_compressed, int_is_compressed, global_mode)
|
||||||
end
|
end
|
||||||
|
=#
|
||||||
|
else
|
||||||
|
println("DEBUG: Using compressed parser.")
|
||||||
|
spectra_metadata = parse_compressed(stream, hIbd, param_groups, width, height, num_spectra,
|
||||||
|
default_mz_format, default_intensity_format,
|
||||||
|
mz_is_compressed, int_is_compressed, global_mode)
|
||||||
|
end
|
||||||
|
|
||||||
println("DEBUG: Metadata parsing complete.")
|
println("DEBUG: Metadata parsing complete.")
|
||||||
|
|
||||||
@ -306,37 +342,46 @@ function parse_uncompressed(stream::IO, hIbd::IO, param_groups::Dict{String, Spe
|
|||||||
width::Int32, height::Int32, num_spectra::Int32,
|
width::Int32, height::Int32, num_spectra::Int32,
|
||||||
mz_format::DataType, intensity_format::DataType,
|
mz_format::DataType, intensity_format::DataType,
|
||||||
mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode)
|
mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode)
|
||||||
# Your existing working skip-based parser
|
|
||||||
println("DEBUG: Learning file structure from first spectrum...")
|
println("DEBUG: Learning file structure from first spectrum...")
|
||||||
start_of_spectra_xml = position(stream)
|
start_of_spectra_xml = position(stream)
|
||||||
attr = get_spectrum_attributes(stream, hIbd)
|
|
||||||
|
# Skip UUID at beginning of IBD file (from converter)
|
||||||
|
seek(hIbd, 16)
|
||||||
current_ibd_offset = position(hIbd)
|
current_ibd_offset = position(hIbd)
|
||||||
|
|
||||||
|
attr = get_spectrum_attributes_v2(stream, hIbd)
|
||||||
seek(stream, start_of_spectra_xml)
|
seek(stream, start_of_spectra_xml)
|
||||||
println("DEBUG: Initial IBD offset: $current_ibd_offset")
|
println("DEBUG: Initial IBD offset (after UUID): $current_ibd_offset")
|
||||||
|
|
||||||
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
||||||
mz_is_first = attr[3] == 3
|
mz_is_first = attr[3] == 3
|
||||||
|
|
||||||
for k in 1:num_spectra
|
for k in 1:num_spectra
|
||||||
# Store the start position of this spectrum for mode detection
|
|
||||||
spectrum_start_pos = position(stream)
|
spectrum_start_pos = position(stream)
|
||||||
|
|
||||||
# Use skip values learned from the first spectrum
|
# Find X coordinate
|
||||||
skip(stream, attr[5]) # Skip to X coordinate value
|
line = readuntil(stream, "IMS:1000050")
|
||||||
|
if eof(stream)
|
||||||
|
error("Unexpected EOF while looking for X coordinate")
|
||||||
|
end
|
||||||
val_tag_x = find_tag(stream, r"value=\"(\d+)\"")
|
val_tag_x = find_tag(stream, r"value=\"(\d+)\"")
|
||||||
x = parse(Int32, val_tag_x.captures[1])
|
x = parse(Int32, val_tag_x.captures[1])
|
||||||
|
|
||||||
skip(stream, attr[6]) # Skip to Y coordinate value
|
# Find Y coordinate
|
||||||
|
line = readuntil(stream, "IMS:1000051")
|
||||||
val_tag_y = find_tag(stream, r"value=\"(\d+)\"")
|
val_tag_y = find_tag(stream, r"value=\"(\d+)\"")
|
||||||
y = parse(Int32, val_tag_y.captures[1])
|
y = parse(Int32, val_tag_y.captures[1])
|
||||||
|
|
||||||
skip(stream, attr[7]) # Skip to array length value
|
# Find array length - look for external array length
|
||||||
|
line = readuntil(stream, "IMS:1000103")
|
||||||
val_tag_len = find_tag(stream, r"value=\"(\d+)\"")
|
val_tag_len = find_tag(stream, r"value=\"(\d+)\"")
|
||||||
nPoints = parse(Int32, val_tag_len.captures[1])
|
nPoints = parse(Int32, val_tag_len.captures[1])
|
||||||
|
|
||||||
# For uncompressed data, use simple calculation
|
# CRITICAL FIX: Use the actual formats from the XML, not hardcoded values
|
||||||
mz_len_bytes = nPoints * sizeof(mz_format)
|
# The converter writes Float64 for mz, Float32 for intensity
|
||||||
int_len_bytes = nPoints * sizeof(intensity_format)
|
mz_len_bytes = nPoints * sizeof(Float64) # Converter uses Float64 for mz
|
||||||
|
int_len_bytes = nPoints * sizeof(Float32) # Converter uses Float32 for intensity
|
||||||
|
|
||||||
local mz_offset, int_offset
|
local mz_offset, int_offset
|
||||||
if mz_is_first
|
if mz_is_first
|
||||||
@ -347,7 +392,7 @@ function parse_uncompressed(stream::IO, hIbd::IO, param_groups::Dict{String, Spe
|
|||||||
mz_offset = int_offset + int_len_bytes
|
mz_offset = int_offset + int_len_bytes
|
||||||
end
|
end
|
||||||
|
|
||||||
# Mode detection from spectrum XML
|
# Mode detection
|
||||||
current_pos = position(stream)
|
current_pos = position(stream)
|
||||||
seek(stream, spectrum_start_pos)
|
seek(stream, spectrum_start_pos)
|
||||||
spectrum_buffer = IOBuffer()
|
spectrum_buffer = IOBuffer()
|
||||||
@ -369,14 +414,16 @@ function parse_uncompressed(stream::IO, hIbd::IO, param_groups::Dict{String, Spe
|
|||||||
end
|
end
|
||||||
seek(stream, current_pos)
|
seek(stream, current_pos)
|
||||||
|
|
||||||
# Create SpectrumAsset objects
|
# CRITICAL FIX: Use correct data types that match the converter output
|
||||||
mz_asset = SpectrumAsset(mz_format, mz_is_compressed, mz_offset, nPoints, :mz)
|
mz_asset = SpectrumAsset(Float64, mz_is_compressed, mz_offset, nPoints, :mz)
|
||||||
int_asset = SpectrumAsset(intensity_format, int_is_compressed, int_offset, nPoints, :intensity)
|
int_asset = SpectrumAsset(Float32, int_is_compressed, int_offset, nPoints, :intensity)
|
||||||
|
|
||||||
spectra_metadata[k] = SpectrumMetadata(x, y, "", spectrum_mode, mz_asset, int_asset)
|
spectra_metadata[k] = SpectrumMetadata(x, y, "", spectrum_mode, mz_asset, int_asset)
|
||||||
|
|
||||||
current_ibd_offset += mz_len_bytes + int_len_bytes
|
current_ibd_offset += mz_len_bytes + int_len_bytes
|
||||||
skip(stream, attr[8]) # Skip to the end of the spectrum tag
|
|
||||||
|
# Skip to next spectrum
|
||||||
|
line = readuntil(stream, "</spectrum>")
|
||||||
end
|
end
|
||||||
|
|
||||||
return spectra_metadata
|
return spectra_metadata
|
||||||
@ -385,8 +432,7 @@ end
|
|||||||
function parse_compressed(stream::IO, hIbd::IO, param_groups::Dict{String, SpecDim},
|
function parse_compressed(stream::IO, hIbd::IO, param_groups::Dict{String, SpecDim},
|
||||||
width::Int32, height::Int32, num_spectra::Int32,
|
width::Int32, height::Int32, num_spectra::Int32,
|
||||||
default_mz_format::DataType, default_intensity_format::DataType,
|
default_mz_format::DataType, default_intensity_format::DataType,
|
||||||
mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode) # Changed Int64 to SpectrumMode
|
mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode)
|
||||||
# New parser for compressed data
|
|
||||||
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
||||||
|
|
||||||
# Use concrete type for array_data
|
# Use concrete type for array_data
|
||||||
@ -480,29 +526,35 @@ function parse_compressed(stream::IO, hIbd::IO, param_groups::Dict{String, SpecD
|
|||||||
mz_data = filter(d -> d.is_mz, array_data)
|
mz_data = filter(d -> d.is_mz, array_data)
|
||||||
int_data = filter(d -> !d.is_mz, array_data)
|
int_data = filter(d -> !d.is_mz, array_data)
|
||||||
|
|
||||||
|
# FIX: Handle empty spectra gracefully instead of throwing errors
|
||||||
|
current_ibd_pos = position(hIbd)
|
||||||
|
|
||||||
if length(mz_data) != 1 || length(int_data) != 1
|
if length(mz_data) != 1 || length(int_data) != 1
|
||||||
error("Spectrum $k: Expected exactly one m/z and one intensity array")
|
# Create placeholder metadata for empty/invalid spectrum
|
||||||
|
println("DEBUG: Spectrum $k is empty or invalid - creating placeholder metadata")
|
||||||
|
mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, current_ibd_pos, 0, :mz)
|
||||||
|
int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, current_ibd_pos, 0, :intensity)
|
||||||
|
else
|
||||||
|
mz_info = mz_data[1]
|
||||||
|
int_info = int_data[1]
|
||||||
|
|
||||||
|
# DEBUG: Print first spectrum details
|
||||||
|
if k == 1
|
||||||
|
println("DEBUG First spectrum parsed:")
|
||||||
|
println(" Coordinates: x=$x, y=$y")
|
||||||
|
println(" Mode: $spectrum_mode")
|
||||||
|
println(" m/z array: array_length=$(mz_info.array_length), encoded_length=$(mz_info.encoded_length), offset=$(mz_info.offset)")
|
||||||
|
println(" intensity array: array_length=$(int_info.array_length), encoded_length=$(int_info.encoded_length), offset=$(int_info.offset)")
|
||||||
|
println(" Expected m/z bytes: $(mz_info.array_length * sizeof(default_mz_format))")
|
||||||
|
println(" Expected intensity bytes: $(int_info.array_length * sizeof(default_intensity_format))")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create SpectrumAsset objects
|
||||||
|
mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, mz_info.offset,
|
||||||
|
mz_is_compressed ? mz_info.encoded_length : mz_info.array_length, :mz)
|
||||||
|
int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, int_info.offset,
|
||||||
|
int_is_compressed ? int_info.encoded_length : int_info.array_length, :intensity)
|
||||||
end
|
end
|
||||||
|
|
||||||
mz_info = mz_data[1]
|
|
||||||
int_info = int_data[1]
|
|
||||||
|
|
||||||
# DEBUG: Print first spectrum details
|
|
||||||
if k == 1
|
|
||||||
println("DEBUG First spectrum parsed:")
|
|
||||||
println(" Coordinates: x=$x, y=$y")
|
|
||||||
println(" Mode: $spectrum_mode")
|
|
||||||
println(" m/z array: array_length=$(mz_info.array_length), encoded_length=$(mz_info.encoded_length), offset=$(mz_info.offset)")
|
|
||||||
println(" intensity array: array_length=$(int_info.array_length), encoded_length=$(int_info.encoded_length), offset=$(int_info.offset)")
|
|
||||||
println(" Expected m/z bytes: $(mz_info.array_length * sizeof(default_mz_format))")
|
|
||||||
println(" Expected intensity bytes: $(int_info.array_length * sizeof(default_intensity_format))")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create SpectrumAsset objects
|
|
||||||
mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, mz_info.offset,
|
|
||||||
mz_is_compressed ? mz_info.encoded_length : mz_info.array_length, :mz)
|
|
||||||
int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, int_info.offset,
|
|
||||||
int_is_compressed ? int_info.encoded_length : int_info.array_length, :intensity)
|
|
||||||
|
|
||||||
spectra_metadata[k] = SpectrumMetadata(x, y, "", spectrum_mode, mz_asset, int_asset)
|
spectra_metadata[k] = SpectrumMetadata(x, y, "", spectrum_mode, mz_asset, int_asset)
|
||||||
end
|
end
|
||||||
@ -514,7 +566,6 @@ function parse_neofx(stream::IO, hIbd::IO, param_groups::Dict{String, SpecDim},
|
|||||||
width::Int32, height::Int32, num_spectra::Int32,
|
width::Int32, height::Int32, num_spectra::Int32,
|
||||||
default_mz_format::DataType, default_intensity_format::DataType,
|
default_mz_format::DataType, default_intensity_format::DataType,
|
||||||
mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode)
|
mz_is_compressed::Bool, int_is_compressed::Bool, global_mode::SpectrumMode)
|
||||||
# New parser for compressed data
|
|
||||||
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
||||||
|
|
||||||
array_data_type = @NamedTuple{is_mz::Bool, array_length::Int32, encoded_length::Int64, offset::Int64}
|
array_data_type = @NamedTuple{is_mz::Bool, array_length::Int32, encoded_length::Int64, offset::Int64}
|
||||||
@ -607,29 +658,35 @@ function parse_neofx(stream::IO, hIbd::IO, param_groups::Dict{String, SpecDim},
|
|||||||
mz_data = filter(d -> d.is_mz, array_data)
|
mz_data = filter(d -> d.is_mz, array_data)
|
||||||
int_data = filter(d -> !d.is_mz, array_data)
|
int_data = filter(d -> !d.is_mz, array_data)
|
||||||
|
|
||||||
|
# FIX: Handle empty spectra gracefully instead of throwing errors
|
||||||
|
current_ibd_pos = position(hIbd)
|
||||||
|
|
||||||
if length(mz_data) != 1 || length(int_data) != 1
|
if length(mz_data) != 1 || length(int_data) != 1
|
||||||
error("Spectrum $k: Expected exactly one m/z and one intensity array")
|
# Create placeholder metadata for empty/invalid spectrum
|
||||||
|
println("DEBUG: Spectrum $k is empty or invalid - creating placeholder metadata")
|
||||||
|
mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, current_ibd_pos, 0, :mz)
|
||||||
|
int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, current_ibd_pos, 0, :intensity)
|
||||||
|
else
|
||||||
|
mz_info = mz_data[1]
|
||||||
|
int_info = int_data[1]
|
||||||
|
|
||||||
|
# DEBUG: Print first spectrum details
|
||||||
|
if k == 1
|
||||||
|
println("DEBUG First spectrum parsed:")
|
||||||
|
println(" Coordinates: x=$x, y=$y")
|
||||||
|
println(" Mode: $spectrum_mode")
|
||||||
|
println(" m/z array: array_length=$(mz_info.array_length), encoded_length=$(mz_info.encoded_length), offset=$(mz_info.offset)")
|
||||||
|
println(" intensity array: array_length=$(int_info.array_length), encoded_length=$(int_info.encoded_length), offset=$(int_info.offset)")
|
||||||
|
println(" Expected m/z bytes: $(mz_info.array_length * sizeof(default_mz_format))")
|
||||||
|
println(" Expected intensity bytes: $(int_info.array_length * sizeof(default_intensity_format))")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create SpectrumAsset objects
|
||||||
|
mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, mz_info.offset,
|
||||||
|
mz_is_compressed ? mz_info.encoded_length : mz_info.array_length, :mz)
|
||||||
|
int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, int_info.offset,
|
||||||
|
int_is_compressed ? int_info.encoded_length : int_info.array_length, :intensity)
|
||||||
end
|
end
|
||||||
|
|
||||||
mz_info = mz_data[1]
|
|
||||||
int_info = int_data[1]
|
|
||||||
|
|
||||||
# DEBUG: Print first spectrum details
|
|
||||||
if k == 1
|
|
||||||
println("DEBUG First spectrum parsed:")
|
|
||||||
println(" Coordinates: x=$x, y=$y")
|
|
||||||
println(" Mode: $spectrum_mode")
|
|
||||||
println(" m/z array: array_length=$(mz_info.array_length), encoded_length=$(mz_info.encoded_length), offset=$(mz_info.offset)")
|
|
||||||
println(" intensity array: array_length=$(int_info.array_length), encoded_length=$(int_info.encoded_length), offset=$(int_info.offset)")
|
|
||||||
println(" Expected m/z bytes: $(mz_info.array_length * sizeof(default_mz_format))")
|
|
||||||
println(" Expected intensity bytes: $(int_info.array_length * sizeof(default_intensity_format))")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create SpectrumAsset objects
|
|
||||||
mz_asset = SpectrumAsset(default_mz_format, mz_is_compressed, mz_info.offset,
|
|
||||||
mz_is_compressed ? mz_info.encoded_length : mz_info.array_length, :mz)
|
|
||||||
int_asset = SpectrumAsset(default_intensity_format, int_is_compressed, int_info.offset,
|
|
||||||
int_is_compressed ? int_info.encoded_length : int_info.array_length, :intensity)
|
|
||||||
|
|
||||||
spectra_metadata[k] = SpectrumMetadata(x, y, "", spectrum_mode, mz_asset, int_asset)
|
spectra_metadata[k] = SpectrumMetadata(x, y, "", spectrum_mode, mz_asset, int_asset)
|
||||||
end
|
end
|
||||||
@ -656,6 +713,11 @@ This optimized version uses binary search for efficiency.
|
|||||||
"""
|
"""
|
||||||
function find_mass(mz_array::AbstractVector{<:Real}, intensity_array::AbstractVector{<:Real},
|
function find_mass(mz_array::AbstractVector{<:Real}, intensity_array::AbstractVector{<:Real},
|
||||||
target_mass::Real, tolerance::Real)
|
target_mass::Real, tolerance::Real)
|
||||||
|
# Fast-path rejection: if the array is empty or the target is out of range
|
||||||
|
if isempty(mz_array) || target_mass + tolerance < first(mz_array) || target_mass - tolerance > last(mz_array)
|
||||||
|
return 0.0
|
||||||
|
end
|
||||||
|
|
||||||
lower_bound = target_mass - tolerance
|
lower_bound = target_mass - tolerance
|
||||||
upper_bound = target_mass + tolerance
|
upper_bound = target_mass + tolerance
|
||||||
|
|
||||||
@ -811,9 +873,12 @@ This is a highly performant function that iterates through the full dataset only
|
|||||||
function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, tolerance::Real; mask_path::Union{String, Nothing}=nothing)
|
function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, tolerance::Real; mask_path::Union{String, Nothing}=nothing)
|
||||||
width, height = data.image_dims
|
width, height = data.image_dims
|
||||||
|
|
||||||
|
# Sort masses to improve cache locality during search
|
||||||
|
sorted_masses = sort(masses)
|
||||||
|
|
||||||
# 1. Initialize a dictionary to hold the output slice matrices
|
# 1. Initialize a dictionary to hold the output slice matrices
|
||||||
slice_dict = Dict{Real, Matrix{Float64}}()
|
slice_dict = Dict{Real, Matrix{Float64}}()
|
||||||
for mass in masses
|
for mass in sorted_masses
|
||||||
slice_dict[mass] = zeros(Float64, height, width)
|
slice_dict[mass] = zeros(Float64, height, width)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -836,7 +901,7 @@ function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, t
|
|||||||
indices_to_check = masked_indices === nothing ? (1:length(data.spectra_metadata)) : masked_indices
|
indices_to_check = masked_indices === nothing ? (1:length(data.spectra_metadata)) : masked_indices
|
||||||
|
|
||||||
# 3. Find all spectra that could contain *any* of the requested masses.
|
# 3. Find all spectra that could contain *any* of the requested masses.
|
||||||
for mass in masses
|
for mass in sorted_masses
|
||||||
target_min = mass - tolerance
|
target_min = mass - tolerance
|
||||||
target_max = mass + tolerance
|
target_max = mass + tolerance
|
||||||
for i in indices_to_check
|
for i in indices_to_check
|
||||||
@ -858,7 +923,7 @@ function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, t
|
|||||||
_iterate_spectra_fast(data, collect(candidate_indices)) do idx, mz_array, intensity_array
|
_iterate_spectra_fast(data, collect(candidate_indices)) do idx, mz_array, intensity_array
|
||||||
meta = data.spectra_metadata[idx]
|
meta = data.spectra_metadata[idx]
|
||||||
# For this single spectrum, check all masses of interest
|
# For this single spectrum, check all masses of interest
|
||||||
for mass in masses
|
for mass in sorted_masses
|
||||||
# Check if this spectrum's range actually covers the current mass
|
# Check if this spectrum's range actually covers the current mass
|
||||||
# This is a finer-grained check than the initial filtering
|
# This is a finer-grained check than the initial filtering
|
||||||
if !isempty(mz_array) && (mass + tolerance) >= first(mz_array) && (mass - tolerance) <= last(mz_array)
|
if !isempty(mz_array) && (mass + tolerance) >= first(mz_array) && (mass - tolerance) <= last(mz_array)
|
||||||
@ -873,7 +938,7 @@ function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, t
|
|||||||
end
|
end
|
||||||
|
|
||||||
# 5. Clean up and return
|
# 5. Clean up and return
|
||||||
for mass in masses
|
for mass in sorted_masses
|
||||||
replace!(slice_dict[mass], NaN => 0.0)
|
replace!(slice_dict[mass], NaN => 0.0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1066,24 +1131,21 @@ within these bounds.
|
|||||||
function TrIQ(pixMap::AbstractMatrix{<:Real}, depth::Integer, prob::Real=0.98; mask_matrix::Union{BitMatrix, Nothing}=nothing)
|
function TrIQ(pixMap::AbstractMatrix{<:Real}, depth::Integer, prob::Real=0.98; mask_matrix::Union{BitMatrix, Nothing}=nothing)
|
||||||
local values_for_thres
|
local values_for_thres
|
||||||
if mask_matrix !== nothing
|
if mask_matrix !== nothing
|
||||||
# Extract values only from the masked region for threshold calculation
|
|
||||||
values_for_thres = pixMap[mask_matrix]
|
values_for_thres = pixMap[mask_matrix]
|
||||||
# Filter out only zeros created by the mask.
|
|
||||||
filter!(x -> x != 0, values_for_thres)
|
filter!(x -> x != 0, values_for_thres)
|
||||||
else
|
else
|
||||||
values_for_thres = pixMap
|
values_for_thres = pixMap
|
||||||
end
|
end
|
||||||
|
|
||||||
# If the relevant values are empty or all zero, return a zero matrix
|
|
||||||
if isempty(values_for_thres) || all(iszero, values_for_thres)
|
if isempty(values_for_thres) || all(iszero, values_for_thres)
|
||||||
bounds = (0.0, 0.0)
|
bounds = (0.0, 0.0)
|
||||||
|
quantized = zeros(UInt8, size(pixMap))
|
||||||
else
|
else
|
||||||
# Compute new dynamic range from the (potentially filtered) values
|
|
||||||
bounds = get_outlier_thres(values_for_thres, prob)
|
bounds = get_outlier_thres(values_for_thres, prob)
|
||||||
|
quantized = set_pixel_depth(pixMap, bounds, depth)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set intensity dynamic range for the *entire* pixMap, using bounds derived from masked data
|
return quantized, bounds # Return both!
|
||||||
return set_pixel_depth(pixMap, bounds, depth)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -1134,22 +1196,18 @@ function quantize_intensity(slice::AbstractMatrix{<:Real}, levels::Integer=256;
|
|||||||
end
|
end
|
||||||
|
|
||||||
if max_val <= 0
|
if max_val <= 0
|
||||||
return zeros(UInt8, size(slice))
|
bounds = (0.0, 0.0)
|
||||||
|
quantized = zeros(UInt8, size(slice))
|
||||||
|
else
|
||||||
|
bounds = (0.0, max_val)
|
||||||
|
quantized = similar(slice, UInt8)
|
||||||
|
scale = (levels - 1) / max_val
|
||||||
|
@inbounds for i in eachindex(slice)
|
||||||
|
quantized[i] = round(UInt8, clamp(slice[i] * scale, 0, levels - 1))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Scale relative to the absolute maximum value to preserve the zero point.
|
return quantized, bounds # Return both!
|
||||||
# The original logic used 'colorLevel' which was the max value (e.g., 255).
|
|
||||||
scale = (levels - 1) / max_val
|
|
||||||
|
|
||||||
# Pre-allocate result for better performance
|
|
||||||
result = similar(slice, UInt8)
|
|
||||||
|
|
||||||
# Use inbounds for faster access
|
|
||||||
@inbounds for i in eachindex(slice)
|
|
||||||
result[i] = round(UInt8, clamp(slice[i] * scale, 0, levels - 1))
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -1523,28 +1581,25 @@ end
|
|||||||
# Viridis color palette (256 colors) - defined as constant
|
# Viridis color palette (256 colors) - defined as constant
|
||||||
const ViridisPalette = generate_palette(ColorSchemes.viridis)
|
const ViridisPalette = generate_palette(ColorSchemes.viridis)
|
||||||
|
|
||||||
function generate_colorbar_image(slice_data::AbstractMatrix, color_levels::Int, output_path::String; use_triq::Bool=false, triq_prob::Float64=0.98, mask_path::Union{String, Nothing}=nothing)
|
function generate_colorbar_image(slice_data::AbstractMatrix, color_levels::Int, output_path::String,
|
||||||
# 1. Determine bounds based on whether TrIQ is used and if a mask is applied
|
bounds::Tuple{Float64, Float64};
|
||||||
local data_for_bounds
|
use_triq::Bool=false, triq_prob::Float64=0.98,
|
||||||
if mask_path !== nothing
|
mask_path::Union{String, Nothing}=nothing)
|
||||||
height, width = size(slice_data)
|
# Use the provided bounds instead of recalculating
|
||||||
mask_matrix = load_and_prepare_mask(mask_path, (width, height))
|
min_val, max_val = bounds
|
||||||
# Extract only the non-zero values within the mask to accurately calculate the range
|
|
||||||
data_for_bounds = slice_data[mask_matrix]
|
# If bounds are invalid, calculate fallback
|
||||||
filter!(x -> x > 0, data_for_bounds)
|
if min_val == max_val == 0.0
|
||||||
else
|
local data_for_bounds
|
||||||
data_for_bounds = vec(slice_data)
|
if mask_path !== nothing
|
||||||
end
|
height, width = size(slice_data)
|
||||||
|
mask_matrix = load_and_prepare_mask(mask_path, (width, height))
|
||||||
# Handle cases where data_for_bounds might be empty after filtering
|
data_for_bounds = slice_data[mask_matrix]
|
||||||
if isempty(data_for_bounds)
|
filter!(x -> x > 0, data_for_bounds)
|
||||||
min_val, max_val = 0.0, 1.0 # Default range if no data in ROI
|
|
||||||
else
|
|
||||||
min_val, max_val = if use_triq
|
|
||||||
get_outlier_thres(data_for_bounds, triq_prob)
|
|
||||||
else
|
else
|
||||||
extrema(data_for_bounds)
|
data_for_bounds = vec(slice_data)
|
||||||
end
|
end
|
||||||
|
min_val, max_val = isempty(data_for_bounds) ? (0.0, 1.0) : extrema(data_for_bounds)
|
||||||
end
|
end
|
||||||
|
|
||||||
# 2. Replicate the tick calculation logic from plot_slices
|
# 2. Replicate the tick calculation logic from plot_slices
|
||||||
|
|||||||
17
src/mzML.jl
17
src/mzML.jl
@ -36,10 +36,13 @@ determine the data type, compression, and axis type.
|
|||||||
function get_spectrum_asset_metadata(stream::IO)
|
function get_spectrum_asset_metadata(stream::IO)
|
||||||
start_pos = position(stream)
|
start_pos = position(stream)
|
||||||
|
|
||||||
bda_tag = find_tag(stream, r"<binaryDataArray\s+encodedLength=\"(\d+)\"" )
|
bda_tag = find_tag(stream, r"<binaryDataArray\s+encodedLength=\"(\d+)\"")
|
||||||
if bda_tag === nothing
|
|
||||||
error("Cannot find binaryDataArray")
|
if bda_tag === nothing
|
||||||
end
|
|
||||||
|
throw(FileFormatError("Cannot find binaryDataArray"))
|
||||||
|
|
||||||
|
end
|
||||||
encoded_length = parse(Int32, bda_tag.captures[1])
|
encoded_length = parse(Int32, bda_tag.captures[1])
|
||||||
|
|
||||||
# Initialize parameters as separate variables with concrete types
|
# Initialize parameters as separate variables with concrete types
|
||||||
@ -202,7 +205,7 @@ function find_index_offset(stream::IO)::Int64
|
|||||||
|
|
||||||
index_offset_match = match(r"<indexListOffset>(\d+)</indexListOffset>", footer)
|
index_offset_match = match(r"<indexListOffset>(\d+)</indexListOffset>", footer)
|
||||||
if index_offset_match === nothing
|
if index_offset_match === nothing
|
||||||
error("Could not find <indexListOffset>. File may not be an indexed mzML.")
|
throw(FileFormatError("Could not find <indexListOffset>. File may not be an indexed mzML."))
|
||||||
end
|
end
|
||||||
|
|
||||||
return parse(Int64, index_offset_match.captures[1])
|
return parse(Int64, index_offset_match.captures[1])
|
||||||
@ -235,14 +238,14 @@ function load_mzml_lazy(file_path::String; cache_size::Int=100)
|
|||||||
|
|
||||||
println("DEBUG: Searching for '<index name=\"spectrum\">'.")
|
println("DEBUG: Searching for '<index name=\"spectrum\">'.")
|
||||||
if find_tag(stream, r"<index\s+name=\"spectrum\"") === nothing
|
if find_tag(stream, r"<index\s+name=\"spectrum\"") === nothing
|
||||||
error("Could not find spectrum index.")
|
throw(FileFormatError("Could not find spectrum index."))
|
||||||
end
|
end
|
||||||
println("DEBUG: Found spectrum index tag.")
|
println("DEBUG: Found spectrum index tag.")
|
||||||
|
|
||||||
println("DEBUG: Parsing spectrum offsets...")
|
println("DEBUG: Parsing spectrum offsets...")
|
||||||
spectrum_offsets = parse_offset_list(stream)
|
spectrum_offsets = parse_offset_list(stream)
|
||||||
if isempty(spectrum_offsets)
|
if isempty(spectrum_offsets)
|
||||||
error("No spectrum offsets found.")
|
throw(FileFormatError("No spectrum offsets found."))
|
||||||
end
|
end
|
||||||
num_spectra = length(spectrum_offsets)
|
num_spectra = length(spectrum_offsets)
|
||||||
println("DEBUG: Found $num_spectra spectrum offsets.")
|
println("DEBUG: Found $num_spectra spectrum offsets.")
|
||||||
|
|||||||
@ -13,6 +13,9 @@ using Genie
|
|||||||
# Load and configure Genie
|
# Load and configure Genie
|
||||||
Genie.loadapp()
|
Genie.loadapp()
|
||||||
|
|
||||||
|
# Remove html parser error discrepancy
|
||||||
|
redirect_stderr(devnull)
|
||||||
|
|
||||||
# Start the Genie server
|
# Start the Genie server
|
||||||
@async begin
|
@async begin
|
||||||
up(host="127.0.0.1", port=1481)
|
up(host="127.0.0.1", port=1481)
|
||||||
|
|||||||
@ -76,8 +76,8 @@ const COORDS_TO_PLOT = (50, 50) # Example coordinates (X, Y)
|
|||||||
const RESULTS_DIR = "test/results"
|
const RESULTS_DIR = "test/results"
|
||||||
|
|
||||||
test1 = false
|
test1 = false
|
||||||
test2 = false
|
test2 = true
|
||||||
test3 = true
|
test3 = false
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# DATA VALIDATION UTILITY
|
# DATA VALIDATION UTILITY
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user