diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa5ac62..b836d91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] + branches: [ main, development ] jobs: test: @@ -51,7 +51,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.14' - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -63,6 +63,9 @@ jobs: pip install maturin pytest numpy pip install fisher-py + - name: Decompress test data + run: gunzip -d -k test_data/*.gz + - name: Build and test run: | source .venv/bin/activate diff --git a/.gitignore b/.gitignore index 3abe4f5..df5af1f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,9 @@ coverage.xml venv/ pip-log.txt pip-delete-this-directory.txt +Pipfile +Pipfile.lock +.python-version # Rust target/ @@ -66,4 +69,8 @@ publish_output*.txt *output*txt *.dylib* _build/ -/tmp/ \ No newline at end of file +/tmp/ +venv3.11/pyvenv.cfg +/scratch +test_data/MS2_MS1_zoom.raw + diff --git a/native/ThermoNativeReader/NativeApi.cs b/native/ThermoNativeReader/NativeApi.cs index 301dd1f..d6d69a4 100644 --- a/native/ThermoNativeReader/NativeApi.cs +++ b/native/ThermoNativeReader/NativeApi.cs @@ -17,6 +17,7 @@ public static class NativeApi { private static IRawDataPlus? _rawFile; + private static string SafeGetFilterString(IScanFilter filter) { if (filter == null) return ""; @@ -39,6 +40,7 @@ static NativeApi() { // Force compiler to keep these types _dummyFilter = (ThermoFisher.CommonCore.Data.Interfaces.IScanFilter?)null; var t = typeof(ThermoFisher.CommonCore.Data.Interfaces.MetaFilterType); + var t2 = typeof(ThermoFisher.CommonCore.Data.Business.CentroidStream); } [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ThermoFisher.CommonCore.Data.Interfaces.MetaFilterType))] @@ -84,6 +86,7 @@ public static int IsCentroid(int scanNumber) try { var scanStatistics = _rawFile.GetScanStatsForScanNumber(scanNumber); + if (scanStatistics == null) return 0; return scanStatistics.IsCentroidScan ? 1 : 0; } catch @@ -120,28 +123,37 @@ public static unsafe int GetSpectrum(int scanNumber, double* masses, double* int } } - [UnmanagedCallersOnly(EntryPoint = "get_centroid_stream")] - public static unsafe int GetCentroidStream(int scanNumber, double* masses, double* intensities, int maxLength) + [UnmanagedCallersOnly(EntryPoint = "get_centroid_stream_full")] + public static unsafe int GetCentroidStreamFull(int scanNumber, double* masses, double* intensities, double* baselines, double* noises, int* charges, double* noiseRes, int maxLength) { if (_rawFile == null) return -1; - try { var scan = _rawFile.GetCentroidStream(scanNumber, false); - if (scan == null) { return -2; } - if (scan.Masses == null || scan.Intensities == null) { return -3; } + if (scan == null) return 0; int count = Math.Min(scan.Length, maxLength); for (int i = 0; i < count; i++) { - masses[i] = scan.Masses[i]; - intensities[i] = scan.Intensities[i]; + if (masses != null && scan.Masses != null && i < scan.Masses.Length) masses[i] = scan.Masses[i]; + if (intensities != null && scan.Intensities != null && i < scan.Intensities.Length) intensities[i] = scan.Intensities[i]; + if (baselines != null && scan.Baselines != null && i < scan.Baselines.Length) baselines[i] = scan.Baselines[i]; + if (noises != null && scan.Noises != null && i < scan.Noises.Length) noises[i] = scan.Noises[i]; + if (charges != null && scan.Charges != null && i < scan.Charges.Length) charges[i] = (int)scan.Charges[i]; + } + + if (noiseRes != null) + { + noiseRes[0] = scan.BasePeakNoise; + noiseRes[1] = scan.BasePeakResolution; } + return count; } - catch (Exception) + catch (Exception ex) { - return -1; + Console.WriteLine($"Native Error in GetCentroidStreamFull: {ex.Message}"); + return -1; } } @@ -610,6 +622,25 @@ public static double GetSampleDilutionFactor() return _rawFile.SampleInformation.DilutionFactor; } + [UnmanagedCallersOnly(EntryPoint = "get_sample_injection_volume")] + public static double GetSampleInjectionVolume() + { + if (_rawFile == null) return 0.0; + return _rawFile.SampleInformation.InjectionVolume; + } + + [UnmanagedCallersOnly(EntryPoint = "get_sample_instrument_method_file")] + public static unsafe int GetSampleInstrumentMethodFile(byte* buffer, int length) + { + if (_rawFile == null) return -1; + var str = _rawFile.SampleInformation.InstrumentMethodFile ?? ""; + var bytes = System.Text.Encoding.UTF8.GetBytes(str); + int count = Math.Min(bytes.Length, length - 1); + for (int i = 0; i < count; i++) buffer[i] = bytes[i]; + buffer[count] = 0; + return count; + } + [UnmanagedCallersOnly(EntryPoint = "get_ms_order")] public static int GetMsOrder(int scanNumber) { @@ -1096,7 +1127,8 @@ public static unsafe int GetScanStats(int scanNumber, double* data) data[4] = stats.BasePeakMass; data[5] = stats.BasePeakIntensity; data[6] = stats.PacketCount; - return 7; + data[7] = stats.IsCentroidScan ? 1.0 : 0.0; + return 8; } catch { return -1; } } @@ -1471,5 +1503,102 @@ public static int GetScanFilterIndexToMultipleActivationIndex(int scanNumber) { return GetFilterInt(scanNumber, "IndexToMultipleActivationIndex"); } + [UnmanagedCallersOnly(EntryPoint = "select_instrument")] + public static void SelectInstrument(int deviceType, int deviceNumber) + { + if (_rawFile == null) return; + try { + _rawFile.SelectInstrument((Device)deviceType, deviceNumber); + } catch {} + } + + [UnmanagedCallersOnly(EntryPoint = "get_instrument_method_count")] + public static int GetInstrumentMethodCount() + { + if (_rawFile == null) return 0; + try { + return _rawFile.InstrumentMethodsCount; + } catch { return 0; } + } + + [UnmanagedCallersOnly(EntryPoint = "get_instrument_method")] + public static unsafe int GetInstrumentMethod(int index, byte* buffer, int maxLength) + { + if (_rawFile == null) return -1; + try + { + string method = _rawFile.GetInstrumentMethod(index); + if (string.IsNullOrEmpty(method)) return 0; + + byte[] bytes = System.Text.Encoding.UTF8.GetBytes(method); + int len = Math.Min(bytes.Length, maxLength - 1); + for (int i = 0; i < len; i++) buffer[i] = bytes[i]; + buffer[len] = 0; + return len; + } + catch + { + return -1; + } + } + [UnmanagedCallersOnly(EntryPoint = "get_autosampler_tray_index")] + public static int GetAutoSamplerTrayIndex() + { + if (_rawFile == null) return -1; + try { return _rawFile.AutoSamplerInformation.TrayIndex; } + catch (Exception ex) { Console.WriteLine($"Native Error in GetAutoSamplerTrayIndex: {ex.Message}"); return -1; } + } + + [UnmanagedCallersOnly(EntryPoint = "get_autosampler_vial_index")] + public static int GetAutoSamplerVialIndex() + { + if (_rawFile == null) return -1; + try { return _rawFile.AutoSamplerInformation.VialIndex; } + catch (Exception ex) { Console.WriteLine($"Native Error in GetAutoSamplerVialIndex: {ex.Message}"); return -1; } + } + + [UnmanagedCallersOnly(EntryPoint = "get_autosampler_tray_name")] + public static unsafe int GetAutoSamplerTrayName(byte* buffer, int maxLength) + { + if (_rawFile == null) return 0; + try + { + string name = _rawFile.AutoSamplerInformation.TrayName ?? ""; + byte[] bytes = System.Text.Encoding.UTF8.GetBytes(name); + int len = Math.Min(bytes.Length, maxLength - 1); + for (int i = 0; i < len; i++) buffer[i] = bytes[i]; + buffer[len] = 0; + return len; + } + catch { return 0; } + } + + [UnmanagedCallersOnly(EntryPoint = "get_autosampler_tray_shape")] + public static int GetAutoSamplerTrayShape() + { + if (_rawFile == null) return 0; + try { return (int)_rawFile.AutoSamplerInformation.TrayShape; } catch { return 0; } + } + + [UnmanagedCallersOnly(EntryPoint = "get_autosampler_vials_per_tray")] + public static int GetAutoSamplerVialsPerTray() + { + if (_rawFile == null) return -1; + try { return _rawFile.AutoSamplerInformation.VialsPerTray; } catch { return -1; } + } + + [UnmanagedCallersOnly(EntryPoint = "get_autosampler_vials_per_tray_x")] + public static int GetAutoSamplerVialsPerTrayX() + { + if (_rawFile == null) return -1; + try { return _rawFile.AutoSamplerInformation.VialsPerTrayX; } catch { return -1; } + } + + [UnmanagedCallersOnly(EntryPoint = "get_autosampler_vials_per_tray_y")] + public static int GetAutoSamplerVialsPerTrayY() + { + if (_rawFile == null) return -1; + try { return _rawFile.AutoSamplerInformation.VialsPerTrayY; } catch { return -1; } + } } } diff --git a/native_fisher_py/Cargo.lock b/native_fisher_py/Cargo.lock index f3e004e..c65adbb 100644 --- a/native_fisher_py/Cargo.lock +++ b/native_fisher_py/Cargo.lock @@ -56,7 +56,7 @@ dependencies = [ [[package]] name = "native_fisher_py" -version = "0.1.0" +version = "0.3.1" dependencies = [ "libloading", "pyo3", diff --git a/native_fisher_py/Cargo.toml b/native_fisher_py/Cargo.toml index ffe3970..0950283 100644 --- a/native_fisher_py/Cargo.toml +++ b/native_fisher_py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "native_fisher_py" -version = "0.1.0" +version = "0.3.1" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/native_fisher_py/python/native_fisher_py/data/classes.py b/native_fisher_py/python/native_fisher_py/data/classes.py index 068e9ab..7379019 100644 --- a/native_fisher_py/python/native_fisher_py/data/classes.py +++ b/native_fisher_py/python/native_fisher_py/data/classes.py @@ -584,7 +584,7 @@ def device_type(self): return 1 def instrument_index(self): return 0 class ScanStatistics(CommonCoreDataObject): - def __init__(self, start_time=0.0, low_mass=0.0, high_mass=0.0, tic=0.0, base_peak_mass=0.0, base_peak_intensity=0.0, packet_count=0, scan_number=0, ms_order=0): + def __init__(self, start_time=0.0, low_mass=0.0, high_mass=0.0, tic=0.0, base_peak_mass=0.0, base_peak_intensity=0.0, packet_count=0, scan_number=0, ms_order=0, is_centroid_scan=False): self._start_time = start_time self._low_mass = low_mass self._high_mass = high_mass @@ -594,6 +594,7 @@ def __init__(self, start_time=0.0, low_mass=0.0, high_mass=0.0, tic=0.0, base_pe self._packet_count = packet_count self._scan_number = scan_number self._ms_order = ms_order + self._is_centroid_scan = bool(is_centroid_scan) @property def start_time(self): return self._start_time @@ -624,7 +625,7 @@ def deep_clone(self): raise NotImplementedError @property def frequency(self): raise NotImplementedError @property - def is_centroid_scan(self): raise NotImplementedError + def is_centroid_scan(self): return self._is_centroid_scan @property def is_uniform_time(self): raise NotImplementedError @property @@ -881,9 +882,15 @@ def to_centroid(self): return None def tolerance_unit(self): return 0 class CentroidStream(CommonCoreDataObject): - def __init__(self, masses=None, intensities=None): + def __init__(self, masses=None, intensities=None, baselines=None, noises=None, charges=None, base_peak_noise=0.0, base_peak_resolution=0.0, scan_number=0): self._masses = masses if masses is not None else np.array([]) self._intensities = intensities if intensities is not None else np.array([]) + self._baselines = baselines if baselines is not None else np.array([]) + self._noises = noises if noises is not None else np.array([]) + self._charges = charges if charges is not None else np.array([]) + self._base_peak_noise = base_peak_noise + self._base_peak_resolution = base_peak_resolution + self._scan_number = scan_number @property def base_intensity(self): return np.max(self._intensities) if self._intensities.size > 0 else 0.0 @@ -892,57 +899,40 @@ def base_peak_intensity(self): return self.base_intensity @property def base_peak_mass(self): return self._masses[np.argmax(self._intensities)] if self._intensities.size > 0 else 0.0 @property - def base_peak_noise(self): raise NotImplementedError + def base_peak_noise(self): return self._base_peak_noise @property - def base_peak_resolution(self): raise NotImplementedError + def base_peak_resolution(self): return self._base_peak_resolution @property - def baselines(self): raise NotImplementedError + def baselines(self): return self._baselines @property - def charges(self): raise NotImplementedError - def clear(self): raise NotImplementedError - def clone(self): return self + def charges(self): return self._charges @property - def coefficients(self): - if _IS_SPHINX: return np.array([]) - raise NotImplementedError + def coefficients_count(self): return 0 @property - def coefficients_count(self): - if _IS_SPHINX: return 0 - raise NotImplementedError - def deep_clone(self): - if _IS_SPHINX: return self - raise NotImplementedError + def coefficients(self): return np.array([]) @property - def flags(self): - if _IS_SPHINX: return [] - raise NotImplementedError - def get_centroids(self): - if _IS_SPHINX: return [] - raise NotImplementedError - def get_label_peak(self, i): - if _IS_SPHINX: return None - raise NotImplementedError - def get_label_peaks(self): - if _IS_SPHINX: return [] - raise NotImplementedError + def flags(self): return 0 + def get_centroids(self): return self._masses, self._intensities + def get_label_peak(self, index): return None + def get_label_peaks(self): return None + @property + def noises(self): return self._noises @property def intensities(self): return self._intensities @property def length(self): return len(self._masses) if self._masses is not None else 0 @property def masses(self): return self._masses - @property - def noises(self): raise NotImplementedError def refresh_base_details(self): pass @property def resolutions(self): return np.array([]) @property - def scan_number(self): return 0 - def set_label_peaks(self, p): pass + def scan_number(self): return self._scan_number + def set_label_peaks(self, peaks): pass @property - def sum_intensities(self): return 0.0 + def sum_intensities(self): return np.sum(self._intensities) if self._intensities.size > 0 else 0.0 @property - def sum_masses(self): return 0.0 + def sum_masses(self): return np.sum(self._masses) if self._masses.size > 0 else 0.0 def to_scan(self): return None def to_segmented_scan(self): return None def to_simple_scan(self): return None @@ -1058,12 +1048,10 @@ def deep_copy(self): raise NotImplementedError def dilution_factor(self): return get_sample_dilution_factor() @property def injection_volume(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError + return get_sample_injection_volume() @property def instrument_method_file(self): - if _IS_SPHINX: return "" - raise NotImplementedError + return get_sample_instrument_method_file() @property def istd_amount(self): if _IS_SPHINX: return 0.0 @@ -1098,6 +1086,8 @@ def vial(self): return "" def raw_file_name(self) -> str: return get_file_name() @property def path(self) -> str: return get_path() + @property + def autosampler_information(self): return AutoSamplerInformation() class FileHeader(CommonCoreDataObject): @property @@ -1144,36 +1134,28 @@ def warning_message(self): return "" class AutoSamplerInformation(CommonCoreDataObject): @property def tray_index(self): - if _IS_SPHINX: return -1 - raise NotImplementedError + return get_autosampler_tray_index() @property def tray_name(self): - if _IS_SPHINX: return "Any" - raise NotImplementedError + return get_autosampler_tray_name() @property def tray_shape(self): - if _IS_SPHINX: return TrayShape.Unknown - raise NotImplementedError + return TrayShape(get_autosampler_tray_shape()) @property def tray_shape_as_string(self): - if _IS_SPHINX: return "Unknown" - raise NotImplementedError + return str(self.tray_shape) @property def vial_index(self): - if _IS_SPHINX: return -1 - raise NotImplementedError + return get_autosampler_vial_index() @property def vials_per_tray(self): - if _IS_SPHINX: return -1 - raise NotImplementedError + return get_autosampler_vials_per_tray() @property def vials_per_tray_x(self): - if _IS_SPHINX: return -1 - raise NotImplementedError + return get_autosampler_vials_per_tray_x() @property def vials_per_tray_y(self): - if _IS_SPHINX: return -1 - raise NotImplementedError + return get_autosampler_vials_per_tray_y() class RunHeader(CommonCoreDataObject): def __init__(self, raw_file=None): self._raw_file = raw_file @@ -1306,56 +1288,44 @@ def filter_mass_precision(self): raise NotImplementedError @property def high_mass(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError + return get_high_mass() @property def in_acquisition(self): - if _IS_SPHINX: return 0 - raise NotImplementedError + return in_acquisition() @property def low_mass(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError + return get_low_mass() @property def mass_resolution(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError + return get_mass_resolution() @property def max_integrated_intensity(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError + return get_max_integrated_intensity() @property def max_intensity(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError + return get_max_intensity() @property def spectra_count(self): - if _IS_SPHINX: return 0 - raise NotImplementedError + return get_num_scans() @property def status_log_count(self): - if _IS_SPHINX: return 0 - raise NotImplementedError + return get_status_log_count() @property def trailer_extra_count(self): - if _IS_SPHINX: return 0 - raise NotImplementedError + return get_trailer_extra_count() @property def trailer_scan_event_count(self): if _IS_SPHINX: return 0 raise NotImplementedError @property def tune_data_count(self): - if _IS_SPHINX: return 0 - raise NotImplementedError + return get_tune_data_count() @property def first_spectrum(self): - if _IS_SPHINX: return 0 - raise NotImplementedError + return get_first_scan() @property def last_spectrum(self): - if _IS_SPHINX: return 0 - raise NotImplementedError + return get_last_scan() @property def start_time(self): if _IS_SPHINX: return 0.0 @@ -1387,92 +1357,71 @@ def name(self): return get_scan_event_string(self._scan_number) @property def accurate_mass(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("accurate_mass") + return EventAccurateMass(get_scan_filter_accurate_mass(self._scan_number)) @property def mass_analyzer(self) -> int: - if _IS_SPHINX: return 0 - raise NotImplementedError("mass_analyzer") + return MassAnalyzer(get_scan_filter_mass_analyzer(self._scan_number)) @property def polarity(self) -> int: - if _IS_SPHINX: return 1 - raise NotImplementedError("polarity") + return PolarityType(get_scan_filter_polarity(self._scan_number)) @property def scan_mode(self) -> int: - if _IS_SPHINX: return 0 - raise NotImplementedError("scan_mode") + return ScanModeType(get_scan_filter_scan_mode(self._scan_number)) @property def ionization_mode(self) -> int: - if _IS_SPHINX: return 0 - raise NotImplementedError("ionization_mode") + return IonizationModeType(get_scan_filter_ionization_mode(self._scan_number)) @property def is_valid(self) -> bool: - if _IS_SPHINX: return True - raise NotImplementedError("is_valid") + return bool(get_instrument_is_valid()) @property def compensation_volt_type(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("compensation_volt_type") + return CompensationVoltageType(get_scan_filter_compensation_volt_type(self._scan_number)) @property def compensation_voltage(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("compensation_voltage") + return TriState(get_scan_event_compensation_voltage(self._scan_number)) @property def corona(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("corona") + return TriState(get_scan_filter_corona(self._scan_number)) @property def dependent(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("dependent") + return TriState(get_scan_filter_dependent(self._scan_number)) @property def detector(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("detector") + return DetectorType(get_scan_filter_detector(self._scan_number)) @property def detector_value(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("detector_value") + return get_scan_filter_detector_value(self._scan_number) @property def electron_capture_dissociation(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("electron_capture_dissociation") + return TriState(get_scan_filter_electron_capture_dissociation(self._scan_number)) @property def electron_capture_dissociation_value(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("electron_capture_dissociation_value") + return get_scan_filter_electron_capture_dissociation_value(self._scan_number) @property def electron_transfer_dissociation(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("electron_transfer_dissociation") + return TriState(get_scan_filter_electron_transfer_dissociation(self._scan_number)) @property def electron_transfer_dissociation_value(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("electron_transfer_dissociation_value") + return get_scan_filter_electron_transfer_dissociation_value(self._scan_number) @property def enhanced(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("enhanced") + return TriState(get_scan_filter_enhanced(self._scan_number)) @property def field_free_region(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("field_free_region") + return FieldFreeRegionType(get_scan_filter_field_free_region(self._scan_number)) @property def higher_energy_ci_d(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("higher_energy_ci_d") + return TriState(get_scan_filter_higher_energy_cid(self._scan_number)) @property def higher_energy_ci_d_value(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("higher_energy_ci_d_value") + return get_scan_filter_higher_energy_cid_value(self._scan_number) @property def is_custom(self): if _IS_SPHINX: return 0 raise NotImplementedError("is_custom") @property def lock(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("lock") + return TriState(get_scan_filter_lock(self._scan_number)) @property def mass_calibrator_count(self): if _IS_SPHINX: return -1 @@ -1483,60 +1432,48 @@ def mass_range_count(self): raise NotImplementedError("mass_range_count") @property def multi_notch(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("multi_notch") + return TriState(get_scan_filter_multi_notch(self._scan_number)) @property def multi_state_activation(self): if _IS_SPHINX: return 0 raise NotImplementedError("multi_state_activation") @property def multiple_photon_dissociation(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("multiple_photon_dissociation") + return TriState(get_scan_filter_multiple_photon_dissociation(self._scan_number)) @property def multiple_photon_dissociation_value(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("multiple_photon_dissociation_value") + return get_scan_filter_multiple_photon_dissociation_value(self._scan_number) @property def multiplex(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("multiplex") + return TriState(get_scan_filter_multiplex(self._scan_number)) @property def param_a(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("param_a") + return get_scan_filter_param_a(self._scan_number) @property def param_b(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("param_b") + return get_scan_filter_param_b(self._scan_number) @property def param_f(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("param_f") + return get_scan_filter_param_f(self._scan_number) @property def param_r(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("param_r") + return get_scan_filter_param_r(self._scan_number) @property def param_v(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("param_v") + return get_scan_filter_param_v(self._scan_number) @property def photo_ionization(self): if _IS_SPHINX: return 0 raise NotImplementedError("photo_ionization") @property def pulsed_q_dissociation(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("pulsed_q_dissociation") + return TriState(get_scan_filter_pulsed_q_dissociation(self._scan_number)) @property def pulsed_q_dissociation_value(self): - if _IS_SPHINX: return 0.0 - raise NotImplementedError("pulsed_q_dissociation_value") + return get_scan_filter_pulsed_q_dissociation_value(self._scan_number) @property def scan_data(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("scan_data") + return ScanDataType(get_scan_filter_scan_data(self._scan_number)) @property def scan_type_index(self): if _IS_SPHINX: return -1 @@ -1547,8 +1484,7 @@ def sector_scan(self): raise NotImplementedError("sector_scan") @property def source_fragmentation(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("source_fragmentation") + return TriState(get_scan_filter_source_fragmentation(self._scan_number)) @property def source_fragmentation_info_count(self): if _IS_SPHINX: return -1 @@ -1559,24 +1495,19 @@ def source_fragmentation_mass_range_count(self): raise NotImplementedError("source_fragmentation_mass_range_count") @property def source_fragmentation_type(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("source_fragmentation_type") + return SourceFragmentationValueType(get_scan_filter_source_fragmentation_type(self._scan_number)) @property def supplemental_activation(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("supplemental_activation") + return TriState(get_scan_filter_supplemental_activation(self._scan_number)) @property def turbo_scan(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("turbo_scan") + return TriState(get_scan_filter_turbo_scan(self._scan_number)) @property def ultra(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("ultra") + return TriState(get_scan_filter_ultra(self._scan_number)) @property def wideband(self): - if _IS_SPHINX: return 0 - raise NotImplementedError("wideband") + return TriState(get_scan_filter_wideband(self._scan_number)) def get_energy_valid(self, index): if _IS_SPHINX: return 0 raise NotImplementedError("get_energy_valid") @@ -1613,8 +1544,7 @@ def get_source_fragmentation_mass_range(self, index): class ScanEvents(CommonCoreDataObject): def get_event(self, index): - if _IS_SPHINX: return ScanEvent() - raise NotImplementedError("get_event") + return ScanEvent(index + 1) def get_event_by_segment(self, segment, event): if _IS_SPHINX: return ScanEvent() raise NotImplementedError("get_event_by_segment") @@ -1642,8 +1572,12 @@ def low(self): return self._low def high(self): return self._high def compare_to(self, other): - if _IS_SPHINX: return 0 - raise NotImplementedError + if not isinstance(other, Range): return -1 + if self._low < other._low: return -1 + if self._low > other._low: return 1 + if self._high < other._high: return -1 + if self._high > other._high: return 1 + return 0 @staticmethod def create(l, h): return Range(l, h) @staticmethod diff --git a/native_fisher_py/python/native_fisher_py/raw_file.py b/native_fisher_py/python/native_fisher_py/raw_file.py index 436ce8d..b23f890 100644 --- a/native_fisher_py/python/native_fisher_py/raw_file.py +++ b/native_fisher_py/python/native_fisher_py/raw_file.py @@ -58,7 +58,7 @@ def _raw_file_access(self): return self def select_instrument(self, device_type: int, device_number: int): - pass + select_instrument(device_type, device_number) def average_scans(self, start, end): return None def average_scans_in_scan_range(self, start, end, options): return None @@ -66,10 +66,13 @@ def average_scans_in_scan_range(self, start, end, options): return None def default_mass_options(self): return MassOptions() def dispose(self): self.close() def get_all_instrument_names_from_instrument_method(self): return [] - def get_instrument_method(self, index): return "" + def get_instrument_method(self, index): + return get_instrument_method(index) + def get_instrument_methods_count(self) -> int: + return get_instrument_method_count() def get_instrument_type(self): return 0 def get_segment_event_table(self): return [] - def has_instrument_method(self): return False + def has_instrument_method(self): return self.get_instrument_methods_count() > 0 def is_centroid_scan_from_scan_number(self, scan_number): return is_centroid(scan_number) def refresh_view_of_file(self): pass @@ -90,7 +93,7 @@ def __repr__(self): @property def path(self) -> str: - return get_path() + return self._path @property def number_of_scans(self) -> int: @@ -196,8 +199,20 @@ def get_scan_event_string_for_scan_number(self, scan_number: int): def get_centroid_stream(self, scan_number: int, include_ref_peaks: bool = False): from .native_fisher_py_backend import get_centroid_stream from .data.classes import CentroidStream - masses, intensities = get_centroid_stream(scan_number, 1000000) - return CentroidStream(masses=masses, intensities=intensities) + import numpy as np + + masses, intensities, baselines, noises, charges, bp_noise, bp_res = get_centroid_stream(scan_number, 1000000) + + return CentroidStream( + masses=np.array(masses), + intensities=np.array(intensities), + baselines=np.array(baselines), + noises=np.array(noises), + charges=np.array(charges), + base_peak_noise=bp_noise, + base_peak_resolution=bp_res, + scan_number=scan_number + ) def get_segmented_scan_from_scan_number(self, scan_number: int, stats = None): from .native_fisher_py_backend import get_spectrum @@ -216,7 +231,8 @@ def get_scan_stats_for_scan_number(self, scan_number: int): tic=data[3], base_peak_mass=data[4], base_peak_intensity=data[5], - packet_count=int(data[6]) + packet_count=int(data[6]), + is_centroid_scan=bool(data[7]) ) def get_chromatogram_data(self, settings, start_scan, end_scan, tolerance = None): diff --git a/native_fisher_py/python/native_fisher_py/utils/gradient.py b/native_fisher_py/python/native_fisher_py/utils/gradient.py new file mode 100644 index 0000000..0156b22 --- /dev/null +++ b/native_fisher_py/python/native_fisher_py/utils/gradient.py @@ -0,0 +1,37 @@ +import re +from typing import List, Tuple + +def parse_vanquish_neo_gradient(method_text: str) -> dict: + """ + Parses Vanquish Neo style pump lines from instrument method text. + Returns a dictionary with 'solvents' and 'gradient'. + """ + results = { + "solvents": {"A": None, "B": None}, + "gradient": [] + } + + # Extract Solvents + # Format: Neo.PumpModule.Pump.%A_Solvent: H2O + a_match = re.search(r'Pump\.%A_Solvent:\s+(.*)', method_text) + if a_match: + results["solvents"]["A"] = a_match.group(1).strip() + + b_match = re.search(r'Pump\.%B_Solvent:\s+(.*)', method_text) + if b_match: + results["solvents"]["B"] = b_match.group(1).strip() + + # Split by time points like "71.800 [min]" + segments = re.split(r'(\d+\.\d+)\s+\[min\]', method_text) + + current_time = None + for part in segments: + if re.match(r'^\d+\.\d+$', part): + current_time = float(part) + elif current_time is not None: + match = re.search(r'Pump\.%B\.Value:\s+(\d+\.\d+)\s+\[%\]', part) + if match: + percent_b = float(match.group(1)) + results["gradient"].append((current_time, percent_b)) + + return results diff --git a/native_fisher_py/src/lib.rs b/native_fisher_py/src/lib.rs index 8456b4a..f184a67 100644 --- a/native_fisher_py/src/lib.rs +++ b/native_fisher_py/src/lib.rs @@ -141,22 +141,41 @@ fn is_centroid(scan_number: i32) -> PyResult { } #[pyfunction] -fn get_centroid_stream(scan_number: i32, max_length: i32) -> PyResult<(Vec, Vec)> { +fn get_centroid_stream(scan_number: i32, max_length: i32) -> PyResult<(Vec, Vec, Vec, Vec, Vec, f64, f64)> { let lib = get_lib()?; let mut masses = vec![0.0f64; max_length as usize]; let mut intensities = vec![0.0f64; max_length as usize]; - - unsafe { - let func: Symbol i32> = lib.get(b"get_centroid_stream") - .map_err(|e| PyErr::new::(format!("get function get_centroid_stream: {}", e)))?; - let actual_len = func(scan_number, masses.as_mut_ptr(), intensities.as_mut_ptr(), max_length); - if actual_len < 0 { - return Err(PyErr::new::("get_centroid_stream failed")); - } + let mut baselines = vec![0.0f64; max_length as usize]; + let mut noises = vec![0.0f64; max_length as usize]; + let mut charges = vec![0i32; max_length as usize]; + let mut noise_res = vec![0.0f64; 2]; + + unsafe { + let func: Symbol i32> = + lib.get(b"get_centroid_stream_full") + .map_err(|e| PyErr::new::(format!("get function get_centroid_stream_full: {}", e)))?; + + let actual_len = func( + scan_number, + masses.as_mut_ptr(), + intensities.as_mut_ptr(), + baselines.as_mut_ptr(), + noises.as_mut_ptr(), + charges.as_mut_ptr(), + noise_res.as_mut_ptr(), + max_length + ); + + if actual_len < 0 { return Err(PyErr::new::("get_centroid_stream_full failed")); } + let final_len = std::cmp::min(actual_len, max_length) as usize; masses.truncate(final_len); intensities.truncate(final_len); - Ok((masses, intensities)) + baselines.truncate(final_len); + noises.truncate(final_len); + charges.truncate(final_len); + + Ok((masses, intensities, baselines, noises, charges, noise_res[0], noise_res[1])) } } @@ -1147,7 +1166,7 @@ fn get_scan_event_collision_energy(scan_number: i32, index: i32) -> PyResult PyResult> { let lib = get_lib()?; - let mut data = vec![0.0f64; 7]; + let mut data = vec![0.0f64; 8]; unsafe { let func: Symbol i32> = lib.get(b"get_scan_stats") .map_err(|e| PyErr::new::(format!("get function get_scan_stats: {}", e)))?; @@ -1544,6 +1563,140 @@ fn get_instrument_is_tsq_quantum_file() -> PyResult { Ok(func() != 0) } } + +#[pyfunction] +fn select_instrument(device_type: i32, device_number: i32) -> PyResult<()> { + let lib = get_lib()?; + unsafe { + let func: Symbol = lib.get(b"select_instrument") + .map_err(|e| PyErr::new::(format!("get function select_instrument: {}", e)))?; + func(device_type, device_number); + Ok(()) + } +} + +#[pyfunction] +fn get_instrument_method_count() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol i32> = lib.get(b"get_instrument_method_count") + .map_err(|e| PyErr::new::(format!("get function get_instrument_method_count: {}", e)))?; + Ok(func()) + } +} + +#[pyfunction] +fn get_instrument_method(index: i32) -> PyResult { + let lib = get_lib()?; + let mut buffer = vec![0u8; 128 * 1024]; // 128KB buffer for method text + unsafe { + let func: Symbol i32> = lib.get(b"get_instrument_method") + .map_err(|e| PyErr::new::(format!("get function get_instrument_method: {}", e)))?; + let actual_len = func(index, buffer.as_mut_ptr(), 128 * 1024); + if actual_len < 0 { return Err(PyErr::new::("get_instrument_method failed")); } + let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len()); + Ok(String::from_utf8_lossy(&buffer[..end]).into_owned()) + } +} + +#[pyfunction] +fn get_autosampler_tray_index() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol i32> = lib.get(b"get_autosampler_tray_index") + .map_err(|e| PyErr::new::(format!("get function get_autosampler_tray_index: {}", e)))?; + Ok(func()) + } +} + +#[pyfunction] +fn get_autosampler_vial_index() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol i32> = lib.get(b"get_autosampler_vial_index") + .map_err(|e| PyErr::new::(format!("get function get_autosampler_vial_index: {}", e)))?; + Ok(func()) + } +} + +#[pyfunction] +fn get_autosampler_tray_name() -> PyResult { + let lib = get_lib()?; + let mut buffer = vec![0u8; 1024]; + unsafe { + let func: Symbol i32> = lib.get(b"get_autosampler_tray_name") + .map_err(|e| PyErr::new::(format!("get function get_autosampler_tray_name: {}", e)))?; + let actual_len = func(buffer.as_mut_ptr(), 1024); + if actual_len < 0 { return Err(PyErr::new::("get_autosampler_tray_name failed")); } + let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len()); + Ok(String::from_utf8_lossy(&buffer[..end]).into_owned()) + } +} + +#[pyfunction] +fn get_autosampler_tray_shape() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol i32> = lib.get(b"get_autosampler_tray_shape") + .map_err(|e| PyErr::new::(format!("get function get_autosampler_tray_shape: {}", e)))?; + Ok(func()) + } +} + +#[pyfunction] +fn get_autosampler_vials_per_tray() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol i32> = lib.get(b"get_autosampler_vials_per_tray") + .map_err(|e| PyErr::new::(format!("get function get_autosampler_vials_per_tray: {}", e)))?; + Ok(func()) + } +} + +#[pyfunction] +fn get_autosampler_vials_per_tray_x() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol i32> = lib.get(b"get_autosampler_vials_per_tray_x") + .map_err(|e| PyErr::new::(format!("get function get_autosampler_vials_per_tray_x: {}", e)))?; + Ok(func()) + } +} + +#[pyfunction] +fn get_autosampler_vials_per_tray_y() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol i32> = lib.get(b"get_autosampler_vials_per_tray_y") + .map_err(|e| PyErr::new::(format!("get function get_autosampler_vials_per_tray_y: {}", e)))?; + Ok(func()) + } +} + +#[pyfunction] +fn get_sample_instrument_method_file() -> PyResult { + let lib = get_lib()?; + let mut buffer = vec![0u8; 1024]; + unsafe { + let func: Symbol i32> = lib.get(b"get_sample_instrument_method_file") + .map_err(|e| PyErr::new::(format!("get function get_sample_instrument_method_file: {}", e)))?; + let actual_len = func(buffer.as_mut_ptr(), 1024); + if actual_len < 0 { return Err(PyErr::new::("get_sample_instrument_method_file failed")); } + let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len()); + Ok(String::from_utf8_lossy(&buffer[..end]).into_owned()) + } +} + +#[pyfunction] +fn get_sample_injection_volume() -> PyResult { + let lib = get_lib()?; + unsafe { + let func: Symbol f64> = lib.get(b"get_sample_injection_volume") + .map_err(|e| PyErr::new::(format!("get function get_sample_injection_volume: {}", e)))?; + Ok(func()) + } +} + #[pymodule] fn native_fisher_py_backend(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(open_raw_file, m)?)?; @@ -1565,6 +1718,7 @@ fn native_fisher_py_backend(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(get_low_mass, m)?)?; m.add_function(wrap_pyfunction!(get_high_mass, m)?)?; m.add_function(wrap_pyfunction!(get_file_name, m)?)?; + m.add_function(wrap_pyfunction!(get_path, m)?)?; m.add_function(wrap_pyfunction!(get_ms_order, m)?)?; m.add_function(wrap_pyfunction!(get_mass_analyzer, m)?)?; m.add_function(wrap_pyfunction!(get_precursor_mass, m)?)?; @@ -1588,6 +1742,8 @@ fn native_fisher_py_backend(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(get_creator_id, m)?)?; m.add_function(wrap_pyfunction!(get_sample_type, m)?)?; m.add_function(wrap_pyfunction!(get_sample_row_number, m)?)?; + m.add_function(wrap_pyfunction!(get_sample_instrument_method_file, m)?)?; + m.add_function(wrap_pyfunction!(get_sample_injection_volume, m)?)?; m.add_function(wrap_pyfunction!(get_sample_dilution_factor, m)?)?; m.add_function(wrap_pyfunction!(get_instrument_model, m)?)?; m.add_function(wrap_pyfunction!(get_instrument_name, m)?)?; @@ -1671,6 +1827,16 @@ fn native_fisher_py_backend(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(get_scan_filter_meta_filters, m)?)?; m.add_function(wrap_pyfunction!(get_scan_filter_field_free_region, m)?)?; m.add_function(wrap_pyfunction!(get_scan_filter_index_to_multiple_activation_index, m)?)?; + m.add_function(wrap_pyfunction!(get_instrument_method_count, m)?)?; + m.add_function(wrap_pyfunction!(get_instrument_method, m)?)?; + m.add_function(wrap_pyfunction!(select_instrument, m)?)?; + m.add_function(wrap_pyfunction!(get_autosampler_tray_index, m)?)?; + m.add_function(wrap_pyfunction!(get_autosampler_vial_index, m)?)?; + m.add_function(wrap_pyfunction!(get_autosampler_tray_name, m)?)?; + m.add_function(wrap_pyfunction!(get_autosampler_tray_shape, m)?)?; + m.add_function(wrap_pyfunction!(get_autosampler_vials_per_tray, m)?)?; + m.add_function(wrap_pyfunction!(get_autosampler_vials_per_tray_x, m)?)?; + m.add_function(wrap_pyfunction!(get_autosampler_vials_per_tray_y, m)?)?; m.add_function(wrap_pyfunction!(close_raw_file, m)?)?; Ok(()) } diff --git a/test_data/MS2_MS1_zoom.raw.gz b/test_data/MS2_MS1_zoom.raw.gz new file mode 100644 index 0000000..3b96f8b Binary files /dev/null and b/test_data/MS2_MS1_zoom.raw.gz differ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0ba60f1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +import pytest +import os +from native_fisher_py.raw_file import RawFile + +@pytest.fixture(scope="session") +def zoom_raw_path(): + path = os.path.join("test_data", "MS2_MS1_zoom.raw") + if not os.path.exists(path): + pytest.skip(f"Test file {path} not found") + return path + +@pytest.fixture(scope="session") +def zoom_raw_file(zoom_raw_path): + raw = RawFile(zoom_raw_path) + yield raw + raw.close() diff --git a/tests/test_centroid.py b/tests/test_centroid.py new file mode 100644 index 0000000..e2c8813 --- /dev/null +++ b/tests/test_centroid.py @@ -0,0 +1,39 @@ +import pytest +import numpy as np + +def test_centroid_stream_retrieval(zoom_raw_file): + # Scan 1 is usually a good starting point + scan_number = 1 + cs = zoom_raw_file.get_centroid_stream(scan_number) + + assert cs is not None + assert cs.scan_number == scan_number + assert isinstance(cs.masses, np.ndarray) + assert isinstance(cs.intensities, np.ndarray) + assert len(cs.masses) == len(cs.intensities) + + if len(cs.masses) > 0: + assert cs.masses[0] > 0 + assert cs.intensities[0] >= 0 + assert cs.base_peak_intensity == np.max(cs.intensities) + assert cs.sum_intensities == np.sum(cs.intensities) + +def test_centroid_stream_extras(zoom_raw_file): + scan_number = 1 + cs = zoom_raw_file.get_centroid_stream(scan_number) + + # Extra data might be zeros if not available, but should be returned as arrays + assert isinstance(cs.baselines, np.ndarray) + assert isinstance(cs.noises, np.ndarray) + assert isinstance(cs.charges, np.ndarray) + + assert len(cs.baselines) == len(cs.masses) + assert len(cs.noises) == len(cs.masses) + assert len(cs.charges) == len(cs.masses) + +def test_is_centroid_scan(zoom_raw_file): + # Test a few scans to see if they are correctly identified + # Using small numbers to avoid potential instability in large scan ranges + for i in range(1, 5): + is_c = zoom_raw_file.is_centroid_scan_from_scan_number(i) + assert isinstance(is_c, bool) diff --git a/tests/test_collision_energy.py b/tests/test_collision_energy.py new file mode 100644 index 0000000..cee3b4f --- /dev/null +++ b/tests/test_collision_energy.py @@ -0,0 +1,38 @@ +import pytest +import os +from native_fisher_py.raw_file import RawFile + +@pytest.fixture(scope="session") +def raw_path(): + return os.path.join("test_data", "MS2_MS1_orbitrap.raw") + +@pytest.fixture(scope="session") +def raw_file(raw_path): + if not os.path.exists(raw_path): + pytest.skip(f"Test file {raw_path} not found") + + raw = RawFile(raw_path) + yield raw + raw.close() + +def test_collision_energy(raw_file): + # Test values obtained from a reference run + energy_expectations = { + 2: 28.0, + 3: 30.0, + 4: 30.0, + 6: 30.0, + 8: 30.0 + } + + for scan_num, expected_energy in energy_expectations.items(): + scan_event = raw_file.get_scan_event_for_scan_number(scan_num) + actual_energy = scan_event.get_energy(0) + assert actual_energy == pytest.approx(expected_energy), f"Scan {scan_num} energy mismatch" + +def test_reaction_collision_energy(raw_file): + # Check first reaction specifically + scan_num = 2 + scan_event = raw_file.get_scan_event_for_scan_number(scan_num) + reaction = scan_event.get_reaction(0) + assert reaction.collision_energy == pytest.approx(28.0) diff --git a/tests/test_gradient.py b/tests/test_gradient.py new file mode 100644 index 0000000..b4d83ac --- /dev/null +++ b/tests/test_gradient.py @@ -0,0 +1,38 @@ +import pytest +from native_fisher_py.utils.gradient import parse_vanquish_neo_gradient + +def test_extract_gradient_from_zoom_file(zoom_raw_file): + # Get the number of methods + method_count = zoom_raw_file.get_instrument_methods_count() + assert method_count > 0, "No instrument methods found" + + result = None + for i in range(method_count): + method_text = zoom_raw_file.get_instrument_method(i) + result = parse_vanquish_neo_gradient(method_text) + if result["gradient"]: + break + + assert result is not None, "Could not extract result from any instrument method" + gradient = result["gradient"] + solvents = result["solvents"] + + assert len(gradient) > 0, "Could not extract gradient" + assert solvents["A"] == "H2O" + assert solvents["B"] == "ACN80" + + # Expected points for 300SPD method + expected_gradient = [ + (0.0, 6.0), + (0.0, 6.0), + (0.2, 10.0), + (2.7, 28.0), + (3.0, 55.0), + (3.1, 99.0), + (3.4, 99.0) + ] + + assert len(gradient) == len(expected_gradient) + for i, (time, b) in enumerate(expected_gradient): + assert gradient[i][0] == pytest.approx(time) + assert gradient[i][1] == pytest.approx(b) diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..10758eb --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,14 @@ +import pytest +from native_fisher_py.data.classes import TrayShape + +def test_metadata_from_zoom_file(zoom_raw_file): + si = zoom_raw_file.sample_information + ai = si.autosampler_information + + assert si.injection_volume == pytest.approx(1.0) + assert si.instrument_method_file.endswith(".meth") + + assert ai.tray_name == "R" + assert ai.tray_index == -1 + assert ai.tray_shape == TrayShape.Circular + assert ai.vial_index == -1 diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..e54860b --- /dev/null +++ b/todo.md @@ -0,0 +1,63 @@ +# Unimplemented API Endpoints + +This document lists the API endpoints and properties that are defined in the `native-fisher-py` codebase but currently lack implementation. +They are ranked by importance for typical mass spectrometry data processing workflows. + +## 1. Critical Importance (Core Data & MS/MS) +*Missing foundational data required for standard LC-MS/MS analysis.* + +| Class | Method / Property | Status | +| :--- | :--- | :--- | +| `ScanEvent` | `get_isolation_width(index)` | ❌ Missing in Native & Python | +| `ScanEvent` | `get_mass_range(index)` | ❌ Missing in Native & Python | +| `Reaction` | `isolation_width` | ❌ Python Placeholder | +| `Reaction` | `isolation_width_offset` | ❌ Python Placeholder | +| `Reaction` | `precursor_range_is_valid` | ❌ Python Placeholder | +| `ScanDependentDetails` | `precursor_mass_array` | ❌ Python Placeholder | +| `ScanDependentDetails` | `isolation_width_array` | ❌ Python Placeholder | + +## 2. High Importance (Metadata & Run Stats) +*Essential for file overview and scan tracking.* + +| Class | Method / Property | Status | +| :--- | :--- | :--- | +| `RunHeader` | `end_time` | ❌ Python Placeholder | +| `RunHeader` | `expected_runtime` | ❌ Python Placeholder | +| `RunHeader` | `high_mass` / `low_mass` | ❌ Python Placeholder | +| `RunHeader` | `spectra_count` | ⚠️ Partially implemented in some subclasses | +| `ScanStatistics` | `cycle_number` | ❌ Python Placeholder | +| `ScanStatistics` | `segment_number` | ❌ Python Placeholder | +| `ScanStatistics` | `scan_type` | ❌ Python Placeholder | +| `ScanStatistics` | `spectrum_packet_type` | ❌ Python Placeholder | + +## 3. Medium Importance (Diagnostic & Advanced Info) +*Useful for peak quality assessment and instrument status.* + +| Class | Method / Property | Status | +| :--- | :--- | :--- | +| `CentroidStream` | `base_peak_noise` | ✅ Implemented | +| `CentroidStream` | `base_peak_resolution` | ✅ Implemented | +| `CentroidStream` | `baselines` | ✅ Implemented | +| `CentroidStream` | `charges` | ✅ Implemented | +| `CentroidStream` | `noises` | ✅ Implemented | +| `SampleInformation` | `injection_volume` | ✅ Implemented | +| `SampleInformation` | `instrument_method_file` | ✅ Implemented | +| `AutoSamplerInformation`| `tray_index` / `vial_index` | ✅ Implemented | +| `AutoSamplerInformation`| `tray_name` / `tray_shape` | ✅ Implemented | + +## 4. Low Importance (Verbose Metadata) +*Auxiliary information rarely used in downstream analysis.* + +| Class | Method / Property | Status | +| :--- | :--- | :--- | +| `ErrorLogEntry` | `message` | ❌ Python Placeholder | +| `ErrorLogEntry` | `retention_time` | ❌ Python Placeholder | +| `HeaderItem` | `is_numeric` | ❌ Python Placeholder | +| `HeaderItem` | `format_value` | ❌ Python Placeholder | +| `FileHeader` | `revision` | ❌ Python Placeholder | +| `FileHeader` | `number_of_times_calibrated`| ❌ Python Placeholder | +| `ScanEvent` | `is_custom` | ❌ Python Placeholder | +| `ScanEvents` | `get_event_by_segment` | ❌ Python Placeholder | + +--- +**Note:** Many Python placeholders return default values when `_IS_SPHINX` is true to allow documentation builds, but will raise `NotImplementedError` during actual execution.