corems.molecular_id.search.spectral_library_cache

Parquet sidecar cache for parsed spectral library DataFrames.

  1"""Parquet sidecar cache for parsed spectral library DataFrames."""
  2
  3from __future__ import annotations
  4
  5import json
  6from pathlib import Path
  7
  8import numpy as np
  9import pandas as pd
 10
 11# Bump when standardized cache columns or precursor policy change.
 12COREMS_LIB_CACHE_V = 1
 13CACHE_SUFFIX = ".corems-lib.parquet"
 14_META_KEY = b"corems_lib_cache"
 15
 16
 17def default_cache_path(source_path) -> Path:
 18    """Return default sidecar path next to the source library file."""
 19    return Path(source_path).with_name(Path(source_path).name + CACHE_SUFFIX)
 20
 21
 22def _source_fingerprint(source_path) -> dict:
 23    path = Path(source_path)
 24    stat = path.stat()
 25    return {
 26        "schema_version": COREMS_LIB_CACHE_V,
 27        "source_name": path.name,
 28        "source_size": stat.st_size,
 29        "source_mtime_ns": stat.st_mtime_ns,
 30    }
 31
 32
 33def is_valid(cache_path, source_path, extra_meta: dict | None = None) -> bool:
 34    """Return True if cache exists and matches source fingerprint + schema."""
 35    cache_path = Path(cache_path)
 36    source_path = Path(source_path)
 37    if not cache_path.is_file() or not source_path.is_file():
 38        return False
 39    try:
 40        import pyarrow.parquet as pq
 41
 42        meta = pq.read_schema(cache_path).metadata or {}
 43        raw = meta.get(_META_KEY)
 44        if raw is None:
 45            return False
 46        stored = json.loads(raw.decode("utf-8"))
 47    except Exception:
 48        return False
 49
 50    expected = _source_fingerprint(source_path)
 51    if extra_meta:
 52        expected.update(extra_meta)
 53    for key, value in expected.items():
 54        if stored.get(key) != value:
 55            return False
 56    return True
 57
 58
 59def pack_peaks_for_parquet(df: pd.DataFrame) -> pd.DataFrame:
 60    """Convert a peaks column of (N, 2) arrays into mz/intensity list columns."""
 61    out = df.copy()
 62    if "peaks" not in out.columns:
 63        return out
 64    mz_col = []
 65    intensity_col = []
 66    for peaks in out["peaks"]:
 67        if peaks is None or (isinstance(peaks, float) and np.isnan(peaks)):
 68            mz_col.append([])
 69            intensity_col.append([])
 70            continue
 71        arr = np.asarray(peaks, dtype=np.float64)
 72        if arr.size == 0:
 73            mz_col.append([])
 74            intensity_col.append([])
 75        elif arr.ndim == 2 and arr.shape[1] == 2:
 76            mz_col.append(arr[:, 0].tolist())
 77            intensity_col.append(arr[:, 1].tolist())
 78        else:
 79            raise ValueError("peaks entries must be array-like of shape (N, 2)")
 80    out["mz"] = mz_col
 81    out["intensity"] = intensity_col
 82    out = out.drop(columns=["peaks"])
 83    return out
 84
 85
 86def unpack_peaks_from_parquet(df: pd.DataFrame) -> pd.DataFrame:
 87    """Rebuild peaks (N, 2) arrays from mz/intensity list columns."""
 88    out = df.copy()
 89    if "mz" not in out.columns or "intensity" not in out.columns:
 90        return out
 91    peaks = []
 92    for mz, intensity in zip(out["mz"], out["intensity"]):
 93        mz_arr = np.asarray(mz if mz is not None else [], dtype=np.float64)
 94        i_arr = np.asarray(intensity if intensity is not None else [], dtype=np.float64)
 95        if mz_arr.size == 0:
 96            peaks.append(np.zeros((0, 2), dtype=np.float64))
 97        else:
 98            peaks.append(np.column_stack([mz_arr, i_arr]))
 99    out["peaks"] = peaks
100    out = out.drop(columns=["mz", "intensity"])
101    return out
102
103
104def read(cache_path) -> pd.DataFrame:
105    """Load a cached library DataFrame and restore peaks arrays."""
106    df = pd.read_parquet(cache_path)
107    return unpack_peaks_from_parquet(df)
108
109
110def write(
111    cache_path, df: pd.DataFrame, source_path, extra_meta: dict | None = None
112) -> None:
113    """Write standardized library DataFrame as a parquet sidecar."""
114    import pyarrow as pa
115    import pyarrow.parquet as pq
116
117    cache_path = Path(cache_path)
118    packed = pack_peaks_for_parquet(df)
119    table = pa.Table.from_pandas(packed, preserve_index=False)
120    meta = _source_fingerprint(source_path)
121    if extra_meta:
122        meta.update(extra_meta)
123    existing = table.schema.metadata or {}
124    existing = dict(existing)
125    existing[_META_KEY] = json.dumps(meta).encode("utf-8")
126    table = table.replace_schema_metadata(existing)
127    cache_path.parent.mkdir(parents=True, exist_ok=True)
128    pq.write_table(table, cache_path)
COREMS_LIB_CACHE_V = 1
CACHE_SUFFIX = '.corems-lib.parquet'
def default_cache_path(source_path) -> pathlib.Path:
18def default_cache_path(source_path) -> Path:
19    """Return default sidecar path next to the source library file."""
20    return Path(source_path).with_name(Path(source_path).name + CACHE_SUFFIX)

Return default sidecar path next to the source library file.

def is_valid(cache_path, source_path, extra_meta: dict | None = None) -> bool:
34def is_valid(cache_path, source_path, extra_meta: dict | None = None) -> bool:
35    """Return True if cache exists and matches source fingerprint + schema."""
36    cache_path = Path(cache_path)
37    source_path = Path(source_path)
38    if not cache_path.is_file() or not source_path.is_file():
39        return False
40    try:
41        import pyarrow.parquet as pq
42
43        meta = pq.read_schema(cache_path).metadata or {}
44        raw = meta.get(_META_KEY)
45        if raw is None:
46            return False
47        stored = json.loads(raw.decode("utf-8"))
48    except Exception:
49        return False
50
51    expected = _source_fingerprint(source_path)
52    if extra_meta:
53        expected.update(extra_meta)
54    for key, value in expected.items():
55        if stored.get(key) != value:
56            return False
57    return True

Return True if cache exists and matches source fingerprint + schema.

def pack_peaks_for_parquet(df: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
60def pack_peaks_for_parquet(df: pd.DataFrame) -> pd.DataFrame:
61    """Convert a peaks column of (N, 2) arrays into mz/intensity list columns."""
62    out = df.copy()
63    if "peaks" not in out.columns:
64        return out
65    mz_col = []
66    intensity_col = []
67    for peaks in out["peaks"]:
68        if peaks is None or (isinstance(peaks, float) and np.isnan(peaks)):
69            mz_col.append([])
70            intensity_col.append([])
71            continue
72        arr = np.asarray(peaks, dtype=np.float64)
73        if arr.size == 0:
74            mz_col.append([])
75            intensity_col.append([])
76        elif arr.ndim == 2 and arr.shape[1] == 2:
77            mz_col.append(arr[:, 0].tolist())
78            intensity_col.append(arr[:, 1].tolist())
79        else:
80            raise ValueError("peaks entries must be array-like of shape (N, 2)")
81    out["mz"] = mz_col
82    out["intensity"] = intensity_col
83    out = out.drop(columns=["peaks"])
84    return out

Convert a peaks column of (N, 2) arrays into mz/intensity list columns.

def unpack_peaks_from_parquet(df: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
 87def unpack_peaks_from_parquet(df: pd.DataFrame) -> pd.DataFrame:
 88    """Rebuild peaks (N, 2) arrays from mz/intensity list columns."""
 89    out = df.copy()
 90    if "mz" not in out.columns or "intensity" not in out.columns:
 91        return out
 92    peaks = []
 93    for mz, intensity in zip(out["mz"], out["intensity"]):
 94        mz_arr = np.asarray(mz if mz is not None else [], dtype=np.float64)
 95        i_arr = np.asarray(intensity if intensity is not None else [], dtype=np.float64)
 96        if mz_arr.size == 0:
 97            peaks.append(np.zeros((0, 2), dtype=np.float64))
 98        else:
 99            peaks.append(np.column_stack([mz_arr, i_arr]))
100    out["peaks"] = peaks
101    out = out.drop(columns=["mz", "intensity"])
102    return out

Rebuild peaks (N, 2) arrays from mz/intensity list columns.

def read(cache_path) -> pandas.core.frame.DataFrame:
105def read(cache_path) -> pd.DataFrame:
106    """Load a cached library DataFrame and restore peaks arrays."""
107    df = pd.read_parquet(cache_path)
108    return unpack_peaks_from_parquet(df)

Load a cached library DataFrame and restore peaks arrays.

def write( cache_path, df: pandas.core.frame.DataFrame, source_path, extra_meta: dict | None = None) -> None:
111def write(
112    cache_path, df: pd.DataFrame, source_path, extra_meta: dict | None = None
113) -> None:
114    """Write standardized library DataFrame as a parquet sidecar."""
115    import pyarrow as pa
116    import pyarrow.parquet as pq
117
118    cache_path = Path(cache_path)
119    packed = pack_peaks_for_parquet(df)
120    table = pa.Table.from_pandas(packed, preserve_index=False)
121    meta = _source_fingerprint(source_path)
122    if extra_meta:
123        meta.update(extra_meta)
124    existing = table.schema.metadata or {}
125    existing = dict(existing)
126    existing[_META_KEY] = json.dumps(meta).encode("utf-8")
127    table = table.replace_schema_metadata(existing)
128    cache_path.parent.mkdir(parents=True, exist_ok=True)
129    pq.write_table(table, cache_path)

Write standardized library DataFrame as a parquet sidecar.