diff --git a/pes_to_spec/bnn.py b/pes_to_spec/bnn.py index 887512e093e555bb65bef8442585eb0c84801146..5f8832ee8448f975f21851ef514c934b581aae62 100644 --- a/pes_to_spec/bnn.py +++ b/pes_to_spec/bnn.py @@ -2,13 +2,259 @@ from sklearn.base import BaseEstimator, RegressorMixin from typing import Any, Dict, Optional, Union, Tuple import numpy as np +import math from scipy.special import gamma import torch import torch.nn as nn -import torchbnn as bnn +import torch.nn.functional as F from torch.utils.data import TensorDataset, DataLoader +class BayesLinearEmpiricalPrior(nn.Module): + """ + Applies Bayesian Linear + + Args: + prior_mu (Float): mean of prior normal distribution. + prior_sigma (Float): sigma of prior normal distribution. + + """ + __constants__ = ['prior_mu', 'prior_sigma', 'bias', 'in_features', 'out_features'] + + def __init__(self, prior_mu, prior_sigma, in_features, out_features, bias=True): + super(BayesLinearEmpiricalPrior, self).__init__() + self.in_features = in_features + self.out_features = out_features + + self.prior_mu = prior_mu + self.prior_sigma = prior_sigma + + self.prior_log_sigma_w = nn.Parameter(torch.ones((out_features, in_features))*np.log(prior_sigma)) + self.prior_log_sigma_b = nn.Parameter(torch.ones((out_features,))*np.log(prior_sigma)) + + self.weight_mu = nn.Parameter(torch.Tensor(out_features, in_features)) + self.weight_log_sigma = nn.Parameter(torch.Tensor(out_features, in_features)) + self.register_buffer('weight_eps', None) + + if bias is None or bias is False : + self.bias = False + else : + self.bias = True + + if self.bias: + self.bias_mu = nn.Parameter(torch.Tensor(out_features)) + self.bias_log_sigma = nn.Parameter(torch.Tensor(out_features)) + self.register_buffer('bias_eps', None) + else: + self.register_parameter('bias_mu', None) + self.register_parameter('bias_log_sigma', None) + self.register_buffer('bias_eps', None) + + self.reset_parameters() + + def reset_parameters(self): + stdv = 1. / np.sqrt(self.weight_mu.size(1)) + self.weight_mu.data.uniform_(-stdv, stdv) + self.weight_log_sigma.data.fill_(np.log(self.prior_sigma)) + if self.bias : + self.bias_mu.data.uniform_(-stdv, stdv) + self.bias_log_sigma.data.fill_(np.log(self.prior_sigma)) + + def freeze(self) : + self.weight_eps = torch.randn_like(self.weight_log_sigma) + if self.bias : + self.bias_eps = torch.randn_like(self.bias_log_sigma) + + def unfreeze(self) : + self.weight_eps = None + if self.bias : + self.bias_eps = None + + def forward(self, input): + r""" + Overriden. + """ + if self.weight_eps is None : + weight = self.weight_mu + torch.exp(self.weight_log_sigma) * torch.randn_like(self.weight_log_sigma) + else : + weight = self.weight_mu + torch.exp(self.weight_log_sigma) * self.weight_eps + + if self.bias: + if self.bias_eps is None : + bias = self.bias_mu + torch.exp(self.bias_log_sigma) * torch.randn_like(self.bias_log_sigma) + else : + bias = self.bias_mu + torch.exp(self.bias_log_sigma) * self.bias_eps + else : + bias = None + + return F.linear(input, weight, bias) + + def extra_repr(self): + r""" + Overriden. + """ + return 'prior_mu={}, prior_sigma={}, in_features={}, out_features={}, bias={}'.format(self.prior_mu, self.prior_sigma, self.in_features, self.out_features, self.bias is not None) + +class BayesLinear(nn.Module): + r""" + Applies Bayesian Linear + + Arguments: + prior_mu (Float): mean of prior normal distribution. + prior_sigma (Float): sigma of prior normal distribution. + + """ + __constants__ = ['prior_mu', 'bias', 'in_features', 'out_features'] + + def __init__(self, prior_mu, prior_sigma, in_features, out_features, bias=True): + super(BayesLinear, self).__init__() + self.in_features = in_features + self.out_features = out_features + + self.prior_mu = prior_mu + self.prior_log_sigma = nn.Parameter(torch.ones(1)*np.log(prior_sigma), requires_grad=True) + + self.weight_mu = nn.Parameter(torch.Tensor(out_features, in_features)) + self.weight_log_sigma = nn.Parameter(torch.Tensor(out_features, in_features)) + self.register_buffer('weight_eps', None) + + if bias is None or bias is False: + self.bias = False + else : + self.bias = True + + if self.bias: + self.bias_mu = nn.Parameter(torch.Tensor(out_features)) + self.bias_log_sigma = nn.Parameter(torch.Tensor(out_features)) + self.register_buffer('bias_eps', None) + else: + self.register_parameter('bias_mu', None) + self.register_parameter('bias_log_sigma', None) + self.register_buffer('bias_eps', None) + + self.reset_parameters() + + def reset_parameters(self): + stdv = 1. / np.sqrt(self.weight_mu.size(1)) + self.weight_mu.data.uniform_(-stdv, stdv) + self.weight_log_sigma.data.fill_(self.prior_log_sigma.detach()[0]) + if self.bias : + self.bias_mu.data.uniform_(-stdv, stdv) + self.bias_log_sigma.data.fill_(self.prior_log_sigma.detach()[0]) + + def freeze(self) : + self.weight_eps = torch.randn_like(self.weight_log_sigma) + if self.bias : + self.bias_eps = torch.randn_like(self.bias_log_sigma) + + def unfreeze(self) : + self.weight_eps = None + if self.bias: + self.bias_eps = None + + def forward(self, input): + r""" + Overriden. + """ + if self.weight_eps is None : + weight = self.weight_mu + torch.exp(self.weight_log_sigma) * torch.randn_like(self.weight_log_sigma) + else : + weight = self.weight_mu + torch.exp(self.weight_log_sigma) * self.weight_eps + + if self.bias: + if self.bias_eps is None : + bias = self.bias_mu + torch.exp(self.bias_log_sigma) * torch.randn_like(self.bias_log_sigma) + else : + bias = self.bias_mu + torch.exp(self.bias_log_sigma) * self.bias_eps + else : + bias = None + + return F.linear(input, weight, bias) + + def extra_repr(self): + r""" + Overriden. + """ + return 'prior_mu={}, prior_sigma={}, in_features={}, out_features={}, bias={}'.format(self.prior_mu, self.prior_sigma, self.in_features, self.out_features, self.bias is not None) + +def _kl_loss(mu_0, log_sigma_0, mu_1, log_sigma_1) : + """ + An method for calculating KL divergence between two Normal distribtuion. + + Arguments: + mu_0 (Float) : mean of normal distribution. + log_sigma_0 (Float): log(standard deviation of normal distribution). + mu_1 (Float): mean of normal distribution. + log_sigma_1 (Float): log(standard deviation of normal distribution). + + """ + if isinstance(log_sigma_1, float): + sigma_1 = np.exp(log_sigma_1) + else: + sigma_1 = torch.exp(log_sigma_1) + kl = log_sigma_1 - log_sigma_0 + \ + (torch.exp(log_sigma_0)**2 + (mu_0-mu_1)**2)/(2*sigma_1**2) - 0.5 + return kl.sum() + +class BKLLoss(nn.Module): + """ + Loss for calculating KL divergence of baysian neural network model. + + Args: + reduction (string, optional): Specifies the reduction to apply to the output: + ``'mean'``: the sum of the output will be divided by the number of + elements of the output. + ``'sum'``: the output will be summed. + last_layer_only (Bool): True for return only the last layer's KL divergence. + """ + __constants__ = ['reduction'] + + def __init__(self, reduction='mean', last_layer_only=False): + super(BKLLoss, self).__init__() + self.last_layer_only = last_layer_only + self.reduction = reduction + + def forward(self, model): + """ + Args: + model (nn.Module): a model to be calculated for KL-divergence. + """ + #return bayesian_kl_loss(model, reduction=self.reduction, last_layer_only=self.last_layer_only) + device = torch.device("cuda" if next(model.parameters()).is_cuda else "cpu") + kl = torch.Tensor([0]).to(device) + kl_sum = torch.Tensor([0]).to(device) + n = torch.Tensor([0]).to(device) + + for m in model.modules() : + if isinstance(m, (BayesLinearEmpiricalPrior)): + kl = _kl_loss(m.weight_mu, m.weight_log_sigma, m.prior_mu, m.prior_log_sigma_w) + kl_sum += kl + n += len(m.weight_mu.view(-1)) + + if m.bias : + kl = _kl_loss(m.bias_mu, m.bias_log_sigma, m.prior_mu, m.prior_log_sigma_b) + kl_sum += kl + n += len(m.bias_mu.view(-1)) + if isinstance(m, (BayesLinear)): + kl = _kl_loss(m.weight_mu, m.weight_log_sigma, m.prior_mu, m.prior_log_sigma) + kl_sum += kl + n += len(m.weight_mu.view(-1)) + + if m.bias : + kl = _kl_loss(m.bias_mu, m.bias_log_sigma, m.prior_mu, m.prior_log_sigma) + kl_sum += kl + n += len(m.bias_mu.view(-1)) + + if self.last_layer_only or n == 0 : + return kl + + if self.reduction == 'mean': + return kl_sum/n + elif self.reduction == 'sum': + return kl_sum + else: + raise ValueError(f"{self.reduction} is not valid") + class AverageMeter(object): """Computes and stores the average and current value""" def __init__(self, name, fmt=':f'): @@ -49,6 +295,7 @@ class ProgressMeter(object): return '[' + fmt + '/' + fmt.format(num_batches) + ']' + class BNN(nn.Module): """ A model Bayesian Neural network. @@ -61,13 +308,13 @@ class BNN(nn.Module): between the prediction and the true value. The standard deviation of the Gaussian is left as a parameter to be fit: sigma. """ - def __init__(self, input_dimension: int=1, output_dimension: int=1): + def __init__(self, input_dimension: int=1, output_dimension: int=1, rvm: bool=False): super(BNN, self).__init__() hidden_dimension = 50 # controls the aleatoric uncertainty self.log_isigma2 = nn.Parameter(-torch.ones(1, output_dimension)*np.log(0.1**2), requires_grad=True) # controls the weight hyperprior - self.log_ilambda2 = nn.Parameter(-torch.ones(1)*np.log(0.1**2), requires_grad=True) + self.log_ilambda2 = -np.log(0.1**2) # inverse Gamma hyper prior alpha and beta # @@ -82,8 +329,8 @@ class BNN(nn.Module): # and the only regularization is to prevent the weights from becoming > 18 + 3 sqrt(var) ~= 50, making this a very loose regularization. # An alternative would be to set the (alpha, beta) both to very low values, whichmakes the hyper prior become closer to the non-informative Jeffrey's prior. # Using this alternative (ie: (0.1, 0.1) for the weights' hyper prior) leads to very large lambda and numerical issues with the fit. - self.alpha_lambda = 0.001 - self.beta_lambda = 0.001 + self.alpha_lambda = 0.0001 + self.beta_lambda = 0.0001 # Hyperprior choice on the likelihood noise level: # The likelihood noise level is controlled by sigma in the likelihood and it should be allowed to be very broad, but different @@ -92,20 +339,45 @@ class BNN(nn.Module): # Making both alpha and beta small makes the gamma distribution closer to the Jeffey's prior, which makes it non-informative # This seems to lead to a larger training time, though. # Since, after standardization, we know to expect the variance to be of order (1), we can select also alpha and beta leading to high variance in this range - self.alpha_sigma = 0.001 - self.beta_sigma = 0.001 - - self.model = nn.Sequential( - bnn.BayesLinear(prior_mu=0.0, - prior_sigma=torch.exp(-0.5*self.log_ilambda2), + self.alpha_sigma = 0.0001 + self.beta_sigma = 0.0001 + + if rvm: + self.model = nn.Sequential( + BayesLinearEmpiricalPrior(prior_mu=0.0, + prior_sigma=np.exp(-0.5*self.log_ilambda2), + in_features=input_dimension, + out_features=hidden_dimension), + nn.ReLU(), + BayesLinearEmpiricalPrior(prior_mu=0.0, + prior_sigma=np.exp(-0.5*self.log_ilambda2), + in_features=hidden_dimension, + out_features=output_dimension) + ) + else: + self.model = nn.Sequential( + BayesLinear(prior_mu=0.0, + prior_sigma=np.exp(-0.5*self.log_ilambda2), in_features=input_dimension, out_features=hidden_dimension), - nn.ReLU(), - bnn.BayesLinear(prior_mu=0.0, - prior_sigma=torch.exp(-0.5*self.log_ilambda2), + nn.ReLU(), + BayesLinear(prior_mu=0.0, + prior_sigma=np.exp(-0.5*self.log_ilambda2), in_features=hidden_dimension, out_features=output_dimension) - ) + ) + self.rvm = rvm + + def prune(self): + """Prune weights.""" + with torch.no_grad(): + for layer in self.model.modules(): + if isinstance(layer, BayesLinearEmpiricalPrior): + log_isigma2 = -2.0*layer.prior_log_sigma_w + isigma2 = torch.exp(log_isigma2) + keep = isigma2 < 1e4 + layer.weight_mu[~keep] *= 0.0 + layer.weight_log_sigma[~keep] = -12.0 def forward(self, x: torch.Tensor) -> torch.Tensor: """ @@ -138,10 +410,21 @@ class BNN(nn.Module): # with a standardized input, this hyperprior forces sigma to be # on avg. 1 and it is broad enough to allow for different sigma isigma2 = torch.exp(self.log_isigma2) - neg_log_hyperprior_noise = self.neg_log_gamma(self.log_isigma2, isigma2, self.alpha_sigma, self.beta_sigma) - ilambda2 = torch.exp(self.log_ilambda2) - neg_log_hyperprior_weights = self.neg_log_gamma(self.log_ilambda2, ilambda2, self.alpha_lambda, self.beta_lambda) - return neg_log_hyperprior_noise.sum() + neg_log_hyperprior_weights.sum() + neg_log_hyperprior_noise = self.neg_log_gamma(self.log_isigma2, isigma2, self.alpha_sigma, self.beta_sigma).sum() + if self.rvm: + log_ilambda2 = [-2.0*self.model[0].prior_log_sigma_w, + -2.0*self.model[2].prior_log_sigma_w, + -2.0*self.model[0].prior_log_sigma_b, + -2.0*self.model[2].prior_log_sigma_b + ] + else: + log_ilambda2 = [-2.0*self.model[0].prior_log_sigma, + -2.0*self.model[2].prior_log_sigma, + ] + ilambda2 = [torch.exp(k) for k in log_ilambda2] + neg_log_hyperprior_weights = sum(self.neg_log_gamma(log_k, k, self.alpha_lambda, self.beta_lambda).sum() + for log_k, k in zip(log_ilambda2, ilambda2)) + return neg_log_hyperprior_noise + neg_log_hyperprior_weights def aleatoric_uncertainty(self) -> torch.Tensor: """ @@ -154,7 +437,18 @@ class BNN(nn.Module): """ Get the weights precision. """ - return torch.exp(self.log_ilambda2[0]) + if self.rvm: + log_ilambda2 = [-2.0*self.model[0].prior_log_sigma_w, + -2.0*self.model[2].prior_log_sigma_w, + -2.0*self.model[0].prior_log_sigma_b, + -2.0*self.model[2].prior_log_sigma_b + ] + else: + log_ilambda2 = [-2.0*self.model[0].prior_log_sigma, + -2.0*self.model[2].prior_log_sigma, + ] + ilambda2 = [torch.exp(k) for k in log_ilambda2] + return sum(k.mean() for k in ilambda2)/len(ilambda2) class BNNModel(RegressorMixin, BaseEstimator): """ @@ -162,14 +456,16 @@ class BNNModel(RegressorMixin, BaseEstimator): Args: """ - def __init__(self, state_dict=None): + def __init__(self, state_dict=None, rvm: bool=False, n_epochs: int=250): if state_dict is not None: Nx = state_dict["model.0.weight_mu"].shape[1] Ny = state_dict["model.2.weight_mu"].shape[0] - self.model = BNN(Nx, Ny) + self.model = BNN(Nx, Ny, rvm=rvm) self.model.load_state_dict(state_dict) else: - self.model = BNN() + self.model = BNN(rvm=rvm) + self.rvm = rvm + self.n_epochs = n_epochs self.model.eval() def state_dict(self) -> Dict[str, Any]: @@ -197,7 +493,7 @@ class BNNModel(RegressorMixin, BaseEstimator): torch.from_numpy(weights)) # create model - self.model = BNN(X.shape[1], y.shape[1]) + self.model = BNN(X.shape[1], y.shape[1], rvm=self.rvm) # prepare data loader B = 50 @@ -218,12 +514,11 @@ class BNNModel(RegressorMixin, BaseEstimator): weight_prior /= float(B) # KL loss - kl_loss = bnn.BKLLoss(reduction='sum', last_layer_only=False) + kl_loss = BKLLoss(reduction='sum', last_layer_only=False) # train self.model.train() - epochs = 500 - for epoch in range(epochs): + for epoch in range(self.n_epochs): meter = {k: AverageMeter(k, ':6.3f') for k in ('loss', '-log(lkl)', '-log(prior)', '-log(hyper)', 'sigma', 'w.prec.')} progress = ProgressMeter( @@ -251,6 +546,9 @@ class BNNModel(RegressorMixin, BaseEstimator): meter['w.prec.'].update(self.model.w_precision().detach().cpu().item(), B) progress.display(len(loader)) + if self.rvm: + self.model.prune() + self.model.eval() return self diff --git a/pes_to_spec/model.py b/pes_to_spec/model.py index 5751510b4a5e98187ffd1081849c4dba0223d2fa..4d93ec82f883e304963e96c4c698b2cd100c2a1d 100644 --- a/pes_to_spec/model.py +++ b/pes_to_spec/model.py @@ -260,12 +260,14 @@ class SelectRelevantLowResolution(TransformerMixin, BaseEstimator): raise NotImplementedError("The low-resolution data cannot be transformed before the prompt has been identified. Call the fit function first.") if pulse_spacing is None: pulse_spacing = {ch: [0] for ch in X.keys()} - y = X + y = {channel: item for channel, item in X.items() + if channel in self.channels} if self.delta_tof is not None: first = max(0, self.tof_start - self.delta_tof) last = min(X[self.channels[0]].shape[1], self.tof_start + self.delta_tof) y = {channel: np.stack([item[:, (first + delta):(last + delta)] for delta in pulse_spacing[channel]], axis=1) - for channel, item in X.items()} + for channel, item in X.items() + if channel in self.channels} if not keep_dictionary_structure: selected = list(y.values()) if pulse_energy is not None: @@ -319,9 +321,9 @@ class SelectRelevantLowResolution(TransformerMixin, BaseEstimator): self.tof_start = self.estimate_prompt_peak(X) X_tr = self.transform(X, keep_dictionary_structure=True) self.mean = {ch: np.mean(X_tr[ch], axis=0, keepdims=True) - for ch in X.keys()} + for ch in X_tr.keys()} self.std = {ch: np.std(X_tr[ch], axis=0, keepdims=True) - for ch in X.keys()} + for ch in X_tr.keys()} return self def debug_peak_finding(self, X: Dict[str, np.ndarray], filename: str): @@ -574,7 +576,9 @@ class Model(TransformerMixin, BaseEstimator): Set to None to perform no selection. validation_size: Fraction (number between 0 and 1) of the data to take for validation and systematic uncertainty estimate. - model_type: Which model to use. "bnn" for a BNN, "ridge" for Ridge and "ard" for ARD. + model_type: Which model to use. "bnn" for a BNN, "bnn_rvm" for a BNN with RVM, "ridge" for Ridge and "ard" for ARD. + n_peaks: Minimum numbr of peaks in the grating spectrometer. + n_bnn_epochs: Number of BNN epochs for training. """ def __init__(self, @@ -586,11 +590,13 @@ class Model(TransformerMixin, BaseEstimator): tof_start: Optional[int]=None, delta_tof: Optional[int]=300, validation_size: float=0.05, - model_type: Literal["bnn", "ridge", "ard"]="ard", + model_type: Literal["bnn", "bnn_rvm", "ridge", "ard"]="ard", + n_peaks: int=0, + n_bnn_epochs: int=500, ): self.high_res_sigma = high_res_sigma # models - self.x_select = SelectRelevantLowResolution(channels, tof_start, delta_tof, poly=(model_type not in ["bnn"])) + self.x_select = SelectRelevantLowResolution(channels, tof_start, delta_tof, poly=(model_type not in ["bnn", "bnn_rvm"])) x_model_steps = list() x_model_steps += [ ('pca', PCA(n_pca_lr, whiten=True)), @@ -605,7 +611,9 @@ class Model(TransformerMixin, BaseEstimator): self.ood = {ch: UncorrelatedDeviation(sigma=5) for ch in channels+['full']} if model_type == "bnn": - self.fit_model = BNNModel() + self.fit_model = BNNModel(n_epochs=n_bnn_epochs) + elif model_type == "bnn_rvm": + self.fit_model = BNNModel(n_epochs=n_bnn_epochs, rvm=True) elif model_type == "ridge": self.fit_model = MultiOutputRidgeWithStd(BayesianRidge(n_iter=300, tol=1e-8, verbose=True), n_jobs=8) elif model_type == "ard": @@ -625,9 +633,12 @@ class Model(TransformerMixin, BaseEstimator): # size of the test subset self.validation_size = validation_size + # minimum number of peaks + self.n_peaks = n_peaks + def n_pars(self) -> float: """Get number of parameters.""" - if self.model_type == "bnn": + if self.model_type in ("bnn", "bnn_rvm"): return sum(p.numel() for p in self.fit_model.model.parameters()) return sum(len(estimator.coef_) + 1 for estimator in self.fit_model.estimators_) @@ -735,7 +746,7 @@ class Model(TransformerMixin, BaseEstimator): """ print("Checking data quality in high-resolution data.") peaks = self.count_peaks(high_res_data, high_res_photon_energy) - filter_hr = (peaks > 3) + filter_hr = (peaks >= self.n_peaks) print("Fitting PCA on low-resolution data.") self.x_select.fit(low_res_data) @@ -985,6 +996,7 @@ class Model(TransformerMixin, BaseEstimator): unc=unc.reshape((B, P, -1)), pca=pca_unc, total_unc=total_unc.reshape((B, P, -1)), + nopca_unc=unc.reshape((B, P, -1)), expected_pca=high_pca.reshape((B, P, -1)), expected_pca_unc=high_pca_unc.reshape((B, P, -1)), ) @@ -1015,7 +1027,7 @@ class Model(TransformerMixin, BaseEstimator): joblib.dump([self.x_select, self.x_model, self.y_model, - self.fit_model.state_dict() if self.model_type == "bnn" else self.fit_model, + self.fit_model.state_dict() if self.model_type in ("bnn", "bnn_rvm") else self.fit_model, self.channel_pca, #self.channel_fit_model DataHolder(dict( @@ -1073,7 +1085,9 @@ class Model(TransformerMixin, BaseEstimator): obj.x_model = x_model obj.y_model = y_model if obj.model_type == "bnn": - obj.fit_model = BNNModel(state_dict=fit_model) + obj.fit_model = BNNModel(state_dict=fit_model, rvm=False) + elif obj.model_type == "bnn_rvm": + obj.fit_model = BNNModel(state_dict=fit_model, rvm=True) else: obj.fit_model = fit_model obj.channel_pca = channel_pca diff --git a/pes_to_spec/test/channel_sensitivity.py b/pes_to_spec/test/channel_sensitivity.py new file mode 100755 index 0000000000000000000000000000000000000000..2ff437238966b8a8a2f2665245bb2a956841a16d --- /dev/null +++ b/pes_to_spec/test/channel_sensitivity.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python + +import sys +sys.path.append('.') +sys.path.append('..') + +import os +import argparse + +import numpy as np +from extra_data import open_run, by_id, RunDirectory +from pes_to_spec.model import Model, matching_ids + +# for helper plots +from pes_to_spec.model import SelectRelevantLowResolution +from sklearn.decomposition import PCA + +from itertools import product + +import pandas as pd +from copy import deepcopy +import scipy +from scipy.signal import fftconvolve + +from typing import Dict, Optional + +from time import time_ns +import pandas as pd + +def get_gas(run, tids): + gas_sources = [ + "SA3_XTD10_PES/DCTRL/V30300S_NITROGEN", + "SA3_XTD10_PES/DCTRL/V30310S_NEON", + "SA3_XTD10_PES/DCTRL/V30320S_KRYPTON", + "SA3_XTD10_PES/DCTRL/V30330S_XENON", + ] + gas_active = list() + for gas in gas_sources: + # check if this gas source is interlocked + if gas in run.all_sources and run[gas, "interlock.AActionState.value"].ndarray().sum() == 0: + # it is not, so this gas was used + gas_active += [gas.split("/")[-1].split("_")[-1]] + gas = "_".join(gas_active) + return gas + + +def save_result(filename: str, + spec_pred: Dict[str, np.ndarray], + spec_smooth: np.ndarray, + spec_raw_pe: np.ndarray, + intensity: float, + #spec_raw_int: Optional[np.ndarray]=None, + pes: Optional[np.ndarray]=None, + pes_to_show: Optional[str]="", + first: Optional[int]=None, + last: Optional[int]=None, + ): + """ + Plot result with uncertainty band. + + Args: + filename: Output file name. + spec_pred: Predicted result with uncertainty bands in a dictionary. + spec_smooth: Smoothened expected result with shape (features,). + spec_raw_pe: x axis with the photon energy in eV. + spec_raw_int: Original true expected result with shape (features,). + pes: PES spectrum for the inset. + pes_to_show: Name of the channel shown. + intensity: The XGM intensity in uJ. + + """ + unc_stat = spec_pred["unc"] + unc_pca = spec_pred["pca"] + unc = np.sqrt(unc_stat**2 + unc_pca**2) + df = pd.DataFrame(dict(energy=spec_raw_pe, + spec=spec_smooth, + prediction=spec_pred["expected"], + unc=unc, + unc_pca=unc_pca, + unc_stat=unc_stat, + beam_intensity=intensity*1e-3*np.ones_like(spec_raw_pe), + deconvolved=spec_pred["deconvolved"] + )) + df.to_csv(filename) + if pes is not None: + pes_data = deepcopy(pes) + pes_data['bin'] = np.arange(len(pes['channel_1_D'])) + pes_data['first'] = first*np.ones_like(pes_data['bin']) + pes_data['last'] = last*np.ones_like(pes_data['bin']) + df = pd.DataFrame(pes_data) + df.to_csv(filename.replace('.pdf', '_pes.csv')) + +def save_pes_result(filename: str, + pes: Optional[np.ndarray]=None, + first: Optional[int]=None, + last: Optional[int]=None, + ): + """ + Plot result with uncertainty band. + + Args: + filename: Output file name. + spec_pred: Predicted result with uncertainty bands in a dictionary. + spec_smooth: Smoothened expected result with shape (features,). + spec_raw_pe: x axis with the photon energy in eV. + spec_raw_int: Original true expected result with shape (features,). + pes: PES spectrum for the inset. + pes_to_show: Name of the channel shown. + intensity: The XGM intensity in uJ. + + """ + pes_data = deepcopy(pes) + pes_data['bin'] = np.arange(len(pes['channel_1_D'])) + pes_data['first'] = first*np.ones_like(pes_data['bin']) + pes_data['last'] = last*np.ones_like(pes_data['bin']) + df = pd.DataFrame(pes_data) + df.to_csv(filename) + +def main(): + """ + Main entry point. Reads some data, trains and predicts. + """ + parser = argparse.ArgumentParser(prog="offline_analysis", description="Test pes2spec doing an offline analysis of the data.") + parser.add_argument('-p', '--proposal', type=int, metavar='INT', help='Proposal number', default=2828) + parser.add_argument('-r', '--run', type=str, metavar='INT,INT,...', help='Run numbers, comma-separated.', default=206) + parser.add_argument('-t', '--test-run', type=int, metavar='INT', help='Run to test', default=None) + parser.add_argument('-d', '--directory', type=str, metavar='DIRECTORY', default=".", help='Where to save the results.') + parser.add_argument('-S', '--spec', type=str, metavar='NAME', default="SA3_XTD10_SPECT/MDL/SPECTROMETER_SCS_NAVITAR:output", help='SPEC name') + parser.add_argument('-P', '--pes', type=str, metavar='NAME', default="SA3_XTD10_PES/ADC/1:network", help='PES name') + parser.add_argument('-X', '--xgm', type=str, metavar='NAME', default="SA3_XTD10_XGM/XGM/DOOCS:output", help='XGM name') + parser.add_argument('-o', '--offset', type=int, metavar='INT', default=0, help='Train ID offset') + parser.add_argument('-c', '--xgm_cut', type=float, metavar='INTENSITY', default=0, help='XGM intensity threshold in uJ.') + parser.add_argument('-T', '--model-type', type=str, metavar='TYPE', default="ard", choices=["bnn", "bnn_rvm", "ridge", "ard"], help='Which model type to use.') + parser.add_argument('-w', '--weight', action="store_true", default=False, help='Whether to reweight data as a function of the pulse energy to make it invariant to that.') + + args = parser.parse_args() + + print("Opening run ...") + runs = args.run.split(',') + runs = [int(r) for r in runs] + # get run + run_list = [open_run(proposal=args.proposal, run=r) for r in runs] + run = run_list[0] + if len(run_list) > 1: + run = run.union(*run_list[1:]) + + run_test = run + other_run_test = False + if "test_run" in args and args.test_run is not None: + other_run_test = True + run_test = open_run(proposal=args.proposal, run=args.test_run) + + spec_offset = args.offset + spec_name = args.spec + pes_name = args.pes + xgm_name = args.xgm + + pes_tid = run[pes_name, "digitizers.trainId"].ndarray() + xgm_tid = run[xgm_name, "data.trainId"].ndarray() + + spec_tid = spec_offset + run[spec_name, "data.trainId"].ndarray() + # these are the train ID intersection + # this could have been done by a select call in the RunDirectory, but it would not correct for the spec_offset + tids = matching_ids(spec_tid, pes_tid, xgm_tid) + + # read the spec photon energy and intensity + spec_raw_pe = run[spec_name, "data.photonEnergy"].select_trains(by_id[tids - spec_offset]).ndarray() + spec_raw_int = run[spec_name, "data.intensityDistribution"].select_trains(by_id[tids - spec_offset]).ndarray() + + + # reserve part of it for the test stage + train_tids = tids[:-10] + if other_run_test: + pes_tidt = run_test[pes_name, "digitizers.trainId"].ndarray() + xgm_tidt = run_test[xgm_name, "data.trainId"].ndarray() + spec_tidt = run_test[spec_name, "data.trainId"].ndarray() + test_tids = matching_ids(spec_tidt, pes_tidt, xgm_tidt) + else: + test_tids = tids + print(f"Number of train IDs: {len(train_tids)}") + print(f"Number of test IDs: {len(test_tids)}") + + # read the PES data for each channel + channels = [f"channel_{i}_{l}" + for i, l in product([1,3,4], ["A", "B", "C", "D"])] + pes_raw = {ch: run[pes_name, f"digitizers.{ch}.raw.samples"].select_trains(by_id[tids]).ndarray() + for ch in channels} + pes_raw_t = {ch: run_test[pes_name, f"digitizers.{ch}.raw.samples"].select_trains(by_id[test_tids]).ndarray() + for ch in channels} + + # select test SPEC data + spec_raw_pe_t = run_test[spec_name, "data.photonEnergy"].select_trains(by_id[test_tids - spec_offset]).ndarray() + spec_raw_int_t = run_test[spec_name, "data.intensityDistribution"].select_trains(by_id[test_tids - spec_offset]).ndarray() + + print("Data in memory.") + + # read the XGM information + #xgm_pressure = run['SA3_XTD10_XGM/XGM/DOOCS', "pressure.pressureFiltered.value"].select_trains(by_id[tids]).ndarray() + #xgm_pe = run['SA3_XTD10_XGM/XGM/DOOCS:output', "data.intensitySa3TD"].select_trains(by_id[tids]).ndarray() + #retvol_raw = run["SA3_XTD10_PES/MDL/DAQ_MPOD", "u212.value"].select_trains(by_id[tids]).ndarray() + #retvol_raw_timestamp = run["SA3_XTD10_PES/MDL/DAQ_MPOD", "u212.timestamp"].select_trains(by_id[tids]).ndarray() + + xgm_flux = run['SA3_XTD10_XGM/XGM/DOOCS:output', "data.intensitySa3TD"].select_trains(by_id[tids]).ndarray()[:, 0][:, np.newaxis] + xgm_flux_t = run_test['SA3_XTD10_XGM/XGM/DOOCS:output', "data.intensitySa3TD"].select_trains(by_id[test_tids]).ndarray()[:, 0][:, np.newaxis] + + print(f"Intensity in training: {np.mean(xgm_flux):.2e} +/- {np.std(xgm_flux):.2e}") + print(f"Intensity in testing: {np.mean(xgm_flux_t):.2e} +/- {np.std(xgm_flux_t):.2e}") + + pressure = run["SA3_XTD10_PES/GAUGE/G30310F", "value"].select_trains(by_id[tids]).ndarray() + pressure_t = run_test["SA3_XTD10_PES/GAUGE/G30310F", "value"].select_trains(by_id[test_tids]).ndarray() + print(f"Pressure in training: {np.mean(pressure):.2e} +/- {np.std(pressure):.2e}") + print(f"Pressure in testing: {np.mean(pressure_t):.2e} +/- {np.std(pressure_t):.2e}") + + voltage = run["SA3_XTD10_PES/MDL/DAQ_MPOD", "u212.value"].select_trains(by_id[tids]).ndarray() + voltage_t = run_test["SA3_XTD10_PES/MDL/DAQ_MPOD", "u212.value"].select_trains(by_id[test_tids]).ndarray() + print(f"Voltage in training: {np.mean(voltage):.2f} +/- {np.std(voltage):.2f}") + print(f"Voltage in testing: {np.mean(voltage_t):.2f} +/- {np.std(voltage_t):.2f}") + + gas = get_gas(run, tids) + gas_t = get_gas(run_test, test_tids) + print(f"Gas in training: {gas}") + print(f"Gas in testing: {gas_t}") + + t = list() + t_names = list() + t_nch = list() + + train_idx = np.isin(tids, train_tids) & (xgm_flux[:,0] > args.xgm_cut) + + # we just need this for training and we need to avoid copying it, which blows up the memoray usage + for k in pes_raw.keys(): + pes_raw[k] = pes_raw[k][train_idx] + + + nch_axis = np.arange(1, len(channels)+1) + resolution = list() + rmse = list() + delta_rmse = list() + chi2_prepca = list() + unc = list() + delta_unc = list() + for nch in nch_axis: + model = Model(channels=channels[:nch], model_type=args.model_type) + + print(f"Fitting using {nch} channels") + start = time_ns() + model.uniformize(xgm_flux[train_idx]) + model.fit(pes_raw, + spec_raw_int[train_idx], + spec_raw_pe[train_idx], + pulse_energy=xgm_flux[train_idx], + ) + t += [time_ns() - start] + t_names += ["Fit"] + t_nch += [nch] + + resolution += [model.resolution] + + # transfer function + print(f"Resolution: {model.resolution:.2f} eV") + + # test + print("Predict") + start = time_ns() + spec_pred = model.predict(pes_raw_t, pulse_energy=xgm_flux_t) + spec_smooth = model.preprocess_high_res(spec_raw_int_t) + t += [time_ns() - start] + t_names += ["Predict"] + t_nch += [nch] + + spec_smooth_pca = model.y_model['pca'].transform(spec_smooth) + unc2 = spec_pred["expected_pca_unc"]**2 + pca_var = (spec_pred["expected_pca"].std(axis=0, keepdims=True)**2).reshape(1, 1, -1) + ndof_prepca = float(spec_smooth_pca.shape[-1]) + print("Expected pca std:", pca_var) + chi2_prepca += [np.mean(np.sum((spec_smooth_pca[:, np.newaxis, :] - spec_pred["expected_pca"])**2/unc2, axis=(-1, -2)))/ndof_prepca] + rmse += [np.mean(np.sqrt(np.mean((spec_smooth[:, np.newaxis, :] - spec_pred["expected"])**2, axis=(-1, -2))))] + delta_rmse += [np.std(np.sqrt(np.mean((spec_smooth[:, np.newaxis, :] - spec_pred["expected"])**2, axis=(-1, -2))))] + unc += [np.mean(np.mean(spec_pred["total_unc"], axis=(-1, -2)))] + delta_unc += [np.std(np.mean(spec_pred["total_unc"], axis=(-1, -2)))] + + df = pd.DataFrame(dict(number_channels=nch_axis, + resolution=resolution, + rmse=rmse, + delta_rmse=delta_rmse, + chi2_prepca=chi2_prepca, + unc=unc, + delta_unc=delta_unc, + )) + df.to_csv(os.path.join(args.directory, "number_channel_effect.csv")) + + + print("Time taken in ms") + df_time = pd.DataFrame(data=dict(time=t, name=t_names, nch=t_nch)) + df_time.time *= 1e-6 + df_time.to_csv(os.path.join(args.directory, "number_channel_time.csv")) + +if __name__ == '__main__': + main() + diff --git a/pes_to_spec/test/offline_analysis.py b/pes_to_spec/test/offline_analysis.py index a9ba79450dffcacbcc0b93b19eec85337c741174..3911d0f630ed8f4239d528be1ff1bc288ea7ceae 100755 --- a/pes_to_spec/test/offline_analysis.py +++ b/pes_to_spec/test/offline_analysis.py @@ -11,17 +11,14 @@ import numpy as np from extra_data import open_run, by_id, RunDirectory from pes_to_spec.model import Model, matching_ids -from itertools import product +# for helper plots +from pes_to_spec.model import SelectRelevantLowResolution +from sklearn.decomposition import PCA -import matplotlib -matplotlib.use('Agg') +from itertools import product import pandas as pd from copy import deepcopy -import matplotlib.pyplot as plt -from matplotlib.gridspec import GridSpec -from mpl_toolkits.axes_grid1.inset_locator import InsetPosition -import seaborn as sns import scipy from scipy.signal import fftconvolve @@ -30,18 +27,6 @@ from typing import Dict, Optional from time import time_ns import pandas as pd -SMALL_SIZE = 12 -MEDIUM_SIZE = 18 -BIGGER_SIZE = 22 - -plt.rc('font', size=BIGGER_SIZE) # controls default text sizes -plt.rc('axes', titlesize=BIGGER_SIZE) # fontsize of the axes title -plt.rc('axes', labelsize=BIGGER_SIZE) # fontsize of the x and y labels -plt.rc('xtick', labelsize=BIGGER_SIZE) # fontsize of the tick labels -plt.rc('ytick', labelsize=BIGGER_SIZE) # fontsize of the tick labels -plt.rc('legend', fontsize=MEDIUM_SIZE) # legend fontsize -plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title - def get_gas(run, tids): gas_sources = [ "SA3_XTD10_PES/DCTRL/V30300S_NITROGEN", @@ -59,27 +44,7 @@ def get_gas(run, tids): return gas -def plot_pes(filename: str, pes_raw_int: np.ndarray, first: int, last: int): - """ - Plot low-resolution spectrum. - - Args: - filename: Output file name. - pes_raw_int: Low-resolution spectrum. - - """ - fig = plt.figure(figsize=(16, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - ax.plot(np.arange(first, last), pes_raw_int, c='b', lw=3, label="Low-resolution measurement") - ax.legend() - ax.set(title=f"", - xlabel="ToF index", - ylabel="Intensity") - fig.savefig(filename) - plt.close(fig) - -def plot_result(filename: str, +def save_result(filename: str, spec_pred: Dict[str, np.ndarray], spec_smooth: np.ndarray, spec_raw_pe: np.ndarray, @@ -87,7 +52,8 @@ def plot_result(filename: str, #spec_raw_int: Optional[np.ndarray]=None, pes: Optional[np.ndarray]=None, pes_to_show: Optional[str]="", - pes_bin: Optional[np.ndarray]=None, + first: Optional[int]=None, + last: Optional[int]=None, ): """ Plot result with uncertainty band. @@ -100,7 +66,6 @@ def plot_result(filename: str, spec_raw_int: Original true expected result with shape (features,). pes: PES spectrum for the inset. pes_to_show: Name of the channel shown. - pes_bin: PES bins. intensity: The XGM intensity in uJ. """ @@ -111,64 +76,45 @@ def plot_result(filename: str, spec=spec_smooth, prediction=spec_pred["expected"], unc=unc, - beam_intensity=intensity*1e-3*np.ones_like(spec_raw_pe) + unc_pca=unc_pca, + unc_stat=unc_stat, + beam_intensity=intensity*1e-3*np.ones_like(spec_raw_pe), + deconvolved=spec_pred["deconvolved"] )) - df.to_csv(filename.replace('.png', '.csv')) + df.to_csv(filename) + if pes is not None: + pes_data = deepcopy(pes) + pes_data['bin'] = np.arange(len(pes['channel_1_D'])) + pes_data['first'] = first*np.ones_like(pes_data['bin']) + pes_data['last'] = last*np.ones_like(pes_data['bin']) + df = pd.DataFrame(pes_data) + df.to_csv(filename.replace('.pdf', '_pes.csv')) + +def save_pes_result(filename: str, + pes: Optional[np.ndarray]=None, + first: Optional[int]=None, + last: Optional[int]=None, + ): + """ + Plot result with uncertainty band. + + Args: + filename: Output file name. + spec_pred: Predicted result with uncertainty bands in a dictionary. + spec_smooth: Smoothened expected result with shape (features,). + spec_raw_pe: x axis with the photon energy in eV. + spec_raw_int: Original true expected result with shape (features,). + pes: PES spectrum for the inset. + pes_to_show: Name of the channel shown. + intensity: The XGM intensity in uJ. + + """ pes_data = deepcopy(pes) pes_data['bin'] = np.arange(len(pes['channel_1_D'])) + pes_data['first'] = first*np.ones_like(pes_data['bin']) + pes_data['last'] = last*np.ones_like(pes_data['bin']) df = pd.DataFrame(pes_data) - df.to_csv(filename.replace('.png', '_pes.csv')) - - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - ax.plot(spec_raw_pe, spec_smooth, c='b', lw=3, label="High-res. measurement (smoothened)") - ax.plot(spec_raw_pe, spec_pred["expected"], c='r', ls='--', lw=3, label="High-res. prediction") - #ax.fill_between(spec_raw_pe, spec_pred["expected"] - unc, spec_pred["expected"] + unc, facecolor='green', alpha=0.6, label="68% unc.") - ax.fill_between(spec_raw_pe, spec_pred["expected"] - unc, spec_pred["expected"] + unc, facecolor='gold', alpha=0.5, label="68% unc.") - #ax.fill_between(spec_raw_pe, spec_pred["expected"] - unc_stat, spec_pred["expected"] + unc_stat, facecolor='red', alpha=0.6, label="68% unc. (stat.)") - #ax.fill_between(spec_raw_pe, spec_pred["expected"] - unc_pca, spec_pred["expected"] + unc_pca, facecolor='magenta', alpha=0.6, label="68% unc. (syst., PCA)") - #if spec_raw_int is not None: - # ax.plot(spec_raw_pe, spec_raw_int, c='b', lw=1, ls='--', label="High-resolution measurement") - #if wiener is not None: - # deconvolved = fftconvolve(spec_pred["expected"], wiener, mode="same") - #ax.plot(spec_raw_pe, spec_pred["deconvolved"], c='g', ls='-.', lw=3, label="Wiener filter result") - Y = np.amax(spec_smooth) - ax.legend(frameon=False, borderaxespad=0, loc='upper left') - ax.set_title(f"Beam intensity: {intensity*1e-3:.1f} mJ", loc="left") - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.set( - xlabel="Photon energy [eV]", - ylabel="Intensity", - ylim=(0, 1.3*Y)) - if pes is not None: - ax2 = plt.axes([0,0,1,1]) - # Manually set the position and relative size of the inset axes within ax1 - #ip = InsetPosition(ax, [0.65,0.6,0.35,0.4]) - ip = InsetPosition(ax, [0.72,0.7,0.35,0.4]) - ax2.set_axes_locator(ip) - if pes_to_show == "sum": - pes_plot = sum([pes[k][pes_bin] for k in pes.keys()]) - pes_label = r"$\sum$ PES channels" - else: - pes_plot = pes[pes_to_show][pes_bin] - pes_label = pes_to_show - ax2.plot(pes_bin, pes_plot, c='black', lw=3) - ax2.set(title=f"Low-resolution example data", - xlabel="Bin", - ylabel=pes_label, - ylim=(0, None), - #labelsize=SMALL_SIZE, - #xticklabels=dict(fontdict=dict(fontsize=SMALL_SIZE)), - #yticklabels=dict(fontdict=dict(fontsize=SMALL_SIZE)), - ) - ax2.title.set_size(SMALL_SIZE) - ax2.xaxis.label.set_size(SMALL_SIZE) - ax2.yaxis.label.set_size(SMALL_SIZE) - ax2.tick_params(axis='both', which='major', labelsize=SMALL_SIZE) - fig.savefig(filename) - plt.close(fig) + df.to_csv(filename) def main(): """ @@ -180,12 +126,12 @@ def main(): parser.add_argument('-t', '--test-run', type=int, metavar='INT', help='Run to test', default=None) parser.add_argument('-m', '--model', type=str, metavar='FILENAME', default="", help='Model to load. If given, do not train a model and just do inference with this one.') parser.add_argument('-d', '--directory', type=str, metavar='DIRECTORY', default=".", help='Where to save the results.') - parser.add_argument('-S', '--spec', type=str, metavar='NAME', default="SA3_XTD10_SPECT/MDL/SPECTROMETER_SQS_NAVITAR:output", help='SPEC name') + parser.add_argument('-S', '--spec', type=str, metavar='NAME', default="SA3_XTD10_SPECT/MDL/SPECTROMETER_SCS_NAVITAR:output", help='SPEC name') parser.add_argument('-P', '--pes', type=str, metavar='NAME', default="SA3_XTD10_PES/ADC/1:network", help='PES name') parser.add_argument('-X', '--xgm', type=str, metavar='NAME', default="SA3_XTD10_XGM/XGM/DOOCS:output", help='XGM name') parser.add_argument('-o', '--offset', type=int, metavar='INT', default=0, help='Train ID offset') parser.add_argument('-c', '--xgm_cut', type=float, metavar='INTENSITY', default=0, help='XGM intensity threshold in uJ.') - parser.add_argument('-T', '--model-type', type=str, metavar='TYPE', default="ard", choices=["bnn", "ridge", "ard"], help='Which model type to use.') + parser.add_argument('-T', '--model-type', type=str, metavar='TYPE', default="ard", choices=["bnn", "bnn_rvm", "ridge", "ard"], help='Which model type to use.') parser.add_argument('-w', '--weight', action="store_true", default=False, help='Whether to reweight data as a function of the pulse energy to make it invariant to that.') args = parser.parse_args() @@ -253,8 +199,10 @@ def main(): print(f"Number of test IDs: {len(test_tids)}") # read the PES data for each channel + #channels = [f"channel_{i}_{l}" + # for i, l in product(range(1, 5), ["A", "B", "C", "D"])] channels = [f"channel_{i}_{l}" - for i, l in product(range(1, 5), ["A", "B", "C", "D"])] + for i, l in product([1,3,4], ["A", "B", "C", "D"])] pes_raw = {ch: run[pes_name, f"digitizers.{ch}.raw.samples"].select_trains(by_id[tids]).ndarray() for ch in channels} pes_raw_t = {ch: run_test[pes_name, f"digitizers.{ch}.raw.samples"].select_trains(by_id[test_tids]).ndarray() @@ -296,14 +244,14 @@ def main(): t = list() t_names = list() - model = Model(model_type=args.model_type) + model = Model(channels=channels, model_type=args.model_type) train_idx = np.isin(tids, train_tids) & (xgm_flux[:,0] > args.xgm_cut) # we just need this for training and we need to avoid copying it, which blows up the memoray usage for k in pes_raw.keys(): pes_raw[k] = pes_raw[k][train_idx] - model.debug_peak_finding(pes_raw, os.path.join(args.directory, "test_peak_finding.png")) + model.debug_peak_finding(pes_raw, os.path.join(args.directory, "test_peak_finding.pdf")) if len(args.model) == 0: print("Fitting") start = time_ns() @@ -334,44 +282,36 @@ def main(): t += [time_ns() - start] t_names += ["Load"] + # save PCA information + pes_raw_select = model.x_select.transform(pes_raw, pulse_energy=xgm_flux[train_idx]) + ch = channels[0] + idx = 0 + first = model.x_select.tof_start - model.x_select.delta_tof + last = model.x_select.tof_start + model.x_select.delta_tof + B, P, _ = pes_raw_select.shape + pes_raw_select = pes_raw_select.reshape((B*P, -1)) + pca = PCA(None, whiten=True) + pca.fit(pes_raw_select) + df = pd.DataFrame(dict(variance_ratio=pca.explained_variance_ratio_, + n_comp=600*np.ones_like(pca.explained_variance_ratio_), + )) + df.to_csv(os.path.join(args.directory, "pca_pes.csv")) + + pca_spec = PCA(None, whiten=True) + pca_spec.fit(spec_raw_int[train_idx]) + df = pd.DataFrame(dict(variance_ratio=pca_spec.explained_variance_ratio_, + n_comp=20*np.ones_like(pca_spec.explained_variance_ratio_), + )) + df.to_csv(os.path.join(args.directory, "pca_spec.csv")) + # transfer function - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - plt.plot(model.wiener_energy, np.absolute(model.impulse_response)) - ax.set(title=f"", - xlabel=r"Energy [eV]", - ylabel="Response [a.u.]", - yscale='log', - ) - fig.savefig(os.path.join(args.directory, "impulse.png")) - plt.close(fig) print(f"Resolution: {model.resolution:.2f} eV") - # plot Wiener filter - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - plt.plot(np.fft.fftshift(model.wiener_energy_ft), np.fft.fftshift(np.absolute(model.wiener_filter_ft))) - ax.set(title=f"", - xlabel=r"Reciprocal energy [1/eV]", - ylabel="Filter intensity [a.u.]", - yscale='log', - ) - fig.savefig(os.path.join(args.directory, "wiener_ft.png")) - plt.close(fig) - - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - plt.plot(model.wiener_energy, np.absolute(model.wiener_filter)) - ax.set(title=f"", - xlabel=r"Energy [eV]", - ylabel="Filter value [a.u.]", - yscale='log', - ) - fig.savefig(os.path.join(args.directory, "wiener.png")) - plt.close(fig) + df = pd.DataFrame(dict(wiener_energy=model.wiener_energy, + wiener_filter=model.wiener_filter, + impulse=model.impulse_response, + resolution=model.resolution*np.ones_like(model.wiener_energy))) + df.to_csv(os.path.join(args.directory, "model.csv")) print("Check consistency") start = time_ns() @@ -410,206 +350,52 @@ def main(): chi2 = np.sum((spec_smooth[:, np.newaxis, :] - spec_pred["expected"])**2/(spec_pred["total_unc"]**2), axis=(-1, -2)) ndof = spec_smooth.shape[1] print(f"Chi2 after PCA: {np.mean(chi2):.2f}, ndof: {ndof}, chi2/ndof: {np.mean(chi2/ndof):.2f}") - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - ax.scatter(chi2/ndof, xgm_flux_t[:,0], c='r', s=20) - ax.set(title=f"", #avg(stat unc) = {unc_stat}, avg(pca unc) = {unc_pca}", - xlabel=r"$\chi^2/$ndof", - ylabel="XGM intensity [uJ]", - xlim=(0, 5), - ) - ax2 = plt.axes([0,0,1,1]) - # Manually set the position and relative size of the inset axes within ax1 - ip = InsetPosition(ax, [0.65,0.6,0.35,0.4]) - ax2.set_axes_locator(ip) - ax2.scatter(chi2/ndof, xgm_flux_t[:,0], c='r', s=30) - #ax2.scatter(chi2/ndof, np.sum(spec_pred["expected"], axis=1)*de, c='b', s=30) - #ax2.scatter(chi2/ndof, np.sum(spec_raw_int, axis=1)*de, c='g', s=30) - ax2.set(title="", - xlabel=r"$\chi^2/$ndof", - ylabel=f"XGM intensity [uJ]", - ) - ax2.title.set_size(SMALL_SIZE) - ax2.xaxis.label.set_size(SMALL_SIZE) - ax2.yaxis.label.set_size(SMALL_SIZE) - ax2.tick_params(axis='both', which='major', labelsize=SMALL_SIZE) - fig.savefig(os.path.join(args.directory, "intensity_vs_chi2.png")) - plt.close(fig) - - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - sns.histplot(x=chi2/ndof, kde=True, linewidth=3, ax=ax) - ax.set(title=f"", - xlabel=r"$\chi^2/$ndof", - ylabel="Counts [a.u.]", - xlim=(0, 5), - ) - #ax.text(0.90, 0.95, fr"$\mu = ${np.mean(chi2/ndof):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - #ax.text(0.90, 0.90, fr"$\sigma = ${np.std(chi2/ndof):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - fig.savefig(os.path.join(args.directory, "chi2.png")) - plt.close(fig) spec_smooth_pca = model.y_model['pca'].transform(spec_smooth) - chi2_prepca = np.sum((spec_smooth_pca[:, np.newaxis, :] - spec_pred["expected_pca"])**2/(spec_pred["expected_pca_unc"]**2), axis=(-1, -2)) + unc2 = spec_pred["expected_pca_unc"]**2 + pca_var = (spec_pred["expected_pca"].std(axis=0, keepdims=True)**2).reshape(1, 1, -1) + print("Expected pca std:", pca_var) + chi2_prepca = np.sum((spec_smooth_pca[:, np.newaxis, :] - spec_pred["expected_pca"])**2/unc2, axis=(-1, -2)) + ndof_prepca = float(spec_smooth_pca.shape[-1]) print(f"Chi2 before PCA: {np.mean(chi2_prepca):.2f}, ndof: {ndof_prepca}, chi2/ndof: {np.mean(chi2_prepca/ndof_prepca):.2f} +/- {np.std(chi2_prepca/ndof_prepca):.2f}") - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - sns.histplot(x=chi2_prepca/ndof_prepca, kde=True, linewidth=3, ax=ax) - ax.set(title=f"", - xlabel=r"$\chi^2/$ndof before undoing PCA", - ylabel="Counts [a.u.]", - xlim=(0, 5), - ) - #ax.text(0.90, 0.95, fr"$\mu = ${np.mean(chi2/ndof):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - #ax.text(0.90, 0.90, fr"$\sigma = ${np.std(chi2/ndof):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - fig.savefig(os.path.join(args.directory, "chi2_prepca.png")) - plt.close(fig) - - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - ax.scatter(chi2_prepca/ndof_prepca, xgm_flux_t[:,0], c='r', s=20) - ax.set(title=f"", - xlabel=r"$\chi^2/$ndof before undoing PCA", - ylabel="XGM intensity [uJ]", - xlim=(0, 5), - ylim=(0, np.mean(xgm_flux_t) + 3*np.std(xgm_flux_t)) - ) - fig.savefig(os.path.join(args.directory, "intensity_vs_chi2_prepca.png")) - plt.close(fig) res_prepca = np.sum((spec_smooth_pca[:, np.newaxis, :] - spec_pred["expected_pca"])/spec_pred["expected_pca_unc"], axis=1) - n_plots = res_prepca.shape[1]//10 - fig = plt.figure(figsize=(8*n_plots, 8)) - gs = GridSpec(1, n_plots) - for i_plot in range(n_plots): - ax = fig.add_subplot(gs[0, i_plot]) - sns.kdeplot(data={f"Dim. {k+1}": res_prepca[:, k] for k in range(i_plot*10, i_plot*10 + 10)}, - linewidth=3, ax=ax) - ax.set(title=f"", - xlabel=r"residue/uncertainty [a.u.]", - ylabel="Counts [a.u.]", - xlim=(-3, 3), - ) - ax.legend(frameon=False) - fig.savefig(os.path.join(args.directory, "res_prepca.png")) - plt.close(fig) - - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - sns.histplot(x=xgm_flux_t[:,0], kde=True, linewidth=3, ax=ax) - ax.set(title=f"", - xlabel="XGM intensity [uJ]", - ylabel="Counts [a.u.]", - ) - #ax.text(0.90, 0.95, fr"$\mu = ${np.mean(xgm_flux_t[:,0]):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - #ax.text(0.90, 0.90, fr"$\sigma = ${np.std(xgm_flux_t[:,0]):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - plt.tight_layout() - fig.savefig(os.path.join(args.directory, "intensity.png")) - plt.close(fig) # rmse rmse = np.sqrt(np.mean((spec_smooth[:, np.newaxis, :] - spec_pred["expected"])**2, axis=(-1, -2))) - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - ax.scatter(rmse, xgm_flux_t[:,0], c='r', s=30) - ax = plt.gca() - ax.set(title=f"", - xlabel=r"Root-mean-squared error", - ylabel="XGM intensity [uJ]", - ) - fig.savefig(os.path.join(args.directory, "intensity_vs_rmse.png")) - plt.close(fig) - - fig = plt.figure(figsize=(12, 8)) - gs = GridSpec(1, 1) - ax = fig.add_subplot(gs[0, 0]) - sns.histplot(x=rmse, kde=True, linewidth=3, ax=ax) - ax.set(title=f"", - xlabel="Root-mean-squared error", - ylabel="Counts [a.u.]", - ) - #ax.text(0.90, 0.95, fr"$\mu = ${np.mean(rmse):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - #ax.text(0.90, 0.90, fr"$\sigma = ${np.std(rmse):.2f}", - # verticalalignment='top', horizontalalignment='right', - # transform=ax.transAxes, - # color='black', fontsize=15) - fig.savefig(os.path.join(args.directory, "rmse.png")) - plt.close(fig) - - ## SPEC integral w.r.t XGM intensity - #fig = plt.figure(figsize=(12, 8)) - #gs = GridSpec(1, 1) - #ax = fig.add_subplot(gs[0, 0]) - #sns.regplot(x=np.sum(spec_raw_int_t, axis=1)*de, y=xgm_flux_t[:,0], color='r', robust=True, ax=ax) - #ax.set(title=f"", - # xlabel="SPEC (raw) integral", - # ylabel="XGM Intensity [uJ]", - # ) - #fig.savefig(os.path.join(args.directory, "xgm_vs_intensity.png")) - #plt.close(fig) - - ## SPEC integral w.r.t XGM intensity - #fig = plt.figure(figsize=(12, 8)) - #gs = GridSpec(1, 1) - #ax = fig.add_subplot(gs[0, 0]) - #sns.regplot(x=np.sum(spec_raw_int_t, axis=-1)*de, y=np.sum(spec_pred["expected"], axis=(-1, -2))*de, color='r', robust=True, ax=ax) - #ax.set(title=f"", - # xlabel="SPEC (raw) integral", - # ylabel="Predicted integral", - # ) - #fig.savefig(os.path.join(args.directory, "expected_vs_intensity.png")) - #plt.close(fig) - - #fig = plt.figure(figsize=(12, 8)) - #gs = GridSpec(1, 1) - #ax = fig.add_subplot(gs[0, 0]) - #sns.regplot(x=np.sum(spec_pred["expected"], axis=(-1, -2))*de, y=xgm_flux_t[:,0], color='r', robust=True, ax=ax) - #ax.set(title=f"", - # xlabel="Predicted integral", - # ylabel="XGM intensity [uJ]", - # ) - #fig.savefig(os.path.join(args.directory, "xgm_vs_expected.png")) - #plt.close(fig) + nopca_unc = np.sqrt(np.mean(spec_pred["nopca_unc"]**2, axis=(-1, -2))) + total_unc = np.sqrt(np.mean(spec_pred["total_unc"]**2, axis=(-1, -2))) + median_unc = np.median(spec_pred["total_unc"], axis=(-1, -2)) + + q = dict(chi2_prepca=chi2_prepca, + ndof=spec_smooth_pca.shape[-1]*np.ones_like(chi2_prepca), + xgm_flux_t=xgm_flux_t[:,0], + rmse=rmse, + nopca_unc=nopca_unc, + total_unc=total_unc, + median_unc=median_unc, + root_mean_squared_pca_unc=np.sqrt((spec_pred["expected_pca_unc"][:, 0, :]**2).mean(axis=-1)) + ) + q.update({f'res_prepca_{k}': res_prepca[:, k] + for k in range(res_prepca.shape[1]) + } + ) + q.update({f'unc_prepca_{k}': spec_pred["expected_pca_unc"][:, 0, k] + for k in range(spec_pred["expected_pca_unc"].shape[-1]) + } + ) + df = pd.DataFrame(q) + df.to_csv(os.path.join(args.directory, "quality.csv")) first, last = model.get_low_resolution_range() - first = max(0, first+250) - last = min(last, pes_raw_t["channel_1_D"].shape[1]-1) - pes_to_show = 'sum' # plot high_int_idx = np.argsort(xgm_flux_t[:,0]) for q in [10, 25, 50, 75, 100]: qi = int(len(high_int_idx)*(q/100.0)) for idx in high_int_idx[qi-10:qi]: tid = test_tids[idx] - plot_result(os.path.join(args.directory, f"test_q{q}_{tid}.png"), + save_result(os.path.join(args.directory, f"test_q{q}_{tid}.csv"), {k: item[idx, 0, ...] if k != "pca" else item[0, ...] for k, item in spec_pred.items()}, @@ -617,14 +403,13 @@ def main(): spec_raw_pe_t[idx, :] if showSpec else None, #spec_raw_int_t[idx, :] if showSpec else None, intensity=xgm_flux_t[idx,0], + ) + save_pes_result(os.path.join(args.directory, f"test_q{q}_{tid}_pes.csv"), pes={k: -item[idx, :] for k, item in pes_raw_t.items()}, - pes_to_show=pes_to_show, - pes_bin=np.arange(first, last), + first=first, + last=last, ) - #for ch in channels: - # plot_pes(os.path.join(args.directory, f"test_pes_{tid}_{ch}.png"), - # pes_raw_t[ch][idx, first:last], first, last) if __name__ == '__main__': main() diff --git a/pes_to_spec/test/plot_channel_sensitivity.py b/pes_to_spec/test/plot_channel_sensitivity.py new file mode 100755 index 0000000000000000000000000000000000000000..813b4ecb5f4785e47b79a5b710ad286bbb28e22e --- /dev/null +++ b/pes_to_spec/test/plot_channel_sensitivity.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +import os +import re + +from typing import Optional, Tuple, Dict + +import matplotlib +matplotlib.use('Agg') +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec +import seaborn as sns + +SMALL_SIZE = 12 +MEDIUM_SIZE = 22 +BIGGER_SIZE = 26 + +plt.rc('font', size=BIGGER_SIZE) # controls default text sizes +plt.rc('axes', titlesize=BIGGER_SIZE) # fontsize of the axes title +plt.rc('axes', labelsize=BIGGER_SIZE) # fontsize of the x and y labels +plt.rc('xtick', labelsize=BIGGER_SIZE) # fontsize of the tick labels +plt.rc('ytick', labelsize=BIGGER_SIZE) # fontsize of the tick labels +plt.rc('legend', fontsize=MEDIUM_SIZE) # legend fontsize +plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title + +def plot_resolution(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + ax.plot(df.number_channels, df.resolution, c='b', lw=3) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.set( + xlabel="Number of channels", + ylabel="Average resolution [eV]", + ) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_unc(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + #ax.plot(df.number_channels, 2*df.unc, c='tab:blue', lw=3, alpha=0.7, label="Avg. 95% CL uncertainty band") + #ax.fill_between(df.number_channels, 2*df.unc - 2*df.delta_unc, 2*df.unc + 2*df.delta_unc, color='tab:blue', alpha=0.2) + ax.errorbar(df.number_channels, 2*df.unc, yerr=2*df.delta_unc, color='tab:blue', alpha=0.5, marker='o', markersize=20, lw=3, linestyle='none', label="95% CL uncertainty band") + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.set( + xlabel="Number of channels", + ylabel="Grating spectrometer intensity [a.u.]", + ) + #rax = ax.twinx() + rax = ax + #rax.plot(df.number_channels, df.rmse, c='tab:red', lw=3, alpha=0.7, label="Avg. root-mean-squared error") + #rax.fill_between(df.number_channels, df.rmse - df.delta_rmse, df.rmse + df.delta_rmse, color='tab:red', alpha=0.2) + rax.errorbar(df.number_channels, df.rmse, yerr=df.delta_rmse, color='tab:red', alpha=0.5, marker='^', markersize=20, lw=3, linestyle='none', label="Root-mean-squared error") + #rax.spines['right'].set_color('tab:red') + #rax.spines['top'].set_visible(False) + #rax.tick_params(axis='y', colors='tab:red') + #rax.set_ylabel("Root-mean-squared error [a.u.]") + #rax.yaxis.label.set_color("tab:red") + ax.legend(frameon=False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_rmse(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + ax.plot(df.number_channels, df.rmse, c='b', lw=3) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.set( + xlabel="Number of channels", + ylabel="Root-mean-squared error [a.u.]", + ) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +if __name__ == '__main__': + indir = 'p900331r69t70' + df = pd.read_csv(f'{indir}/number_channel_effect.csv') + + plot_rmse(df, "rmse.pdf") + plot_unc(df, "unc.pdf") + plot_resolution(df, "resolution.pdf") + diff --git a/pes_to_spec/test/prepare_plots.py b/pes_to_spec/test/prepare_plots.py new file mode 100755 index 0000000000000000000000000000000000000000..3f6ecf64f0b7199a1b69bc22b0293cb649e016a7 --- /dev/null +++ b/pes_to_spec/test/prepare_plots.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python + +import os +import re + +from typing import Optional, Tuple, Dict + +import matplotlib +matplotlib.use('Agg') +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec +import seaborn as sns + +SMALL_SIZE = 12 +MEDIUM_SIZE = 22 +BIGGER_SIZE = 26 + +plt.rc('font', size=BIGGER_SIZE) # controls default text sizes +plt.rc('axes', titlesize=BIGGER_SIZE) # fontsize of the axes title +plt.rc('axes', labelsize=BIGGER_SIZE) # fontsize of the x and y labels +plt.rc('xtick', labelsize=BIGGER_SIZE) # fontsize of the tick labels +plt.rc('ytick', labelsize=BIGGER_SIZE) # fontsize of the tick labels +plt.rc('legend', fontsize=MEDIUM_SIZE) # legend fontsize +plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title + +def plot_final(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + ax.plot(df.energy, df.spec, c='b', lw=3, label="Grating spectrometer") + ax.plot(df.energy, df.prediction, c='r', ls='--', lw=3, label="Prediction") + ax.fill_between(df.energy, df.prediction - 2*df.unc, df.prediction + 2*df.unc, facecolor='gold', alpha=0.5, label="95% unc. (total)") + ax.fill_between(df.energy, df.prediction - 2*df.unc_pca, df.prediction + 2*df.unc_pca, facecolor='magenta', alpha=0.5, label="95% unc. (PCA only)") + Y = np.amax(df.spec) + ax.legend(frameon=False, borderaxespad=0, loc='upper left') + ax.set_title(f"Beam intensity: {df.beam_intensity.iloc[0]:.1f} mJ", loc="left") + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.set( + xlabel="Photon energy [eV]", + ylabel="Intensity [a.u.]", + ylim=(0, 1.3*Y)) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_chi2(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + sns.histplot(x=df.chi2_prepca/df.ndof.iloc[0], kde=True, linewidth=3, ax=ax) + ax.set(title=f"", + xlabel=r"$\chi^2/$ndof", + ylabel="Counts [a.u.]", + xlim=(0, 5), + ) + fig.savefig(filename) + plt.close(fig) + +def plot_rmse(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + sns.histplot(x=df.rmse, kde=True, linewidth=3, ax=ax) + ax.set(title=f"", + xlabel=r"Root-mean-square-error", + ylabel="Counts [a.u.]", + ) + fig.savefig(filename) + plt.close(fig) + +def plot_residue(df: pd.DataFrame, filename: str): + cols = [k for k in df.columns if "res_prepca" in k] + df_res = df.loc[:, cols] + n_plots = len(df_res.columns)//10 + fig = plt.figure(figsize=(8*n_plots, 8)) + gs = GridSpec(1, n_plots) + for i_plot in range(n_plots): + ax = fig.add_subplot(gs[0, i_plot]) + sns.kdeplot(data={f"Dim. {k+1}": df_res.loc[:, cols[k]] for k in range(i_plot*10, i_plot*10 + 10)}, + linewidth=3, ax=ax) + ax.set(title=f"", + xlabel=r"residue/uncertainty [a.u.]", + ylabel="Counts [a.u.]", + xlim=(-3, 3), + ) + ax.legend(frameon=False) + fig.savefig(filename) + plt.close(fig) + +def plot_residue_corr(df: pd.DataFrame, filename: str): + cols = [k for k in df.columns if "res_prepca" in k] + df_res = df.loc[:, cols] + df_res.columns = [re.match(r"res_prepca_([0-9]*)", k).groups()[0] for k in df_res.columns] + fig = plt.figure(figsize=(8, 8)) + corr = df_res.corr() + mask = np.triu(np.ones_like(corr, dtype=bool)) + cmap = sns.diverging_palette(230, 20, as_cmap=True) + sns.heatmap(corr, mask=mask, cmap=cmap, center=0, + square=True, linewidths=0.5, vmin=-1, vmax=1) + fig.savefig(filename) + plt.close(fig) + +def plot_chi2_intensity(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + sns.kdeplot(x=df.chi2_prepca/df.ndof.iloc[0], y=df.xgm_flux_t*1e-3, + fill=True, + ax=ax) + sns.scatterplot(x=df.chi2_prepca/df.ndof.iloc[0], y=df.xgm_flux_t*1e-3, + s=5, + alpha=0.4, + c="tab:red", + #size=df.root_mean_squared_pca_unc, + #sizes=(20, 200), + ax=ax) + ax = plt.gca() + ax.set(title=f"", + xlabel=r"$\chi^2/$ndof", + ylabel="Beam intensity [mJ]", + xlim=(0, 5), + ylim=(0, df.xgm_flux_t.mean()*1e-3 + 3*df.xgm_flux_t.std()*1e-3) + ) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_rmse_intensity(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + sns.kdeplot(x=df.rmse, y=df.xgm_flux_t*1e-3, + fill=True, + ax=ax) + sns.scatterplot(x=df.rmse, y=df.xgm_flux_t*1e-3, + s=5, + alpha=0.4, + c="tab:red", + #size=df.root_mean_squared_pca_unc, + #sizes=(20, 200), + ax=ax) + ax = plt.gca() + ax.set(title=f"", + xlabel=r"Root-mean-squared error [a.u.]", + ylabel="Beam intensity [mJ]", + ylim=(0, df.xgm_flux_t.mean()*1e-3 + 3*df.xgm_flux_t.std()*1e-3) + ) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_unc_intensity(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + sns.kdeplot(x=df.total_unc, y=df.xgm_flux_t*1e-3, + fill=True, + ax=ax) + sns.scatterplot(x=df.total_unc, y=df.xgm_flux_t*1e-3, + s=5, + alpha=0.4, + c="tab:red", + #size=df.root_mean_squared_pca_unc, + #sizes=(20, 200), + ax=ax) + ax = plt.gca() + ax.set(title=f"", + xlabel=r"Uncertainty [a.u.]", + ylabel="Beam intensity [mJ]", + ylim=(0, df.xgm_flux_t.mean()*1e-3 + 3*df.xgm_flux_t.std()*1e-3) + ) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_unc_rmse(df: pd.DataFrame, filename: str): + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + sns.kdeplot(x=2*df.total_unc, y=df.rmse, + fill=True, + ax=ax) + sns.scatterplot(x=2*df.total_unc, y=df.rmse, + s=5, + alpha=0.4, + c="tab:red", + #size=df.root_mean_squared_pca_unc, + #sizes=(20, 200), + ax=ax) + ax = plt.gca() + ax.set(title=f"", + xlabel=r"Root-mean-squared unc. [a.u.]", + ylabel="Root-mean-squared error [a.u.]", + ) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def pca_variance_plot(df: pd.DataFrame, filename: str, max_comp_frac: float=0.99): + """ + Plot variance contribution. + + Args: + filename: Output file name. + variance_ratio: Contribution of each component's variance. + + """ + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + c = np.cumsum(df.variance_ratio) + n_comp = int(df.n_comp.iloc[0]) + ax.bar(1+np.arange(len(df.variance_ratio)), df.variance_ratio*100, color='tab:red', alpha=0.3, label="Per component") + ax.plot(1+np.arange(len(df.variance_ratio)), c*100, c='tab:blue', lw=5, label="Cumulative") + ax.plot([n_comp, n_comp], [0, c[n_comp]*100], lw=3, ls='--', c='m', label="Components kept") + ax.plot([0, n_comp], [c[n_comp]*100, c[n_comp]*100], lw=3, ls='--', c='m') + ax.legend(frameon=False) + print(f"PCA plot: total n. components: {len(df.variance_ratio)}") + x_max = np.where(c > max_comp_frac)[0][0] + print(f"Fraction of variance: {c[n_comp]}") + ax.set_yscale('log') + ax.set(title=f"", + xlabel="Component", + ylabel="Variance contribution [%]", + xlim=(1, x_max), + ylim=(0.01, 100)) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def moving_average(a, n=3): + ret = np.cumsum(a) + ret[n:] = ret[n:] - ret[:-n] + return ret[n - 1:] / n + +def plot_impulse(df: pd.DataFrame, filename: str): + """ + Plot variance contribution. + + Args: + filename: Output file name. + variance_ratio: Contribution of each component's variance. + + """ + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + x = df.wiener_energy.to_numpy() + y = np.absolute(df.impulse.to_numpy()) + #x_new = np.linspace(-6, 6, 601) + #spl = make_interp_spline(x, np.log10(y), k=3) + #y_new = np.power(10, spl(x_new)) + x_new = moving_average(x, n=5) + y_new = moving_average(y, n=5) + sel = (x_new >= -5.1) & (x_new <= 5.1) + ax.plot(x_new[sel], y_new[sel], c='tab:blue', lw=3) + ax.set_yscale('log') + ax.set(title=f"", + xlabel="Energy [eV]", + ylim=(1e-4, 0.4), + ylabel="Response [a.u.]", + ) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_wiener(df: pd.DataFrame, filename: str): + """ + Plot variance contribution. + + Args: + filename: Output file name. + variance_ratio: Contribution of each component's variance. + + """ + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + ax.plot(df.wiener_energy, np.absolute(df.wiener_filter), c='tab:blue', lw=3) + ax.set_yscale('log') + ax.set(title=f"", + xlabel="Energy [eV]", + ylim=(1e-3, 1), + ylabel="Response [a.u.]", + ) + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +def plot_pes(df: pd.DataFrame, channel: Dict[str, int], filename: str, fast_range: Optional[Tuple[int, int]]=None, Ne1s: Optional[Tuple[int, int]]=None, label: Optional[Dict[str, str]]=None, refs: Optional[Dict[str, Dict[int, float]]]=None, counts_to_mv: Optional[float]=None): + """ + Plot low-resolution spectrum. + + Args: + filename: Output file name. + pes_raw_int: Low-resolution spectrum. + + """ + fig = plt.figure(figsize=(12, 8)) + gs = GridSpec(1, 1) + ax = fig.add_subplot(gs[0, 0]) + first, last = df.loc[:, 'first'].iloc[0], df.loc[:, 'last'].iloc[0] + first = first+220 + last = last-270 + print("Range:", first, last) + sel = (df.bin >= first) & (df.bin < last) + x = df.loc[sel, "bin"].to_numpy() + col = dict() + colors = ["tab:red", "tab:blue"] + p = list() + # plot each channel + for ich, ch in enumerate(channel.keys()): + if label is None: + sch = ch.replace('_', '')[-2:] + else: + sch = label[ch] + y = df.loc[sel, ch].to_numpy().astype(np.float32) + if counts_to_mv is not None: + y *= counts_to_mv + c = colors[ich] + col[ch] = c + p += [ax.plot(x, y, lw=2, c=c, label=sch)] + ax.set(title=f"", + ylim=(0, None), + #xlabel="Time-of-flight index", + xlabel="Samples", + ylabel="Counts [a.u.]" if counts_to_mv is None else "Digitizer reading [mV]") + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + minY, maxY = ax.get_ylim() + # show reference energy lines + if refs is not None: + for ich, ch in enumerate(channel.keys()): + for tof, energy in refs[ch].items(): + ax.axvline(tof, 0, 0.5 + ich*0.17, ls='-.', lw=1, c=col[ch]) + ax.text(tof-1, (0.51 + ich*0.18)*maxY, f"{energy} eV", fontsize=14, rotation="vertical", color=col[ch]) + # show prompt line + for ch, prompt in channel.items(): + ax.axvline(x=prompt, ls='--', lw=1, c=col[ch]) + ax.text(prompt-3, 0.5*maxY, "Prompt", fontsize=16, rotation="vertical", color=col[ch]) + # show the fast electrons range + if fast_range is not None: + x1, x2 = fast_range + xtext = int(x1 + (x2 - x1)*0.3) + ytext = 0.9*maxY + ax.fill_between([x1, x2], minY, maxY, alpha=0.2, facecolor="tab:olive") + ax.text(xtext, ytext, "Valence", fontsize=18, fontweight='bold') + ax.text(xtext, ytext-0.05*maxY, "Auger", fontsize=18, fontweight='bold') + # show the Ne 1s range + if Ne1s is not None: + x1, x2 = Ne1s + xtext = int(x1 + (x2 - x1)*0.3) + ytext = 0.9*maxY + ax.fill_between([x1, x2], minY, maxY, alpha=0.2, facecolor="tab:cyan") + ax.text(xtext, ytext, "Ne 1s", fontsize=22, fontweight='bold') + ns_per_sample = 0.5 + cax = dict() + def f_(ch): + return (lambda kk: (np.array(kk) - int(channel[ch]))*ns_per_sample) + def i_(ch): + return (lambda kk: np.array(kk)/ns_per_sample + int(channel[ch])) + forward_ = {ch: f_(ch) for ch in channel} + inverse_ = {ch: i_(ch) for ch in channel} + for ich, (ch, prompt) in enumerate(channel.items()): + cax[ch] = ax.secondary_xaxis(1.0+0.07*ich, functions=(forward_[ch], inverse_[ch])) + #cax[ch].spines['left'].set_visible(False) + cax[ch].spines['top'].set_position(('outward', 10)) + cax[ch].spines['top'].set_color(col[ch]) + cax[ch].tick_params(axis='x', colors=col[ch], labelsize=16) + if ich == len(channel)-1: + cax[ch].set_xlabel('Time-of-flight [ns]', fontsize=16) + #cax[ch].xaxis.label.set_color(col[ch]) + #cax[ch].title.set_color(col[ch]) + ax.legend(frameon=False, loc='center') + plt.tight_layout() + fig.savefig(filename) + plt.close(fig) + +if __name__ == '__main__': + indir = 'p900331r69t70' + channel = {'channel_4_A': 2639, + 'channel_3_B': 2646, + } + label = {'channel_4_A': r'22.5$^\circ$', + 'channel_3_B': r'225$^\circ$', + } + Ne1s = (2710, 2742) + fast_range = (2650, 2670) + refs={'channel_4_A': {2716:1002.5, 2722:997.5}, + 'channel_3_B': {2723:1002.5, 2729:997.5} + } + counts_to_mv = 40.0/100.0 + #channel = 'sum' + #for fname in os.listdir(indir): + # if re.match(r'test_q100_[0-9]*\.csv', fname): + # fname = fname[:-4] + # print(f"Plotting {fname}") + # plot_final(pd.read_csv(f'{indir}/{fname}.csv'), f'{fname}.pdf') + # plot_pes(pd.read_csv(f'{indir}/{fname}_pes.csv'), channel, f'{fname}_pes.pdf') + + for fname in ('test_q100_1724098413', 'test_q100_1724098596', 'test_q50_1724099445'): + plot_final(pd.read_csv(f'{indir}/{fname}.csv'), f'{fname}.pdf') + plot_pes(pd.read_csv(f'{indir}/{fname}_pes.csv'), channel, f'{fname}_pes.pdf', + fast_range=fast_range, Ne1s=Ne1s, label=label, refs=refs, + counts_to_mv=counts_to_mv) + + plot_chi2(pd.read_csv(f'{indir}/quality.csv'), f'chi2_prepca.pdf') + plot_chi2_intensity(pd.read_csv(f'{indir}/quality.csv'), f'intensity_vs_chi2_prepca.pdf') + plot_unc_intensity(pd.read_csv(f'{indir}/quality.csv'), f'intensity_vs_unc.pdf') + plot_unc_rmse(pd.read_csv(f'{indir}/quality.csv'), f'rmse_vs_unc.pdf') + plot_rmse(pd.read_csv(f'{indir}/quality.csv'), f'rmse.pdf') + plot_rmse_intensity(pd.read_csv(f'{indir}/quality.csv'), f'intensity_vs_rmse.pdf') + + plot_residue(pd.read_csv(f'{indir}/quality.csv'), f'residue.pdf') + plot_residue_corr(pd.read_csv(f'{indir}/quality.csv'), f'residue_corr.pdf') + + df_model = pd.read_csv(f'{indir}/model.csv') + df_model.impulse = df_model.impulse.str.replace('i','j').apply(lambda x: np.complex(x)) + df_model.wiener_filter = df_model.wiener_filter.str.replace('i','j').apply(lambda x: np.complex(x)) + plot_impulse(df_model, f'impulse.pdf') + plot_wiener(df_model, f'wiener.pdf') + + pca_variance_plot(pd.read_csv(f'{indir}/pca_spec.csv'), f'pca_spec.pdf', max_comp_frac=0.99) + pca_variance_plot(pd.read_csv(f'{indir}/pca_pes.csv'), f'pca_pes.pdf', max_comp_frac=0.95) + diff --git a/pyproject.toml b/pyproject.toml index 86ea8d577381b8e30672742abe3d63e0d5ff09b6..9ea807b4f8cce6209cbb409d43aec2cc6e905656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ dependencies = [ "scipy>=1.6", "scikit-learn>=1.2.0", "torch", - "torchbnn", ] [project.optional-dependencies] diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000000000000000000000000000000000000..41fb02678541b9814f347b9666bc11c9658bf56a --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,71 @@ +#!/bin/bash +#SBATCH --partition=exfel +#SBATCH --nodes=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --time=8:00:00 +#SBATCH --job-name=pes2spec +#SBATCH -o slurm.%x.err.txt +#SBATCH -e slurm.%x.err.txt +#SBATCH --reservation=exfel_ml + +optstring="d:" + +DIR=results +while getopts ${optstring} arg; do + case ${arg} in + d) + DIR=${OPTARG} + S=$OPTIND + ;; + *) + S=$((OPTIND)) + break + ;; + esac +done +OPTS="${@:$S}" +echo "Options: $OPTS" + +source /usr/share/Modules/init/sh + +module load exfel exfel_anaconda3 + +cd $HOME/scratch/karabo/devices/pes_to_spec +source env/bin/activate +pwd +export PYTHONPATH=$PYTHONPATH:$PWD + +mkdir $DIR + +do_it() { + p=$1 + r=$2 + rt=$3 + output=$DIR/p${p}r${r}t${rt} + mkdir -p $output + echo "Proposal $p, run $r, test at run $rt" + CMD=(./pes_to_spec/test/offline_analysis.py -p $p -r $r -t $rt -d $output ${@:4}) + echo "${CMD[*]}" + ${CMD[*]} 2>&1 | tee $output/log.txt +} + +do_it 900331 69 70 $OPTS + +# train in run 2 and test in run 3 +#do_it 3384 2 3 $OPTS + +# new runs: +#for run in 2 4 +#do +# do_it 3384 $run $run $OPTS +#done + +# train in run 4 and test in run 3 +#do_it 3384 4 3 $OPTS + +# old run +#do_it 2828 206 206 $OPTS +#do_it 2828 206 207 $OPTS + +#do_it 2828 207 207 $OPTS + diff --git a/run_tests_number_channels.sh b/run_tests_number_channels.sh new file mode 100644 index 0000000000000000000000000000000000000000..3609bfb528e88460d60f043fb0bbcc7802a6ac84 --- /dev/null +++ b/run_tests_number_channels.sh @@ -0,0 +1,53 @@ +#!/bin/bash +#SBATCH --partition=exfel +#SBATCH --nodes=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --time=8:00:00 +#SBATCH --job-name=nch_pes2spec +#SBATCH -o slurm.%x.err.txt +#SBATCH -e slurm.%x.err.txt +#SBATCH --reservation=exfel_ml + +optstring="d:" + +DIR=results +while getopts ${optstring} arg; do + case ${arg} in + d) + DIR=${OPTARG} + S=$OPTIND + ;; + *) + S=$((OPTIND)) + break + ;; + esac +done +OPTS="${@:$S}" +echo "Options: $OPTS" + +source /usr/share/Modules/init/sh + +module load exfel exfel_anaconda3 + +cd $HOME/scratch/karabo/devices/pes_to_spec +source env/bin/activate +pwd +export PYTHONPATH=$PYTHONPATH:$PWD + +mkdir $DIR + +do_it() { + p=$1 + r=$2 + rt=$3 + output=$DIR/p${p}r${r}t${rt} + mkdir -p $output + echo "Proposal $p, run $r, test at run $rt" + CMD=(./pes_to_spec/test/channel_sensitivity.py -p $p -r $r -t $rt -d $output ${@:4}) + echo "${CMD[*]}" + ${CMD[*]} 2>&1 | tee $output/log.txt +} + +do_it 900331 69 70 $OPTS +