SMART / smart_core.py
liupf's picture
Upload 23 files
40c9a4a verified
Raw History Blame Contribute Delete
7.93 kB
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
class AnalogRouter(nn.Module):
def __init__(self, dim, fp_dim=2048, hidden=64):
super().__init__()
self.dim = int(dim)
if self.dim < 2 or int(fp_dim) < 1 or int(hidden) < 1:
raise ValueError('Invalid spectrum/fingerprint/hidden dimension')
self.encoder = nn.Sequential(nn.Linear(int(fp_dim), int(hidden)), nn.SiLU())
self.context = nn.Sequential(nn.Linear(3 * int(hidden) + 4, int(hidden)), nn.SiLU())
self.router = nn.Sequential(nn.Linear(int(hidden) + 3, 32), nn.SiLU(), nn.Linear(32, 3))
self.register_buffer('mz', torch.arange(self.dim, dtype=torch.float32), persistent=False)
def forward(self, fpq, fpr, ref_l1, massq, massr, sim, mode='transport', return_valid=False):
if mode not in ('transport', 'reweight'):
raise ValueError('Unknown router mode: ' + str(mode))
if ref_l1.ndim != 2 or ref_l1.shape[1] != self.dim:
raise ValueError('ref_l1 must have shape [batch, spectrum dimension]')
batch = ref_l1.shape[0]
massq, massr, sim = (massq.reshape(batch), massr.reshape(batch), sim.reshape(batch))
eq, er = (self.encoder(fpq), self.encoder(fpr))
globals_ = torch.stack((sim, massq / self.dim, massr / self.dim, (massq - massr) / self.dim), dim=1)
context = self.context(torch.cat((eq, er, eq - er, globals_), dim=1))
mz = self.mz.to(dtype=ref_l1.dtype).expand(batch, -1)
ref = ref_l1.clamp_min(0)
ref = ref / ref.sum(1, keepdim=True).clamp_min(1e-12)
peak_features = torch.stack((mz / self.dim, (massr[:, None] - mz) / self.dim, torch.sqrt(ref.clamp_min(0))), dim=2)
logits = self.router(torch.cat((context[:, None, :].expand(-1, self.dim, -1), peak_features), dim=2))
log_weights = F.log_softmax(logits, dim=2)
original = self.mz.to(torch.long).expand(batch, -1)
delta = torch.round(massq).to(torch.long) - torch.round(massr).to(torch.long)
shifted = original + delta[:, None] if mode == 'transport' else original
upper = torch.minimum(torch.round(massq).to(torch.long) + 3, torch.full_like(delta, self.dim - 1))
source_valid = original >= 1
copy_valid = source_valid & (original <= upper[:, None])
shift_valid = source_valid & (shifted >= 1) & (shifted <= upper[:, None]) & (shifted < self.dim)
positive = ref > 0
safe_ref = torch.where(positive, ref, torch.ones_like(ref))
log_ref = torch.log(safe_ref)
retained = torch.cat((copy_valid & positive, shift_valid & positive), dim=1)
log_mass = torch.cat((log_ref + log_weights[:, :, 0], log_ref + log_weights[:, :, 1]), dim=1)
log_mass = log_mass.masked_fill(~retained, -float('inf'))
valid = retained.any(dim=1)
safe_log_mass = torch.where(valid[:, None], log_mass, torch.zeros_like(log_mass))
centered_log_mass = safe_log_mass - safe_log_mass.amax(dim=1, keepdim=True)
log_total = torch.logsumexp(centered_log_mass, dim=1, keepdim=True)
normalized_mass = torch.where(retained, torch.exp(centered_log_mass - log_total), torch.zeros_like(safe_log_mass))
copy_mass, shift_mass = normalized_mass.split(self.dim, dim=1)
output = torch.zeros_like(ref)
output.scatter_add_(1, original, copy_mass)
output.scatter_add_(1, shifted.clamp(0, self.dim - 1), shift_mass)
return (output, valid) if return_valid else output
def _router_data(data):
required = ('features', 'masses', 'spectra', 'neighbors', 'similarities')
missing = [key for key in required if key not in data]
if missing:
raise ValueError('Missing router data fields: ' + ', '.join(missing))
features = np.asarray(data['features'], dtype=np.float32)
masses = np.asarray(data['masses'], dtype=np.float32)
spectra = np.asarray(data['spectra'], dtype=np.float32)
neighbors = np.asarray(data['neighbors'], dtype=np.int64)
similarities = np.asarray(data['similarities'], dtype=np.float32)
n = len(features)
if features.ndim != 2 or features.shape[1] < 1 or masses.shape != (n,):
raise ValueError('Invalid fingerprint/mass shapes')
if spectra.ndim != 2 or spectra.shape[0] != n or spectra.shape[1] < 2:
raise ValueError('Invalid spectrum shape')
if neighbors.ndim != 2 or neighbors.shape[0] != n or similarities.shape != neighbors.shape:
raise ValueError('Invalid neighbour/similarity shapes')
if not all((np.isfinite(x).all() for x in (features, masses, spectra, similarities))):
raise ValueError('Router data contain non-finite values')
if (spectra < 0).any() or (masses <= 0).any():
raise ValueError('Spectra must be nonnegative and masses positive')
if (neighbors < -1).any() or (neighbors >= n).any():
raise ValueError('Neighbour indices must be -1 or valid molecular indices')
if (similarities < 0).any() or (similarities > 1 + 1e-06).any():
raise ValueError('Similarities must lie in [0, 1]')
return dict(features=features, masses=masses, spectra=spectra, neighbors=neighbors, similarities=similarities)
def _router_indices(indices, n, label):
x = np.asarray(indices, dtype=np.int64).reshape(-1)
if (x < 0).any() or (x >= n).any() or len(np.unique(x)) != len(x):
raise ValueError(label + ' must contain distinct valid molecular indices')
return x
def _router_eligible(data, indices, min_similarity):
refs = data['neighbors'][indices]
sims = data['similarities'][indices]
eligible = (refs >= 0) & (sims >= float(min_similarity))
eligible &= refs != indices[:, None]
safe = np.maximum(refs, 0)
eligible &= data['spectra'][safe].sum(2) > 1e-12
return eligible
def _router_forward_pairs(model, data, queries, refs, sims, device, mode):
tensor = lambda x: torch.as_tensor(x, dtype=torch.float32, device=device)
return model(tensor(data['features'][queries]), tensor(data['features'][refs]), tensor(data['spectra'][refs]), tensor(data['masses'][queries]), tensor(data['masses'][refs]), tensor(sims), mode=mode, return_valid=True)
def predict_router(model, data, query_indices, device='cpu', mode='transport', batch_size=64, min_similarity=0.35):
data = _router_data(data)
queries = _router_indices(query_indices, len(data['features']), 'query_indices')
if int(batch_size) < 1:
raise ValueError('batch_size must be positive')
eligible = _router_eligible(data, queries, min_similarity)
where_q, where_k = np.nonzero(eligible)
result = np.zeros((len(queries), data['spectra'].shape[1]), dtype=np.float32)
weight_sum = np.zeros(len(queries), dtype=np.float64)
previous_training = model.training
model.to(device)
model.eval()
try:
with torch.no_grad():
for start in range(0, len(where_q), int(batch_size)):
positions = where_q[start:start + int(batch_size)]
columns = where_k[start:start + int(batch_size)]
q = queries[positions]
r = data['neighbors'][q, columns]
sims = data['similarities'][q, columns]
p, valid = _router_forward_pairs(model, data, q, r, sims, device, mode)
p = p.cpu().numpy()
weights = np.power(sims.astype(np.float64), 4) * valid.cpu().numpy()
np.add.at(result, positions, p * weights[:, None].astype(np.float32))
np.add.at(weight_sum, positions, weights)
finally:
model.train(previous_training)
covered = weight_sum > 1e-12
result[covered] /= weight_sum[covered, None].astype(np.float32)
totals = result.sum(1, dtype=np.float64)
covered &= totals > 1e-12
result[covered] /= totals[covered, None].astype(np.float32)
result[~covered] = 0
return (result, covered)