finished test script, included instructions in test/readme.md, the test performs mzml, imzml and conversion tests, along with throwing reference plots and images which result from the files
This commit is contained in:
parent
b1d7838a60
commit
cd1115368f
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ public/css/imgOver.png
|
|||||||
!public/css/LABI_logo.png
|
!public/css/LABI_logo.png
|
||||||
log/*
|
log/*
|
||||||
R_original_scripts/
|
R_original_scripts/
|
||||||
|
test/results/
|
||||||
|
|||||||
1109
Manifest.toml
1109
Manifest.toml
File diff suppressed because it is too large
Load Diff
10
Project.toml
10
Project.toml
@ -1,6 +1,15 @@
|
|||||||
|
name = "MSI_src"
|
||||||
|
authors = ["JJSA"]
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||||
|
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
|
||||||
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
|
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
|
||||||
|
ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
|
||||||
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
|
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
|
||||||
|
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
|
||||||
|
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
||||||
Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e"
|
Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e"
|
||||||
GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353"
|
GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353"
|
||||||
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
|
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
|
||||||
@ -9,6 +18,7 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
|||||||
NativeFileDialog = "e1fe445b-aa65-4df4-81c1-2041507f0fd4"
|
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"
|
||||||
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
||||||
StipplePlotly = "ec984513-233d-481d-95b0-a3b58b97af2b"
|
StipplePlotly = "ec984513-233d-481d-95b0-a3b58b97af2b"
|
||||||
julia_mzML_imzML = "38eb50d3-2fb6-4afa-992a-964ed8562ed9"
|
julia_mzML_imzML = "38eb50d3-2fb6-4afa-992a-964ed8562ed9"
|
||||||
|
|||||||
6
app.jl
6
app.jl
@ -6,7 +6,7 @@ using Libz
|
|||||||
using PlotlyBase
|
using PlotlyBase
|
||||||
using CairoMakie
|
using CairoMakie
|
||||||
using Colors
|
using Colors
|
||||||
# using julia_mzML_imzML
|
using julia_mzML_imzML
|
||||||
using Statistics
|
using Statistics
|
||||||
using NaturalSort
|
using NaturalSort
|
||||||
using Images
|
using Images
|
||||||
@ -15,7 +15,7 @@ using NativeFileDialog # Opens the file explorer depending on the OS
|
|||||||
using StipplePlotly
|
using StipplePlotly
|
||||||
using Base.Filesystem: mv # To rename files in the system
|
using Base.Filesystem: mv # To rename files in the system
|
||||||
include("./julia_imzML_visual.jl")
|
include("./julia_imzML_visual.jl")
|
||||||
include("./src/imzML.jl")
|
|
||||||
@genietools
|
@genietools
|
||||||
|
|
||||||
# == Reactive code ==
|
# == Reactive code ==
|
||||||
@ -363,7 +363,7 @@ include("./src/imzML.jl")
|
|||||||
if isfile(full_route) && Nmass > 0 && Tol > 0 && Tol <=1 && colorLevel > 1 && colorLevel < 257
|
if isfile(full_route) && Nmass > 0 && Tol > 0 && Tol <=1 && colorLevel > 1 && colorLevel < 257
|
||||||
msg="File exists, Nmass=$(Nmass) Tol=$(Tol). Loading file will begin, please be patient."
|
msg="File exists, Nmass=$(Nmass) Tol=$(Tol). Loading file will begin, please be patient."
|
||||||
try
|
try
|
||||||
spectra=load_imzml(full_route)
|
spectra=LoadImzml(full_route)
|
||||||
msg="File loaded. Creating spectra with the specific mass and tolerance, please be patient."
|
msg="File loaded. Creating spectra with the specific mass and tolerance, please be patient."
|
||||||
slice=GetMzSliceJl(spectra,Nmass,Tol)
|
slice=GetMzSliceJl(spectra,Nmass,Tol)
|
||||||
fig=CairoMakie.Figure(size=(150, 250)) # Container
|
fig=CairoMakie.Figure(size=(150, 250)) # Container
|
||||||
|
|||||||
399
src/Bitmap.jl
399
src/Bitmap.jl
@ -1,399 +0,0 @@
|
|||||||
# ********************************************************************
|
|
||||||
# SaveBitmap
|
|
||||||
# ********************************************************************
|
|
||||||
"""
|
|
||||||
SaveBitmap( name, pixMap, colorTable )
|
|
||||||
|
|
||||||
Save a discretized imzML slice as a bitmap file
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* ` name`: Full path name of target bitmap
|
|
||||||
* ` pixMap`: [UInt8] Bidimensional matrix with image info
|
|
||||||
* `colorTable`: [UInt32] Vector with RGB colors for each gray level
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
# Saves a bitmap with black and white alternate image squares
|
|
||||||
img = zeros(UInt8, 32, 32)
|
|
||||||
img[17:32,1:16] .= 1
|
|
||||||
img[1:16,17:32] .= 1
|
|
||||||
SaveBitmap( "test.bmp", img, UInt32[0, 0x00FFFFFF] )
|
|
||||||
```
|
|
||||||
|
|
||||||
The following image will be created on your hard disk
|
|
||||||
|
|
||||||

|
|
||||||
"""
|
|
||||||
function SaveBitmap( name,
|
|
||||||
pixMap::Array{UInt8,2},
|
|
||||||
colorTable::Array{UInt32,1} )
|
|
||||||
|
|
||||||
# Get image dimensions
|
|
||||||
dim = size( pixMap )
|
|
||||||
if length( dim ) != 2
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
|
|
||||||
# Compute row padding
|
|
||||||
padding = ( 4 - dim[1] & 0x3 ) & 0x3
|
|
||||||
|
|
||||||
# Compute file dimensions. Header = 14 + 40 + ( 256 * 4 ) = 1078
|
|
||||||
offset = 1078
|
|
||||||
imgBytes = dim[2] * ( dim[1] + padding )
|
|
||||||
|
|
||||||
# Create file
|
|
||||||
stream = open( name, "w" )
|
|
||||||
|
|
||||||
# Save file header
|
|
||||||
write( stream, UInt16( 0x4D42 ) )
|
|
||||||
write( stream, UInt32[ offset + imgBytes, 0 , offset ] )
|
|
||||||
|
|
||||||
# Save info header
|
|
||||||
write( stream, UInt32[ 40, dim[1], dim[2], 0x80001, 0 ] )
|
|
||||||
write( stream, UInt32[ imgBytes, 0, 0, 256, 0 ] )
|
|
||||||
|
|
||||||
# Save color table
|
|
||||||
write( stream, colorTable )
|
|
||||||
if length( colorTable ) < 256
|
|
||||||
fixTable = zeros( UInt32, 256 - length( colorTable ) )
|
|
||||||
write( stream, fixTable )
|
|
||||||
end
|
|
||||||
|
|
||||||
# Save image pixels
|
|
||||||
if padding == 0
|
|
||||||
for i = 1:dim[2]
|
|
||||||
write( stream, pixMap[:,i] )
|
|
||||||
end
|
|
||||||
else
|
|
||||||
zeroPad = zeros( UInt8, padding )
|
|
||||||
for i in 1:dim[2]
|
|
||||||
write( stream, pixMap[:,i] )
|
|
||||||
write( stream, zeroPad )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Close file
|
|
||||||
close( stream )
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************************
|
|
||||||
# FindMass
|
|
||||||
# massVector: mz Vector sorted in ascending order
|
|
||||||
# mass: target mz value
|
|
||||||
# tolerance: bi-axial tolerance to find mass
|
|
||||||
# ********************************************************************
|
|
||||||
function FindMass( massVector, mass, tolerance )
|
|
||||||
|
|
||||||
index = Int( 0 )
|
|
||||||
lower = Int( 1 )
|
|
||||||
higher = Int( length( massVector ) )
|
|
||||||
|
|
||||||
while lower <= higher
|
|
||||||
|
|
||||||
# Compute mid element
|
|
||||||
index = ( lower + higher ) ÷ 2
|
|
||||||
|
|
||||||
# Go to lower portion ?
|
|
||||||
if massVector[ index ] > ( mass + tolerance )
|
|
||||||
higher = index - 1
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
# Go to Higher portion?
|
|
||||||
if massVector[ index ] < ( mass - tolerance )
|
|
||||||
lower = index + 1
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
# mass was found
|
|
||||||
return index
|
|
||||||
end
|
|
||||||
|
|
||||||
# mass was not found
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************************
|
|
||||||
# GetSlice
|
|
||||||
# imzML: imzML data
|
|
||||||
# mass: mz target value
|
|
||||||
# tolerance: bi-axial tolerance to find mass
|
|
||||||
# ********************************************************************
|
|
||||||
"""
|
|
||||||
GetSlice( imzML, mass, tolerance )
|
|
||||||
|
|
||||||
Extract a mz-image from an imzML image data array loaded with
|
|
||||||
`LoadImzml` function. The resulting image array preserves the same
|
|
||||||
data type of the y-axis vectors in the file.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* ` imzML`: Image array loaded with `LoadImzml` function
|
|
||||||
* ` mass`: mz value for image extraction
|
|
||||||
* `tolerance`: Maximum lateral tolerance for a valid mz search
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
# Load DESI MSI Carcinoma 885.55 mz slice
|
|
||||||
spectra = LoadImzml( "80TopL, 50TopR, 70BottomL, 60BottomR-centroid.imzML" )
|
|
||||||
slice = GetSlice( spectra, 885.55, 0.005 )
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function GetSlice( imzML, mass, tolerance )
|
|
||||||
|
|
||||||
# Alloc space for slice
|
|
||||||
width = maximum( imzML[1,:] )
|
|
||||||
height = maximum( imzML[2,:] )
|
|
||||||
image = zeros( Float64, width, height )
|
|
||||||
|
|
||||||
for i in 1:size( imzML )[2]
|
|
||||||
index = FindMass( imzML[3,i], mass, tolerance )
|
|
||||||
if index != 0
|
|
||||||
image[ imzML[1,i], imzML[2,i] ] = imzML[4,i][index]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************************
|
|
||||||
# IntQuant, Discretize image amplitude in 0:255 range
|
|
||||||
# slice: Image matriz in measured arbitrary units
|
|
||||||
# ********************************************************************
|
|
||||||
"""
|
|
||||||
IntQuant( slice )
|
|
||||||
|
|
||||||
Zero Memory intensity quantizer. Discretize the image?s continuos
|
|
||||||
intensity range in discrete bins from 0 to 255 gray levels.
|
|
||||||
Returns a UInt8 bidimensional matrix
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* `slice`: slice returned by the `GetSlice` function
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
# Save DESI MSI Carcinoma 885.55 mz slice as a bitmap file
|
|
||||||
spectra = LoadImzml( "80TopL, 50TopR, 70BottomL, 60BottomR-centroid.imzML" )
|
|
||||||
slice = GetSlice( spectra, 885.55, 0.005
|
|
||||||
SaveBitmap( "Slice.bmp", IntQuant( slice ), ViridisPalette )
|
|
||||||
```
|
|
||||||
|
|
||||||
The following image will be created on your hard disk
|
|
||||||
|
|
||||||

|
|
||||||
"""
|
|
||||||
function IntQuant( slice )
|
|
||||||
|
|
||||||
# Compute scale factor for amplitude discretization
|
|
||||||
lower = minimum( slice )
|
|
||||||
scale = 255 / maximum( slice )
|
|
||||||
dim = size( slice )
|
|
||||||
image = zeros( UInt8, dim[1], dim[2] )
|
|
||||||
|
|
||||||
for i in 1:length( slice )
|
|
||||||
image[i] = convert( UInt8, floor( slice[i] * scale + 0.5 ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************************
|
|
||||||
# TrIQ, Discretize image amplitude in 0:255 range, grouping outliers
|
|
||||||
# in the highest bin
|
|
||||||
# slice: Image matrix in measured arbitrary units
|
|
||||||
# ********************************************************************
|
|
||||||
function GetOutlierThres( slice, prob )
|
|
||||||
|
|
||||||
# Get bin width
|
|
||||||
low = minimum( slice )
|
|
||||||
upp = maximum( slice )
|
|
||||||
|
|
||||||
# Compute histogram's bin count & separation
|
|
||||||
if ( upp - low + 1 ) >= 100
|
|
||||||
bins = convert( Int32, 100 )
|
|
||||||
step = ( upp + (upp-low)/(bins-1) - low ) / bins
|
|
||||||
else
|
|
||||||
bins = convert( Int32, ceil( upp ) - floor( low ) + 1 )
|
|
||||||
step = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
# Compute histogram
|
|
||||||
histCount = zeros( bins, 1 )
|
|
||||||
for k in slice
|
|
||||||
index = convert( Int64, floor( ( k - low ) / step ) + 1 )
|
|
||||||
histCount[ index ] += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get bin index, that accounts for desired probability
|
|
||||||
prob = 0.95
|
|
||||||
delta = cumsum( histCount, dims=1 ) / sum( histCount ) .- prob
|
|
||||||
index = findmin( broadcast( abs, delta ) )[2][1]
|
|
||||||
|
|
||||||
# Find max intensity image value
|
|
||||||
key = low + step * index
|
|
||||||
upp = -1
|
|
||||||
|
|
||||||
for k in slice[:]
|
|
||||||
if key > k && k > upp
|
|
||||||
upp = k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return [ low, upp ]
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************************
|
|
||||||
# Discretize image's intensity using a given intensity range
|
|
||||||
# slice: Image matrix in measured arbitrary units
|
|
||||||
# bounds: max and min intensity discretizing range
|
|
||||||
# depth: number of intensity discrete steps in final image
|
|
||||||
# ********************************************************************
|
|
||||||
function SetPixelDepth( slice, bounds, depth )
|
|
||||||
|
|
||||||
# Compute intensity bins
|
|
||||||
bins = depth - 1
|
|
||||||
limits = collect(
|
|
||||||
range( bounds[1], stop = bounds[2], length = depth ) )
|
|
||||||
|
|
||||||
# Reserve memory for output image
|
|
||||||
imgBytes = zeros( UInt8, size( slice ) )
|
|
||||||
nPixels = length( slice )
|
|
||||||
step = limits[2] - limits[1]
|
|
||||||
|
|
||||||
# Set intensity depth
|
|
||||||
for k in 1:nPixels
|
|
||||||
|
|
||||||
# Find bin
|
|
||||||
i::Int64 = floor( ( slice[k] - limits[1] ) / step ) + 1
|
|
||||||
if i < bins
|
|
||||||
imgBytes[k] = i + ( slice[k] > limits[i] ) - 1
|
|
||||||
else
|
|
||||||
imgBytes[k] = bins
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return imgBytes
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************************
|
|
||||||
# Discretize image's intensity removing intensity outliers
|
|
||||||
# slice: Image matrix in measured arbitrary units
|
|
||||||
# depth: Number of intensity discrete steps in final image
|
|
||||||
# prob: Proportion of pixesl to take into account in discretization
|
|
||||||
# ********************************************************************
|
|
||||||
"""
|
|
||||||
TrIQ( slice, depth, prob = 0.98 )
|
|
||||||
|
|
||||||
TrIQ intensity quantizer as described in _DOI 10.7717/peerj-cs.585_.
|
|
||||||
Discretize the image?s continuos intensity range in discrete bins
|
|
||||||
from 0 to 255 gray levels, groupping intensity outiers in the
|
|
||||||
highehst bin. Returns a UInt8 bidimensional matrix
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* `slice`: slice returned by the `GetSlice` function
|
|
||||||
* `depth`: Number ob discrete bins for intensity quantization
|
|
||||||
* ` prob`: Cummulative distribute function cutting value
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
# Save DESI MSI Carcinoma 885.55 mz slice as a bitmap file
|
|
||||||
spectra = LoadImzml( "80TopL, 50TopR, 70BottomL, 60BottomR-centroid.imzML" )
|
|
||||||
slice = GetSlice( spectra, 885.55, 0.005 )
|
|
||||||
SaveBitmap( "TrIQ.bmp", TrIQ( slice, 256, 0.95 ), ViridisPalette )
|
|
||||||
```
|
|
||||||
|
|
||||||
The following image will be created on your hard disk
|
|
||||||
|
|
||||||

|
|
||||||
"""
|
|
||||||
function TrIQ( slice, depth, prob )
|
|
||||||
|
|
||||||
return SetPixelDepth(
|
|
||||||
slice,
|
|
||||||
GetOutlierThres( slice, prob ),
|
|
||||||
depth
|
|
||||||
)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# ********************************************************************
|
|
||||||
# Viridis color palette 256 color levels
|
|
||||||
# ********************************************************************
|
|
||||||
ViridisPalette = UInt32[
|
|
||||||
0x440154, 0x440256, 0x450457, 0x450559,
|
|
||||||
0x46075A, 0x46085C, 0x460A5D, 0x460B5E,
|
|
||||||
0x470D60, 0x470E61, 0x471063, 0x471164,
|
|
||||||
0x471365, 0x481467, 0x481668, 0x481769,
|
|
||||||
0x48186A, 0x481A6C, 0x481B6D, 0x481C6E,
|
|
||||||
0x481D6F, 0x481F70, 0x482071, 0x482173,
|
|
||||||
0x482374, 0x482475, 0x482576, 0x482677,
|
|
||||||
0x482878, 0x482979, 0x472A7A, 0x472C7A,
|
|
||||||
0x472D7B, 0x472E7C, 0x472F7D, 0x46307E,
|
|
||||||
0x46327E, 0x46337F, 0x463480, 0x453581,
|
|
||||||
0x453781, 0x453882, 0x443983, 0x443A83,
|
|
||||||
0x443B84, 0x433D84, 0x433E85, 0x423F85,
|
|
||||||
0x424086, 0x424186, 0x414287, 0x414487,
|
|
||||||
0x404588, 0x404688, 0x3F4788, 0x3F4889,
|
|
||||||
0x3E4989, 0x3E4A89, 0x3E4C8A, 0x3D4D8A,
|
|
||||||
0x3D4E8A, 0x3C4F8A, 0x3C508B, 0x3B518B,
|
|
||||||
0x3B528B, 0x3A538B, 0x3A548C, 0x39558C,
|
|
||||||
0x39568C, 0x38588C, 0x38598C, 0x375A8C,
|
|
||||||
0x375B8D, 0x365C8D, 0x365D8D, 0x355E8D,
|
|
||||||
0x355F8D, 0x34608D, 0x34618D, 0x33628D,
|
|
||||||
0x33638D, 0x32648E, 0x32658E, 0x31668E,
|
|
||||||
0x31678E, 0x31688E, 0x30698E, 0x306A8E,
|
|
||||||
0x2F6B8E, 0x2F6C8E, 0x2E6D8E, 0x2E6E8E,
|
|
||||||
0x2E6F8E, 0x2D708E, 0x2D718E, 0x2C718E,
|
|
||||||
0x2C728E, 0x2C738E, 0x2B748E, 0x2B758E,
|
|
||||||
0x2A768E, 0x2A778E, 0x2A788E, 0x29798E,
|
|
||||||
0x297A8E, 0x297B8E, 0x287C8E, 0x287D8E,
|
|
||||||
0x277E8E, 0x277F8E, 0x27808E, 0x26818E,
|
|
||||||
0x26828E, 0x26828E, 0x25838E, 0x25848E,
|
|
||||||
0x25858E, 0x24868E, 0x24878E, 0x23888E,
|
|
||||||
0x23898E, 0x238A8D, 0x228B8D, 0x228C8D,
|
|
||||||
0x228D8D, 0x218E8D, 0x218F8D, 0x21908D,
|
|
||||||
0x21918C, 0x20928C, 0x20928C, 0x20938C,
|
|
||||||
0x1F948C, 0x1F958B, 0x1F968B, 0x1F978B,
|
|
||||||
0x1F988B, 0x1F998A, 0x1F9A8A, 0x1E9B8A,
|
|
||||||
0x1E9C89, 0x1E9D89, 0x1F9E89, 0x1F9F88,
|
|
||||||
0x1FA088, 0x1FA188, 0x1FA187, 0x1FA287,
|
|
||||||
0x20A386, 0x20A486, 0x21A585, 0x21A685,
|
|
||||||
0x22A785, 0x22A884, 0x23A983, 0x24AA83,
|
|
||||||
0x25AB82, 0x25AC82, 0x26AD81, 0x27AD81,
|
|
||||||
0x28AE80, 0x29AF7F, 0x2AB07F, 0x2CB17E,
|
|
||||||
0x2DB27D, 0x2EB37C, 0x2FB47C, 0x31B57B,
|
|
||||||
0x32B67A, 0x34B679, 0x35B779, 0x37B878,
|
|
||||||
0x38B977, 0x3ABA76, 0x3BBB75, 0x3DBC74,
|
|
||||||
0x3FBC73, 0x40BD72, 0x42BE71, 0x44BF70,
|
|
||||||
0x46C06F, 0x48C16E, 0x4AC16D, 0x4CC26C,
|
|
||||||
0x4EC36B, 0x50C46A, 0x52C569, 0x54C568,
|
|
||||||
0x56C667, 0x58C765, 0x5AC864, 0x5CC863,
|
|
||||||
0x5EC962, 0x60CA60, 0x63CB5F, 0x65CB5E,
|
|
||||||
0x67CC5C, 0x69CD5B, 0x6CCD5A, 0x6ECE58,
|
|
||||||
0x70CF57, 0x73D056, 0x75D054, 0x77D153,
|
|
||||||
0x7AD151, 0x7CD250, 0x7FD34E, 0x81D34D,
|
|
||||||
0x84D44B, 0x86D549, 0x89D548, 0x8BD646,
|
|
||||||
0x8ED645, 0x90D743, 0x93D741, 0x95D840,
|
|
||||||
0x98D83E, 0x9BD93C, 0x9DD93B, 0xA0DA39,
|
|
||||||
0xA2DA37, 0xA5DB36, 0xA8DB34, 0xAADC32,
|
|
||||||
0xADDC30, 0xB0DD2F, 0xB2DD2D, 0xB5DE2B,
|
|
||||||
0xB8DE29, 0xBADE28, 0xBDDF26, 0xC0DF25,
|
|
||||||
0xC2DF23, 0xC5E021, 0xC8E020, 0xCAE11F,
|
|
||||||
0xCDE11D, 0xD0E11C, 0xD2E21B, 0xD5E21A,
|
|
||||||
0xD8E219, 0xDAE319, 0xDDE318, 0xDFE318,
|
|
||||||
0xE2E418, 0xE5E419, 0xE7E419, 0xEAE51A,
|
|
||||||
0xECE51B, 0xEFE51C, 0xF1E51D, 0xF4E61E,
|
|
||||||
0xF6E620, 0xF8E621, 0xFBE723, 0xFDE725 ]
|
|
||||||
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
# *******************************************************************
|
|
||||||
# VectorConfig, structure with axis decoding instructions
|
|
||||||
# *******************************************************************
|
|
||||||
mutable struct SpecDim
|
|
||||||
Format::DataType # Float32, Float64
|
|
||||||
Packed::Bool # 0: none 1: zlib compression
|
|
||||||
Axis::Int32 # 1: m/z 2: Amplitude
|
|
||||||
Skip::Int64 # Bytes to skip
|
|
||||||
end
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Fill VectorConfig fields from "cvParam" tags
|
|
||||||
# *******************************************************************
|
|
||||||
function ConfigureSpecDim( stream )
|
|
||||||
|
|
||||||
# Vector configuration
|
|
||||||
axis = SpecDim( Float64, false, 1, 0 )
|
|
||||||
|
|
||||||
# Initial values
|
|
||||||
offset = position( stream )
|
|
||||||
currLine = ""
|
|
||||||
matchInfo = RegexMatch
|
|
||||||
|
|
||||||
while true
|
|
||||||
|
|
||||||
# Next field
|
|
||||||
currLine = readline( stream )
|
|
||||||
matchInfo = match( r"^\s*<(cvParam)", currLine )
|
|
||||||
|
|
||||||
if matchInfo === nothing
|
|
||||||
matchInfo = match( r"^\s*", currLine )
|
|
||||||
axis.Skip = position( stream ) - offset -
|
|
||||||
length( currLine ) + length( matchInfo.match )
|
|
||||||
return axis
|
|
||||||
end
|
|
||||||
|
|
||||||
index = length( matchInfo.captures[1] )
|
|
||||||
matchInfo = GetAttribute( currLine[index:end], "accession" )
|
|
||||||
|
|
||||||
if matchInfo.captures[1] == "MS:1000515" # intensity array
|
|
||||||
axis.Axis = 2
|
|
||||||
continue
|
|
||||||
|
|
||||||
elseif matchInfo.captures[1] == "MS:1000519" # 32-bit integer
|
|
||||||
axis.Format = Int32
|
|
||||||
continue
|
|
||||||
|
|
||||||
elseif matchInfo.captures[1] == "MS:1000521" # 32-bit float
|
|
||||||
axis.Format = Float32
|
|
||||||
continue
|
|
||||||
|
|
||||||
elseif matchInfo.captures[1] == "MS:1000522" # 64-bit integer
|
|
||||||
axis.Format = Int64
|
|
||||||
continue
|
|
||||||
|
|
||||||
elseif matchInfo.captures[1] == "MS:1000574" # zlib compresion
|
|
||||||
axis.Packed = true
|
|
||||||
continue
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Read lines in file until regex match
|
|
||||||
# stream: Source file stream
|
|
||||||
# regex: target tag e.g. "binaryDataArray"
|
|
||||||
# *******************************************************************
|
|
||||||
function FindTag( stream, regex )
|
|
||||||
|
|
||||||
while true
|
|
||||||
isTag = match( regex, readline( stream ) )
|
|
||||||
if isTag !== nothing
|
|
||||||
return isTag
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Retrieve attribute/value pair
|
|
||||||
# source: String with atr="value" pairs
|
|
||||||
# tag: Optional attribute, e.g. "\\spectrum"
|
|
||||||
# *******************************************************************
|
|
||||||
function GetAttribute( source, tag = "([^=]+)" )
|
|
||||||
|
|
||||||
# Build regex and retrieve attribute/value pair
|
|
||||||
regStr = Regex( "\\s" * tag * "=\"([^\"]*)\"" )
|
|
||||||
return( match( regStr, source ) )
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
# src/DataAccess.jl
|
|
||||||
|
|
||||||
"""
|
|
||||||
load_spectra(fileName::String)
|
|
||||||
|
|
||||||
Eagerly loads all spectra from a .mzML or .imzML file into a 2xN matrix.
|
|
||||||
This function follows the eager-loading pattern of the original `LoadMzml`
|
|
||||||
for backward compatibility and for analyses that require all data in memory.
|
|
||||||
|
|
||||||
Each column in the returned matrix represents a single spectrum:
|
|
||||||
- Row 1: m/z array
|
|
||||||
- Row 2: Intensity array
|
|
||||||
|
|
||||||
For .imzML files, the (x,y) spatial coordinates are discarded.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* `fileName`: Full path to the .mzML or .imzML file.
|
|
||||||
|
|
||||||
# Returns
|
|
||||||
- An `Array{Any, 2}` containing all spectra.
|
|
||||||
"""
|
|
||||||
function load_spectra(fileName::String)
|
|
||||||
if endswith(lowercase(fileName), ".mzml")
|
|
||||||
# Detected .mzML file. Using the existing eager loader.
|
|
||||||
return LoadMzml(fileName)
|
|
||||||
|
|
||||||
elseif endswith(lowercase(fileName), ".imzml")
|
|
||||||
# Detected .imzML file. Eagerly loading all spectra from .ibd.
|
|
||||||
|
|
||||||
# Lazily load metadata to get offsets and counts
|
|
||||||
imzml_data = load_imzml(fileName)
|
|
||||||
|
|
||||||
try
|
|
||||||
num_spectra = length(imzml_data.spectra_metadata)
|
|
||||||
if num_spectra == 0
|
|
||||||
return Array{Any}(undef, (2, 0))
|
|
||||||
end
|
|
||||||
|
|
||||||
spectra_matrix = Array{Any}(undef, (2, num_spectra))
|
|
||||||
|
|
||||||
hIbd = imzml_data.ibd_handle
|
|
||||||
mz_format = imzml_data.mz_format
|
|
||||||
int_format = imzml_data.intensity_format
|
|
||||||
|
|
||||||
# For performance, pre-allocate one large buffer for reading.
|
|
||||||
# This avoids re-allocating memory for each spectrum inside the loop.
|
|
||||||
max_points = maximum(meta.mz_count for meta in imzml_data.spectra_metadata)
|
|
||||||
mz_array_buffer = Array{mz_format}(undef, max_points)
|
|
||||||
intensity_array_buffer = Array{int_format}(undef, max_points)
|
|
||||||
|
|
||||||
for i in 1:num_spectra
|
|
||||||
meta = imzml_data.spectra_metadata[i]
|
|
||||||
|
|
||||||
# Create a view into the buffer with the correct size for this spectrum
|
|
||||||
current_mz_array = view(mz_array_buffer, 1:meta.mz_count)
|
|
||||||
current_int_array = view(intensity_array_buffer, 1:meta.intensity_count)
|
|
||||||
|
|
||||||
# Read binary data directly into the sized views
|
|
||||||
seek(hIbd, meta.mz_offset)
|
|
||||||
read!(hIbd, current_mz_array)
|
|
||||||
|
|
||||||
seek(hIbd, meta.intensity_offset)
|
|
||||||
read!(hIbd, current_int_array)
|
|
||||||
|
|
||||||
# Store a copy in the final matrix. A copy is necessary because
|
|
||||||
# the buffer is overwritten in the next iteration.
|
|
||||||
spectra_matrix[1, i] = copy(current_mz_array)
|
|
||||||
spectra_matrix[2, i] = copy(current_int_array)
|
|
||||||
end
|
|
||||||
|
|
||||||
return spectra_matrix
|
|
||||||
finally
|
|
||||||
# Ensure the .ibd file handle is closed, as this is an eager load.
|
|
||||||
if isopen(imzml_data.ibd_handle)
|
|
||||||
close(imzml_data.ibd_handle)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
|
||||||
error("Unsupported file type. Please provide a .mzML or .imzML file.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
369
src/MSIData.jl
Normal file
369
src/MSIData.jl
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
# src/MSIData.jl
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file defines the unified MSIData object and the associated data access layer,
|
||||||
|
including caching and iteration logic, for handling large mzML and imzML datasets
|
||||||
|
efficiently.
|
||||||
|
"""
|
||||||
|
|
||||||
|
using Base64, Libz # For reading binary data
|
||||||
|
|
||||||
|
# Abstract type for different data sources (e.g., mzML, imzML)
|
||||||
|
# This allows dispatching to the correct binary reading logic.
|
||||||
|
abstract type MSDataSource end
|
||||||
|
|
||||||
|
# Concrete type for imzML data sources
|
||||||
|
struct ImzMLSource <: MSDataSource
|
||||||
|
ibd_handle::IO
|
||||||
|
mz_format::Type
|
||||||
|
intensity_format::Type
|
||||||
|
end
|
||||||
|
|
||||||
|
# Concrete type for mzML data sources
|
||||||
|
struct MzMLSource <: MSDataSource
|
||||||
|
file_handle::IO
|
||||||
|
mz_format::Type
|
||||||
|
intensity_format::Type
|
||||||
|
end
|
||||||
|
|
||||||
|
# Struct to hold info about a binary data array (mz or intensity)
|
||||||
|
struct SpectrumAsset
|
||||||
|
format::Type
|
||||||
|
is_compressed::Bool
|
||||||
|
offset::Int64
|
||||||
|
encoded_length::Int32
|
||||||
|
# For mzML, axis_type is needed to distinguish mz from intensity.
|
||||||
|
# For imzML, this can be ignored as the order is fixed.
|
||||||
|
axis_type::Symbol
|
||||||
|
end
|
||||||
|
|
||||||
|
# Metadata for a single spectrum, common to both formats
|
||||||
|
struct SpectrumMetadata
|
||||||
|
# For imzML
|
||||||
|
x::Int32
|
||||||
|
y::Int32
|
||||||
|
|
||||||
|
# For mzML
|
||||||
|
id::String
|
||||||
|
|
||||||
|
# Common binary data info
|
||||||
|
mz_asset::SpectrumAsset
|
||||||
|
int_asset::SpectrumAsset
|
||||||
|
end
|
||||||
|
|
||||||
|
# The main unified data object
|
||||||
|
mutable struct MSIData
|
||||||
|
source::MSDataSource
|
||||||
|
spectra_metadata::Vector{SpectrumMetadata}
|
||||||
|
image_dims::Tuple{Int, Int} # (width, height) for imaging data
|
||||||
|
coordinate_map::Union{Matrix{Int}, Nothing} # Maps (x,y) to linear index for imzML
|
||||||
|
|
||||||
|
# LRU Cache implementation
|
||||||
|
cache::Dict{Int, Tuple{Vector, Vector}}
|
||||||
|
cache_order::Vector{Int} # Stores indices, with most recently used at the end
|
||||||
|
cache_size::Int # Max number of spectra in cache
|
||||||
|
|
||||||
|
function MSIData(source, metadata, dims, coordinate_map, cache_size)
|
||||||
|
obj = new(source, metadata, dims, coordinate_map, Dict(), [], cache_size)
|
||||||
|
|
||||||
|
# Ensure file handles are closed when the object is garbage collected
|
||||||
|
finalizer(obj) do o
|
||||||
|
if o.source isa ImzMLSource && isopen(o.source.ibd_handle)
|
||||||
|
close(o.source.ibd_handle)
|
||||||
|
elseif o.source isa MzMLSource && isopen(o.source.file_handle)
|
||||||
|
close(o.source.file_handle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return obj
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# --- Internal function for reading binary data --- #
|
||||||
|
|
||||||
|
function read_binary_vector(io::IO, asset::SpectrumAsset)
|
||||||
|
seek(io, asset.offset)
|
||||||
|
raw_b64 = read(io, asset.encoded_length)
|
||||||
|
decoded_bytes = base64decode(strip(String(raw_b64)))
|
||||||
|
bytes_io = IOBuffer(asset.is_compressed ? Libz.inflate(decoded_bytes) : decoded_bytes)
|
||||||
|
|
||||||
|
n_elements = Int(bytes_io.size / sizeof(asset.format))
|
||||||
|
out_array = Array{asset.format}(undef, n_elements)
|
||||||
|
read!(bytes_io, out_array)
|
||||||
|
return out_array
|
||||||
|
end
|
||||||
|
|
||||||
|
# Overload for different source types
|
||||||
|
function read_spectrum_from_disk(source::ImzMLSource, meta::SpectrumMetadata)
|
||||||
|
# For imzML, the binary data is raw, not base64 encoded.
|
||||||
|
# The `encoded_length` field in this case holds the number of points.
|
||||||
|
mz = Array{source.mz_format}(undef, meta.mz_asset.encoded_length)
|
||||||
|
intensity = Array{source.intensity_format}(undef, meta.int_asset.encoded_length)
|
||||||
|
|
||||||
|
seek(source.ibd_handle, meta.mz_asset.offset)
|
||||||
|
read!(source.ibd_handle, mz)
|
||||||
|
|
||||||
|
seek(source.ibd_handle, meta.int_asset.offset)
|
||||||
|
read!(source.ibd_handle, intensity)
|
||||||
|
|
||||||
|
return mz, intensity
|
||||||
|
end
|
||||||
|
|
||||||
|
function read_spectrum_from_disk(source::MzMLSource, meta::SpectrumMetadata)
|
||||||
|
# For mzML, data is Base64 encoded within the XML
|
||||||
|
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
||||||
|
intensity = read_binary_vector(source.file_handle, meta.int_asset)
|
||||||
|
return mz, intensity
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Public API --- #
|
||||||
|
|
||||||
|
"""
|
||||||
|
GetSpectrum(data::MSIData, index::Int)
|
||||||
|
|
||||||
|
Retrieves a single spectrum by its index, utilizing a cache for performance.
|
||||||
|
|
||||||
|
This function is the core of the "Indexed" and "Cache" access patterns.
|
||||||
|
"""
|
||||||
|
function GetSpectrum(data::MSIData, index::Int)
|
||||||
|
if index < 1 || index > length(data.spectra_metadata)
|
||||||
|
error("Spectrum index $index out of bounds.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Phase 1: Check the cache
|
||||||
|
if haskey(data.cache, index)
|
||||||
|
# Cache Hit: Move item to the end of the LRU list and return from cache
|
||||||
|
filter!(x -> x != index, data.cache_order)
|
||||||
|
push!(data.cache_order, index)
|
||||||
|
return data.cache[index]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Phase 2: Cache Miss - Read from disk
|
||||||
|
meta = data.spectra_metadata[index]
|
||||||
|
spectrum = read_spectrum_from_disk(data.source, meta)
|
||||||
|
|
||||||
|
# Phase 3: Update cache
|
||||||
|
if data.cache_size > 0
|
||||||
|
if length(data.cache) >= data.cache_size
|
||||||
|
# Evict the least recently used item (at the front of the list)
|
||||||
|
lru_index = popfirst!(data.cache_order)
|
||||||
|
delete!(data.cache, lru_index)
|
||||||
|
end
|
||||||
|
data.cache[index] = spectrum
|
||||||
|
push!(data.cache_order, index)
|
||||||
|
end
|
||||||
|
|
||||||
|
return spectrum
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
GetSpectrum(data::MSIData, x::Int, y::Int)
|
||||||
|
|
||||||
|
Retrieves a single spectrum by its (x, y) coordinates for imaging data.
|
||||||
|
Utilizes a coordinate map for efficient lookup and then the cache.
|
||||||
|
"""
|
||||||
|
function GetSpectrum(data::MSIData, x::Int, y::Int)
|
||||||
|
if data.coordinate_map === nothing
|
||||||
|
error("Coordinate map not available. This method is only for imaging data loaded from .imzML files.")
|
||||||
|
end
|
||||||
|
width, height = data.image_dims
|
||||||
|
if x < 1 || x > width || y < 1 || y > height
|
||||||
|
error("Coordinates ($x, $y) out of bounds for image dimensions ($width, $height).")
|
||||||
|
end
|
||||||
|
|
||||||
|
index = data.coordinate_map[x, y]
|
||||||
|
if index == 0
|
||||||
|
error("No spectrum found at coordinates ($x, $y).")
|
||||||
|
end
|
||||||
|
|
||||||
|
return GetSpectrum(data, index) # Call the existing method
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
get_total_spectrum(msi_data::MSIData; num_bins::Int=20000) -> Tuple{Vector{Float64}, Vector{Float64}}
|
||||||
|
|
||||||
|
Calculates the sum of all spectra in the dataset by binning.
|
||||||
|
The m/z range is determined dynamically by finding the global min/max m/z values
|
||||||
|
across the entire dataset.
|
||||||
|
|
||||||
|
Returns a tuple containing two vectors: the binned m/z axis and the summed intensities.
|
||||||
|
"""
|
||||||
|
function get_total_spectrum(msi_data::MSIData; num_bins::Int=20000)
|
||||||
|
println("Calculating total spectrum...")
|
||||||
|
|
||||||
|
# 1. First Pass: Find the global m/z range
|
||||||
|
println(" Pass 1: Finding global m/z range...")
|
||||||
|
global_min_mz = Inf
|
||||||
|
global_max_mz = -Inf
|
||||||
|
|
||||||
|
_iterate_spectra_fast(msi_data) do idx, mz, _
|
||||||
|
if !isempty(mz)
|
||||||
|
local_min, local_max = extrema(mz)
|
||||||
|
global_min_mz = min(global_min_mz, local_min)
|
||||||
|
global_max_mz = max(global_max_mz, local_max)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if !isfinite(global_min_mz)
|
||||||
|
@warn "Could not determine a valid m/z range. All spectra might be empty."
|
||||||
|
return (Float64[], Float64[])
|
||||||
|
end
|
||||||
|
println(" Global m/z range found: [$(global_min_mz), $(global_max_mz)]")
|
||||||
|
|
||||||
|
# 2. Define Bins
|
||||||
|
mz_bins = range(global_min_mz, stop=global_max_mz, length=num_bins)
|
||||||
|
intensity_sum = zeros(Float64, num_bins)
|
||||||
|
bin_step = step(mz_bins)
|
||||||
|
|
||||||
|
# 3. Second Pass: Sum intensities into bins
|
||||||
|
println(" Pass 2: Summing intensities into $num_bins bins...")
|
||||||
|
_iterate_spectra_fast(msi_data) do idx, mz, intensity
|
||||||
|
if isempty(mz)
|
||||||
|
return # continue equivalent
|
||||||
|
end
|
||||||
|
for i in eachindex(mz)
|
||||||
|
# Find the correct bin for the current m/z value
|
||||||
|
bin_index = clamp(round(Int, (mz[i] - global_min_mz) / bin_step) + 1, 1, num_bins)
|
||||||
|
intensity_sum[bin_index] += intensity[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
println("Total spectrum calculation complete.")
|
||||||
|
return (collect(mz_bins), intensity_sum)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
get_average_spectrum(msi_data::MSIData; num_bins::Int=20000) -> Tuple{Vector{Float64}, Vector{Float64}}
|
||||||
|
|
||||||
|
Calculates the average of all spectra in the dataset by binning.
|
||||||
|
This is effectively the total ion chromatogram (TIC) divided by the number of spectra.
|
||||||
|
|
||||||
|
Returns a tuple containing two vectors: the binned m/z axis and the averaged intensities.
|
||||||
|
"""
|
||||||
|
function get_average_spectrum(msi_data::MSIData; num_bins::Int=20000)
|
||||||
|
# This function uses the exact same logic as get_total_spectrum...
|
||||||
|
mz_bins, intensity_sum = get_total_spectrum(msi_data, num_bins=num_bins)
|
||||||
|
|
||||||
|
if isempty(intensity_sum)
|
||||||
|
return (mz_bins, intensity_sum)
|
||||||
|
end
|
||||||
|
|
||||||
|
# ...with one final step: dividing by the number of spectra to get the average.
|
||||||
|
println("Averaging spectrum...")
|
||||||
|
num_spectra = length(msi_data.spectra_metadata)
|
||||||
|
average_intensity = intensity_sum ./ num_spectra
|
||||||
|
|
||||||
|
return (mz_bins, average_intensity)
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Iterator Implementation --- #
|
||||||
|
|
||||||
|
struct MSIDataIterator
|
||||||
|
data::MSIData
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
IterateSpectra(data::MSIData)
|
||||||
|
|
||||||
|
Returns an iterator that yields each spectrum, processing the file sequentially
|
||||||
|
with minimal memory overhead.
|
||||||
|
|
||||||
|
This function is the core of the "Event-driven" access pattern.
|
||||||
|
"""
|
||||||
|
function IterateSpectra(data::MSIData)
|
||||||
|
return MSIDataIterator(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Define the iteration interface for the custom iterator
|
||||||
|
Base.length(it::MSIDataIterator) = length(it.data.spectra_metadata)
|
||||||
|
Base.eltype(it::MSIDataIterator) = Tuple{Int, Tuple{Vector, Vector}} # Index, (mz, intensity)
|
||||||
|
|
||||||
|
function Base.iterate(it::MSIDataIterator, state=1)
|
||||||
|
if state > length(it.data.spectra_metadata)
|
||||||
|
return nothing # End of iteration
|
||||||
|
end
|
||||||
|
|
||||||
|
# GetSpectrum uses the cache, so iteration is also cached
|
||||||
|
spectrum = GetSpectrum(it.data, state)
|
||||||
|
|
||||||
|
# Yield the index and the spectrum
|
||||||
|
return ((state, spectrum), state + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# --- High-performance Internal Iterator --- #
|
||||||
|
|
||||||
|
"""
|
||||||
|
_iterate_spectra_fast(f::Function, data::MSIData)
|
||||||
|
|
||||||
|
An internal, high-performance iterator for bulk processing.
|
||||||
|
It avoids the overhead of `GetSpectrum` by reading directly from the file stream
|
||||||
|
and reusing pre-allocated buffers.
|
||||||
|
|
||||||
|
- `f`: A function to call for each spectrum, with signature `f(index, mz, intensity)`.
|
||||||
|
- `data`: The `MSIData` object.
|
||||||
|
"""
|
||||||
|
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::ImzMLSource)
|
||||||
|
# This implementation is for imzML and is optimized for performance by
|
||||||
|
# using pre-allocated buffers.
|
||||||
|
max_points = 0
|
||||||
|
for meta in data.spectra_metadata
|
||||||
|
# For imzML, encoded_length is the number of points
|
||||||
|
max_points = max(max_points, meta.mz_asset.encoded_length)
|
||||||
|
end
|
||||||
|
|
||||||
|
# If all spectra are empty, just call f with empty arrays
|
||||||
|
if max_points == 0 && !isempty(data.spectra_metadata)
|
||||||
|
for i in 1:length(data.spectra_metadata)
|
||||||
|
f(i, source.mz_format[], source.intensity_format[])
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
mz_buffer = Vector{source.mz_format}(undef, max_points)
|
||||||
|
int_buffer = Vector{source.intensity_format}(undef, max_points)
|
||||||
|
|
||||||
|
for i in 1:length(data.spectra_metadata)
|
||||||
|
meta = data.spectra_metadata[i]
|
||||||
|
nPoints = meta.mz_asset.encoded_length
|
||||||
|
|
||||||
|
if nPoints == 0
|
||||||
|
f(i, source.mz_format[], source.intensity_format[])
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
mz_view = view(mz_buffer, 1:nPoints)
|
||||||
|
int_view = view(int_buffer, 1:nPoints)
|
||||||
|
|
||||||
|
seek(source.ibd_handle, meta.mz_asset.offset)
|
||||||
|
read!(source.ibd_handle, mz_view)
|
||||||
|
|
||||||
|
seek(source.ibd_handle, meta.int_asset.offset)
|
||||||
|
read!(source.ibd_handle, int_view)
|
||||||
|
|
||||||
|
f(i, mz_view, int_view)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::MzMLSource)
|
||||||
|
# This implementation is for mzML. It iterates through each spectrum,
|
||||||
|
# decodes it, and then calls the function `f`. It's less performant than
|
||||||
|
# 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.
|
||||||
|
for i in 1:length(data.spectra_metadata)
|
||||||
|
meta = data.spectra_metadata[i]
|
||||||
|
|
||||||
|
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
||||||
|
intensity = read_binary_vector(source.file_handle, meta.int_asset)
|
||||||
|
|
||||||
|
f(i, mz, intensity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function _iterate_spectra_fast(f::Function, data::MSIData)
|
||||||
|
# Dispatch to the correct implementation based on the source type
|
||||||
|
_iterate_spectra_fast_impl(f, data, data.source)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
33
src/MSI_src.jl
Normal file
33
src/MSI_src.jl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module MSI_src
|
||||||
|
|
||||||
|
# Export the public API
|
||||||
|
export OpenMSIData, GetSpectrum, IterateSpectra, ImportMzmlFile, load_slices, plot_slices, plot_slice, get_total_spectrum, get_average_spectrum, LoadMzml, LoadSpectra
|
||||||
|
|
||||||
|
# Include all source files directly into the main module
|
||||||
|
include("MSIData.jl")
|
||||||
|
include("ParserHelpers.jl")
|
||||||
|
include("mzML.jl")
|
||||||
|
include("imzML.jl")
|
||||||
|
include("MzmlConverter.jl")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main Entry Point --- #
|
||||||
|
|
||||||
|
"""
|
||||||
|
OpenMSIData(filepath::String; cache_size=100)
|
||||||
|
|
||||||
|
Opens a .mzML or .imzML file and prepares it for data access.
|
||||||
|
|
||||||
|
This is the main entry point for the new data access API.
|
||||||
|
"""
|
||||||
|
function OpenMSIData(filepath::String; cache_size=100)
|
||||||
|
if endswith(lowercase(filepath), ".mzml")
|
||||||
|
return load_mzml_lazy(filepath, cache_size=cache_size)
|
||||||
|
elseif endswith(lowercase(filepath), ".imzml")
|
||||||
|
return load_imzml_lazy(filepath, cache_size=cache_size)
|
||||||
|
else
|
||||||
|
error("Unsupported file type: $filepath. Please provide a .mzML or .imzML file.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end # module MSI_src
|
||||||
@ -1,16 +1,14 @@
|
|||||||
# src/MzmlConverter.jl
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This file contains the workflow for converting .mzML files (with one spectrum per pixel)
|
This file contains the workflow for converting .mzML files (with one spectrum per pixel)
|
||||||
into a proper .imzML/.ibd file pair, using a separate synchronization file.
|
into a proper .imzML/.ibd file pair, using a separate synchronization file.
|
||||||
It replicates the functionality of the project's original R scripts that use MALDIquant.
|
It replicates the functionality of the original R scripts that use MALDIquant.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
using CSV, DataFrames
|
using DataFrames, Printf, CSV
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
# Note: This file assumes that `LoadSpectra` from DataAccess.jl and the necessary
|
|
||||||
# structs/functions from ParserHelpers.jl, mzML.jl, and imzML.jl are available
|
|
||||||
# in the execution context (e.g., included in the main app.jl).
|
|
||||||
|
|
||||||
# A struct to hold the processed pixel data before exporting
|
# A struct to hold the processed pixel data before exporting
|
||||||
struct ProcessedPixel
|
struct ProcessedPixel
|
||||||
@ -19,11 +17,18 @@ struct ProcessedPixel
|
|||||||
intensity::Vector{Float32}
|
intensity::Vector{Float32}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This struct will hold the metadata needed for the XML file
|
||||||
|
struct BinaryMetadata
|
||||||
|
mz_offset::UInt64
|
||||||
|
mz_length::UInt64
|
||||||
|
int_offset::UInt64
|
||||||
|
int_length::UInt64
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
GetMzmlScanTime(fileName::String)
|
GetMzmlScanTime(fileName::String)
|
||||||
|
|
||||||
Parses a .mzML file to extract the scan start time for each spectrum.
|
Parses a .mzML file to extract the scan start time for each spectrum.
|
||||||
This is a Julia implementation of the `GetMzmlScanTime` function from the R scripts.
|
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
* `fileName`: Path to the .mzML file.
|
* `fileName`: Path to the .mzML file.
|
||||||
@ -32,48 +37,75 @@ This is a Julia implementation of the `GetMzmlScanTime` function from the R scri
|
|||||||
- A `Matrix{Int64}` where each row is `[spectrum_index, time_in_milliseconds]`.
|
- A `Matrix{Int64}` where each row is `[spectrum_index, time_in_milliseconds]`.
|
||||||
"""
|
"""
|
||||||
function GetMzmlScanTime(fileName::String)
|
function GetMzmlScanTime(fileName::String)
|
||||||
spec_count = 0
|
|
||||||
times = Tuple{Int64, Int64}[]
|
times = Tuple{Int64, Int64}[]
|
||||||
|
|
||||||
|
# Pre-allocate based on an estimate to reduce re-allocations
|
||||||
|
try
|
||||||
|
file_size = filesize(fileName)
|
||||||
|
estimated_spectra = max(1000, file_size ÷ 10000) # Heuristic
|
||||||
|
sizehint!(times, estimated_spectra)
|
||||||
|
catch
|
||||||
|
# Ignore if filesize fails, sizehint! is just an optimization
|
||||||
|
end
|
||||||
|
|
||||||
open(fileName, "r") do stream
|
open(fileName, "r") do stream
|
||||||
file_content = read(stream, String)
|
state = :outside_spectrum
|
||||||
|
current_index = 0
|
||||||
|
current_time = nothing
|
||||||
|
|
||||||
# First, find the total number of spectra to pre-allocate
|
for line in eachline(stream)
|
||||||
spectrum_list_match = match(r"<spectrumList count=\"(\d+)\"", file_content)
|
if state == :outside_spectrum
|
||||||
if spectrum_list_match === nothing
|
if occursin("<spectrum ", line)
|
||||||
error("Could not find <spectrumList> tag or count attribute.")
|
state = :in_spectrum
|
||||||
|
current_index = 0
|
||||||
|
current_time = nothing
|
||||||
|
|
||||||
|
# Process index from the start tag itself
|
||||||
|
idx_match = match(r"index=\"(\d+)\"", line)
|
||||||
|
if idx_match !== nothing
|
||||||
|
current_index = parse(Int, idx_match.captures[1]) + 1
|
||||||
end
|
end
|
||||||
total_spectra = parse(Int, spectrum_list_match.captures[1])
|
end
|
||||||
sizehint!(times, total_spectra)
|
elseif state == :in_spectrum
|
||||||
|
# Look for time cvParam
|
||||||
# Iterate over each spectrum block
|
if occursin("<cvParam", line)
|
||||||
for spectrum_match in eachmatch(r"<spectrum index=\"(\d+)\"[^>]*>.*?<\/spectrum>", file_content, overlay=true)
|
if occursin("MS:1000016", line) || occursin("MS:1000015", line)
|
||||||
spec_block = spectrum_match.match
|
time_match = match(r"value=\"([\d\.]+)\"", line)
|
||||||
# The R script appears to treat the 0-based mzML index as 1-based, so we add 1.
|
|
||||||
spec_index = parse(Int, spectrum_match.captures[1]) + 1
|
|
||||||
|
|
||||||
# Find the scan start time within the spectrum block
|
|
||||||
time_match = match(r"<cvParam accession=\"MS:1000016\".*?value=\"([\d\.]+)\"", spec_block)
|
|
||||||
|
|
||||||
if time_match !== nothing
|
if time_match !== nothing
|
||||||
# The value is in minutes. Convert to milliseconds and round.
|
time_val = parse(Float64, time_match.captures[1])
|
||||||
time_in_minutes = parse(Float64, time_match.captures[1])
|
# MS:1000016 is minutes, MS:1000015 is seconds
|
||||||
time_in_ms = round(Int64, time_in_minutes * 60000)
|
unit_scale = occursin("MS:1000016", line) ? 60000 : 1000
|
||||||
push!(times, (spec_index, time_in_ms))
|
current_time = round(Int64, time_val * unit_scale)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Normalize times so the first scan is at t=0
|
# Check for end of spectrum
|
||||||
|
if occursin("</spectrum>", line)
|
||||||
|
state = :outside_spectrum
|
||||||
|
if current_index > 0 && current_time !== nothing
|
||||||
|
push!(times, (current_index, current_time))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sort by spectrum index to ensure correct order before normalization
|
||||||
|
sort!(times, by = x -> x[1])
|
||||||
|
|
||||||
|
# Normalize times relative to the first scan
|
||||||
if !isempty(times)
|
if !isempty(times)
|
||||||
sort!(times, by = x -> x[1]) # Ensure scans are sorted by index
|
|
||||||
first_time = times[1][2]
|
first_time = times[1][2]
|
||||||
for i in eachindex(times)
|
for i in eachindex(times)
|
||||||
times[i] = (times[i][1], times[i][2] - first_time)
|
times[i] = (times[i][1], times[i][2] - first_time)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # open
|
|
||||||
|
|
||||||
# Convert vector of tuples to a matrix
|
# Convert vector of tuples to a matrix
|
||||||
|
if isempty(times)
|
||||||
|
return Matrix{Int64}(undef, 0, 2)
|
||||||
|
end
|
||||||
return permutedims(hcat(collect.(times)...))
|
return permutedims(hcat(collect.(times)...))
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -92,144 +124,491 @@ This is a Julia implementation of the `MatchAcquireTime` function from the R scr
|
|||||||
# Returns
|
# Returns
|
||||||
- A `Matrix{Int64}` where each row is `[pixel_index, pixel_time_ms, first_scan_index, last_scan_index]`.
|
- A `Matrix{Int64}` where each row is `[pixel_index, pixel_time_ms, first_scan_index, last_scan_index]`.
|
||||||
"""
|
"""
|
||||||
function MatchAcquireTime(sync_file_path::String, scans::Matrix{Int64})
|
function MatchAcquireTime(sync_file_path::String, scans::Matrix{Int64}; img_width::Int=0, img_height::Int=0)
|
||||||
if !isfile(sync_file_path)
|
if !isfile(sync_file_path)
|
||||||
error("Synchronization file not found: $sync_file_path")
|
error("Synchronization file not found: $sync_file_path")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load xyz stage pixel delays from the sync file, skipping the first 2 header lines.
|
|
||||||
pixel_df = CSV.read(sync_file_path, DataFrame, header=false, skipto=3)
|
pixel_df = CSV.read(sync_file_path, DataFrame, header=false, skipto=3)
|
||||||
pixel = Matrix(pixel_df)
|
pixel_matrix = Matrix(pixel_df)
|
||||||
|
|
||||||
# Normalize pixel times so the first is at t=0
|
num_pixels_original = size(pixel_matrix, 1)
|
||||||
pixel[:, 2] .-= pixel[1, 2]
|
if num_pixels_original == 0
|
||||||
|
return zeros(Int64, 0, 5)
|
||||||
num_pixels = size(pixel, 1)
|
|
||||||
num_scans = size(scans, 1)
|
|
||||||
|
|
||||||
index_matrix = zeros(Int64, num_pixels, 2)
|
|
||||||
|
|
||||||
scan_row = 1
|
|
||||||
for pixel_row in 1:(num_pixels - 1)
|
|
||||||
start_scan = scan_row
|
|
||||||
|
|
||||||
# Find the scan that ends after the *next* pixel starts
|
|
||||||
while scan_row <= num_scans && scans[scan_row, 2] < pixel[pixel_row + 1, 2]
|
|
||||||
scan_row += 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
index_matrix[pixel_row, 1] = start_scan
|
# Determine image dimensions and generate coordinates
|
||||||
index_matrix[pixel_row, 2] = scan_row - 1
|
current_img_width = img_width
|
||||||
|
current_img_height = img_height
|
||||||
|
|
||||||
# The next pixel's scans start from the one that crossed the boundary
|
if current_img_width == 0 || current_img_height == 0
|
||||||
scan_row = max(1, scan_row - 1)
|
if size(pixel_matrix, 2) >= 3
|
||||||
|
@info "Sync file contains X, Y coordinates"
|
||||||
|
coordinates = convert(Matrix{Int}, pixel_matrix[:, 1:2])
|
||||||
|
pixel_times = convert(Vector{Int64}, pixel_matrix[:, 3])
|
||||||
|
current_img_width = maximum(coordinates[:, 1])
|
||||||
|
current_img_height = maximum(coordinates[:, 2])
|
||||||
|
|
||||||
|
# Apply R's pixel truncation logic
|
||||||
|
num_pixels = (floor(Int, (num_pixels_original - 1) / current_img_width)) * current_img_width
|
||||||
|
if num_pixels <= 0
|
||||||
|
error("Calculated num_pixels to process is zero or negative: $num_pixels")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Assign scans for the last pixel
|
# Truncate arrays
|
||||||
index_matrix[num_pixels, 1] = scan_row
|
final_coordinates = coordinates[1:num_pixels, :]
|
||||||
index_matrix[num_pixels, 2] = num_scans
|
final_pixel_times = pixel_times[1:num_pixels]
|
||||||
|
|
||||||
|
else
|
||||||
|
@info "Sync file contains Index and Time; generating coordinates"
|
||||||
|
pixel_times = convert(Vector{Int64}, pixel_matrix[:, 2])
|
||||||
|
|
||||||
|
# R's width detection logic
|
||||||
|
diffs = pixel_matrix[2:end, 1] .- pixel_matrix[1:end-1, 1]
|
||||||
|
width_indices = findall(x -> x != 1, diffs)
|
||||||
|
if !isempty(width_indices)
|
||||||
|
current_img_width = width_indices[1]
|
||||||
|
else
|
||||||
|
current_img_width = num_pixels_original
|
||||||
|
end
|
||||||
|
current_img_height = num_pixels_original ÷ current_img_width
|
||||||
|
|
||||||
|
# R's pixel truncation logic
|
||||||
|
num_pixels = (floor(Int, (num_pixels_original - 1) / current_img_width)) * current_img_width
|
||||||
|
if num_pixels <= 0
|
||||||
|
error("Calculated num_pixels to process is zero or negative: $num_pixels")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate coordinates for truncated pixels
|
||||||
|
final_coordinates = zeros(Int, num_pixels, 2)
|
||||||
|
final_pixel_times = zeros(Int64, num_pixels)
|
||||||
|
|
||||||
# Handle cases where some pixels have no scans by creating an empty range
|
|
||||||
for i in 1:num_pixels
|
for i in 1:num_pixels
|
||||||
if index_matrix[i, 2] < index_matrix[i, 1]
|
idx = pixel_matrix[i, 1]
|
||||||
index_matrix[i, 2] = index_matrix[i, 1] - 1
|
final_coordinates[i, 1] = ((idx - 1) % current_img_width) + 1
|
||||||
|
final_coordinates[i, 2] = fld(idx - 1, current_img_width) + 1
|
||||||
|
final_pixel_times[i] = pixel_times[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@info "Using provided dimensions: $(current_img_width)x$(current_img_height)"
|
||||||
|
pixel_times = convert(Vector{Int64}, pixel_matrix[:, 2])
|
||||||
|
|
||||||
|
# R's pixel truncation logic (FIXED: consistent formula)
|
||||||
|
num_pixels = (floor(Int, (num_pixels_original - 1) / current_img_width)) * current_img_width
|
||||||
|
if num_pixels <= 0
|
||||||
|
error("Calculated num_pixels to process is zero or negative: $num_pixels")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate coordinates
|
||||||
|
final_coordinates = zeros(Int, num_pixels, 2)
|
||||||
|
final_pixel_times = zeros(Int64, num_pixels)
|
||||||
|
|
||||||
|
for i in 1:num_pixels
|
||||||
|
idx = pixel_matrix[i, 1]
|
||||||
|
final_coordinates[i, 1] = ((idx - 1) % current_img_width) + 1
|
||||||
|
final_coordinates[i, 2] = fld(idx - 1, current_img_width) + 1
|
||||||
|
final_pixel_times[i] = pixel_times[i]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return hcat(pixel, index_matrix)
|
# Normalize pixel times
|
||||||
|
if !isempty(final_pixel_times)
|
||||||
|
min_pixel_time = minimum(final_pixel_times)
|
||||||
|
final_pixel_times .-= min_pixel_time
|
||||||
|
end
|
||||||
|
|
||||||
|
num_scans = size(scans, 1)
|
||||||
|
if num_scans == 0
|
||||||
|
return hcat(final_coordinates, final_pixel_times, zeros(Int64, num_pixels, 2))
|
||||||
|
end
|
||||||
|
|
||||||
|
# FIXED R's time matching algorithm with bounds checking
|
||||||
|
first_idx = 1
|
||||||
|
last_idx = 1
|
||||||
|
index_matrix = zeros(Int, num_pixels, 2)
|
||||||
|
|
||||||
|
for i_pixel in 1:num_pixels
|
||||||
|
pixel_time = final_pixel_times[i_pixel]
|
||||||
|
|
||||||
|
# Find the first scan that reaches or exceeds pixel time
|
||||||
|
while last_idx <= num_scans && scans[last_idx, 2] < pixel_time
|
||||||
|
last_idx += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ensure valid indices
|
||||||
|
if last_idx > num_scans
|
||||||
|
# No more scans available for remaining pixels
|
||||||
|
index_matrix[i_pixel:end, 1] .= num_scans + 1 # Invalid index
|
||||||
|
index_matrix[i_pixel:end, 2] .= num_scans # Invalid index
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
# Assign scan indices with bounds checking
|
||||||
|
start_scan = max(1, first_idx)
|
||||||
|
end_scan = max(1, last_idx - 1)
|
||||||
|
|
||||||
|
# Ensure start_scan <= end_scan
|
||||||
|
if start_scan > end_scan
|
||||||
|
start_scan = end_scan
|
||||||
|
end
|
||||||
|
|
||||||
|
index_matrix[i_pixel, 1] = start_scan
|
||||||
|
index_matrix[i_pixel, 2] = end_scan
|
||||||
|
|
||||||
|
# Update for next iteration (R's algorithm)
|
||||||
|
first_idx = last_idx - 1
|
||||||
|
last_idx = first_idx
|
||||||
|
|
||||||
|
# Ensure first_idx doesn't go below 1
|
||||||
|
if first_idx < 1
|
||||||
|
first_idx = 1
|
||||||
|
last_idx = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return hcat(final_coordinates, final_pixel_times, index_matrix)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
function RenderPixel(pixel_info, scans, spectra, scan_time_deltas, pixel_time_deltas)
|
function RenderPixel(pixel_info, scans, msi_data::MSIData, scan_time_deltas, pixel_time_deltas)
|
||||||
first_scan = pixel_info[3]
|
pixel_time = pixel_info[3]
|
||||||
last_scan = pixel_info[4]
|
first_scan = pixel_info[4]
|
||||||
|
last_scan = pixel_info[5]
|
||||||
num_actions = last_scan - first_scan
|
num_actions = last_scan - first_scan
|
||||||
|
|
||||||
# Use the m/z array of the first scan as the reference
|
# Get reference m/z array from the first scan involved
|
||||||
mz_array = spectra[1, first_scan]
|
mz_array, _ = GetSpectrum(msi_data, first_scan)
|
||||||
new_intensity = zeros(Float32, length(mz_array))
|
new_intensity = zeros(Float32, length(mz_array))
|
||||||
|
|
||||||
|
# SAFETY: Ensure we have valid scan indices
|
||||||
|
if first_scan < 1 || last_scan > size(scans, 1) || first_scan > last_scan
|
||||||
|
return (mz_array, new_intensity) # Return zero intensity for invalid ranges
|
||||||
|
end
|
||||||
|
|
||||||
if num_actions == 0 # Single scan contributes to the pixel
|
if num_actions == 0 # Single scan contributes to the pixel
|
||||||
scale = pixel_time_deltas[pixel_info[1]] / scan_time_deltas[first_scan]
|
_, intensity = GetSpectrum(msi_data, first_scan)
|
||||||
new_intensity .+= spectra[2, first_scan] .* scale
|
# SAFETY: Ensure positive scaling
|
||||||
|
scale = max(pixel_time_deltas[first_scan] / scan_time_deltas[first_scan], 0.0f0)
|
||||||
|
new_intensity .= intensity .* scale
|
||||||
|
|
||||||
elseif num_actions == 1 # Two partial scans contribute
|
elseif num_actions == 1 # Two partial scans contribute
|
||||||
# First partial scan
|
# First partial scan
|
||||||
scale1 = (scans[last_scan, 2] - pixel_info[2]) / scan_time_deltas[first_scan]
|
_, intensity1 = GetSpectrum(msi_data, first_scan)
|
||||||
new_intensity .+= spectra[2, first_scan] .* scale1
|
scale1 = max((scans[last_scan, 2] - pixel_time) / scan_time_deltas[first_scan], 0.0f0)
|
||||||
|
new_intensity .+= intensity1 .* scale1
|
||||||
|
|
||||||
# Second partial scan
|
# Second partial scan
|
||||||
next_pixel_time = pixel_info[2] + pixel_time_deltas[pixel_info[1]]
|
_, intensity2 = GetSpectrum(msi_data, last_scan)
|
||||||
scale2 = (next_pixel_time - scans[last_scan, 2]) / scan_time_deltas[last_scan]
|
next_pixel_time = pixel_time + pixel_time_deltas[first_scan]
|
||||||
new_intensity .+= spectra[2, last_scan] .* scale2
|
scale2 = max((next_pixel_time - scans[last_scan, 2]) / scan_time_deltas[last_scan], 0.0f0)
|
||||||
|
new_intensity .+= intensity2 .* scale2
|
||||||
|
|
||||||
elseif num_actions > 1 # Multiple scans contribute
|
elseif num_actions > 1 # Multiple scans contribute
|
||||||
# First partial scan
|
# First partial scan
|
||||||
scale1 = (scans[first_scan + 1, 2] - pixel_info[2]) / scan_time_deltas[first_scan]
|
_, intensity1 = GetSpectrum(msi_data, first_scan)
|
||||||
new_intensity .+= spectra[2, first_scan] .* scale1
|
scale1 = max((scans[first_scan + 1, 2] - pixel_time) / scan_time_deltas[first_scan], 0.0f0)
|
||||||
|
new_intensity .+= intensity1 .* scale1
|
||||||
|
|
||||||
# Full scans in the middle
|
# Full scans in the middle
|
||||||
for i in (first_scan + 1):(last_scan - 1)
|
for i in (first_scan + 1):(last_scan - 1)
|
||||||
new_intensity .+= spectra[2, i]
|
_, intensity_middle = GetSpectrum(msi_data, i)
|
||||||
|
new_intensity .+= intensity_middle
|
||||||
end
|
end
|
||||||
|
|
||||||
# Last partial scan
|
# Last partial scan
|
||||||
next_pixel_time = pixel_info[2] + pixel_time_deltas[pixel_info[1]]
|
_, intensity2 = GetSpectrum(msi_data, last_scan)
|
||||||
scale2 = (next_pixel_time - scans[last_scan, 2]) / scan_time_deltas[last_scan]
|
next_pixel_time = pixel_time + pixel_time_deltas[first_scan]
|
||||||
new_intensity .+= spectra[2, last_scan] .* scale2
|
scale2 = max((next_pixel_time - scans[last_scan, 2]) / scan_time_deltas[last_scan], 0.0f0)
|
||||||
|
new_intensity .+= intensity2 .* scale2
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# FINAL SAFETY: Clamp any negative values to zero
|
||||||
|
new_intensity = max.(new_intensity, 0.0f0)
|
||||||
|
|
||||||
return (mz_array, new_intensity)
|
return (mz_array, new_intensity)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConvertMzmlToImzml(source_file::String, timing_matrix::Matrix{Int64}, scans::Matrix{Int64})
|
function ConvertMzmlToImzml(source_file::String, timing_matrix::Matrix{Int64}, scans::Matrix{Int64})
|
||||||
# Get image width from timing info
|
if size(timing_matrix, 1) == 0
|
||||||
width = findfirst(i -> timing_matrix[i, 1] - timing_matrix[i-1, 1] != 1, 2:size(timing_matrix, 1))
|
return ProcessedPixel[], (0, 0)
|
||||||
height = size(timing_matrix, 1) ÷ width
|
end
|
||||||
|
|
||||||
# Load the full mzML data
|
# Get image dimensions from timing_matrix (assuming columns 1 and 2 are X and Y)
|
||||||
spectra = LoadSpectra(source_file)
|
width = maximum(timing_matrix[:, 1])
|
||||||
|
height = maximum(timing_matrix[:, 2])
|
||||||
|
|
||||||
# Pre-calculate time deltas
|
# Open the mzML file using the new memory-efficient API
|
||||||
scan_time_deltas = diff(scans[:, 2])
|
msi_data = OpenMSIData(source_file)
|
||||||
pixel_time_deltas = diff(timing_matrix[:, 2])
|
|
||||||
# Append a final delta for the last element
|
# Pre-calculate time deltas robustly
|
||||||
push!(scan_time_deltas, scan_time_deltas[end])
|
scan_time_deltas = zeros(Int64, size(scans, 1))
|
||||||
push!(pixel_time_deltas, pixel_time_deltas[end])
|
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) # Ensure delta is at least 1
|
||||||
|
end
|
||||||
|
scan_time_deltas[end] = max(1, scan_time_deltas[end-1]) # Use previous delta for last scan, ensure at least 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)
|
||||||
|
# Assumes time is in the 3rd column of the timing_matrix
|
||||||
|
delta = timing_matrix[i+1, 3] - timing_matrix[i, 3]
|
||||||
|
pixel_time_deltas[i] = max(1, delta) # Ensure delta is at least 1
|
||||||
|
end
|
||||||
|
pixel_time_deltas[end] = max(1, pixel_time_deltas[end-1]) # Use previous delta for last pixel, ensure at least 1
|
||||||
|
end
|
||||||
|
|
||||||
processed_pixels = ProcessedPixel[]
|
processed_pixels = ProcessedPixel[]
|
||||||
sizehint!(processed_pixels, size(timing_matrix, 1))
|
sizehint!(processed_pixels, size(timing_matrix, 1))
|
||||||
|
empty_pixel_count = 0
|
||||||
|
|
||||||
|
# DEBUG: Scan coverage analysis
|
||||||
|
println("DEBUG: Scan coverage analysis:")
|
||||||
|
covered_pixels_count = 0
|
||||||
|
for i in 1:size(timing_matrix, 1)
|
||||||
|
first_scan = timing_matrix[i, 4]
|
||||||
|
last_scan = timing_matrix[i, 5]
|
||||||
|
if first_scan <= last_scan && first_scan >= 1 && last_scan <= size(scans, 1)
|
||||||
|
covered_pixels_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
println(" Pixels with potential scans: $covered_pixels_count/$(size(timing_matrix, 1))")
|
||||||
|
println(" Total scans available: $(size(scans, 1))")
|
||||||
|
|
||||||
for i in 1:size(timing_matrix, 1)
|
for i in 1:size(timing_matrix, 1)
|
||||||
pixel_info = timing_matrix[i, :]
|
pixel_info = timing_matrix[i, :]
|
||||||
first_scan = pixel_info[3]
|
x = pixel_info[1]
|
||||||
last_scan = pixel_info[4]
|
y = pixel_info[2]
|
||||||
|
first_scan = pixel_info[4]
|
||||||
|
last_scan = pixel_info[5]
|
||||||
|
|
||||||
# Skip if there are no scans for this pixel
|
if first_scan > last_scan || first_scan < 1 || last_scan > size(scans, 1)
|
||||||
(first_scan > last_scan) && continue
|
empty_pixel_count += 1
|
||||||
|
push!(processed_pixels, ProcessedPixel((x, y), Float64[], Float32[]))
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
# Calculate coordinates
|
mz, intensity = RenderPixel(pixel_info, scans, msi_data, scan_time_deltas, pixel_time_deltas)
|
||||||
x = ((pixel_info[1] - 1) % width) + 1
|
|
||||||
y = fld(pixel_info[1] - 1, width) + 1
|
|
||||||
|
|
||||||
mz, intensity = RenderPixel(pixel_info, scans, spectra, scan_time_deltas, pixel_time_deltas)
|
|
||||||
push!(processed_pixels, ProcessedPixel((x, y), mz, intensity))
|
push!(processed_pixels, ProcessedPixel((x, y), mz, intensity))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@info "Found and created $empty_pixel_count empty pixels out of $(size(timing_matrix, 1)) total."
|
||||||
return processed_pixels, (width, height)
|
return processed_pixels, (width, height)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ExportImzml(target_file::String, pixels::Vector{ProcessedPixel}, dims::Tuple{Int, Int})
|
function ExportImzml(target_file::String, pixels::Vector{ProcessedPixel}, dims::Tuple{Int, Int})
|
||||||
@warn "ExportImzml is not yet implemented. The processed pixel data has been generated but not saved to .imzML/.ibd files."
|
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
|
||||||
# TODO: Implement the logic to write the .imzML (XML) and .ibd (binary) files.
|
|
||||||
# 1. Open target_file.imzML and target_file.ibd for writing.
|
# Ensure we have valid pixels
|
||||||
# 2. Write all m/z and intensity arrays sequentially to the .ibd file, tracking offsets and lengths.
|
if isempty(pixels)
|
||||||
# 3. Write the .imzML XML structure, including:
|
@error "No pixels to export"
|
||||||
# - Boilerplate headers
|
|
||||||
# - <fileDescription>, <referenceableParamGroupList>, <scanSettingsList>
|
|
||||||
# - A <spectrumList> with a <spectrum> for each pixel.
|
|
||||||
# - Each <spectrum> must contain cvParams for x/y coords and external data pointers to the .ibd file.
|
|
||||||
return false
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
binary_meta = BinaryMetadata[]
|
||||||
|
sizehint!(binary_meta, length(pixels))
|
||||||
|
|
||||||
|
try
|
||||||
|
# Step 1: Write .ibd file with proper binary format
|
||||||
|
open(ibd_file, "w") do ibd_stream
|
||||||
|
# Write UUID as first 16 bytes (placeholder)
|
||||||
|
write(ibd_stream, zeros(UInt8, 16))
|
||||||
|
|
||||||
|
# Write in little-endian format (standard for imzML)
|
||||||
|
for pixel in pixels
|
||||||
|
# Write m/z array as 64-bit little-endian floats
|
||||||
|
mz_offset = position(ibd_stream)
|
||||||
|
for val in pixel.mz
|
||||||
|
write(ibd_stream, htol(Float64(val))) # Host to little-endian
|
||||||
|
end
|
||||||
|
mz_length = position(ibd_stream) - mz_offset
|
||||||
|
|
||||||
|
# Write intensity array as 32-bit little-endian floats
|
||||||
|
int_offset = position(ibd_stream)
|
||||||
|
for val in pixel.intensity
|
||||||
|
write(ibd_stream, htol(Float32(val))) # Host to little-endian
|
||||||
|
end
|
||||||
|
int_length = position(ibd_stream) - int_offset
|
||||||
|
|
||||||
|
push!(binary_meta, BinaryMetadata(mz_offset, mz_length, int_offset, int_length))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Step 2: Write proper .imzML XML file
|
||||||
|
open(target_file, "w") do imzml_stream
|
||||||
|
# Use indexedmzML wrapper
|
||||||
|
write(imzml_stream, """<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||||
|
<indexedmzML xmlns="http://psi.hupo.org/ms/mzml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.0_idx.xsd">
|
||||||
|
<mzML version="1.1" id="$(splitext(basename(target_file))[1])">
|
||||||
|
""")
|
||||||
|
|
||||||
|
# CV List
|
||||||
|
write(imzml_stream, """ <cvList count="3">
|
||||||
|
<cv id="MS" fullName="Proteomics Standards Initiative Mass Spectrometry Ontology" version="1.3.1" URI="http://psidev.info/ms/mzML/psi-ms.obo"/>
|
||||||
|
<cv id="UO" fullName="Unit Ontology" version="1.15" URI="http://obo.cvs.sourceforge.net/obo/obo/ontology/phenotype/unit.obo"/>
|
||||||
|
<cv id="IMS" fullName="Imaging MS Ontology" version="0.9.1" URI="http://www.maldi-msi.org/download/imzml/imagingMS.obo"/>
|
||||||
|
</cvList>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# File Description
|
||||||
|
write(imzml_stream, """ <fileDescription>
|
||||||
|
<fileContent>
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000579" name="MS1 spectrum"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000080" name="mass spectrum"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000031" name="processed"/>
|
||||||
|
</fileContent>
|
||||||
|
</fileDescription>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Referenceable Param Groups
|
||||||
|
write(imzml_stream, """ <referenceableParamGroupList count="2">
|
||||||
|
<referenceableParamGroup id="mzArray">
|
||||||
|
<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="IMS" accession="IMS:1000101" name="external data" value="true"/>
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000523" name="64-bit float"/>
|
||||||
|
</referenceableParamGroup>
|
||||||
|
<referenceableParamGroup id="intensityArray">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000515" name="intensity array" unitCvRef="MS" unitAccession="MS:1000131" unitName="number of counts"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000101" name="external data" value="true"/>
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000521" name="32-bit float"/>
|
||||||
|
</referenceableParamGroup>
|
||||||
|
</referenceableParamGroupList>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Sample List
|
||||||
|
write(imzml_stream, """ <sampleList count="1">
|
||||||
|
<sample id="sample1" name="ImagingSample">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000001" name="sample number" value="1"/>
|
||||||
|
</sample>
|
||||||
|
</sampleList>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Software List - OPEN SOURCE
|
||||||
|
write(imzml_stream, """ <softwareList count="1">
|
||||||
|
<software id="MSIConverter" version="1.0">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000799" name="custom unreleased software tool" value="mzML to imzML converter"/>
|
||||||
|
</software>
|
||||||
|
</softwareList>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Scan Settings IMAGING
|
||||||
|
write(imzml_stream, """ <scanSettingsList count="1">
|
||||||
|
<scanSettings id="scanSettings1">
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000042" name="max count of pixel x" value="$(dims[1])"/>
|
||||||
|
<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:1000047" name="pixel size y" value="1" unitCvRef="UO" unitAccession="UO:0000017" unitName="micrometer"/>
|
||||||
|
</scanSettings>
|
||||||
|
</scanSettingsList>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Instrument Configuration MALDI/TOF
|
||||||
|
write(imzml_stream, """ <instrumentConfigurationList count="1">
|
||||||
|
<instrumentConfiguration id="instrument1">
|
||||||
|
<componentList count="3">
|
||||||
|
<source order="1">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000075" name="MALDI source"/>
|
||||||
|
</source>
|
||||||
|
<analyzer order="2">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000084" name="time-of-flight"/>
|
||||||
|
</analyzer>
|
||||||
|
<detector order="3">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000253" name="electron multiplier"/>
|
||||||
|
</detector>
|
||||||
|
</componentList>
|
||||||
|
<softwareRef ref="MSIConverter"/>
|
||||||
|
</instrumentConfiguration>
|
||||||
|
</instrumentConfigurationList>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Data Processing
|
||||||
|
write(imzml_stream, """ <dataProcessingList count="1">
|
||||||
|
<dataProcessing id="conversionProcessing">
|
||||||
|
<processingMethod order="1" softwareRef="MSIConverter">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000544" name="Conversion to imzML"/>
|
||||||
|
</processingMethod>
|
||||||
|
</dataProcessing>
|
||||||
|
</dataProcessingList>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Run and Spectrum List
|
||||||
|
spectrum_offsets = UInt64[]
|
||||||
|
write(imzml_stream, """ <run defaultInstrumentConfigurationRef="instrument1" id="run1" sampleRef="sample1">
|
||||||
|
<spectrumList count="$(length(pixels))" defaultDataProcessingRef="conversionProcessing">
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Write each spectrum
|
||||||
|
for (i, pixel) in enumerate(pixels)
|
||||||
|
meta = binary_meta[i]
|
||||||
|
x, y = pixel.coords
|
||||||
|
|
||||||
|
spectrum_start = position(imzml_stream)
|
||||||
|
push!(spectrum_offsets, spectrum_start)
|
||||||
|
|
||||||
|
write(imzml_stream, """ <spectrum id="Scan=$(i)" defaultArrayLength="$(length(pixel.mz))" index="$(i-1)">
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="1"/>
|
||||||
|
<cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum"/>
|
||||||
|
<scanList count="1">
|
||||||
|
<scan instrumentConfigurationRef="instrument1">
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000050" name="position x" value="$(x)"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000051" name="position y" value="$(y)"/>
|
||||||
|
</scan>
|
||||||
|
</scanList>
|
||||||
|
<binaryDataArrayList count="2">
|
||||||
|
<binaryDataArray encodedLength="0">
|
||||||
|
<referenceableParamGroupRef ref="mzArray"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.mz_offset)"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(length(pixel.mz))"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000104" name="external encoded length" value="$(meta.mz_length)"/>
|
||||||
|
<binary/>
|
||||||
|
</binaryDataArray>
|
||||||
|
<binaryDataArray encodedLength="0">
|
||||||
|
<referenceableParamGroupRef ref="intensityArray"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.int_offset)"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(length(pixel.intensity))"/>
|
||||||
|
<cvParam cvRef="IMS" accession="IMS:1000104" name="external encoded length" value="$(meta.int_length)"/>
|
||||||
|
<binary/>
|
||||||
|
</binaryDataArray>
|
||||||
|
</binaryDataArrayList>
|
||||||
|
</spectrum>
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
write(imzml_stream, """ </spectrumList>
|
||||||
|
</run>
|
||||||
|
</mzML>
|
||||||
|
""")
|
||||||
|
# Index List
|
||||||
|
index_list_start = position(imzml_stream)
|
||||||
|
write(imzml_stream, """ <indexList count="1">
|
||||||
|
<index name="spectrum">
|
||||||
|
""")
|
||||||
|
for (i, offset) in enumerate(spectrum_offsets)
|
||||||
|
write(imzml_stream, " <offset idRef=\"Scan=$(i)\">$offset</offset>\n")
|
||||||
|
end
|
||||||
|
write(imzml_stream, """ </index>
|
||||||
|
</indexList>
|
||||||
|
<indexListOffset>$index_list_start</indexListOffset>
|
||||||
|
</indexedmzML>
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
println("Successfully created: $target_file")
|
||||||
|
println("Successfully created: $ibd_file")
|
||||||
|
return true
|
||||||
|
|
||||||
|
catch e
|
||||||
|
@error "Failed to export imzML files" exception=(e, catch_backtrace())
|
||||||
|
# Clean up partial files
|
||||||
|
isfile(ibd_file) && rm(ibd_file, force=true)
|
||||||
|
isfile(target_file) && rm(target_file, force=true)
|
||||||
|
return false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -241,13 +620,15 @@ Main workflow function to convert a .mzML file to an .imzML file.
|
|||||||
* `source_file`: Path to the input .mzML file.
|
* `source_file`: Path to the input .mzML file.
|
||||||
* `sync_file`: Path to the synchronization text file.
|
* `sync_file`: Path to the synchronization text file.
|
||||||
* `target_file`: Path for the output .imzML file (the .ibd will be named accordingly).
|
* `target_file`: Path for the output .imzML file (the .ibd will be named accordingly).
|
||||||
|
* `img_width`: width dimention for the creation of the x axis
|
||||||
|
* `img_height`: height dimention for the creation of the y axis
|
||||||
"""
|
"""
|
||||||
function ImportMzmlFile(source_file::String, sync_file::String, target_file::String)
|
function ImportMzmlFile(source_file::String, sync_file::String, target_file::String; img_width::Int=0, img_height::Int=0)
|
||||||
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)
|
||||||
|
|
||||||
println("Step 2: Matching acquisition times...")
|
println("Step 2: Matching acquisition times...")
|
||||||
timing_matrix = MatchAcquireTime(sync_file, scans)
|
timing_matrix = MatchAcquireTime(sync_file, scans; img_width=img_width, img_height=img_height)
|
||||||
|
|
||||||
println("Step 3: Converting spectra...")
|
println("Step 3: Converting spectra...")
|
||||||
processed_pixels, (width, height) = ConvertMzmlToImzml(source_file, timing_matrix, scans)
|
processed_pixels, (width, height) = ConvertMzmlToImzml(source_file, timing_matrix, scans)
|
||||||
@ -264,7 +645,7 @@ function ImportMzmlFile(source_file::String, sync_file::String, target_file::Str
|
|||||||
if success
|
if success
|
||||||
println("Conversion successful: $target_file")
|
println("Conversion successful: $target_file")
|
||||||
else
|
else
|
||||||
println("Conversion failed. Exporting is not yet implemented.")
|
println("Conversion failed.")
|
||||||
end
|
end
|
||||||
return success
|
return success
|
||||||
end
|
end
|
||||||
|
|||||||
@ -51,7 +51,7 @@ Retrieves an attribute's value from an XML tag string.
|
|||||||
"""
|
"""
|
||||||
function get_attribute(source::AbstractString, tag::String = "([^=]+)")
|
function get_attribute(source::AbstractString, tag::String = "([^=]+)")
|
||||||
# Construct the regex pattern string
|
# Construct the regex pattern string
|
||||||
pattern_str = "\s" * tag * "=\"([^"]*)\""
|
pattern_str = "\\s" * tag * "=\"([^\"]*)\""
|
||||||
regStr = Regex(pattern_str)
|
regStr = Regex(pattern_str)
|
||||||
return match(regStr, source)
|
return match(regStr, source)
|
||||||
end
|
end
|
||||||
@ -107,16 +107,3 @@ mutable struct CVParams
|
|||||||
is_compressed::Bool
|
is_compressed::Bool
|
||||||
axis_type::Symbol
|
axis_type::Symbol
|
||||||
end
|
end
|
||||||
|
|
||||||
function update_cv_params!(params::CVParams, acc::String)
|
|
||||||
if acc == "MS:1000514"; params.axis_type = :mz;
|
|
||||||
elseif acc == "MS:1000515"; params.axis_type = :intensity;
|
|
||||||
elseif acc == "MS:1000519"; params.format = Int32;
|
|
||||||
elseif acc == "MS:1000521"; params.format = Float32;
|
|
||||||
elseif acc == "MS:1000522"; params.format = Int64;
|
|
||||||
elseif acc == "MS:1000523"; params.format = Float64;
|
|
||||||
elseif acc == "MS:1000574"; params.is_compressed = true;
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
281
src/imzML.jl
281
src/imzML.jl
@ -1,40 +1,4 @@
|
|||||||
|
using Images, Statistics, CairoMakie, DataFrames, Printf, ColorSchemes
|
||||||
using Images, ImageFiltering, StatsBase, Statistics, CairoMakie, ColorSchemes, DataFrames, CSV, Printf, Dates
|
|
||||||
include("ParserHelpers.jl")
|
|
||||||
|
|
||||||
# --- Lazy Loading Data Structures ---
|
|
||||||
|
|
||||||
# Struct to hold metadata for a single spectrum, for lazy loading.
|
|
||||||
struct SpectrumMetadata
|
|
||||||
x::Int32
|
|
||||||
y::Int32
|
|
||||||
mz_offset::Int64
|
|
||||||
intensity_offset::Int64
|
|
||||||
mz_count::Int32
|
|
||||||
intensity_count::Int32
|
|
||||||
end
|
|
||||||
|
|
||||||
# Struct to hold the parsed imzML metadata and the handle to the .ibd file.
|
|
||||||
mutable struct ImzMLData
|
|
||||||
ibd_handle::IO
|
|
||||||
mz_format::Type
|
|
||||||
intensity_format::Type
|
|
||||||
spectra_metadata::Vector{SpectrumMetadata}
|
|
||||||
width::Int
|
|
||||||
height::Int
|
|
||||||
mz_is_first::Bool # To know the order in the ibd file
|
|
||||||
|
|
||||||
function ImzMLData(ibd_handle::IO, mz_format::Type, intensity_format::Type, spectra_metadata::Vector{SpectrumMetadata}, width::Int, height::Int, mz_is_first::Bool)
|
|
||||||
obj = new(ibd_handle, mz_format, intensity_format, spectra_metadata, width, height, mz_is_first)
|
|
||||||
finalizer(obj) do o
|
|
||||||
if isopen(o.ibd_handle)
|
|
||||||
close(o.ibd_handle)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return obj
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# --- Extracted from imzML.jl ---
|
# --- Extracted from imzML.jl ---
|
||||||
|
|
||||||
@ -43,7 +7,7 @@ This file provides a library for parsing `.imzML` and `.ibd` files in pure Julia
|
|||||||
It is intended to be included by a parent script.
|
It is intended to be included by a parent script.
|
||||||
|
|
||||||
Core Functions:
|
Core Functions:
|
||||||
- `load_imzml`: The main function that orchestrates the parsing.
|
- `load_imzml_lazy`: The main function that orchestrates the parsing.
|
||||||
- Helper functions for reading XML metadata and binary spectral data.
|
- Helper functions for reading XML metadata and binary spectral data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -174,70 +138,66 @@ end
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
load_imzml(file_path::String)
|
load_imzml_lazy(file_path::String; cache_size=100)
|
||||||
Main function to parse an `.imzML`/.ibd file pair and prepare for lazy loading.
|
|
||||||
It parses the metadata from the `.imzML` file, including spectrum coordinates and
|
|
||||||
byte offsets for data in the `.ibd` file, but does not load the spectral data itself.
|
|
||||||
|
|
||||||
# Returns
|
Main function to parse an `.imzML`/.ibd file pair and prepare for lazy loading.
|
||||||
- An `ImzMLData` struct containing the open `.ibd` file handle and all necessary metadata
|
It now returns a unified MSIData object.
|
||||||
for on-demand data loading.
|
|
||||||
"""
|
"""
|
||||||
function load_imzml(file_path::String)
|
function load_imzml_lazy(file_path::String; cache_size=100)
|
||||||
|
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)")
|
error("Provided path is not a file: $(file_path)")
|
||||||
end
|
end
|
||||||
|
|
||||||
ibd_path = replace(file_path, ".imzML" => ".ibd")
|
ibd_path = replace(file_path, r"\.(imzML|mzML)"i => ".ibd")
|
||||||
|
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)")
|
error("Corresponding .ibd file not found for: $(file_path)")
|
||||||
end
|
end
|
||||||
|
|
||||||
stream = open(file_path)
|
println("DEBUG: Opening file streams for .imzML and .ibd")
|
||||||
hIbd = open(ibd_path)
|
stream = open(file_path, "r")
|
||||||
|
hIbd = open(ibd_path, "r")
|
||||||
# We don't use a try/finally block because we need to return the open hIbd handle.
|
|
||||||
# The finalizer on the ImzMLData struct will be responsible for closing it.
|
|
||||||
|
|
||||||
|
try
|
||||||
|
println("DEBUG: Configuring axes...")
|
||||||
axis = axes_config_img(stream)
|
axis = axes_config_img(stream)
|
||||||
|
println("DEBUG: Getting image dimensions...")
|
||||||
imgDim = get_img_dimensions(stream)
|
imgDim = get_img_dimensions(stream)
|
||||||
width, height, num_spectra = imgDim
|
width, height, num_spectra = imgDim
|
||||||
|
println("DEBUG: Image dimensions: $(width)x$(height), $num_spectra spectra.")
|
||||||
|
|
||||||
# Determine which format is for m/z and which for intensity from the referenceableParamGroups
|
|
||||||
mz_config_idx = findfirst(a -> a.Axis == 1, axis)
|
mz_config_idx = findfirst(a -> a.Axis == 1, axis)
|
||||||
int_config_idx = findfirst(a -> a.Axis == 2, axis)
|
int_config_idx = findfirst(a -> a.Axis == 2, axis)
|
||||||
mz_format = axis[mz_config_idx].Format
|
mz_format = axis[mz_config_idx].Format
|
||||||
intensity_format = axis[int_config_idx].Format
|
intensity_format = axis[int_config_idx].Format
|
||||||
|
println("DEBUG: m/z format: $mz_format, Intensity format: $intensity_format")
|
||||||
|
|
||||||
# The original parsing logic is highly optimized and relies on fixed offsets within the XML structure.
|
# --- NEW PARSING LOGIC based on the old, working code ---
|
||||||
# We reuse it to get the initial .ibd file offset and the structural attributes of the <spectrum> tags.
|
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)
|
attr = get_spectrum_attributes(stream, hIbd)
|
||||||
|
|
||||||
# get_spectrum_attributes moves the hIbd pointer to the start of the first spectrum's binary data.
|
|
||||||
current_ibd_offset = position(hIbd)
|
current_ibd_offset = position(hIbd)
|
||||||
|
|
||||||
# We need to re-scan the spectra list from the beginning to get metadata for each spectrum.
|
|
||||||
seek(stream, start_of_spectra_xml)
|
seek(stream, start_of_spectra_xml)
|
||||||
|
println("DEBUG: Initial IBD offset: $current_ibd_offset")
|
||||||
|
|
||||||
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
spectra_metadata = Vector{SpectrumMetadata}(undef, num_spectra)
|
||||||
|
|
||||||
# attr[3] is 4 if the first binary data array is intensity, 3 if it's m/z.
|
|
||||||
mz_is_first = attr[3] == 3
|
mz_is_first = attr[3] == 3
|
||||||
|
|
||||||
|
println("DEBUG: Parsing metadata for $num_spectra spectra using skip-based method...")
|
||||||
for k in 1:num_spectra
|
for k in 1:num_spectra
|
||||||
# These skips are based on the structure of the first spectrum tag, assuming all are identical.
|
# Use skip values learned from the first spectrum, assuming all are identical.
|
||||||
skip(stream, attr[5]) # Skip to X coordinate value
|
skip(stream, attr[5]) # Skip to X coordinate value
|
||||||
val_tag = find_tag(stream, r"value=\"(\d+)\"")
|
val_tag_x = find_tag(stream, r"value=\"(\d+)\"")
|
||||||
x = parse(Int32, val_tag.captures[1])
|
x = parse(Int32, val_tag_x.captures[1])
|
||||||
|
|
||||||
skip(stream, attr[6]) # Skip to Y coordinate value
|
skip(stream, attr[6]) # Skip to Y coordinate value
|
||||||
val_tag = find_tag(stream, r"value=\"(\d+)\"")
|
val_tag_y = find_tag(stream, r"value=\"(\d+)\"")
|
||||||
y = parse(Int32, val_tag.captures[1])
|
y = parse(Int32, val_tag_y.captures[1])
|
||||||
|
|
||||||
skip(stream, attr[7]) # Skip to array length value
|
skip(stream, attr[7]) # Skip to array length value
|
||||||
val_tag = find_tag(stream, r"value=\"(\d+)\"")
|
val_tag_len = find_tag(stream, r"value=\"(\d+)\"")
|
||||||
nPoints = parse(Int32, val_tag.captures[1])
|
nPoints = parse(Int32, val_tag_len.captures[1])
|
||||||
|
|
||||||
mz_len_bytes = nPoints * sizeof(mz_format)
|
mz_len_bytes = nPoints * sizeof(mz_format)
|
||||||
int_len_bytes = nPoints * sizeof(intensity_format)
|
int_len_bytes = nPoints * sizeof(intensity_format)
|
||||||
@ -251,18 +211,42 @@ function load_imzml(file_path::String)
|
|||||||
mz_offset = int_offset + int_len_bytes
|
mz_offset = int_offset + int_len_bytes
|
||||||
end
|
end
|
||||||
|
|
||||||
spectra_metadata[k] = SpectrumMetadata(x, y, mz_offset, int_offset, nPoints, nPoints)
|
# Create modern SpectrumAsset objects
|
||||||
|
mz_asset = SpectrumAsset(mz_format, false, mz_offset, nPoints, :mz)
|
||||||
|
int_asset = SpectrumAsset(intensity_format, false, int_offset, nPoints, :intensity)
|
||||||
|
|
||||||
|
spectra_metadata[k] = SpectrumMetadata(x, y, "", mz_asset, int_asset)
|
||||||
|
|
||||||
# Advance the offset for the next spectrum's data.
|
# Advance the offset for the next spectrum's data.
|
||||||
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(stream, attr[8]) # Skip to the end of the spectrum tag
|
||||||
end
|
end
|
||||||
|
# --- END OF NEW PARSING LOGIC ---
|
||||||
|
|
||||||
|
println("DEBUG: Metadata parsing complete.")
|
||||||
|
|
||||||
|
# Build coordinate map for imzML files
|
||||||
|
println("DEBUG: Building coordinate map...")
|
||||||
|
coordinate_map = zeros(Int, width, height)
|
||||||
|
for (idx, meta) in enumerate(spectra_metadata)
|
||||||
|
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
||||||
|
coordinate_map[meta.x, meta.y] = idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
println("DEBUG: Coordinate map built.")
|
||||||
|
|
||||||
# Close the .imzML file stream, but leave the .ibd stream open for lazy reading.
|
|
||||||
close(stream)
|
close(stream)
|
||||||
|
|
||||||
return ImzMLData(hIbd, mz_format, intensity_format, spectra_metadata, width, height, mz_is_first)
|
source = ImzMLSource(hIbd, mz_format, intensity_format)
|
||||||
|
println("DEBUG: Creating MSIData object.")
|
||||||
|
return MSIData(source, spectra_metadata, (width, height), coordinate_map, cache_size)
|
||||||
|
|
||||||
|
catch e
|
||||||
|
close(stream)
|
||||||
|
close(hIbd)
|
||||||
|
rethrow(e)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# --- End of content from imzML.jl ---
|
# --- End of content from imzML.jl ---
|
||||||
@ -310,9 +294,8 @@ end
|
|||||||
load_slices(folder, masses, tolerance)
|
load_slices(folder, masses, tolerance)
|
||||||
|
|
||||||
Loads image slices for multiple masses from all `.imzML` files in a directory.
|
Loads image slices for multiple masses from all `.imzML` files in a directory.
|
||||||
This function is optimized to read the binary data file (`.ibd`) only once per file.
|
This function is now refactored to use the new MSIData architecture and its
|
||||||
It iterates through each spectrum, reads its data, and finds all target masses
|
caching capabilities.
|
||||||
before proceeding to the next spectrum, minimizing disk I/O.
|
|
||||||
"""
|
"""
|
||||||
function load_slices(folder, masses, tolerance)
|
function load_slices(folder, masses, tolerance)
|
||||||
files = filter(f -> endswith(f, ".imzML"), readdir(folder, join=true))
|
files = filter(f -> endswith(f, ".imzML"), readdir(folder, join=true))
|
||||||
@ -331,43 +314,27 @@ function load_slices(folder, masses, tolerance)
|
|||||||
push!(names, name)
|
push!(names, name)
|
||||||
@info "Processing $(i)/$(n_files): $(name)"
|
@info "Processing $(i)/$(n_files): $(name)"
|
||||||
|
|
||||||
imzML_data = @time load_imzml(file) # This is fast, it's fine.
|
# Load data using the new lazy loader, returning an MSIData object
|
||||||
|
msi_data = @time load_imzml_lazy(file)
|
||||||
|
|
||||||
# Create empty images for all slices for the current file
|
# Create empty images for all slices for the current file
|
||||||
current_file_slices = [zeros(Float64, imzML_data.width, imzML_data.height) for _ in 1:n_slices]
|
width, height = msi_data.image_dims
|
||||||
|
current_file_slices = [zeros(Float64, width, height) for _ in 1:n_slices]
|
||||||
|
|
||||||
hIbd = imzML_data.ibd_handle
|
# Use the high-performance iterator to process all spectra
|
||||||
mz_format = imzML_data.mz_format
|
_iterate_spectra_fast(msi_data) do spec_idx, mz_array, intensity_array
|
||||||
int_format = imzML_data.intensity_format
|
meta = msi_data.spectra_metadata[spec_idx]
|
||||||
|
|
||||||
# Pre-allocate arrays to be reused for each spectrum
|
|
||||||
mz_array = Array{mz_format}(undef, 0)
|
|
||||||
intensity_array = Array{int_format}(undef, 0)
|
|
||||||
|
|
||||||
# Iterate through each spectrum ONCE
|
|
||||||
for meta in imzML_data.spectra_metadata
|
|
||||||
# Resize arrays if the number of points in the spectrum has changed
|
|
||||||
if length(mz_array) != meta.mz_count
|
|
||||||
resize!(mz_array, meta.mz_count)
|
|
||||||
resize!(intensity_array, meta.intensity_count)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Read spectrum data ONCE from the .ibd file
|
|
||||||
seek(hIbd, meta.mz_offset)
|
|
||||||
read!(hIbd, mz_array)
|
|
||||||
seek(hIbd, meta.intensity_offset)
|
|
||||||
read!(hIbd, intensity_array)
|
|
||||||
|
|
||||||
# Now, check for all masses of interest in this single spectrum
|
# Now, check for all masses of interest in this single spectrum
|
||||||
for (j, mass) in enumerate(masses)
|
for (j, mass) in enumerate(masses)
|
||||||
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
||||||
if intensity > 0.0
|
if intensity > 0.0
|
||||||
if 1 <= meta.x <= imzML_data.width && 1 <= meta.y <= imzML_data.height
|
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
||||||
current_file_slices[j][meta.x, meta.y] = intensity
|
current_file_slices[j][meta.y, meta.x] = intensity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # end of spectra loop
|
end # end of fast iterator
|
||||||
|
|
||||||
# Assign the generated images to the main list
|
# Assign the generated images to the main list
|
||||||
for j in 1:n_slices
|
for j in 1:n_slices
|
||||||
@ -379,6 +346,70 @@ function load_slices(folder, masses, tolerance)
|
|||||||
return (img_list, names)
|
return (img_list, names)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
plot_slice(msi_data::MSIData, mass::Float64, tolerance::Float64, output_dir::String; stage_name="slice", bins=256)
|
||||||
|
|
||||||
|
Generates and saves a plot of a single image slice for a given m/z value.
|
||||||
|
This function closely imitates the logic of the original `GetSlice` but uses
|
||||||
|
the modern `MSIData` access patterns and robust peak finding.
|
||||||
|
"""
|
||||||
|
function plot_slice(msi_data::MSIData, mass::Real, tolerance::Real, output_dir::String; stage_name="slice_mz_$(mass)", bins=256)
|
||||||
|
|
||||||
|
# 1. Create an empty image for the slice, with dimensions matching plotting expectations
|
||||||
|
width, height = msi_data.image_dims
|
||||||
|
slice_matrix = zeros(Float64, height, width)
|
||||||
|
|
||||||
|
# 2. Iterate through each spectrum to build the slice
|
||||||
|
println("Generating slice for m/z $mass...")
|
||||||
|
_iterate_spectra_fast(msi_data) do spec_idx, mz_array, intensity_array
|
||||||
|
meta = msi_data.spectra_metadata[spec_idx]
|
||||||
|
|
||||||
|
# Find the peak intensity using the modern, robust find_mass
|
||||||
|
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
||||||
|
|
||||||
|
if intensity > 0.0
|
||||||
|
# Populate the matrix using (y, x) indexing
|
||||||
|
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
||||||
|
slice_matrix[meta.y, meta.x] = intensity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
println("Slice generation complete.")
|
||||||
|
|
||||||
|
# 3. Plot the resulting slice matrix using CairoMakie
|
||||||
|
println("Plotting slice...")
|
||||||
|
|
||||||
|
fig = Figure(size = (600, 500))
|
||||||
|
ax = CairoMakie.Axis(fig[1, 1],
|
||||||
|
aspect=DataAspect(),
|
||||||
|
title=@sprintf("Slice for m/z: %.2f", mass)
|
||||||
|
)
|
||||||
|
hidedecorations!(ax)
|
||||||
|
|
||||||
|
# Use mass-specific bounds for colorrange, ensuring a valid range
|
||||||
|
min_val, max_val = extrema(slice_matrix)
|
||||||
|
if min_val == max_val
|
||||||
|
max_val = min_val + 1.0 # Ensure the color range has a non-zero width
|
||||||
|
end
|
||||||
|
hm = heatmap!(ax, slice_matrix,
|
||||||
|
colormap=cgrad(ColorSchemes.viridis, bins),
|
||||||
|
colorrange=(min_val, max_val)
|
||||||
|
)
|
||||||
|
|
||||||
|
Colorbar(fig[1, 2], hm, label="Intensity")
|
||||||
|
colgap!(fig.layout, 5)
|
||||||
|
|
||||||
|
# 4. Save the plot
|
||||||
|
mkpath(output_dir)
|
||||||
|
filename = "$(stage_name).png"
|
||||||
|
save_path = joinpath(output_dir, filename)
|
||||||
|
save(save_path, fig)
|
||||||
|
@info "Saved slice plot to $save_path"
|
||||||
|
|
||||||
|
return fig
|
||||||
|
end
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
@ -647,53 +678,3 @@ function plot_slices(slices, names, masses, output_dir; stage_name, bins=256, dp
|
|||||||
end
|
end
|
||||||
return fig
|
return fig
|
||||||
end
|
end
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# Main Workflow
|
|
||||||
#
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
function load_config(path)
|
|
||||||
config = Dict{String, Any}()
|
|
||||||
if !isfile(path)
|
|
||||||
@warn "Config file not found at $(path). Using default parameters."
|
|
||||||
# Define defaults here in case the file is missing
|
|
||||||
config["masses"] = [100]
|
|
||||||
config["tolerance"] = 0.1
|
|
||||||
config["color_depth"] = 255
|
|
||||||
config["outlier_prob"] = 0.98
|
|
||||||
return config
|
|
||||||
end
|
|
||||||
|
|
||||||
for line in eachline(path)
|
|
||||||
line = strip(line)
|
|
||||||
if isempty(line) || startswith(line, "#")
|
|
||||||
continue # Skip empty lines and comments
|
|
||||||
end
|
|
||||||
parts = split(line, '=', limit=2)
|
|
||||||
if length(parts) != 2
|
|
||||||
@warn "Skipping malformed line in config: $(line)"
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
key = strip(parts[1])
|
|
||||||
value_str = strip(parts[2])
|
|
||||||
|
|
||||||
try
|
|
||||||
if key == "masses"
|
|
||||||
values = [parse(Float64, s) for s in split(value_str, ',')]
|
|
||||||
config[key] = values
|
|
||||||
elseif key == "tolerance"
|
|
||||||
config[key] = parse(Float64, value_str)
|
|
||||||
elseif key == "color_depth"
|
|
||||||
config[key] = parse(Int, value_str)
|
|
||||||
elseif key == "outlier_prob"
|
|
||||||
config[key] = parse(Float64, value_str)
|
|
||||||
end
|
|
||||||
catch e
|
|
||||||
@error "Could not parse value for key '$(key)': $(value_str)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return config
|
|
||||||
end
|
|
||||||
|
|||||||
246
src/imzML_old.jl
246
src/imzML_old.jl
@ -1,246 +0,0 @@
|
|||||||
# include( "Common.jl" );
|
|
||||||
# *******************************************************************
|
|
||||||
# Load Spectra and return a matrix
|
|
||||||
# fileName: Full name path
|
|
||||||
# *******************************************************************
|
|
||||||
"""
|
|
||||||
LoadImzml( fileName )
|
|
||||||
|
|
||||||
Load an imzML file as a matrix. Each column stores x-pixel position,
|
|
||||||
y-pixel position, x-axis data and y-axis data.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* `fileName`: Full path name of the imzML file
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
# Load DESI MSI Carcinoma image data
|
|
||||||
spectra = LoadImzml( "80TopL, 50TopR, 70BottomL, 60BottomR-centroid.imzML" )
|
|
||||||
size( spectra )
|
|
||||||
(4, 18632)
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
function LoadImzml( fileName )
|
|
||||||
|
|
||||||
# Open file handles
|
|
||||||
if !isfile(fileName)
|
|
||||||
error("provided path is not a file")
|
|
||||||
end
|
|
||||||
|
|
||||||
if endswith(fileName, ".imzML")
|
|
||||||
stream = open(fileName)
|
|
||||||
hIbd = open(replace(fileName, ".imzML" => ".ibd"))
|
|
||||||
else
|
|
||||||
stream = open(fileName * ".imzML")
|
|
||||||
hIbd = open(fileName * ".ibd")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Get axes types and image dimensions
|
|
||||||
axis = AxesConfigImg( stream )
|
|
||||||
imgDim = GetImgDimensions( stream )
|
|
||||||
format = [ axis[1].Format, axis[2].Format ]
|
|
||||||
|
|
||||||
# Locate spectrum attributes
|
|
||||||
start = position( stream )
|
|
||||||
attr = GetSpectrumAttributes( stream, hIbd )
|
|
||||||
|
|
||||||
# Load spectra
|
|
||||||
seek( stream, start )
|
|
||||||
spectra = LoadImgData( stream, hIbd, attr, imgDim[3], format )
|
|
||||||
|
|
||||||
close( stream )
|
|
||||||
close( hIbd )
|
|
||||||
return spectra
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Get axes value type
|
|
||||||
# *******************************************************************
|
|
||||||
function AxesConfigImg( stream )
|
|
||||||
|
|
||||||
# Locate which axis is defined at first
|
|
||||||
tag = FindTag( stream, r"^\s*<(referenceableParamGroup )" )
|
|
||||||
value = GetAttribute( tag.captures[1], "intensityArray" )
|
|
||||||
order = 1 + ( value !== nothing )
|
|
||||||
|
|
||||||
# Read first axis configuration
|
|
||||||
axis = Array{ SpecDim, 1 }( undef, 2 )
|
|
||||||
axis[ order ] = ConfigureSpecDim( stream )
|
|
||||||
|
|
||||||
# Read second axis configuration
|
|
||||||
FindTag( stream, r"^\s*<(referenceableParamGroup )" )
|
|
||||||
axis[ xor( order,3 ) ] = ConfigureSpecDim( stream )
|
|
||||||
return axis
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Get vector's storage options and image dimensions
|
|
||||||
# *******************************************************************
|
|
||||||
function GetImgDimensions( stream )
|
|
||||||
|
|
||||||
# Looks for "scanSettings" tag
|
|
||||||
# FindImgTag( stream, "scanSettings" )
|
|
||||||
FindTag( stream, r"^\s*<(scanSettings )" )
|
|
||||||
|
|
||||||
# Initial values for dimension retrieve
|
|
||||||
n = 2
|
|
||||||
dim = [ 0, 0, 0 ]
|
|
||||||
currLine = ""
|
|
||||||
matchInfo = RegexMatch
|
|
||||||
|
|
||||||
while( n > 0 )
|
|
||||||
|
|
||||||
# Next field
|
|
||||||
currLine = readline( stream )
|
|
||||||
matchInfo = match( r"^\s*<(cvParam)", currLine )
|
|
||||||
index = length( matchInfo.captures[1] )
|
|
||||||
matchInfo = GetAttribute( currLine[index:end], "accession" )
|
|
||||||
|
|
||||||
# Get axis identity
|
|
||||||
if( matchInfo.captures[1] == "IMS:1000042" # max X
|
|
||||||
|| matchInfo.captures[1] == "IMS:1000043" ) # max Y
|
|
||||||
|
|
||||||
# Read dimension's pixels
|
|
||||||
axis = matchInfo.captures[1][end] - '1'
|
|
||||||
index += matchInfo.offsets[1] + length( matchInfo.captures[1] )
|
|
||||||
matchInfo = GetAttribute( currLine[index:end], "value" )
|
|
||||||
dim[axis] = parse( Int32, matchInfo.captures[1] )
|
|
||||||
n -= 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Load stored spectra counter
|
|
||||||
matchInfo = FindTag( stream, r"^\s*<spectrumList(.+)" )
|
|
||||||
matchInfo = GetAttribute( matchInfo.captures[1], "count" )
|
|
||||||
dim[ 3 ] = parse( Int32, matchInfo.captures[1] )
|
|
||||||
|
|
||||||
return dim
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Length of "spectrum" tag without attribute values
|
|
||||||
# *******************************************************************
|
|
||||||
function GetSpectrumTag( stream )
|
|
||||||
|
|
||||||
offset = position( stream )
|
|
||||||
tag = FindTag( stream, r"^\s*<spectrum (.+)" )
|
|
||||||
first = 1
|
|
||||||
|
|
||||||
while true
|
|
||||||
|
|
||||||
value = match( r"[^=]+=\"([^\"]+)\"", tag.captures[1][first:end] )
|
|
||||||
if value === nothing
|
|
||||||
break
|
|
||||||
end
|
|
||||||
first += value.offsets[1] + length( value.captures[1] )
|
|
||||||
offset += length( value.captures[1] )
|
|
||||||
end
|
|
||||||
|
|
||||||
return offset
|
|
||||||
end
|
|
||||||
|
|
||||||
# seek( stream, 7141 )
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Locate spectrum attribute's
|
|
||||||
# [1] 1: x position stored first 2: y position stored first
|
|
||||||
# [2] Second stored position
|
|
||||||
# [3] 1: mzArray stored first 2: intensityArray stored first
|
|
||||||
# [4] Second stored axis
|
|
||||||
# [5] skip chars to find first dimension
|
|
||||||
# [6] skip chars to find second dimension
|
|
||||||
# [7] skip chars to find vector length
|
|
||||||
# [8] skip chars to find next spectrum
|
|
||||||
# *******************************************************************
|
|
||||||
function GetSpectrumAttributes( stream, hIbd )
|
|
||||||
|
|
||||||
# Reserve memory for vector configuration
|
|
||||||
skip = Vector{ UInt32 }( undef, 8 )
|
|
||||||
offset = GetSpectrumTag( stream )
|
|
||||||
|
|
||||||
# Get axis order and position of pixel coordinate values
|
|
||||||
tag = FindTag( stream, r" accession=\"IMS:100005(\d)\"(.+)" )
|
|
||||||
skip[1] = tag.captures[1][1] + 1 - '0'
|
|
||||||
skip[2] = xor( skip[1], 3 )
|
|
||||||
|
|
||||||
value = match( r" value=\"(\d+)\".+", tag.captures[2] )
|
|
||||||
skip[5] = position( stream ) - offset - length( value.match ) - 2
|
|
||||||
value = match( r" value=\"\d+\".+", readline( stream ) )
|
|
||||||
skip[6] = value.offset
|
|
||||||
|
|
||||||
# Get axis order
|
|
||||||
offset = position( stream )
|
|
||||||
tag = FindTag( stream, r"^\s*<referenceableParamGroupRef(.+)" )
|
|
||||||
value = GetAttribute( tag.captures[1], "ref" )
|
|
||||||
skip[3] = ( value.captures[1] == "intensityArray" ) + 3
|
|
||||||
skip[4] = xor( skip[3], 7 )
|
|
||||||
|
|
||||||
k = 2
|
|
||||||
while k != 0
|
|
||||||
|
|
||||||
tag = FindTag( stream, r" accession=\"IMS:100010(\d)\"(.+)" )
|
|
||||||
|
|
||||||
# Set IBD offset for first spectrum
|
|
||||||
if tag.captures[1][1] == '2'
|
|
||||||
value = GetAttribute( tag.match, "value" )
|
|
||||||
seek( hIbd, parse( Int32, value.captures[1] ) )
|
|
||||||
k -= 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
elseif tag.captures[1][1] == '3'
|
|
||||||
skip[7] = position( stream ) - offset - length( tag.match )
|
|
||||||
offset = position( stream )
|
|
||||||
k -= 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Compute characters to skip
|
|
||||||
FindTag( stream, r"^\s*</spectrum" )
|
|
||||||
skip[8] = position( stream ) - offset
|
|
||||||
return skip
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Read spectral data
|
|
||||||
# *******************************************************************
|
|
||||||
function LoadImgData( stream, hIbd, attr, imgDim, format )
|
|
||||||
|
|
||||||
# Reserve spectra memory & update IBD seek offset
|
|
||||||
spectra = Array{Any}( undef, ( 4, imgDim ) )
|
|
||||||
contador = 0;
|
|
||||||
|
|
||||||
for k in 1:imgDim
|
|
||||||
|
|
||||||
# Load image coordinates
|
|
||||||
skip( stream, attr[5] )
|
|
||||||
value = FindTag( stream, r"value=\"(\d+)\"" )
|
|
||||||
spectra[ attr[1],k ] = parse( Int32, value.captures[1] )
|
|
||||||
|
|
||||||
skip( stream, attr[6] )
|
|
||||||
value = FindTag( stream, r"value=\"(\d+)\"" )
|
|
||||||
spectra[ attr[2],k ] = parse( Int32, value.captures[1] )
|
|
||||||
|
|
||||||
# Get vector length
|
|
||||||
skip( stream, attr[7] )
|
|
||||||
value = FindTag( stream, r"value=\"(\d+)\"" )
|
|
||||||
nPoints = parse( Int32, value.captures[1] )
|
|
||||||
|
|
||||||
# Reserve memory and read spectrum
|
|
||||||
spectra[ 3,k ] = Array{ format[1] }( undef, nPoints )
|
|
||||||
spectra[ 4,k ] = Array{ format[2] }( undef, nPoints )
|
|
||||||
read!( hIbd, spectra[ attr[3],k ] )
|
|
||||||
read!( hIbd, spectra[ attr[4],k ] )
|
|
||||||
skip( stream, attr[8] )
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return spectra
|
|
||||||
|
|
||||||
end
|
|
||||||
219
src/mzML.jl
219
src/mzML.jl
@ -1,78 +1,89 @@
|
|||||||
# /home/pixel/Documents/Cinvestav_2025/JuliaMSI/src/mzML_lazy.jl
|
# /home/pixel/Documents/Cinvestav_2025/JuliaMSI/src/mzML.jl
|
||||||
|
|
||||||
using Base64, Libz
|
# This file is responsible for parsing metadata from .mzML files.
|
||||||
include("ParserHelpers.jl")
|
# It has been refactored to produce a unified MSIData object.
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Data Structures for Lazy Loading
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
struct SpectrumAsset
|
|
||||||
format::Type
|
|
||||||
is_compressed::Bool
|
|
||||||
offset::Int64
|
|
||||||
encoded_length::Int32
|
|
||||||
axis_type::Symbol # :mz or :intensity
|
|
||||||
end
|
|
||||||
|
|
||||||
struct MzMLSpectrumMetadata
|
|
||||||
id::String
|
|
||||||
mz_asset::SpectrumAsset
|
|
||||||
intensity_asset::SpectrumAsset
|
|
||||||
end
|
|
||||||
|
|
||||||
mutable struct MzMLData
|
|
||||||
file_handle::IO
|
|
||||||
spectra_metadata::Vector{MzMLSpectrumMetadata}
|
|
||||||
|
|
||||||
function MzMLData(file_handle::IO, spectra_metadata::Vector{MzMLSpectrumMetadata})
|
|
||||||
obj = new(file_handle, spectra_metadata)
|
|
||||||
finalizer(obj) do o
|
|
||||||
if isopen(o.file_handle)
|
|
||||||
close(o.file_handle)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return obj
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# mzML Parser Implementation
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
# This function is kept internal to the mzML parsing process
|
||||||
function get_spectrum_asset_metadata(stream)
|
function get_spectrum_asset_metadata(stream)
|
||||||
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"); end
|
if bda_tag === nothing
|
||||||
|
error("Cannot find binaryDataArray")
|
||||||
|
end
|
||||||
encoded_length = parse(Int32, bda_tag.captures[1])
|
encoded_length = parse(Int32, bda_tag.captures[1])
|
||||||
|
|
||||||
params = CVParams(Float64, false, :mz)
|
# Initialize parameters as separate variables
|
||||||
|
data_format = Float64
|
||||||
|
compression_flag = false
|
||||||
|
axis = :mz
|
||||||
|
|
||||||
while true
|
while true
|
||||||
line = readline(stream)
|
line = readline(stream)
|
||||||
if occursin("</binaryDataArray>", line); break; end
|
if occursin("</binaryDataArray>", line)
|
||||||
|
break
|
||||||
|
end
|
||||||
if occursin("<cvParam", line)
|
if occursin("<cvParam", line)
|
||||||
accession = get_attribute(line, "accession")
|
accession = get_attribute(line, "accession")
|
||||||
if accession === nothing; continue; end
|
if accession === nothing
|
||||||
update_cv_params!(params, accession.captures[1])
|
continue
|
||||||
|
end
|
||||||
|
acc_str = accession.captures[1]
|
||||||
|
# println("DEBUG: Processing accession: $acc_str")
|
||||||
|
|
||||||
|
# Direct inline logic - NO FUNCTION CALLS
|
||||||
|
if acc_str == "MS:1000514"
|
||||||
|
axis = :mz
|
||||||
|
# println("DEBUG: Set axis_type to :mz")
|
||||||
|
elseif acc_str == "MS:1000515"
|
||||||
|
axis = :intensity
|
||||||
|
# println("DEBUG: Set axis_type to :intensity")
|
||||||
|
elseif acc_str == "MS:1000518"
|
||||||
|
data_format = Int16
|
||||||
|
# println("DEBUG: Set format to Int16")
|
||||||
|
elseif acc_str == "MS:1000519"
|
||||||
|
data_format = Int32
|
||||||
|
# println("DEBUG: Set format to Int32")
|
||||||
|
elseif acc_str == "MS:1000520"
|
||||||
|
data_format = Float64
|
||||||
|
# println("DEBUG: Set format to Float64")
|
||||||
|
elseif acc_str == "MS:1000521"
|
||||||
|
data_format = Float32
|
||||||
|
# println("DEBUG: Set format to Float32")
|
||||||
|
elseif acc_str == "MS:1000522"
|
||||||
|
data_format = Int64
|
||||||
|
# println("DEBUG: Set format to Int64")
|
||||||
|
elseif acc_str == "MS:1000523"
|
||||||
|
data_format = Float64
|
||||||
|
# println("DEBUG: Set format to Float64")
|
||||||
|
elseif acc_str == "MS:1000574"
|
||||||
|
compression_flag = true
|
||||||
|
# println("DEBUG: Set is_compressed to true")
|
||||||
|
elseif acc_str == "MS:1000576"
|
||||||
|
compression_flag = false
|
||||||
|
# println("DEBUG: Set is_compressed to false")
|
||||||
|
else
|
||||||
|
# println("DEBUG: Unknown accession: $acc_str")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
seek(stream, start_pos)
|
seek(stream, start_pos)
|
||||||
binary_tag = find_tag(stream, r"<binary>")
|
readuntil(stream, "<binary>")
|
||||||
if binary_tag === nothing; error("Cannot find binary tag"); end
|
|
||||||
binary_offset = position(stream)
|
binary_offset = position(stream)
|
||||||
|
|
||||||
find_tag(stream, r"</binaryDataArray>") # Position for next call
|
# Move stream to the end of the binary data array for the next iteration
|
||||||
|
readuntil(stream, "</binaryDataArray>")
|
||||||
|
|
||||||
return SpectrumAsset(params.format, params.is_compressed, binary_offset, encoded_length, params.axis_type)
|
# Create SpectrumAsset directly from the variables
|
||||||
|
return SpectrumAsset(data_format, compression_flag, binary_offset, encoded_length, axis)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This function is updated to return the generic SpectrumMetadata struct
|
||||||
function parse_spectrum_metadata(stream, offset::Int64)
|
function parse_spectrum_metadata(stream, offset::Int64)
|
||||||
seek(stream, offset)
|
seek(stream, offset)
|
||||||
|
|
||||||
id_match = find_tag(stream, r"<spectrum\s+index=\"\d+\"\s+id=\"([^"]+)\"")
|
id_match = find_tag(stream, r"<spectrum\s+index=\"\d+\"\s+id=\"([^\"]+)" )
|
||||||
id = id_match === nothing ? "" : id_match.captures[1]
|
id = id_match === nothing ? "" : id_match.captures[1]
|
||||||
|
|
||||||
asset1 = get_spectrum_asset_metadata(stream)
|
asset1 = get_spectrum_asset_metadata(stream)
|
||||||
@ -81,111 +92,113 @@ function parse_spectrum_metadata(stream, offset::Int64)
|
|||||||
mz_asset = asset1.axis_type == :mz ? asset1 : asset2
|
mz_asset = asset1.axis_type == :mz ? asset1 : asset2
|
||||||
int_asset = asset1.axis_type == :intensity ? asset1 : asset2
|
int_asset = asset1.axis_type == :intensity ? asset1 : asset2
|
||||||
|
|
||||||
return MzMLSpectrumMetadata(id, mz_asset, int_asset)
|
# Create the new unified metadata object
|
||||||
|
# For mzML, x and y coordinates are not applicable, so we use 0.
|
||||||
|
return SpectrumMetadata(0, 0, id, mz_asset, int_asset)
|
||||||
end
|
end
|
||||||
|
|
||||||
function parse_offset_list(stream)
|
function parse_offset_list(stream)
|
||||||
offsets = Int64[]
|
offsets = Int64[]
|
||||||
while true
|
offset_regex = r"<offset[^>]*>(\d+)</offset>"
|
||||||
|
|
||||||
|
# Actively search for offset tags, ignoring other lines until the end of the index is found.
|
||||||
|
while !eof(stream)
|
||||||
line = readline(stream)
|
line = readline(stream)
|
||||||
m = match(r"<offset\s+idRef=\"\S+\">(\d+)</offset>", line)
|
|
||||||
if m !== nothing
|
# First, check for the end condition.
|
||||||
push!(offsets, parse(Int64, m.captures[1]))
|
if occursin("</index>", line) || occursin("</indexedmzML>", line)
|
||||||
elseif occursin("</index>", line) || occursin("</indexedmzML>", line)
|
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# If it's not the end, see if it's an offset tag.
|
||||||
|
m = match(offset_regex, line)
|
||||||
|
if m !== nothing
|
||||||
|
push!(offsets, parse(Int64, m.captures[1]))
|
||||||
|
end
|
||||||
|
# If it's neither, ignore the line and continue the loop.
|
||||||
end
|
end
|
||||||
return offsets
|
return offsets
|
||||||
end
|
end
|
||||||
|
|
||||||
function load_mzml_lazy(file_path::String)
|
# This is the main lazy-loading function for mzML, now returning an MSIData object.
|
||||||
stream = open(file_path)
|
function load_mzml_lazy(file_path::String; cache_size::Int=100)
|
||||||
|
println("DEBUG: Opening file stream for $file_path")
|
||||||
|
stream = open(file_path, "r")
|
||||||
|
|
||||||
try
|
try
|
||||||
|
println("DEBUG: Seeking to end of file.")
|
||||||
seekend(stream)
|
seekend(stream)
|
||||||
skip(stream, -4096) # Read last 4KB
|
println("DEBUG: Skipping back to read footer.")
|
||||||
|
skip(stream, -4096)
|
||||||
footer = read(stream, String)
|
footer = read(stream, String)
|
||||||
|
println("DEBUG: Footer read successfully.")
|
||||||
|
|
||||||
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
|
||||||
|
close(stream)
|
||||||
error("Could not find <indexListOffset>. File may not be an indexed mzML.")
|
error("Could not find <indexListOffset>. File may not be an indexed mzML.")
|
||||||
end
|
end
|
||||||
|
println("DEBUG: Found indexListOffset.")
|
||||||
|
|
||||||
index_offset = parse(Int64, index_offset_match.captures[1])
|
index_offset = parse(Int64, index_offset_match.captures[1])
|
||||||
|
println("DEBUG: Seeking to index list at offset $index_offset.")
|
||||||
seek(stream, index_offset)
|
seek(stream, index_offset)
|
||||||
|
|
||||||
|
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
|
||||||
|
close(stream)
|
||||||
error("Could not find spectrum index.")
|
error("Could not find spectrum index.")
|
||||||
end
|
end
|
||||||
|
println("DEBUG: Found spectrum index tag. ")
|
||||||
|
|
||||||
|
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)
|
||||||
|
close(stream)
|
||||||
error("No spectrum offsets found.")
|
error("No spectrum offsets found.")
|
||||||
end
|
end
|
||||||
|
println("DEBUG: Found $(length(spectrum_offsets)) spectrum offsets.")
|
||||||
|
|
||||||
|
println("DEBUG: Parsing metadata for each spectrum...")
|
||||||
spectra_metadata = [parse_spectrum_metadata(stream, offset) for offset in spectrum_offsets]
|
spectra_metadata = [parse_spectrum_metadata(stream, offset) for offset in spectrum_offsets]
|
||||||
|
println("DEBUG: Metadata parsing complete.")
|
||||||
|
|
||||||
# We return the stream inside MzMLData, so we don't close it here.
|
# Assuming uniform data formats, take from the first spectrum
|
||||||
# The finalizer on MzMLData will handle it.
|
first_meta = spectra_metadata[1]
|
||||||
return MzMLData(stream, spectra_metadata)
|
mz_format = first_meta.mz_asset.format
|
||||||
|
intensity_format = first_meta.int_asset.format
|
||||||
|
|
||||||
|
source = MzMLSource(stream, mz_format, intensity_format)
|
||||||
|
println("DEBUG: Creating MSIData object.")
|
||||||
|
return MSIData(source, spectra_metadata, (0, 0), nothing, cache_size)
|
||||||
|
|
||||||
catch e
|
catch e
|
||||||
close(stream) # Close file on error
|
close(stream) # Ensure stream is closed on error
|
||||||
rethrow(e)
|
rethrow(e)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Data Access Functions
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
function read_binary_vector(io::IO, asset::SpectrumAsset)
|
|
||||||
seek(io, asset.offset)
|
|
||||||
|
|
||||||
# Read the raw base64 data. The regex is slow. Reading bytes is better.
|
|
||||||
# The old code did `Vector{UInt8}(base64Vec.captures[1])`. This implies ASCII.
|
|
||||||
raw_b64 = read(io, asset.encoded_length)
|
|
||||||
|
|
||||||
decoded_bytes = base64decode(String(raw_b64))
|
|
||||||
|
|
||||||
bytes_io = IOBuffer(asset.is_compressed ? Libz.inflate(decoded_bytes) : decoded_bytes)
|
|
||||||
|
|
||||||
n_elements = Int(bytes_io.size / sizeof(asset.format))
|
|
||||||
out_array = Array{asset.format}(undef, n_elements)
|
|
||||||
read!(bytes_io, out_array)
|
|
||||||
|
|
||||||
return out_array
|
|
||||||
end
|
|
||||||
|
|
||||||
function get_spectrum(data::MzMLData, index::Int)
|
|
||||||
meta = data.spectra_metadata[index]
|
|
||||||
|
|
||||||
mz_array = read_binary_vector(data.file_handle, meta.mz_asset)
|
|
||||||
intensity_array = read_binary_vector(data.file_handle, meta.intensity_asset)
|
|
||||||
|
|
||||||
return (id=meta.id, mz=mz_array, intensity=intensity_array)
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
LoadMzml(fileName::String)
|
LoadMzml(fileName::String)
|
||||||
|
|
||||||
Eagerly loads all spectra from a .mzML file.
|
Eagerly loads all spectra from a .mzML file.
|
||||||
Provided for backward compatibility.
|
Provided for backward compatibility. Now uses the new MSIData architecture.
|
||||||
"""
|
"""
|
||||||
function LoadMzml(fileName::String)
|
function LoadMzml(fileName::String)
|
||||||
mzml_data = load_mzml_lazy(fileName)
|
# Use the lazy loader to get the MSIData object
|
||||||
|
msi_data = load_mzml_lazy(fileName, cache_size=0) # No need to cache if we load all
|
||||||
|
|
||||||
num_spectra = length(mzml_data.spectra_metadata)
|
num_spectra = length(msi_data.spectra_metadata)
|
||||||
spectra_matrix = Array{Any}(undef, (2, num_spectra))
|
spectra_matrix = Array{Any}(undef, (2, num_spectra))
|
||||||
|
|
||||||
for i in 1:num_spectra
|
for i in 1:num_spectra
|
||||||
spectrum = get_spectrum(mzml_data, i)
|
# Use the new GetSpectrum API
|
||||||
spectra_matrix[1, i] = spectrum.mz
|
mz, intensity = GetSpectrum(msi_data, i)
|
||||||
spectra_matrix[2, i] = spectrum.intensity
|
spectra_matrix[1, i] = mz
|
||||||
|
spectra_matrix[2, i] = intensity
|
||||||
end
|
end
|
||||||
|
|
||||||
# Eager load, so we can close the handle now.
|
# The finalizer on MSIData will close the handle, so no need to close it here.
|
||||||
close(mzml_data.file_handle)
|
|
||||||
|
|
||||||
return spectra_matrix
|
return spectra_matrix
|
||||||
end
|
end
|
||||||
|
|||||||
350
src/mzML_old.jl
350
src/mzML_old.jl
@ -1,350 +0,0 @@
|
|||||||
# *******************************************************************
|
|
||||||
# Load scans from mzML file
|
|
||||||
# fileName: Full name path
|
|
||||||
# *******************************************************************
|
|
||||||
"""
|
|
||||||
`LoadMzml( fileName )`
|
|
||||||
|
|
||||||
Load the mzML file as a Matrix. Each column stores a single scan,
|
|
||||||
x-axis and y-axis scan data are stored in the first and second
|
|
||||||
row respectively.
|
|
||||||
|
|
||||||
# Arguments
|
|
||||||
* `fileName`: Full path name of the mzML file
|
|
||||||
|
|
||||||
# Examples
|
|
||||||
```julia
|
|
||||||
spectra = LoadMzml( "T9_A1.mzML" )
|
|
||||||
size( spectra )
|
|
||||||
(2, 10)
|
|
||||||
using Plots
|
|
||||||
plot( spectra[1,4], spectra[2,4] )
|
|
||||||
```
|
|
||||||
|
|
||||||
The following image will appear in an auxiliary window
|
|
||||||
|
|
||||||

|
|
||||||
"""
|
|
||||||
function LoadMzml( fileName )
|
|
||||||
|
|
||||||
# Open file hFile = open( joinpath( data_dir, "Col_1.mzML" ), "r+" )
|
|
||||||
stream = open( fileName, "r+" )
|
|
||||||
intensity = []
|
|
||||||
|
|
||||||
# Load footer
|
|
||||||
seekend( stream )
|
|
||||||
skip( stream, -160 )
|
|
||||||
raw = read( stream, 160 )
|
|
||||||
footer = String( raw )
|
|
||||||
|
|
||||||
# Search "indexListOffset" tag
|
|
||||||
index = match( r"<indexListOffset>(?<Ofst>\d+)</indexListOffset>", footer )
|
|
||||||
|
|
||||||
if index !== nothing
|
|
||||||
|
|
||||||
# Jump to index table
|
|
||||||
seek( stream, parse( Int64, index.captures[1] ) )
|
|
||||||
|
|
||||||
# Find first "index" tag
|
|
||||||
index = FindTag( stream, r"<index +name=\"spectrum" )
|
|
||||||
|
|
||||||
# Load offset list and load spectra
|
|
||||||
if index !== nothing
|
|
||||||
intensity = LoadSpectra( stream, ParseOffsetList( stream ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
# Close file handle
|
|
||||||
close( stream )
|
|
||||||
|
|
||||||
# Return spectral matrix
|
|
||||||
return intensity
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Parse indexList offsets
|
|
||||||
# stream: Source file stream
|
|
||||||
# *******************************************************************
|
|
||||||
function ParseOffsetList( stream )
|
|
||||||
|
|
||||||
# List of 8 elements
|
|
||||||
entry = zeros( Int64, 8 )
|
|
||||||
used = 0
|
|
||||||
unused = 8
|
|
||||||
|
|
||||||
while true
|
|
||||||
|
|
||||||
# Get offset string
|
|
||||||
raw = readline( stream )
|
|
||||||
index = match( r">(?<Ofst>\d+)<", String( raw ) )
|
|
||||||
|
|
||||||
# Exit condition
|
|
||||||
if index === nothing
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
# Save index
|
|
||||||
used += 1
|
|
||||||
unused -= 1
|
|
||||||
entry[used] = parse( Int64, index.captures[1] )
|
|
||||||
|
|
||||||
# Grow list when needed
|
|
||||||
if unused == 0
|
|
||||||
append!( entry, zeros( Int64, 8 ) )
|
|
||||||
unused = 8
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return entry[1:used]
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Read vector data
|
|
||||||
# stream: Source file stream
|
|
||||||
# *******************************************************************
|
|
||||||
function ReadVector( stream, axis )
|
|
||||||
|
|
||||||
# Read base64 vector
|
|
||||||
base64Vec = FindTag( stream, r"<binary>([^<]+)<" )
|
|
||||||
base64Vec = Vector{ UInt8 }( base64Vec.captures[1] )
|
|
||||||
|
|
||||||
# Fill a IO stream with decoded data
|
|
||||||
if axis.Packed == 1
|
|
||||||
io = IOBuffer( Libz.inflate( Decode64( base64Vec ) ) )
|
|
||||||
else
|
|
||||||
io = IOBuffer( Decode64( base64Vec ) )
|
|
||||||
end
|
|
||||||
|
|
||||||
# Convert IO stream content as dataType vector
|
|
||||||
nElem = Int32( io.size / sizeof( axis.Format ) )
|
|
||||||
out = Array{ axis.Format }( undef, nElem )
|
|
||||||
read!( io, out )
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Get the decoding options for the first vector in spectrum
|
|
||||||
# stream: Valid file stream
|
|
||||||
# lastTag: Last tag to skip i.e. "binaryDataArray"
|
|
||||||
# *******************************************************************
|
|
||||||
function AxisConfig( stream )
|
|
||||||
|
|
||||||
offset = position( stream )
|
|
||||||
skip = 0
|
|
||||||
while true
|
|
||||||
|
|
||||||
# Load line
|
|
||||||
raw = readline( stream )
|
|
||||||
tag = match( r"^\s*<([^\s]+)", raw )
|
|
||||||
start = length( tag.match ) + 1
|
|
||||||
|
|
||||||
# Is a "cvParam" tag?
|
|
||||||
if tag.captures[1] == "cvParam"
|
|
||||||
|
|
||||||
# Get accesion field value
|
|
||||||
attribute = GetAttribute( raw[ start:end ], "accession" )
|
|
||||||
|
|
||||||
# Subtract file name length from skip field
|
|
||||||
if attribute.captures[1] != "MS:1000796"
|
|
||||||
continue
|
|
||||||
else
|
|
||||||
start += attribute.offset + length( attribute.match )
|
|
||||||
value = GetAttribute( raw[ start:end ], "value" )
|
|
||||||
skip += length( value.captures[1] )
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
# Exit condition
|
|
||||||
elseif tag.captures[1] == "binaryDataArray"
|
|
||||||
value = GetAttribute( raw[ start:end ], "encodedLength" )
|
|
||||||
offset = position( stream ) - offset
|
|
||||||
front = ConfigureSpecDim( stream )
|
|
||||||
front.Skip += offset - length( value.captures[1] ) - skip
|
|
||||||
return ( front )
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Load Spectra and return a matrix
|
|
||||||
# stream: Source file stream
|
|
||||||
# *******************************************************************
|
|
||||||
function LoadSpectra( stream, offset )
|
|
||||||
|
|
||||||
# Move file pointer towards first spectrum tag
|
|
||||||
seek( stream, offset[1] )
|
|
||||||
|
|
||||||
# Decodes axis information and skip counter
|
|
||||||
firstAxis = AxisConfig( stream )
|
|
||||||
secondAxis = AxisConfig( stream )
|
|
||||||
|
|
||||||
# Allocates memory for spectra storage
|
|
||||||
specCount = length( offset )
|
|
||||||
spectrum = Array{ Any }( undef, ( 2, specCount ) )
|
|
||||||
index = 1
|
|
||||||
order = 1 + ( firstAxis.Axis == 2 )
|
|
||||||
|
|
||||||
for i in 1:specCount
|
|
||||||
|
|
||||||
# Load first vector
|
|
||||||
seek( stream, offset[ i ] )
|
|
||||||
skip( stream, firstAxis.Skip )
|
|
||||||
spectrum[ order, i ] = ReadVector( stream, firstAxis )
|
|
||||||
|
|
||||||
# Load second vector
|
|
||||||
skip( stream, secondAxis.Skip )
|
|
||||||
spectrum[ xor( order,3 ), i ] = ReadVector( stream, secondAxis )
|
|
||||||
end
|
|
||||||
|
|
||||||
return( spectrum )
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# *******************************************************************
|
|
||||||
# Decoding base64 blocks
|
|
||||||
# *******************************************************************
|
|
||||||
|
|
||||||
# Decoding exchange table
|
|
||||||
base64dec = Vector{UInt8}( [
|
|
||||||
62, # (01) + -> 43d, 0x2B
|
|
||||||
0, # (02) ,
|
|
||||||
0, # (03) -
|
|
||||||
0, # (04) .
|
|
||||||
63, # (05) /
|
|
||||||
52, # (06) 0
|
|
||||||
53, # (07) 1
|
|
||||||
54, # (08) 2
|
|
||||||
55, # (09) 3
|
|
||||||
56, # (10) 4
|
|
||||||
57, # (11) 5
|
|
||||||
58, # (12) 6
|
|
||||||
59, # (13) 7
|
|
||||||
60, # (14) 8
|
|
||||||
61, # (15) 9
|
|
||||||
0, # (16) :
|
|
||||||
0, # (17) ;
|
|
||||||
0, # (18) <
|
|
||||||
0, # (19) =
|
|
||||||
0, # (20) >
|
|
||||||
0, # (21) ?
|
|
||||||
0, # (22) @
|
|
||||||
0, # (23) A
|
|
||||||
1, # (24) B
|
|
||||||
2, # (25) C
|
|
||||||
3, # (26) D
|
|
||||||
4, # (27) E
|
|
||||||
5, # (28) F
|
|
||||||
6, # (29) G
|
|
||||||
7, # (40) H
|
|
||||||
8, # (41) I
|
|
||||||
9, # (42) J
|
|
||||||
10, # (43) K
|
|
||||||
11, # (44) L
|
|
||||||
12, # (45) M
|
|
||||||
13, # (46) N
|
|
||||||
14, # (47) O
|
|
||||||
15, # (48) P
|
|
||||||
16, # (49) Q
|
|
||||||
17, # (40) R
|
|
||||||
18, # (41) S
|
|
||||||
19, # (42) T
|
|
||||||
20, # (43) U
|
|
||||||
21, # (44) V
|
|
||||||
22, # (45) W
|
|
||||||
23, # (46) X
|
|
||||||
24, # (47) Y
|
|
||||||
25, # (48) Z
|
|
||||||
0, # (49) [
|
|
||||||
0, # (50) \
|
|
||||||
0, # (51) ]
|
|
||||||
0, # (52) ^
|
|
||||||
0, # (53) _
|
|
||||||
0, # (54) `
|
|
||||||
26, # (55) a
|
|
||||||
27, # (56) b
|
|
||||||
28, # (57) c
|
|
||||||
29, # (58) d
|
|
||||||
30, # (59) e
|
|
||||||
31, # (60) f
|
|
||||||
32, # (61) g
|
|
||||||
33, # (62) h
|
|
||||||
34, # (63) i
|
|
||||||
35, # (64) j
|
|
||||||
36, # (65) k
|
|
||||||
37, # (66) l
|
|
||||||
38, # (67) m
|
|
||||||
39, # (68) n
|
|
||||||
40, # (69) o
|
|
||||||
41, # (70) p
|
|
||||||
42, # (71) q
|
|
||||||
43, # (72) r
|
|
||||||
44, # (73) s
|
|
||||||
45, # (74) t
|
|
||||||
46, # (75) u
|
|
||||||
47, # (76) v
|
|
||||||
48, # (77) w
|
|
||||||
49, # (78) x
|
|
||||||
50, # (79) y
|
|
||||||
51, # (80) z
|
|
||||||
] )
|
|
||||||
|
|
||||||
# base64 decoding
|
|
||||||
function DecodeTriplet( data, base64 )
|
|
||||||
|
|
||||||
# Character to binary equivalent
|
|
||||||
data = data .- 0x2A
|
|
||||||
data[1] = base64[ data[1] ]
|
|
||||||
data[2] = base64[ data[2] ]
|
|
||||||
data[3] = base64[ data[3] ]
|
|
||||||
data[4] = base64[ data[4] ]
|
|
||||||
|
|
||||||
# Get Triplet
|
|
||||||
decode = Array{UInt8}( undef,3 )
|
|
||||||
decode[1] = data[1] << 2 + data[2] >> 4
|
|
||||||
decode[2] = data[2] << 4 + data[3] >> 2
|
|
||||||
decode[3] = data[3] << 6 + data[4]
|
|
||||||
return decode
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function Decode64( data )
|
|
||||||
|
|
||||||
# Reserve uninitialized memory
|
|
||||||
block = divrem( sizeof( data ), 4 )
|
|
||||||
decode = Array{UInt8}( undef, 3*block[1] )
|
|
||||||
|
|
||||||
# Recover binary from Chars
|
|
||||||
index = 1
|
|
||||||
first = 1
|
|
||||||
last = 3
|
|
||||||
|
|
||||||
for i in 1:block[1]
|
|
||||||
decode[first:last] = DecodeTriplet( data[index:index+3], base64dec )
|
|
||||||
index += 4
|
|
||||||
first += 3
|
|
||||||
last += 3
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adjust block size
|
|
||||||
if data[end-1] == 0x3D
|
|
||||||
decode = decode[1:end-2]
|
|
||||||
elseif data[end] == 0x3D
|
|
||||||
decode = decode[1:end-1]
|
|
||||||
end
|
|
||||||
|
|
||||||
return decode
|
|
||||||
|
|
||||||
end
|
|
||||||
21
test/readme.md
Normal file
21
test/readme.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
to run the tests:
|
||||||
|
|
||||||
|
time julia --project=. test/run_tests.jl
|
||||||
|
|
||||||
|
to enable the different test cases, there are boolean variables
|
||||||
|
|
||||||
|
test1 = true
|
||||||
|
test2 = true
|
||||||
|
test3 = true
|
||||||
|
|
||||||
|
set them to true to perform that specific test.
|
||||||
|
|
||||||
|
test 1 consists in mzml validation and spectrum plotting
|
||||||
|
|
||||||
|
test 2 consists in imzml conversion and validation
|
||||||
|
|
||||||
|
test 3 consists in imzml validation and spectrum and slice plotting
|
||||||
|
|
||||||
|
use the global variables:
|
||||||
|
TEST_MZML_FILE, SPECTRUM_TO_PLOT, CONVERSION_SOURCE_MZML, CONVERSION_SYNC_FILE, CONVERSION_TARGET_IMZML, TEST_IMZML_FILE, MZ_VALUE_FOR_SLICE, MZ_TOLERANCE, COORDS_TO_PLOT, RESULTS_DIR
|
||||||
|
to modify input that the test script will recieve.
|
||||||
299
test/run_tests.jl
Normal file
299
test/run_tests.jl
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
# test/run_tests.jl
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# Test Environment for JuliaMSI Package
|
||||||
|
# ===================================================================
|
||||||
|
# This script validates the core functionality of the data processing
|
||||||
|
# workflows, including loading, converting, and visualizing mass
|
||||||
|
# spectrometry data.
|
||||||
|
#
|
||||||
|
# Instructions:
|
||||||
|
# 1. Fill in the placeholder paths in the "CONFIG" section below.
|
||||||
|
# 2. Run the script from the project's root directory:
|
||||||
|
# julia test/run_tests.jl
|
||||||
|
# 3. Check the `test/results/` folder for the output images.
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
using Printf
|
||||||
|
using CairoMakie
|
||||||
|
import Pkg
|
||||||
|
|
||||||
|
# --- Load the MSI_src Module ---
|
||||||
|
# Activate the project environment at the parent directory of this test script
|
||||||
|
Pkg.activate(joinpath(@__DIR__, ".."))
|
||||||
|
using MSI_src
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# CONFIG: PLEASE FILL IN YOUR FILE PATHS HERE
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
# --- Test Case 1: Standard .mzML file ---
|
||||||
|
# A regular, non-imaging mzML file.
|
||||||
|
# const TEST_MZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/mzML/T9_A1.mzML"
|
||||||
|
# const TEST_MZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/CE4_BF_R1/CE4_BF_R1.mzML"
|
||||||
|
const TEST_MZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/CE4_BF_R1/CE4_BF_R1.mzML"
|
||||||
|
const SPECTRUM_TO_PLOT = 1 # Which spectrum to plot from the file
|
||||||
|
|
||||||
|
# --- Test Case 2: .mzML + Sync File for Conversion ---
|
||||||
|
# The special .mzML file with one spectrum per pixel.
|
||||||
|
const CONVERSION_SOURCE_MZML = "/home/pixel/Documents/Cinvestav_2025/Analisis/CE4_BF_R1/CE4_BF_R1.mzML"
|
||||||
|
# The corresponding synchronization text file.
|
||||||
|
const CONVERSION_SYNC_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/CE4_BF_R1/CE4_BF_R1.txt"
|
||||||
|
|
||||||
|
# const CONVERSION_SOURCE_MZML = "/home/pixel/Documents/Cinvestav_2025/Analisis/Imaging_paper_spray/Imaging_paper_spray.mzML"
|
||||||
|
# const CONVERSION_SYNC_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/Imaging_paper_spray/Imaging_paper_spray.txt"
|
||||||
|
|
||||||
|
# The desired output path for the new .imzML file.
|
||||||
|
const CONVERSION_TARGET_IMZML = "test/results/converted_mzml.imzML"
|
||||||
|
|
||||||
|
# --- Test Case 3: Standard .imzML file ---
|
||||||
|
# An existing imzML file (can be the one generated from Case 2).
|
||||||
|
const TEST_IMZML_FILE = CONVERSION_TARGET_IMZML # The output from case 2
|
||||||
|
# const TEST_IMZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/imzML_AP_SMALDI/HR2MSImouseurinarybladderS096.imzML"
|
||||||
|
# const TEST_IMZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/Imaging_paper_spray/Imaging_paper_spray.imzML"
|
||||||
|
# The m/z value to use for creating an image slice.
|
||||||
|
const MZ_VALUE_FOR_SLICE = 309.06 # BF
|
||||||
|
# const MZ_VALUE_FOR_SLICE = 896.0 # HR2MSI
|
||||||
|
# const MZ_VALUE_FOR_SLICE = 76.03 # I PS
|
||||||
|
# const MZ_TOLERANCE = 0.1
|
||||||
|
const MZ_TOLERANCE = 1
|
||||||
|
|
||||||
|
# Coordinates to plot a specific spectrum from imzML
|
||||||
|
const COORDS_TO_PLOT = (50, 50) # Example coordinates (X, Y)
|
||||||
|
|
||||||
|
# --- Output Directory ---
|
||||||
|
const RESULTS_DIR = "test/results"
|
||||||
|
|
||||||
|
test1 = true
|
||||||
|
test2 = true
|
||||||
|
test3 = true
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# DATA VALIDATION UTILITY
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
validate_msi_data(filepath::String)
|
||||||
|
|
||||||
|
Performs a series of checks on a .mzML or .imzML file using the MSIData API.
|
||||||
|
"""
|
||||||
|
function validate_msi_data(filepath::String)
|
||||||
|
println("" * "-"^10 * " Running validation for $filepath " * "-"^10)
|
||||||
|
if !isfile(filepath)
|
||||||
|
println("SKIPPED VALIDATION: File not found: $filepath")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
try
|
||||||
|
# 1. Basic structure validation
|
||||||
|
println("Opening file with OpenMSIData...")
|
||||||
|
msi_data = @time OpenMSIData(filepath)
|
||||||
|
|
||||||
|
# 2. Compare spectrum counts
|
||||||
|
num_spectra = length(msi_data.spectra_metadata)
|
||||||
|
println("Found $num_spectra spectra")
|
||||||
|
@assert num_spectra > 0 "No spectra found in file."
|
||||||
|
|
||||||
|
# 3. Test random access
|
||||||
|
println("Testing random access to spectra...")
|
||||||
|
test_indices = unique([1, max(1, num_spectra ÷ 2), num_spectra])
|
||||||
|
println("Testing indices: $test_indices")
|
||||||
|
for idx in test_indices
|
||||||
|
print("Fetching spectrum #$idx... ")
|
||||||
|
mz, intensity = @time GetSpectrum(msi_data, idx)
|
||||||
|
@assert length(mz) == length(intensity) "Spectrum $idx: mz/intensity length mismatch. Got $(length(mz)) mz values and $(length(intensity)) intensity values."
|
||||||
|
println("OK, $(length(mz)) points.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# 4. Test iteration
|
||||||
|
println("Testing iteration over all spectra...")
|
||||||
|
count = 0
|
||||||
|
iter_time = @elapsed for (idx, (mz, intensity)) in IterateSpectra(msi_data)
|
||||||
|
count += 1
|
||||||
|
# Basic data validation
|
||||||
|
@assert all(isfinite, mz) "Non-finite mz values in spectrum $idx"
|
||||||
|
@assert all(>=(0), intensity) "Negative intensities in spectrum $idx"
|
||||||
|
end
|
||||||
|
println("Iterated over $count spectra in $iter_time seconds.")
|
||||||
|
@assert count == num_spectra "Iteration count mismatch: expected $num_spectra, got $count."
|
||||||
|
|
||||||
|
println("VALIDATION SUCCESSFUL for $filepath")
|
||||||
|
return true
|
||||||
|
|
||||||
|
catch e
|
||||||
|
println("VALIDATION FAILED for $filepath.")
|
||||||
|
showerror(stdout, e, catch_backtrace())
|
||||||
|
println()
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# TEST RUNNER
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
function run_test()
|
||||||
|
println("Starting MSI_src Test Suite...")
|
||||||
|
|
||||||
|
# --- Test Case 1: Process a standard .mzML file ---
|
||||||
|
println("" * "="^20 * " Test Case 1: Processing .mzML " * "="^20)
|
||||||
|
if test1 == true
|
||||||
|
# Run new, stronger validation
|
||||||
|
validate_msi_data(TEST_MZML_FILE)
|
||||||
|
|
||||||
|
# Also run original plotting test to ensure visualization still works
|
||||||
|
if isfile(TEST_MZML_FILE)
|
||||||
|
try
|
||||||
|
# Get the msi data from the mzml
|
||||||
|
println("Plotting a sample spectrum from $TEST_MZML_FILE...")
|
||||||
|
msi_data = OpenMSIData(TEST_MZML_FILE)
|
||||||
|
mz, intensity = GetSpectrum(msi_data, SPECTRUM_TO_PLOT)
|
||||||
|
|
||||||
|
fig = Figure(size = (800, 600))
|
||||||
|
ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Intensity", title="Spectrum #$SPECTRUM_TO_PLOT from $(basename(TEST_MZML_FILE))")
|
||||||
|
lines!(ax, mz, intensity)
|
||||||
|
|
||||||
|
output_path = joinpath(RESULTS_DIR, "test_mzml_spectrum.png")
|
||||||
|
save(output_path, fig)
|
||||||
|
println("SUCCESS: Spectrum plot saved to $output_path")
|
||||||
|
# Get the summed spectrum data
|
||||||
|
mz, intensity = get_total_spectrum(msi_data)
|
||||||
|
|
||||||
|
# Plot the data
|
||||||
|
println("Plotting total spectrum...")
|
||||||
|
fig = Figure(size = (800, 600))
|
||||||
|
ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Total Intensity", title="Total Spectrum from $(basename(TEST_MZML_FILE))")
|
||||||
|
lines!(ax, mz, intensity)
|
||||||
|
|
||||||
|
# Saving the output
|
||||||
|
output_path = joinpath(RESULTS_DIR, "test_mzml_total_spectrum.png")
|
||||||
|
save(output_path, fig)
|
||||||
|
println("SUCCESS: Total spectrum plot saved to $output_path")
|
||||||
|
|
||||||
|
# Get the averaged spectrum data
|
||||||
|
mz, intensity = get_average_spectrum(msi_data)
|
||||||
|
|
||||||
|
# Plot the data
|
||||||
|
println("Plotting averaged spectrum...")
|
||||||
|
fig = Figure(size = (800, 600))
|
||||||
|
ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Average Intensity", title="Average Spectrum from $(basename(TEST_MZML_FILE))")
|
||||||
|
lines!(ax, mz, intensity)
|
||||||
|
|
||||||
|
# Saving the output
|
||||||
|
output_path = joinpath(RESULTS_DIR, "test_mzml_average_spectrum.png")
|
||||||
|
save(output_path, fig)
|
||||||
|
println("SUCCESS: Total spectrum plot saved to $output_path")
|
||||||
|
catch e
|
||||||
|
println("ERROR during plotting in Test Case 1: $e")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
println("SKIPPED Test Case 1.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Test Case 2: Convert .mzML + .txt to .imzML ---
|
||||||
|
println("" * "="^20 * " Test Case 2: Converting to .imzML " * "="^20)
|
||||||
|
if isfile(CONVERSION_SOURCE_MZML) && isfile(CONVERSION_SYNC_FILE) && test2 == true
|
||||||
|
try
|
||||||
|
println("Running conversion process (with profiling)...")
|
||||||
|
success = @time ImportMzmlFile(CONVERSION_SOURCE_MZML, CONVERSION_SYNC_FILE, CONVERSION_TARGET_IMZML)
|
||||||
|
if success
|
||||||
|
println("SUCCESS: Conversion process completed.")
|
||||||
|
# Validate the newly created imzML file
|
||||||
|
validate_msi_data(CONVERSION_TARGET_IMZML)
|
||||||
|
else
|
||||||
|
println("FAILURE: Conversion process failed.")
|
||||||
|
end
|
||||||
|
catch e
|
||||||
|
println("ERROR in Test Case 2: $e")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
println("SKIPPED: Files not found for Test Case 2.")
|
||||||
|
println(" - mzML: $CONVERSION_SOURCE_MZML")
|
||||||
|
println(" - Sync: $CONVERSION_SYNC_FILE")
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Test Case 3: Process an existing .imzML file ---
|
||||||
|
println("
|
||||||
|
" * "="^20 * " Test Case 3: Processing .imzML " * "="^20)
|
||||||
|
if test3 == true
|
||||||
|
# Run new, stronger validation for imzML
|
||||||
|
validate_msi_data(TEST_IMZML_FILE)
|
||||||
|
|
||||||
|
# Also run tests for plotting spectrum and image slice
|
||||||
|
if isfile(TEST_IMZML_FILE)
|
||||||
|
# Add spectrum plotting for imzML to match Test Case 1
|
||||||
|
try
|
||||||
|
# Get the msi data from the imzml
|
||||||
|
println("Plotting a sample spectrum from $TEST_IMZML_FILE...")
|
||||||
|
msi_data = OpenMSIData(TEST_IMZML_FILE)
|
||||||
|
x_coord, y_coord = COORDS_TO_PLOT
|
||||||
|
|
||||||
|
# Get the x y coordinate spectrum data
|
||||||
|
mz, intensity = GetSpectrum(msi_data, x_coord, y_coord)
|
||||||
|
|
||||||
|
# Plot the data
|
||||||
|
fig = Figure(size = (800, 600))
|
||||||
|
ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Intensity", title="Spectrum at ($x_coord, $y_coord) from $(basename(TEST_IMZML_FILE))")
|
||||||
|
lines!(ax, mz, intensity)
|
||||||
|
|
||||||
|
# Saving the output
|
||||||
|
output_path = joinpath(RESULTS_DIR, "test_imzml_spectrum.png")
|
||||||
|
save(output_path, fig)
|
||||||
|
println("SUCCESS: Spectrum plot saved to $output_path")
|
||||||
|
|
||||||
|
# Get the summed spectrum data
|
||||||
|
mz, intensity = get_total_spectrum(msi_data)
|
||||||
|
|
||||||
|
# Plot the data
|
||||||
|
println("Plotting total spectrum...")
|
||||||
|
fig = Figure(size = (800, 600))
|
||||||
|
ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Total Intensity", title="Total Spectrum from $(basename(TEST_IMZML_FILE))")
|
||||||
|
lines!(ax, mz, intensity)
|
||||||
|
|
||||||
|
# Saving the output
|
||||||
|
output_path = joinpath(RESULTS_DIR, "test_imzml_total_spectrum.png")
|
||||||
|
save(output_path, fig)
|
||||||
|
println("SUCCESS: Total spectrum plot saved to $output_path")
|
||||||
|
|
||||||
|
# Get the averaged spectrum data
|
||||||
|
mz, intensity = get_average_spectrum(msi_data)
|
||||||
|
|
||||||
|
# Plot the data
|
||||||
|
println("Plotting averaged spectrum...")
|
||||||
|
fig = Figure(size = (800, 600))
|
||||||
|
ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Average Intensity", title="Average Spectrum from $(basename(TEST_IMZML_FILE))")
|
||||||
|
lines!(ax, mz, intensity)
|
||||||
|
|
||||||
|
# Saving the output
|
||||||
|
output_path = joinpath(RESULTS_DIR, "test_imzml_average_spectrum.png")
|
||||||
|
save(output_path, fig)
|
||||||
|
println("SUCCESS: Total spectrum plot saved to $output_path")
|
||||||
|
catch e
|
||||||
|
println("ERROR during spectrum plotting in Test Case 3: $e")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test the plot_slice function
|
||||||
|
try
|
||||||
|
println("Testing plot_slice function on $TEST_IMZML_FILE...")
|
||||||
|
msi_data = OpenMSIData(TEST_IMZML_FILE)
|
||||||
|
plot_slice(msi_data, MZ_VALUE_FOR_SLICE, MZ_TOLERANCE, RESULTS_DIR, stage_name="test_imzml_single_slice")
|
||||||
|
# The success message is now inside plot_slice
|
||||||
|
catch e
|
||||||
|
println("ERROR during plot_slice test in Test Case 3: $e")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
println("SKIPPED Test Case 3.")
|
||||||
|
end
|
||||||
|
|
||||||
|
println("Tests for all 3 cases is finished.")
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
# Ensure the results directory exists
|
||||||
|
mkpath(RESULTS_DIR)
|
||||||
|
@time run_test()
|
||||||
Loading…
x
Reference in New Issue
Block a user