# 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, Serialization, Printf, DataFrames, Base.Threads # 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 """ ImzMLSource <: MSDataSource A data source for `.imzML` files, holding a handle to the binary `.ibd` file and the expected format for m/z and intensity arrays. """ struct ImzMLSource <: MSDataSource ibd_handle::IO mz_format::Type intensity_format::Type end """ MzMLSource <: MSDataSource A data source for `.mzML` files, holding a handle to the `.mzML` file itself (which contains the binary data encoded in Base64) and the expected data formats. """ struct MzMLSource <: MSDataSource file_handle::IO mz_format::Type intensity_format::Type end """ SpectrumAsset Contains metadata for a single binary data array (m/z or intensity) within a spectrum. # Fields - `format`: The data type of the elements (e.g., `Float32`, `Int64`). - `is_compressed`: A boolean flag indicating if the data is compressed (e.g., with zlib). - `offset`: The byte offset of the data within the file (`.ibd` for imzML, `.mzML` for mzML). - `encoded_length`: The length of the data. For uncompressed imzML, this is the number of elements in the array. For compressed imzML, it is the number of bytes of the compressed data. For mzML, this is the length of the Base64 encoded string. - `axis_type`: A symbol (`:mz` or `:intensity`) indicating the type of data. """ 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 @enum SpectrumMode CENTROID=1 PROFILE=2 UNKNOWN=3 """ SpectrumMetadata Contains all metadata for a single spectrum, common to both imzML and mzML formats. # Fields - `x`, `y`: The spatial coordinates of the spectrum (for imzML only). - `id`: The unique identifier string for the spectrum (for mzML only). - `mode`: The spectrum mode (`CENTROID` or `PROFILE`). - `mz_asset`: A `SpectrumAsset` for the m/z array. - `int_asset`: A `SpectrumAsset` for the intensity array. """ struct SpectrumMetadata # For imzML x::Int32 y::Int32 # For mzML id::String mode::SpectrumMode # Common binary data info mz_asset::SpectrumAsset int_asset::SpectrumAsset end """ MSIData The primary object for interacting with mass spectrometry data. It provides a unified interface for both `.mzML` and `.imzML` files and includes an LRU cache for efficient repeated access to spectra. # Fields - `source`: The underlying `MSDataSource` (`ImzMLSource` or `MzMLSource`). - `spectra_metadata`: A vector of `SpectrumMetadata` for all spectra in the file. - `image_dims`: A tuple `(width, height)` of the spatial dimensions (for imzML). - `coordinate_map`: A matrix mapping `(x, y)` coordinates to a linear spectrum index (for imzML). - `cache`: A dictionary holding cached spectra, mapping index to `(mz, intensity)`. - `cache_order`: A vector of indices tracking usage for the LRU cache policy. - `cache_size`: The maximum number of spectra to store in the cache. - `cache_lock`: A `ReentrantLock` to ensure thread-safe access to the cache. - `global_min_mz`: Cached global minimum m/z value across all spectra. - `global_max_mz`: Cached global maximum m/z value across all spectra. - `spectrum_stats_df`: A `DataFrame` containing pre-computed per-spectrum analytics (e.g., TIC, BPI). """ 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 for GetSpectrum 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 cache_lock::ReentrantLock # To make cache access thread-safe # Pre-computed analytics/metadata global_min_mz::Union{Float64, Nothing} global_max_mz::Union{Float64, Nothing} spectrum_stats_df::Union{DataFrame, Nothing} function MSIData(source, metadata, dims, coordinate_map, cache_size) obj = new(source, metadata, dims, coordinate_map, Dict(), [], cache_size, ReentrantLock(), nothing, nothing, nothing) # Initialize new fields to nothing # 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 --- # """ read_binary_vector(io::IO, asset::SpectrumAsset) Reads and decodes a single binary data vector (e.g., m/z or intensity array) from a `.mzML` file. The data is expected to be Base64-encoded and may be compressed. This internal function handles: 1. Reading the raw Base64 string from the file at the specified offset. 2. Decoding the Base64 string into bytes. 3. Decompressing the bytes using zlib if `asset.is_compressed` is true. 4. Interpreting the resulting bytes as a vector of the specified format. Note: Byte order conversion (e.g., from little-endian to host) is not performed by this function and is assumed to be handled by the caller if necessary. # Arguments - `io`: The IO stream of the `.mzML` file. - `asset`: The `SpectrumAsset` containing metadata for the array. # Returns - A `Vector` of the appropriate type containing the decoded 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 """ read_spectrum_from_disk(source::ImzMLSource, meta::SpectrumMetadata) Reads a single spectrum's m/z and intensity arrays directly from the `.ibd` binary file for an `.imzML` dataset. It handles reading the raw binary data and converting it from little-endian to the host's native byte order. # Arguments - `source`: An `ImzMLSource` containing the file handle and data formats. - `meta`: The `SpectrumMetadata` for the spectrum to be read. # Returns - A tuple `(mz, intensity)` containing the two requested data arrays. """ 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) # imzML data is little-endian. Convert to host byte order. mz .= ltoh.(mz) intensity .= ltoh.(intensity) return mz, intensity end """ read_spectrum_from_disk(source::MzMLSource, meta::SpectrumMetadata) Reads a single spectrum's m/z and intensity arrays from a `.mzML` file. This is a high-level function that orchestrates the reading process by calling `read_binary_vector` for each of the m/z and intensity arrays. # Arguments - `source`: A `MzMLSource` containing the file handle. - `meta`: The `SpectrumMetadata` for the spectrum to be read. # Returns - A tuple `(mz, intensity)` containing the two requested data arrays. """ 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 thread-safe LRU cache for performance. If the spectrum is not in the cache, it is read from disk, and the cache is updated. This function is the core of the "Indexed" and "Cache" access patterns. # Arguments - `data`: The `MSIData` object. - `index`: The linear index of the spectrum to retrieve. # Returns - A tuple `(mz, intensity)` containing the spectrum's data arrays. """ 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 (with lock) lock(data.cache_lock) try 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 finally unlock(data.cache_lock) end # Phase 2: Cache Miss - Read from disk (no lock) meta = data.spectra_metadata[index] spectrum = read_spectrum_from_disk(data.source, meta) # Phase 3: Update cache (with lock) and get final value return lock(data.cache_lock) do if haskey(data.cache, index) # Another thread got here first, use its result return data.cache[index] end # This thread is first to 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 end """ GetSpectrum(data::MSIData, x::Int, y::Int) Retrieves a single spectrum by its (x, y) coordinates for imaging data (`.imzML`). This method uses the `coordinate_map` for efficient index lookup and then calls the indexed `GetSpectrum` method, benefiting from caching. # Arguments - `data`: The `MSIData` object. - `x`: The x-coordinate of the spectrum. - `y`: The y-coordinate of the spectrum. # Returns - A tuple `(mz, intensity)` containing the spectrum's data arrays. """ 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 """ process_spectrum(f::Function, data::MSIData, index::Int) A "function barrier" helper for safely processing a single spectrum. This function retrieves a spectrum and then immediately calls the provided function `f` with the resulting `(mz, intensity)` arrays. This pattern is crucial for performance. While `GetSpectrum` itself is type-unstable (because the array data types are not known at compile time), calling `f` with the result allows Julia's compiler to generate specialized, fast code for `f` based on the *concrete* types it receives. Use this function when you need to perform performance-critical operations on a single spectrum. Your logic should be inside the function passed to this helper. ``` """ function process_spectrum(f::Function, data::MSIData, index::Int) # This call is type-unstable. mz, intensity = GetSpectrum(data, index) # By immediately calling `f`, we create a "function barrier". # Julia will compile a specialized version of `f` for the # concrete types of `mz` and `intensity`. return f(mz, intensity) end """ process_spectrum(f::Function, data::MSIData, x::Int, y::Int) A coordinate-based version of the "function barrier" helper. See `process_spectrum` for details on the performance pattern. """ function process_spectrum(f::Function, data::MSIData, x::Int, y::Int) mz, intensity = GetSpectrum(data, x, y) return f(mz, intensity) end """ precompute_analytics(msi_data::MSIData) Performs a single pass over the entire dataset to pre-compute and cache important analytics. This function populates the `global_min_mz`, `global_max_mz`, and `spectrum_stats_df` fields of the `MSIData` object. The computed statistics include: - Global minimum and maximum m/z values. - Per-spectrum: - Total Ion Count (TIC) - Base Peak Intensity (BPI) - m/z of the base peak - Number of data points - Minimum and maximum m/z Subsequent calls to functions like `get_total_spectrum` will be much faster as they can use this cached data. This function modifies the `MSIData` object in-place and is idempotent. """ function precompute_analytics(msi_data::MSIData) # Idempotency check if msi_data.spectrum_stats_df !== nothing && hasproperty(msi_data.spectrum_stats_df, :MinMZ) println("Analytics have already been pre-computed.") return end println("Pre-computing analytics (single pass)...") start_time = time_ns() num_spectra = length(msi_data.spectra_metadata) # DEBUG: Check the first spectrum's metadata if num_spectra > 0 first_meta = msi_data.spectra_metadata[1] println("DEBUG First spectrum metadata:") println(" mz_asset: format=$(first_meta.mz_asset.format), compressed=$(first_meta.mz_asset.is_compressed)") println(" mz_asset: offset=$(first_meta.mz_asset.offset), encoded_length=$(first_meta.mz_asset.encoded_length)") println(" int_asset: format=$(first_meta.int_asset.format), compressed=$(first_meta.int_asset.is_compressed)") println(" int_asset: offset=$(first_meta.int_asset.offset), encoded_length=$(first_meta.int_asset.encoded_length)") println(" mode: $(first_meta.mode)") end # Initialize variables for global stats g_min_mz = Inf g_max_mz = -Inf # Initialize vectors for per-spectrum stats tics = Vector{Float64}(undef, num_spectra) bpis = Vector{Float64}(undef, num_spectra) bp_mzs = Vector{Float64}(undef, num_spectra) num_points = Vector{Int}(undef, num_spectra) min_mzs = Vector{Float64}(undef, num_spectra) max_mzs = Vector{Float64}(undef, num_spectra) modes = Vector{SpectrumMode}(undef, num_spectra) is_compressed = Vector{Bool}(undef, num_spectra) # DEBUG: Add counter to see how many spectra have data spectra_with_data = 0 empty_spectra = 0 _iterate_spectra_fast(msi_data) do idx, mz, intensity # Store metadata modes[idx] = msi_data.spectra_metadata[idx].mode is_compressed[idx] = msi_data.spectra_metadata[idx].mz_asset.is_compressed || msi_data.spectra_metadata[idx].int_asset.is_compressed if isempty(mz) empty_spectra += 1 tics[idx] = 0.0 bpis[idx] = 0.0 bp_mzs[idx] = 0.0 num_points[idx] = 0 min_mzs[idx] = Inf max_mzs[idx] = -Inf return else spectra_with_data += 1 end # Update global m/z range local_min, local_max = extrema(mz) g_min_mz = min(g_min_mz, local_min) g_max_mz = max(g_max_mz, local_max) min_mzs[idx] = local_min max_mzs[idx] = local_max # Calculate per-spectrum stats tics[idx] = sum(intensity) max_int, max_idx = findmax(intensity) bpis[idx] = max_int bp_mzs[idx] = mz[max_idx] num_points[idx] = length(mz) end # Add mode statistics centroid_count = count(==(CENTROID), modes) profile_count = count(==(PROFILE), modes) unknown_count = count(==(UNKNOWN), modes) println("DEBUG Mode Statistics:") println(" Centroid spectra: $centroid_count") println(" Profile spectra: $profile_count") println(" Unknown mode: $unknown_count") # DEBUG: Print summary println("DEBUG Analytics Summary:") println(" Total spectra: $num_spectra") println(" Spectra with data: $spectra_with_data") println(" Empty spectra: $empty_spectra") println(" Global m/z range: [$g_min_mz, $g_max_mz]") println(" Centroid spectra: $(count(==(CENTROID), modes))") println(" Profile spectra: $(count(==(PROFILE), modes))") println(" Compressed spectra: $(sum(is_compressed))") println(" Average points per spectrum: $(mean(num_points))") # Populate the MSIData object msi_data.global_min_mz = g_min_mz msi_data.global_max_mz = g_max_mz msi_data.spectrum_stats_df = DataFrame( SpectrumID = 1:num_spectra, TIC = tics, BPI = bpis, BasePeakMZ = bp_mzs, NumPoints = num_points, MinMZ = min_mzs, MaxMZ = max_mzs, Mode = modes, IsCompressed = is_compressed ) duration = (time_ns() - start_time) / 1e9 @printf "Analytics pre-computation complete in %.2f seconds.\n" duration return end """ get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000) Internal function to calculate the total spectrum for an `.imzML` file. It uses a fast, two-pass approach: 1. The first pass finds the global m/z range across all spectra. 2. The second pass sums intensities into a pre-defined number of bins. This function is highly optimized for `.imzML` by leveraging direct binary reading and optimized binning logic. It is called by `get_total_spectrum`. """ function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000) println("Calculating total spectrum for imzML (2-pass method)...") total_start_time = time_ns() local global_min_mz, global_max_mz if msi_data.global_min_mz !== nothing println(" Using pre-computed m/z range.") global_min_mz = msi_data.global_min_mz global_max_mz = msi_data.global_max_mz else # 1. First Pass: Find the global m/z range by reading the fast .ibd file pass1_start_time = time_ns() println(" Pass 1: Finding global m/z range...") g_min_mz = Inf g_max_mz = -Inf _iterate_spectra_fast(msi_data) do idx, mz, _ if !isempty(mz) local_min, local_max = extrema(mz) g_min_mz = min(g_min_mz, local_min) g_max_mz = max(g_max_mz, local_max) end end pass1_duration = (time_ns() - pass1_start_time) / 1e9 if !isfinite(g_min_mz) @warn "Could not determine a valid m/z range for imzML. All spectra might be empty." return (Float64[], Float64[]) end # Use and cache the result global_min_mz = g_min_mz global_max_mz = g_max_mz msi_data.global_min_mz = global_min_mz msi_data.global_max_mz = global_max_mz println(" Global m/z range found and cached: [$(global_min_mz), $(global_max_mz)]") @printf " (Pass 1 took %.2f seconds)\n" pass1_duration end # 2. Define Bins and precompute constants mz_bins = range(global_min_mz, stop=global_max_mz, length=num_bins) intensity_sum = zeros(Float64, num_bins) bin_step = step(mz_bins) inv_bin_step = 1.0 / bin_step # Precompute reciprocal to avoid division min_mz = global_min_mz # 3. Second Pass: Optimized binning pass2_start_time = time_ns() println(" Pass 2: Summing intensities into $num_bins bins...") _iterate_spectra_fast(msi_data) do idx, mz, intensity if isempty(mz) return end # Use views to avoid bounds checks on the input arrays mz_view = mz intensity_view = intensity # Manual binning without clamp/round for SIMD @inbounds for i in eachindex(mz_view) # Calculate raw bin index (much faster than round+clamp) raw_index = (mz_view[i] - min_mz) * inv_bin_step + 1.0 bin_index = trunc(Int, raw_index) # Manual bounds checking (faster than clamp) if 1 <= bin_index <= num_bins intensity_sum[bin_index] += intensity_view[i] elseif bin_index < 1 intensity_sum[1] += intensity_view[i] else # bin_index > num_bins intensity_sum[num_bins] += intensity_view[i] end end end pass2_duration = (time_ns() - pass2_start_time) / 1e9 total_duration = (time_ns() - total_start_time) / 1e9 println("\n--- imzML Profiling (Post-Optimization) ---") @printf " Pass 2 (I/O + Binning): %.2f seconds\n" pass2_duration @printf " Total Function Time: %.2f seconds\n" total_duration println("----------------------------------------\n") println("Total spectrum calculation complete for imzML.") return (collect(mz_bins), intensity_sum) end """ get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000) Internal function to calculate the total spectrum for an `.mzML` file. It uses a two-pass approach analogous to the `imzML` implementation: 1. The first pass finds the global m/z range by iterating through all spectra. 2. The second pass sums intensities into a pre-defined number of bins. This function is called by `get_total_spectrum`. """ function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000) println("Calculating total spectrum for mzML (2-pass method)...") total_start_time = time_ns() local global_min_mz, global_max_mz if msi_data.global_min_mz !== nothing println(" Using pre-computed m/z range.") global_min_mz = msi_data.global_min_mz global_max_mz = msi_data.global_max_mz else # --- Pass 1: Find m/z range and cache it --- pass1_start_time = time_ns() println(" Pass 1: Finding global m/z range...") g_min_mz = Inf g_max_mz = -Inf _iterate_spectra_fast(msi_data) do idx, mz, intensity if !isempty(mz) local_min, local_max = extrema(mz) g_min_mz = min(g_min_mz, local_min) g_max_mz = max(g_max_mz, local_max) end end pass1_duration = (time_ns() - pass1_start_time) / 1e9 if !isfinite(g_min_mz) @warn "Could not determine a valid m/z range for mzML. All spectra might be empty." return (Float64[], Float64[]) end # Use and cache the result global_min_mz = g_min_mz global_max_mz = g_max_mz msi_data.global_min_mz = global_min_mz msi_data.global_max_mz = global_max_mz println(" Global m/z range found and cached: [$(global_min_mz), $(global_max_mz)]") @printf " (Pass 1 took %.2f seconds)\n" pass1_duration end # --- Pass 2: Bin intensities --- pass2_start_time = time_ns() println(" Pass 2: Summing intensities into $num_bins 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) inv_bin_step = 1.0 / bin_step # Precompute reciprocal min_mz = global_min_mz _iterate_spectra_fast(msi_data) do idx, mz, intensity if isempty(mz) return end @inbounds for i in eachindex(mz) # Calculate raw bin index raw_index = (mz[i] - min_mz) * inv_bin_step + 1.0 bin_index = trunc(Int, raw_index) # Manual bounds checking if 1 <= bin_index <= num_bins intensity_sum[bin_index] += intensity[i] elseif bin_index < 1 intensity_sum[1] += intensity[i] else # bin_index > num_bins intensity_sum[num_bins] += intensity[i] end end end pass2_duration = (time_ns() - pass2_start_time) / 1e9 total_duration = (time_ns() - total_start_time) / 1e9 println("\n--- mzML Profiling (Post-Optimization) ---") @printf " Pass 2 (I/O + Binning): %.2f seconds\n" pass2_duration @printf " Total Function Time: %.2f seconds\n" total_duration println("----------------------------------------\n") println("Total spectrum calculation complete for mzML.") return (collect(mz_bins), intensity_sum) end """ get_total_spectrum(msi_data::MSIData; num_bins::Int=2000) -> Tuple{Vector{Float64}, Vector{Float64}} Calculates the sum of all spectra in the dataset by binning. This function dispatches to a specialized implementation based on the file type (.imzML or .mzML) for optimal performance. 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=2000) if msi_data.source isa ImzMLSource return get_total_spectrum_imzml(msi_data, num_bins=num_bins) else # MzMLSource return get_total_spectrum_mzml(msi_data, num_bins=num_bins) end end """ get_average_spectrum(msi_data::MSIData; num_bins::Int=2000) -> Tuple{Vector{Float64}, Vector{Float64}} Calculates the average of all spectra in the dataset by binning. This is effectively the total spectrum 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=2000) mz_bins, intensity_sum = get_total_spectrum(msi_data, num_bins=num_bins) if isempty(intensity_sum) return (mz_bins, intensity_sum) end num_spectra = length(msi_data.spectra_metadata) average_intensity = intensity_sum ./ num_spectra return (mz_bins, average_intensity) end # --- Iterator Implementation --- # """ MSIDataIterator A stateful iterator for sequentially accessing spectra from an `MSIData` object. This is created by the `IterateSpectra` function. """ struct MSIDataIterator data::MSIData end """ IterateSpectra(data::MSIData) Returns an iterator that yields each spectrum, processing the file sequentially. This iterator is useful for processing all spectra in a loop and benefits from the caching implemented in `GetSpectrum`. 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) """ Base.iterate(it::MSIDataIterator, state=1) Advances the iterator, returning the next spectrum in the sequence. It calls `GetSpectrum`, which means the iteration process is cached according to the `MSIData` object's cache settings. # Arguments - `it`: The `MSIDataIterator` instance. - `state`: The index of the next spectrum to retrieve. # Returns - A tuple `((index, spectrum), next_state)` or `nothing` if iteration is complete. """ 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 --- # """ read_compressed_array(io::IO, asset::SpectrumAsset, format::Type) Reads a single data array (m/z or intensity) from an `.ibd` file stream, handling both compressed and uncompressed data. - If `asset.is_compressed` is true, it reads the compressed bytes, inflates them using zlib, and reinterprets the result as a vector of the given `format`. - If false, it reads the uncompressed data directly into a vector. # Arguments - `io`: The IO stream of the `.ibd` file. - `asset`: The `SpectrumAsset` for the array. - `format`: The data type of the elements in the array. # Returns - A `Vector` containing the data. """ function read_compressed_array(io::IO, asset::SpectrumAsset, format::Type) seek(io, asset.offset) if asset.is_compressed # Read compressed bytes - use encoded_length as the number of compressed bytes compressed_bytes = read(io, asset.encoded_length) println("DEBUG: Decompressing data - offset=$(asset.offset), compressed_bytes=$(length(compressed_bytes))") local decompressed_bytes try decompressed_bytes = Libz.inflate(compressed_bytes) println("DEBUG: Decompression successful - decompressed_bytes=$(length(decompressed_bytes))") catch e @error "ZLIB DECOMPRESSION FAILED. This is likely due to an incorrect offset or corrupt data in the .ibd file." @error "Asset offset: $(asset.offset), Encoded length: $(asset.encoded_length)" # Print first 16 bytes to stderr for diagnosis bytes_to_print = min(16, length(compressed_bytes)) @error "First $bytes_to_print bytes of the data chunk we tried to decompress:" println(stderr, view(compressed_bytes, 1:bytes_to_print)) rethrow(e) end # Use an IOBuffer to safely read the data bytes_io = IOBuffer(decompressed_bytes) n_elements = bytes_io.size รท sizeof(format) array = Array{format}(undef, n_elements) read!(bytes_io, array) return array else # Read uncompressed data directly # For uncompressed imzML, encoded_length is the number of elements array = Vector{format}(undef, asset.encoded_length) read!(io, array) return array end end """ _iterate_uncompressed_fast(f::Function, data::MSIData, source::ImzMLSource) A highly optimized iterator for uncompressed `.imzML` data. It pre-allocates large buffers for m/z and intensity arrays and reuses them for each spectrum by creating views. This minimizes memory allocations and is significantly faster for bulk processing than reading spectra one by one. # Arguments - `f`: A function to execute for each spectrum, with the signature `f(index, mz_view, int_view)`. - `data`: The `MSIData` object. - `source`: The `ImzMLSource`. """ function _iterate_uncompressed_fast(f::Function, data::MSIData, source::ImzMLSource) # Optimized path for uncompressed data using buffer reuse max_points = maximum(meta -> meta.mz_asset.encoded_length, data.spectra_metadata) 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, view(mz_buffer, 0:-1), view(int_buffer, 0:-1)) continue end mz_view = view(mz_buffer, 1:nPoints) int_view = view(int_buffer, 1:nPoints) if meta.mz_asset.offset < meta.int_asset.offset seek(source.ibd_handle, meta.mz_asset.offset) read!(source.ibd_handle, mz_view) read!(source.ibd_handle, int_view) else seek(source.ibd_handle, meta.int_asset.offset) read!(source.ibd_handle, int_view) read!(source.ibd_handle, mz_view) end mz_view .= ltoh.(mz_view) int_view .= ltoh.(int_view) f(i, mz_view, int_view) end end """ _iterate_compressed_fast(f::Function, data::MSIData, source::ImzMLSource) An iterator for `.imzML` datasets that contain compressed spectra. This function iterates through each spectrum and reads its data individually, decompressing it on the fly if necessary. Because the size of decompressed data is not known in advance, this path cannot use the buffer-reuse optimization and will be slower and allocate more memory than `_iterate_uncompressed_fast`. # Arguments - `f`: A function to execute for each spectrum, with the signature `f(index, mz_array, int_array)`. - `data`: The `MSIData` object. - `source`: The `ImzMLSource`. """ function _iterate_compressed_fast(f::Function, data::MSIData, source::ImzMLSource) # Path for datasets containing at least one compressed spectrum. # This path reads and decompresses each spectrum individually. for i in 1:length(data.spectra_metadata) meta = data.spectra_metadata[i] if meta.mz_asset.encoded_length == 0 && meta.int_asset.encoded_length == 0 f(i, source.mz_format[], source.intensity_format[]) continue end # Read and decompress each array mz_array = read_compressed_array(source.ibd_handle, meta.mz_asset, source.mz_format) intensity_array = read_compressed_array(source.ibd_handle, meta.int_asset, source.intensity_format) mz_array .= ltoh.(mz_array) intensity_array .= ltoh.(intensity_array) f(i, mz_array, intensity_array) end end """ _iterate_spectra_fast_impl(f::Function, data::MSIData, source::ImzMLSource) Internal implementation of the fast iterator for `.imzML` files. It reads data directly from the `.ibd` file stream. This function acts as a dispatcher: - If any spectrum is compressed, it uses a slower path that decompresses each spectrum individually. - If all spectra are uncompressed, it uses a highly optimized path that reuses pre-allocated buffers to minimize memory allocations and overhead. """ function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::ImzMLSource) if isempty(data.spectra_metadata) return end # Check if ANY spectra are compressed and dispatch to the appropriate implementation any_compressed = any(meta -> meta.mz_asset.is_compressed || meta.int_asset.is_compressed, data.spectra_metadata) if any_compressed _iterate_compressed_fast(f, data, source) else _iterate_uncompressed_fast(f, data, source) end end """ _iterate_spectra_fast_impl(f::Function, data::MSIData, source::MzMLSource) Internal implementation of the fast iterator for `.mzML` files. It iterates through each spectrum, decodes the Base64 data on the fly, and calls the provided function. It bypasses the `GetSpectrum` cache to avoid storing all decoded spectra in memory. """ 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 """ _iterate_spectra_fast(f::Function, data::MSIData) An internal, high-performance iterator for bulk processing that bypasses the cache. It dispatches to a specialized implementation based on the data source type (`ImzMLSource` or `MzMLSource`). - `f`: A function to call for each spectrum, with signature `f(index, mz, intensity)`. - `data`: The `MSIData` object. """ 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