from __future__ import annotations import json from pathlib import Path from typing import Iterable import numpy as np def save_embedding_table( path: str | Path, keys: list[str], embeddings: np.ndarray, model_id: str, settings: dict | None = None, ) -> None: if len(keys) != len(embeddings): raise ValueError("Keys and embeddings must have the same row count") destination = Path(path) destination.parent.mkdir(parents=True, exist_ok=True) np.savez_compressed( destination, keys=np.asarray(keys, dtype=str), embeddings=np.asarray(embeddings, dtype=np.float32), model_id=np.asarray(model_id), settings_json=np.asarray(json.dumps(settings or {})), ) def load_embedding_table(path: str | Path) -> tuple[dict[str, np.ndarray], str, dict]: source = Path(path) files = sorted(source.glob("*.npz")) if source.is_dir() else [source] if not files: raise FileNotFoundError(f"No embedding files found at {source}") mapping: dict[str, np.ndarray] = {} model_id = "" settings: dict = {} for file in files: table = np.load(file, allow_pickle=False) current_model = str(table["model_id"].item()) current_settings = ( json.loads(str(table["settings_json"].item())) if "settings_json" in table.files else {} ) if model_id and current_model != model_id: raise ValueError(f"Embedding model mismatch in {file}") if settings and current_settings != settings: raise ValueError(f"Embedding settings mismatch in {file}") model_id = current_model settings = current_settings mapping.update(zip(table["keys"].tolist(), table["embeddings"], strict=True)) return mapping, model_id, settings def build_embedding_features( proteins: Iterable[str], smiles_values: Iterable[str], protein_path: str | Path, molecule_path: str | Path, ) -> tuple[np.ndarray, dict[str, object]]: if not protein_path or not molecule_path: raise ValueError("Both ONNX embedding tables are required") protein_table, protein_model, protein_settings = load_embedding_table(protein_path) molecule_table, molecule_model, molecule_settings = load_embedding_table(molecule_path) try: protein_block = np.stack([protein_table[value] for value in proteins]) except KeyError as error: raise KeyError(f"Missing protein embedding: {str(error)[:80]}") from error try: molecule_block = np.stack([molecule_table[value] for value in smiles_values]) except KeyError as error: raise KeyError(f"Missing molecule embedding: {error}") from error return ( np.concatenate([protein_block, molecule_block], axis=1).astype(np.float32), { "feature_mode": "onnx_embeddings", "protein_model": protein_model, "protein_extraction": protein_settings, "molecule_model": molecule_model, "molecule_extraction": molecule_settings, }, )