spectra_utils.py
AI for Chemistry/code/chemprop_VP-main (1)/chemprop_VP-main/chemprop/spectra_utils.py
from typing import List
import csv
from tqdm import trange
import torch
import numpy as np
def sid_loss(model_spectra: torch.tensor, target_spectra: torch.tensor, mask: torch.tensor, threshold: float = None) -> torch.tensor:
"""
Loss function for use with spectra data type.
:param model_spectra: The predicted spectra output from a model with shape (batch_size,spectrum_length).
:param target_spectra: The target spectra with shape (batch_size,spectrum_length). Values must be normalized so that each spectrum sums to 1.
:param mask: Tensor with boolian indications of where the spectrum output should not be excluded with shape (batch_size,spectrum_length).
:param threshold: Loss function requires that values are positive and nonzero. Values below the threshold will be replaced with the threshold value.
:return: A tensor containing loss values for the batch with shape (batch_size,spectrum_length).
"""
# Move new tensors to torch device
torch_device = model_spectra.device
# Normalize the model spectra before comparison
zero_sub = torch.zeros_like(model_spectra, device=torch_device)
one_sub = torch.ones_like(model_spectra, device=torch_device)
if threshold is not None:
threshold_sub = torch.full(model_spectra.shape,threshold, device=torch_device)
model_spectra = torch.where(model_spectra < threshold, threshold_sub, model_spectra)
model_spectra = torch.where(mask, model_spectra, zero_sub)
sum_model_spectra = torch.sum(model_spectra, axis=1, keepdim=True)
model_spectra = torch.div(model_spectra, sum_model_spectra)
# Calculate loss value
target_spectra = torch.where(mask, target_spectra, one_sub)
model_spectra = torch.where(mask, model_spectra, one_sub) # losses in excluded regions will be zero because log(1/1) = 0.
loss = torch.mul(torch.log(torch.div(model_spectra, target_spectra)), model_spectra) \
+ torch.mul(torch.log(torch.div(target_spectra, model_spectra)), target_spectra)
loss = loss.to(torch_device)
return loss
def sid_metric(model_spectra: List[List[float]], target_spectra: List[List[float]], threshold: float = None, batch_size: int = 50) -> float:
"""
Metric function for use with spectra data type.
:param model_spectra: The predicted spectra output from a model with shape (num_data, spectrum_length).
:param target_spectra: The target spectra with shape (num_data, spectrum_length). Values must be normalized so that each spectrum sums to 1.
Excluded values in target spectra will have a value of None.
:param threshold: Function requires that values are positive and nonzero. Values below the threshold will be replaced with the threshold value.
:param batch_size: Batch size for calculating metric.
:return: The average SID value for the predicted spectra.
"""
losses = []
num_iters, iter_step = len(model_spectra), batch_size
for i in trange(0, num_iters, iter_step):
# Create batches
batch_preds = model_spectra[i:i + iter_step]
batch_preds = np.array(batch_preds)
batch_targets = target_spectra[i:i + iter_step]
batch_mask = np.array([[x is not None for x in b] for b in batch_targets])
batch_targets = np.array([[1 if x is None else x for x in b] for b in batch_targets])
# Normalize the model spectra before comparison
if threshold is not None:
batch_preds[batch_preds < threshold] = threshold
batch_preds[~batch_mask] = 0
sum_preds = np.sum(batch_preds, axis=1, keepdims=True)
batch_preds = batch_preds / sum_preds
# Calculate loss value
batch_preds[~batch_mask] = 1 # losses in excluded regions will be zero because log(1/1) = 0.
loss = batch_preds * np.log(batch_preds / batch_targets) + batch_targets * np.log(batch_targets / batch_preds)
loss = np.sum(loss, axis=1)
# Gather batches
loss = loss.tolist()
losses.extend(loss)
loss = np.mean(loss)
return loss
def wasserstein_loss(model_spectra: torch.tensor, target_spectra: torch.tensor, mask: torch.tensor, threshold: float = None) -> torch.tensor:
"""
Loss function for use with spectra data type. This loss assumes that values are evenly spaced.
:param model_spectra: The predicted spectra output from a model with shape (batch_size,spectrum_length).
:param target_spectra: The target spectra with shape (batch_size,spectrum_length). Values must be normalized so that each spectrum sums to 1.
:param mask: Tensor with boolian indications of where the spectrum output should not be excluded with shape (batch_size,spectrum_length).
:param threshold: Loss function requires that values are positive and nonzero. Values below the threshold will be replaced with the threshold value.
:return: A tensor containing loss values for the batch with shape (batch_size,spectrum_length).
"""
# Move new tensors to torch device
torch_device = model_spectra.device
# Normalize the model spectra before comparison
zero_sub = torch.zeros_like(model_spectra, device=torch_device)
if threshold is not None:
threshold_sub = torch.full(model_spectra.shape,threshold, device=torch_device)
model_spectra = torch.where(model_spectra < threshold, threshold_sub, model_spectra)
model_spectra = torch.where(mask, model_spectra, zero_sub)
sum_model_spectra = torch.sum(model_spectra, axis=1, keepdim=True)
model_spectra = torch.div(model_spectra, sum_model_spectra)
# Calculate loss value
target_cum = torch.cumsum(target_spectra,axis=1).to(torch_device)
model_cum = torch.cumsum(model_spectra,axis=1).to(torch_device)
loss = torch.abs(target_cum - model_cum)
loss = loss.to(torch_device)
return loss
def wasserstein_metric(model_spectra: List[List[float]], target_spectra: List[List[float]], threshold: float = None, batch_size: int = 50) -> float:
"""
Metric function for use with spectra data type. This metric assumes that values are evenly spaced.
:param model_spectra: The predicted spectra output from a model with shape (num_data, spectrum_length).
:param target_spectra: The target spectra with shape (num_data, spectrum_length). Values must be normalized so that each spectrum sums to 1.
Excluded values in target spectra will have value None.
:param threshold: Function requires that values are positive and nonzero. Values below the threshold will be replaced with the threshold value.
:param batch_size: Batch size for calculating metric.
:return: The average wasserstein loss value for the predicted spectra.
"""
losses = []
num_iters, iter_step = len(model_spectra), batch_size
for i in trange(0, num_iters, iter_step):
# Create batches
batch_preds = model_spectra[i:i + iter_step]
batch_preds = np.array(batch_preds)
batch_targets = target_spectra[i:i + iter_step]
batch_mask = np.array([[x is not None for x in b] for b in batch_targets])
batch_targets = np.array([[0 if x is None else x for x in b] for b in batch_targets])
# Normalize the model spectra before comparison
if threshold is not None:
batch_preds[batch_preds < threshold] = threshold
batch_preds[~batch_mask] = 0
sum_preds = np.sum(batch_preds, axis=1, keepdims=True)
batch_preds = batch_preds / sum_preds
# Calculate loss value
target_cum = np.cumsum(batch_targets,axis=1)
preds_cum = np.cumsum(batch_preds,axis=1)
loss = np.abs(target_cum - preds_cum)
loss = np.sum(loss, axis=1)
# Gather batches
loss = loss.tolist()
losses.extend(loss)
loss = np.mean(loss)
return loss
def normalize_spectra(spectra: List[List[float]], phase_features: List[List[float]] = None, phase_mask: List[List[float]] = None, batch_size: int = 50, excluded_sub_value: float = None, threshold: float = None) -> List[List[float]]:
"""
Function takes in spectra and normalize them to sum values to 1. If provided with phase mask information, will remove excluded spectrum regions.
:param spectra: Input spectra with shape (num_spectra, spectrum_length).
:param phase_features: The collection phase of spectrum with shape (num_spectra, num_phases).
:param phase_mask: A mask array showing where in each phase feature to include in predictions and training with shape (num_phases, spectrum_length)
:param batch_size: The size of batches to carry out the normalization operation in.
:param exlcuded_sub_value: Excluded values are replaced with this object, usually None or nan.
:param threshold: Spectra values below threshold are replaced with threshold to remove negative or zero values.
:return: List form array of spectra with shape (num_spectra, spectrum length) with exlcuded values converted to nan.
"""
normalized_spectra = []
phase_exclusion = phase_mask is not None and phase_features is not None
if phase_exclusion:
phase_mask = np.array(phase_mask)
num_iters, iter_step = len(spectra), batch_size
for i in trange(0, num_iters, iter_step):
# prepare batch
batch_spectra = spectra[i:i + iter_step]
batch_mask = np.array([[x is not None for x in b] for b in batch_spectra])
batch_spectra = np.array([[0 if x is None else x for x in b] for b in batch_spectra])
if phase_exclusion:
batch_phases = phase_features[i:i + iter_step]
batch_phases = np.array(batch_phases)
# exclude mask and apply threshold
if threshold is not None:
batch_spectra[batch_spectra < threshold] = threshold
if phase_exclusion:
batch_phase_mask = np.matmul(batch_phases, phase_mask).astype('bool')
batch_mask = ~(~batch_mask + ~batch_phase_mask) # mask shows True only if both components true
batch_spectra[~batch_mask] = 0
# normalize to sum to 1
sum_spectra = np.sum(batch_spectra, axis=1, keepdims=True)
batch_spectra = batch_spectra / sum_spectra
# Collect vectors and revert excluded values to None
batch_spectra = batch_spectra.astype('object')
batch_spectra[~batch_mask] = excluded_sub_value
batch_spectra = batch_spectra.tolist()
normalized_spectra.extend(batch_spectra)
return normalized_spectra
def roundrobin_sid(spectra: np.ndarray, threshold: float = None) -> List[float]:
"""
Takes a block of input spectra and makes a pairwise comparison between each of the input spectra for a given molecule,
returning a list of the spectral informations divergences. To be used evaluating the variation between an ensemble of model spectrum predictions.
:spectra: A 3D array containing each of the spectra to be compared. Shape of (num_spectra, spectrum_length, ensemble_size)
:threshold: SID calculation requires positive values in each position, this value is used to replace any zero or negative values.
:return: A list of average pairwise SID len (num_spectra)
"""
ensemble_size=spectra.shape[2]
spectrum_size=spectra.shape[1]
ensemble_sids=[]
for i in range(len(spectra)):
spectrum = spectra[i]
nan_mask=np.isnan(spectrum[:,0])
if threshold is not None:
spectrum[spectrum < threshold] = threshold
spectrum[nan_mask,:]=1
ensemble_head = np.zeros([spectrum_size,0])
ensemble_tail = np.zeros([spectrum_size,0])
for j in range(ensemble_size-1):
ensemble_tail = np.concatenate((ensemble_tail,spectrum[:,j+1:]),axis=1)
ensemble_head = np.concatenate((ensemble_head,spectrum[:,:-j-1]),axis=1)
loss = ensemble_head * np.log(ensemble_head / ensemble_tail) + ensemble_tail * np.log(ensemble_tail / ensemble_head)
loss[nan_mask,:]=0
loss = np.sum(loss,axis=0)
loss = np.mean(loss)
ensemble_sids.append(loss)
return ensemble_sids
def load_phase_mask(path: str) -> List[List[int]]:
"""
Loads in a matrix used to mark sections of spectra as untrainable due to interference caused by particular phases.
Ignore those spectra regions in training and prediciton.
:param path: Path to a csv file containing the phase mask in shape (num_phases, spectrum_length) with 1s indicating inclusion and 0s indicating exclusion.
:return: A list form array of the phase mask.
"""
if path is None:
return None
data = []
with open(path,'r') as rf:
r=csv.reader(rf)
next(r)
for line in r:
if any([x not in ['0','1'] for x in line[1:]]):
raise ValueError('Phase mask must contain only 0s and 1s, with 0s indicating exclusion regions.')
data_line = [int(x) for x in line[1:]]
data.append(data_line)
return data
Articles liés
cosmo_to_s_profile_ver_1.1.1.py
cosmo_to_s_profile_ver_1.1.1.py — python source code from the AI for Chemistry learning materials (AI for Chemistry/2025_COSMO/data/s-profiles-all/List-9/cosmo_to_s_profile_ver_1.1.1.py).
Lire l'article →area.py
area.py — python source code from the AI for Chemistry learning materials (AI for Chemistry/2025_COSMO/data/s-profiles-all/area_volume-org/area.py).
Lire l'article →area_volume.py
area_volume.py — python source code from the AI for Chemistry learning materials (AI for Chemistry/2025_COSMO/data/s-profiles-all/area_volume-org/area_volume.py).
Lire l'article →volume.py
volume.py — python source code from the AI for Chemistry learning materials (AI for Chemistry/2025_COSMO/data/s-profiles-all/area_volume-org/volume.py).
Lire l'article →check_final_CID.py
check_final_CID.py — python source code from the AI for Chemistry learning materials (AI for Chemistry/2025_COSMO/data/s-profiles-all/check/check_final_CID.py).
Lire l'article →check_for_CID.py
check_for_CID.py — python source code from the AI for Chemistry learning materials (AI for Chemistry/2025_COSMO/data/s-profiles-all/check/check_for_CID.py).
Lire l'article →