# src/mzML.jl # This file is responsible for parsing metadata from .mzML files. # It has been refactored to produce a unified MSIData object. # Constants for CV parameter accessions - defined once for performance const MZ_AXIS_ACCESSION = "MS:1000514" const INTENSITY_AXIS_ACCESSION = "MS:1000515" const COMPRESSION_ACCESSION = "MS:1000574" const NO_COMPRESSION_ACCESSION = "MS:1000576" # Data format accessions as constants const DATA_FORMAT_ACCESSIONS = Dict{String, DataType}( "MS:1000518" => Int16, "MS:1000519" => Int32, "MS:1000520" => Float64, "MS:1000521" => Float32, "MS:1000522" => Int64, "MS:1000523" => Float64 ) function parse_instrument_metadata_mzml(stream::IO) println("DEBUG: Starting mzML instrument metadata parsing...") # Initialize with default values from the InstrumentMetadata constructor instrument_meta = InstrumentMetadata() # Create temporary variables to hold parsed values resolution = instrument_meta.resolution instrument_model = instrument_meta.instrument_model mass_accuracy_ppm = instrument_meta.mass_accuracy_ppm polarity = instrument_meta.polarity calibration_status = instrument_meta.calibration_status laser_settings = Dict{String, Any}() # Use Any for heterogeneous values vendor_preprocessing_steps = String[] # Initialize vendor_preprocessing_steps try # Reset stream and read a sufficiently large header block to find metadata. seekstart(stream) header_block = "" header_read_limit = 50000 # Read up to 50KB of header bytes_read = 0 while !eof(stream) && bytes_read < header_read_limit line_pos = position(stream) line = readline(stream) bytes_read += (position(stream) - line_pos) # Stop at the start of the main data section if occursin("", line) || occursin("", line) break end header_block *= line * "\n" end header_io = IOBuffer(header_block) while !eof(header_io) line = readline(header_io) if occursin("` block within an mzML file to extract metadata for a single data array (e.g., m/z or intensity). It reads CV parameters to determine the data type, compression, and axis type. # Arguments - `stream`: An IO stream positioned at the beginning of a `` block. # Returns - A `SpectrumAsset` struct containing the parsed metadata, including the binary data offset, encoded length, format, and compression status. """ function get_spectrum_asset_metadata(stream::IO) start_pos = position(stream) #println("DEBUG: Entering get_spectrum_asset_metadata to parse binaryDataArray...") bda_tag = find_tag(stream, r"", line) break end if occursin("") binary_offset = position(stream) #println("DEBUG: Binary data offset: $binary_offset") # Move stream to the end of the binary data array for the next iteration readuntil(stream, "") #println("DEBUG: Exiting get_spectrum_asset_metadata.") # Create SpectrumAsset directly from the variables return SpectrumAsset(data_format, compression_flag, binary_offset, encoded_length, axis) end # This function is updated to return the generic SpectrumMetadata struct """ parse_spectrum_metadata(stream::IO, offset::Int64) Parses an entire `` block from an mzML file, given a starting offset. It extracts the spectrum ID and calls `get_spectrum_asset_metadata` to parse the m/z and intensity array metadata. # Arguments - `stream`: An IO stream for the mzML file. - `offset`: The byte offset where the `` block begins. # Returns - A `SpectrumMetadata` struct containing the parsed metadata for one spectrum. """ function parse_spectrum_metadata(stream::IO, offset::Int64) seek(stream, offset) # Read the whole spectrum block to parse mode spectrum_start_pos = position(stream) line = "" spectrum_buffer = IOBuffer() while !eof(stream) line = readline(stream) write(spectrum_buffer, line) if occursin("", line) break end end spectrum_xml = String(take!(spectrum_buffer)) seek(stream, spectrum_start_pos) # Reset for other parsing id_match = match(r"` block in an indexed mzML file to extract the byte offsets for each spectrum. # Arguments - `stream`: An IO stream positioned at the start of the `` block. # Returns - A `Vector{Int64}` containing the byte offsets for all spectra. """ function parse_offset_list(stream::IO) offsets = Int64[] offset_regex = r"]*>(\d+)" # Actively search for offset tags, ignoring other lines until the end of the index is found. while !eof(stream) line = readline(stream) # First, check for the end condition. if occursin("", line) || occursin("", line) break 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 return offsets end """ find_index_offset(stream::IO)::Int64 Finds the index offset in an mzML file by reading from the end. Optimized version with better buffer management. """ function find_index_offset(stream::IO)::Int64 file_size = filesize(stream) seekend(stream) # Read larger chunk for better chance of finding the offset chunk_size = min(8192, file_size) seek(stream, file_size - chunk_size) footer = read(stream, String) index_offset_match = match(r"(\d+)", footer) if index_offset_match === nothing throw(FileFormatError("Could not find . File may not be an indexed mzML.")) end return parse(Int64, index_offset_match.captures[1]) end # This is the main lazy-loading function for mzML, now returning an MSIData object. """ load_mzml_lazy(file_path::String; cache_size::Int=100) Lazily loads an indexed `.mzML` file by parsing only the metadata. It reads the spectrum index from the end of the file to get the offsets of each spectrum, then parses the metadata for each spectrum without loading the binary data. # Arguments - `file_path`: The path to the `.mzML` file. - `cache_size`: The number of spectra to hold in an LRU cache for faster access. # Returns - An `MSIData` object ready for lazy data access. """ function load_mzml_lazy(file_path::String; cache_size::Int=100) println("DEBUG: Opening file stream for $file_path") ts_stream = ThreadSafeFileHandle(file_path, "r") try # --- NEW: Parse instrument metadata from header --- println("DEBUG: Parsing instrument metadata from header...") instrument_meta = parse_instrument_metadata_mzml(ts_stream.handle) println("--- Extracted Instrument Metadata ---") println("Resolution: ", instrument_meta.resolution) println("Acquisition Mode (pre-check): ", instrument_meta.acquisition_mode) println("Calibration Status: ", instrument_meta.calibration_status) println("Instrument Model: ", instrument_meta.instrument_model) println("Mass Accuracy (ppm): ", instrument_meta.mass_accuracy_ppm) println("Laser Settings: ", instrument_meta.laser_settings) println("Polarity: ", instrument_meta.polarity) println("------------------------------------") seekstart(ts_stream.handle) # Reset stream after header parsing println("DEBUG: Finding index offset...") index_offset = find_index_offset(ts_stream.handle) println("DEBUG: Seeking to index list at offset $index_offset.") seek(ts_stream.handle, index_offset) println("DEBUG: Searching for ''.") if find_tag(ts_stream.handle, r" m == CENTROID, modes) num_profile = count(m -> m == PROFILE, modes) acq_mode_symbol = if num_centroid > 0 && num_profile == 0 :centroid elseif num_profile > 0 && num_centroid == 0 :profile elseif num_centroid > 0 && num_profile > 0 :mixed else :unknown end println("DEBUG: Inferred overall acquisition mode: $acq_mode_symbol (Centroid: $num_centroid, Profile: $num_profile)") final_instrument_meta = InstrumentMetadata( instrument_meta.resolution, acq_mode_symbol, # Update with parsed mode instrument_meta.mz_axis_type, instrument_meta.calibration_status, instrument_meta.instrument_model, instrument_meta.mass_accuracy_ppm, instrument_meta.laser_settings, instrument_meta.polarity, instrument_meta.vendor_preprocessing_steps # Add this new field ) source = MzMLSource(ts_stream, mz_format, intensity_format) println("DEBUG: Creating MSIData object.") return MSIData(source, spectra_metadata, final_instrument_meta, (0, 0), nothing, cache_size) catch e close(ts_stream) # Ensure stream is closed on error rethrow(e) end end #= """ LoadMzml(fileName::String) Eagerly loads all spectra from a .mzML file into memory. This function now uses the new lazy-loading architecture internally but presents the data in the old format. # Arguments - `fileName`: The path to the `.mzML` file. # Returns - A `2xN` matrix where `N` is the number of spectra. The first row contains m/z arrays and the second row contains intensity arrays. """ function LoadMzml(fileName::String) # 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(msi_data.spectra_metadata) # FIXED: Use concrete typed arrays instead of Array{Any} spectra_matrix = Vector{Tuple{Vector{Float64}, Vector{Float64}}}(undef, num_spectra) # Pre-allocate and use bounds checking optimization @inbounds for i in 1:num_spectra # Use the new GetSpectrum API mz, intensity = GetSpectrum(msi_data, i) spectra_matrix[i] = (mz, intensity) end # Convert to the expected 2xN format if needed by downstream code # Note: This maintains the original interface but with better typing result_matrix = Array{Any}(undef, (2, num_spectra)) @inbounds for i in 1:num_spectra result_matrix[1, i] = spectra_matrix[i][1] result_matrix[2, i] = spectra_matrix[i][2] end return result_matrix end =# #= """ load_mzml_batch(file_path::String, spectrum_indices::AbstractVector{Int}) Loads a specific batch of spectra from an mzML file efficiently. Useful for parallel processing or when only specific spectra are needed. # Arguments - `file_path`: Path to the mzML file - `spectrum_indices`: Indices of spectra to load # Returns - Vector of (mz_array, intensity_array) tuples """ function load_mzml_batch(file_path::String, spectrum_indices::AbstractVector{Int}) msi_data = load_mzml_lazy(file_path) num_to_load = length(spectrum_indices) # Pre-allocate result with concrete types results = Vector{Tuple{Vector{Float64}, Vector{Float64}}}(undef, num_to_load) @inbounds for (i, idx) in enumerate(spectrum_indices) mz, intensity = GetSpectrum(msi_data, idx) results[i] = (mz, intensity) end return results end =# #= """ get_mzml_summary(file_path::String)::NamedTuple Quickly extracts summary information from an mzML file without loading all metadata. # Returns - Named tuple with: num_spectra, mz_range, intensity_range, data_formats """ function get_mzml_summary(file_path::String)::NamedTuple ts_stream = ThreadSafeFileHandle(file_path, "r") try index_offset = find_index_offset(ts_stream.handle) seek(ts_stream.handle, index_offset) if find_tag(ts_stream.handle, r"]*>(\d+)", index_list = r"(\d+)" ) =#