Copy over code
Correct linting
This commit is contained in:
881
src/AMBER/background.py
Normal file
881
src/AMBER/background.py
Normal file
@ -0,0 +1,881 @@
|
|||||||
|
import numpy as np
|
||||||
|
import scipy
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
# graph Laplacian functions
|
||||||
|
from graph_laplacian import laplacian, unnormalized_laplacian, unnormalized_laplacian_chain, \
|
||||||
|
laplacian_chain, remove_vertex
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
|
||||||
|
class background():
|
||||||
|
"""Background estimation for a given 3D dataset"""
|
||||||
|
|
||||||
|
# range of bins
|
||||||
|
r_range = None
|
||||||
|
|
||||||
|
# signal values
|
||||||
|
X = None
|
||||||
|
|
||||||
|
# background values
|
||||||
|
b = None
|
||||||
|
b_grid = None
|
||||||
|
|
||||||
|
# define
|
||||||
|
u = None
|
||||||
|
|
||||||
|
# list of Laplacian
|
||||||
|
L_list = None
|
||||||
|
|
||||||
|
def __init__(self, dtype=np.float32):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Initialize the class instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# define type
|
||||||
|
self.dtype = dtype
|
||||||
|
|
||||||
|
def set_gridcell_size(self, dqx=0.03, dqy=0.03, dE=0.1):
|
||||||
|
"""
|
||||||
|
Set the volume voxel resolution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- dqx: Resolution in the qx direction (default is 0.03).
|
||||||
|
- dqy: Resolution in the qy direction (default is 0.03).
|
||||||
|
- dE: Energy resolution (default is 0.1).
|
||||||
|
"""
|
||||||
|
self.dqx = dqx
|
||||||
|
self.dqy = dqy
|
||||||
|
self.dE = dE
|
||||||
|
|
||||||
|
def set_volume_from_limits(self, d_min, d_max):
|
||||||
|
"""
|
||||||
|
Set the volume of the data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- d_min: np.array, Minimum values for the volume.
|
||||||
|
- d_max: np.array, Maximum values for the volume.
|
||||||
|
"""
|
||||||
|
self.d_min = d_min
|
||||||
|
self.d_max = d_max
|
||||||
|
|
||||||
|
# by default the dimensions are Qx, Qy and E
|
||||||
|
self.Qx = np.arange(d_min[0], d_max[0], self.dqx, dtype=self.dtype)
|
||||||
|
self.Qy = np.arange(d_min[1], d_max[1], self.dqy, dtype=self.dtype)
|
||||||
|
self.E = np.arange(d_min[2], d_max[2], self.dE, dtype=self.dtype)
|
||||||
|
|
||||||
|
def set_binned_data(self, Qx, Qy, E, Int):
|
||||||
|
"""
|
||||||
|
Set the grid data for the class instance: X2dgrid, Xgrid and Ygrid.
|
||||||
|
Args:
|
||||||
|
- I: np.array (3D array)
|
||||||
|
Input data array.
|
||||||
|
"""
|
||||||
|
# Get minimum and maximum values for intensity
|
||||||
|
self.fmax = np.max(Int)
|
||||||
|
self.fmin = 0.0 # Setting minimum intensity to 0.0
|
||||||
|
|
||||||
|
# Extract coordinates in (Qx, Qy, E) dimensions
|
||||||
|
self.Qx = Qx
|
||||||
|
self.Qy = Qy
|
||||||
|
self.E = E
|
||||||
|
|
||||||
|
# Extract dimensions
|
||||||
|
self.Qx_size = self.Qx.shape[0]
|
||||||
|
self.Qy_size = self.Qy.shape[0]
|
||||||
|
self.E_size = self.E.shape[0]
|
||||||
|
|
||||||
|
# Construct the grid in 3D
|
||||||
|
self.gridQx, self.gridQy, self.gridE = np.meshgrid(self.Qx, self.Qy, self.E, indexing='ij')
|
||||||
|
self.Xgrid = np.concatenate([self.gridQx.ravel().reshape(-1, 1),
|
||||||
|
self.gridQy.ravel().reshape(-1, 1),
|
||||||
|
self.gridE.ravel().reshape(-1, 1)], axis=1)
|
||||||
|
|
||||||
|
# Construct the grid in 2D
|
||||||
|
self.grid2dQx, self.grid2dQy = np.meshgrid(self.Qx, self.Qy, indexing='ij')
|
||||||
|
self.X2dgrid = np.concatenate([self.grid2dQx.ravel().reshape(-1, 1),
|
||||||
|
self.grid2dQy.ravel().reshape(-1, 1)], axis=1)
|
||||||
|
|
||||||
|
# Flatten the intensity data for the entire volume
|
||||||
|
self.Ygrid = np.copy(Int)
|
||||||
|
self.Ygrid = self.Ygrid.reshape(-1, self.Ygrid.shape[-1]).T
|
||||||
|
|
||||||
|
def set_variables(self):
|
||||||
|
"""
|
||||||
|
Initialize variable values.
|
||||||
|
"""
|
||||||
|
# Define signal values as zeros
|
||||||
|
self.X = np.zeros_like(self.Ygrid, dtype=self.dtype)
|
||||||
|
|
||||||
|
# Define background values as zeros
|
||||||
|
self.b = np.zeros((self.E_size, self.n_bins), dtype=self.dtype)
|
||||||
|
|
||||||
|
# Initialize the values of the propagated background as zeros
|
||||||
|
self.b_grid = np.zeros_like(self.Ygrid, dtype=self.dtype)
|
||||||
|
|
||||||
|
def set_radial_bins(self, max_radius=6.0, n_bins=100):
|
||||||
|
"""
|
||||||
|
Define ranges for radial bins.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- max_radius: float
|
||||||
|
Maximum radial distance for the bins.
|
||||||
|
- n_bins: int
|
||||||
|
Number of bins to create.
|
||||||
|
"""
|
||||||
|
# Set the maximum radial distance and the number of bins
|
||||||
|
self.max_radius = max_radius
|
||||||
|
self.n_bins = n_bins
|
||||||
|
|
||||||
|
# Generate radial bins using torch linspace
|
||||||
|
# The range starts from 0 and goes up to the square root of max_radius, creating n_bins+1 points
|
||||||
|
self.r_range = np.linspace(0, self.max_radius, self.n_bins + 1, dtype=self.dtype)
|
||||||
|
|
||||||
|
def R_operator(self, b):
|
||||||
|
"""
|
||||||
|
Take the current background estimation and propagate it in 3D.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- Y_r: np.array
|
||||||
|
Array representing the 2D grid of the signal.
|
||||||
|
- b: np.array
|
||||||
|
Array representing the background estimation.
|
||||||
|
- e_cut: int or None
|
||||||
|
If specified, represents the index of the energy cut; otherwise, None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Array representing the propagated background.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the set of radii
|
||||||
|
# Vector of aggregated values
|
||||||
|
rX = np.sqrt(self.Xgrid[:, 0] ** 2 + self.Xgrid[:, 1] ** 2)
|
||||||
|
|
||||||
|
# Compute the indices of grid vector in a range of radial values
|
||||||
|
indices = np.digitize(rX, self.r_range) - 1
|
||||||
|
indices[indices == self.n_bins] -= 1
|
||||||
|
|
||||||
|
# Store the values at each index location
|
||||||
|
b_grid = np.ones((self.E_size, self.Qx_size * self.Qy_size), dtype=self.dtype)
|
||||||
|
indices_tmp = indices.T.reshape(self.Qx_size, self.Qy_size, self.E_size)
|
||||||
|
indices_tmp = indices_tmp.reshape(-1, self.E_size).T
|
||||||
|
|
||||||
|
for e in range(self.E_size):
|
||||||
|
b_grid[e, :] = b[e, indices_tmp[e, :]]
|
||||||
|
|
||||||
|
# Return the values corresponding to the bin
|
||||||
|
b_grid = b_grid.T.reshape(self.Qx_size, self.Qy_size, self.E_size)
|
||||||
|
b_grid = b_grid.reshape(-1, self.E_size).T
|
||||||
|
|
||||||
|
return b_grid
|
||||||
|
|
||||||
|
def Rstar_operator(self, X):
|
||||||
|
"""
|
||||||
|
Compute the aggregated value of X on a radial line. Basically, sum for each radial bin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- Y_r: np.array
|
||||||
|
Array representing the signal value on a grid.
|
||||||
|
- X: np.array
|
||||||
|
Array representing the signal values on the grid (size_E, size_Qx * size_Qy * size_E).
|
||||||
|
- e_cut: int or None
|
||||||
|
If specified, represents the index of the energy cut; otherwise, None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Array representing the aggregated values on the 2D plane (size_E x nb_radial_bins).
|
||||||
|
"""
|
||||||
|
# Define the set of radius
|
||||||
|
rX = np.sqrt(self.Xgrid[:, 0] ** 2 + self.Xgrid[:, 1] ** 2)
|
||||||
|
|
||||||
|
# Compute the indices of grid vector in a range of radial values
|
||||||
|
indices = np.digitize(rX, self.r_range) - 1
|
||||||
|
indices[indices == self.n_bins] -= 1
|
||||||
|
|
||||||
|
indices_tmp = indices.T.reshape(self.Qx_size, self.Qy_size, self.E_size)
|
||||||
|
indices_tmp = indices_tmp.reshape(-1, self.E_size).T
|
||||||
|
|
||||||
|
v_agg = np.zeros((X.shape[0], self.n_bins), dtype=self.dtype)
|
||||||
|
|
||||||
|
for i, r in enumerate(range(self.n_bins)):
|
||||||
|
for e in range(self.E_size):
|
||||||
|
if np.nansum(indices_tmp[e, :] == i) > 0:
|
||||||
|
v_agg[e, i] = np.nansum(X[e, indices_tmp[e, :] == i])
|
||||||
|
|
||||||
|
return v_agg
|
||||||
|
|
||||||
|
def Radial_mean_operator(self, X):
|
||||||
|
"""
|
||||||
|
Returns the background define as a single line given a background defined on a 3D grid (or 2D grid if e_cut is given).
|
||||||
|
This function is used to define the background to plot within plotQvE function.
|
||||||
|
Args:
|
||||||
|
- Y_r: np.array
|
||||||
|
Array representing the signal value on a grid.
|
||||||
|
- X: np.array
|
||||||
|
Array representing the signal values on the grid (size_E, size_Qx * size_Qy).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Array corresponding to the mean values on 2D grid (size_E x nb_radial_bins).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the set of radius
|
||||||
|
rX = np.sqrt(self.Xgrid[:, 0] ** 2 + self.Xgrid[:, 1] ** 2)
|
||||||
|
|
||||||
|
# Compute the indices of grid vector in a range of radial values
|
||||||
|
indices = np.digitize(rX, self.r_range) - 1
|
||||||
|
indices[indices == self.n_bins] -= 1
|
||||||
|
|
||||||
|
indices_tmp = indices.T.reshape(self.Qx_size, self.Qy_size, self.E_size)
|
||||||
|
indices_tmp = indices_tmp.reshape(-1, self.E_size).T
|
||||||
|
|
||||||
|
v_agg = np.zeros((X.shape[0], self.n_bins), dtype=self.dtype)
|
||||||
|
|
||||||
|
for i, r in enumerate(range(self.n_bins)):
|
||||||
|
for e in range(self.E_size):
|
||||||
|
if np.nansum(indices_tmp[e, :] == i) > 0:
|
||||||
|
v_agg[e, i] = np.nanmean(X[e, indices_tmp[e, :] == i])
|
||||||
|
|
||||||
|
return v_agg
|
||||||
|
|
||||||
|
def Radial_quantile_operator(self, X, q=0.75):
|
||||||
|
"""
|
||||||
|
Returns the background define as a single line given a background defined on a 3D grid (or 2D grid if e_cut is given).
|
||||||
|
This function is used to define the background to plot within plotQvE function.
|
||||||
|
Args:
|
||||||
|
- Y_r: np.array
|
||||||
|
Array representing the signal value on a grid.
|
||||||
|
- X: np.array
|
||||||
|
Array representing the signal values on the grid (size_E, size_Qx * size_Qy * size_E).
|
||||||
|
- e_cut: int or None
|
||||||
|
If specified, represents the index of the energy cut; otherwise, None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Array corresponding to the mean values on 2D grid (size_E x nb_radial_bins).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the set of radius
|
||||||
|
rX = np.sqrt(self.Xgrid[:, 0] ** 2 + self.Xgrid[:, 1] ** 2)
|
||||||
|
|
||||||
|
# Compute the indices of grid vector in a range of radial values
|
||||||
|
indices = np.digitize(rX, self.r_range) - 1
|
||||||
|
indices[indices == self.n_bins] -= 1
|
||||||
|
|
||||||
|
indices_tmp = indices.T.reshape(self.Qx_size, self.Qy_size, self.E_size)
|
||||||
|
indices_tmp = indices_tmp.reshape(-1, self.E_size).T
|
||||||
|
|
||||||
|
v_agg = np.zeros((X.shape[0], self.n_bins), dtype=self.dtype)
|
||||||
|
|
||||||
|
for i, r in enumerate(range(self.n_bins)):
|
||||||
|
for e in range(self.E_size):
|
||||||
|
if np.nansum(indices_tmp[e, :] == i) > 0:
|
||||||
|
v_agg[e, i] = np.nanquantile(X[e, indices_tmp[e, :] == i], q=q)
|
||||||
|
|
||||||
|
return v_agg
|
||||||
|
|
||||||
|
def mask_nans(self, x):
|
||||||
|
"""
|
||||||
|
Mask out the image pixels where the observations are NaNs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- x: np.array
|
||||||
|
Input tensor (size_E x (size_Qx * size_Qy)).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Output tensor with NaNs (size_E x (size_Qx * size_Qy)).
|
||||||
|
"""
|
||||||
|
# Find non-NaN indices
|
||||||
|
Nan_indices = np.where(np.isnan(self.Ygrid) == True)
|
||||||
|
Unan_idx = np.concatenate([Nan_indices[0].reshape(-1, 1), Nan_indices[1].reshape(-1, 1)], axis=1)
|
||||||
|
|
||||||
|
x_temp = np.copy(x)
|
||||||
|
x_temp[Unan_idx[:, 0], Unan_idx[:, 1]] = float('nan')
|
||||||
|
|
||||||
|
return x_temp
|
||||||
|
|
||||||
|
def S_lambda(self, x, lambda_):
|
||||||
|
"""
|
||||||
|
Define the soft-thresholding function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- x: np.array
|
||||||
|
Input tensor.
|
||||||
|
- lambda_: np.array
|
||||||
|
Threshold parameter.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Output tensor after applying the soft-thresholding function.
|
||||||
|
"""
|
||||||
|
return np.sign(x) * np.maximum(np.abs(x) - lambda_, 0.0)
|
||||||
|
|
||||||
|
def L_b(self, e_cut, normalized=True, method='inverse'):
|
||||||
|
"""
|
||||||
|
Define Laplacian matrix L_b of a chain along the radial line. (number of vertices = number of bins)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- e_cut: int
|
||||||
|
Index of the energy cut.
|
||||||
|
- normalized: bool
|
||||||
|
Whether to use a normalized Laplacian matrix or not.
|
||||||
|
- method: str
|
||||||
|
Method for computing unormalized Laplacian matrix ('inverse' or 'eigen').
|
||||||
|
'inverse' computes the inverse
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Output tensor representing the Toeplitz matrix of a discrete differentiator filter.
|
||||||
|
"""
|
||||||
|
# Identify the nonzero elements
|
||||||
|
u_cut = self.u[e_cut, :]
|
||||||
|
idx_z = list(np.where(u_cut < 0.1)[0].ravel())
|
||||||
|
idx_nz = list(np.where(u_cut > 0.0)[0].ravel())
|
||||||
|
|
||||||
|
if normalized:
|
||||||
|
# Define the Laplacian matrix of a chain
|
||||||
|
D = scipy.sparse.csr_matrix(laplacian_chain(self.n_bins))
|
||||||
|
|
||||||
|
# Remove the vertices
|
||||||
|
D = remove_vertex(D, lst_rows=idx_z, lst_cols=idx_z).toarray()
|
||||||
|
L = np.zeros((self.n_bins, self.n_bins))
|
||||||
|
L[np.ix_(idx_nz, idx_nz)] = D
|
||||||
|
else:
|
||||||
|
v_agg = self.Radial_mean_operator(self.Ygrid)
|
||||||
|
D = unnormalized_laplacian_chain(v_agg[e_cut, :], self.n_bins, method)
|
||||||
|
L = D.toarray()
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
def gamma_matrix(self, e_cut=None):
|
||||||
|
"""
|
||||||
|
Define the gamma matrix, which corresponds to the matrix associated with the operator R^*R.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- e_cut: int, default=None
|
||||||
|
Index of the energy cut.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Gamma matrix defined as the number of measurements for each radial bin.
|
||||||
|
The number of non-NaNs values along the wedges for each radius.
|
||||||
|
"""
|
||||||
|
gamma = np.ones(self.n_bins, dtype=self.dtype)
|
||||||
|
|
||||||
|
if e_cut is not None:
|
||||||
|
# Extract the 2D slice of Ygrid for the specified energy cut
|
||||||
|
Ygrid2D = self.Ygrid[e_cut, :]
|
||||||
|
grid = self.X2dgrid[np.isnan(Ygrid2D) == False]
|
||||||
|
|
||||||
|
# Define the set of radii
|
||||||
|
r = np.sqrt(grid[:, 0] ** 2 + grid[:, 1] ** 2)
|
||||||
|
|
||||||
|
# Compute the histogram of the matrix X
|
||||||
|
histo = np.histogram(r, self.r_range)
|
||||||
|
gamma = histo[0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Define the set of radii
|
||||||
|
r = np.sqrt(self.X2dgrid[:, 0] ** 2 + self.X2dgrid[:, 1] ** 2)
|
||||||
|
|
||||||
|
# Compute the histogram of the matrix X
|
||||||
|
histo = np.histogram(r, self.r_range)
|
||||||
|
gamma = histo[0]
|
||||||
|
|
||||||
|
gamma_mat = np.diag(gamma)
|
||||||
|
return gamma_mat
|
||||||
|
|
||||||
|
def set_radial_nans(self, Y):
|
||||||
|
"""
|
||||||
|
Define the radial nans given the observation matrix self.Ygrid
|
||||||
|
|
||||||
|
Args: None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a binary map that indicates the NaNs of self.Ygrid
|
||||||
|
z = np.zeros_like(Y, dtype=self.dtype)
|
||||||
|
z[np.isnan(Y) == False] += 1
|
||||||
|
z[np.isnan(Y) == True] = float('nan')
|
||||||
|
|
||||||
|
# Define a self.E_size x self.n_bins matrix counting the number of measurement for each radial bin
|
||||||
|
self.u = self.Rstar_operator(z)
|
||||||
|
|
||||||
|
# Doesn't count the radial bin containing less than 2 measurement points
|
||||||
|
self.u[self.u < 2.0] = 0.0
|
||||||
|
|
||||||
|
def set_b_design_matrix(self, beta_, e_cut, normalized=True, method='inverse'):
|
||||||
|
"""
|
||||||
|
Define the inverted design matrix of the problem: (Γ + β D)^{-1}.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- beta_: background smoothness penalty coefficient (float64)
|
||||||
|
- e_cut: Int, default=None
|
||||||
|
Index of the energy cut.
|
||||||
|
- normalized: Boolean, default=True
|
||||||
|
Flag to determine whether to use a normalized Laplacian.
|
||||||
|
- method: String, default='inverse'
|
||||||
|
Method to compute Laplacian ('inverse' or 'eigen').
|
||||||
|
"""
|
||||||
|
# Compute gamma matrix
|
||||||
|
gam = self.gamma_matrix(e_cut)
|
||||||
|
|
||||||
|
# Compute L_b matrix
|
||||||
|
L = self.L_b(e_cut, normalized, method)
|
||||||
|
|
||||||
|
# Compute the inverse of the matrix
|
||||||
|
|
||||||
|
if self.u is None:
|
||||||
|
self.set_radial_nans(self.Ygrid)
|
||||||
|
else:
|
||||||
|
u_cut = self.u[e_cut, :]
|
||||||
|
# idx_z = list(np.where(u_cut < 0.1)[0].ravel())
|
||||||
|
idx_nz = list(np.where(u_cut > 0.0)[0].ravel())
|
||||||
|
|
||||||
|
# Define identity matrix for non zero element
|
||||||
|
W = np.eye(self.n_bins)
|
||||||
|
|
||||||
|
# Compute the matrix to invert for background update (Γ + β L_b + \alpha I)^{-1}
|
||||||
|
A = beta_ * L[np.ix_(idx_nz, idx_nz)] + gam[np.ix_(idx_nz, idx_nz)]
|
||||||
|
W[np.ix_(idx_nz, idx_nz)] = A
|
||||||
|
|
||||||
|
return W.copy()
|
||||||
|
|
||||||
|
def compute_laplacian(self, Y_r, normalized=True, method='inverse'):
|
||||||
|
"""
|
||||||
|
Compute the Graph Laplacian of the problem.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- Y_r: np.array
|
||||||
|
Signal values on the grid.
|
||||||
|
- normalized: Boolean, default=True
|
||||||
|
Flag to determine whether to use a normalized Laplacian.
|
||||||
|
- method: String, default='inverse'
|
||||||
|
Method to compute Laplacian ('inverse' or 'eigen').
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Graph Laplacian matrix.
|
||||||
|
"""
|
||||||
|
# If method is true, then compute the graph laplacian
|
||||||
|
if normalized:
|
||||||
|
L = laplacian(Y_r, self.Qx_size, self.Qy_size)
|
||||||
|
else:
|
||||||
|
L = unnormalized_laplacian(Y_r, self.Qx_size, self.Qy_size, method)
|
||||||
|
|
||||||
|
L = L.toarray()
|
||||||
|
return L
|
||||||
|
|
||||||
|
def compute_all_laplacians(self, Y_r, normalized=True, method='inverse'):
|
||||||
|
"""
|
||||||
|
Compute the graph Laplacians for each energy cut.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- Y_r: np.array
|
||||||
|
Signal values on the grid.
|
||||||
|
- normalized: Boolean, default=True
|
||||||
|
Flag to determine whether to use a normalized Laplacian.
|
||||||
|
- method: String, default='inverse'
|
||||||
|
Method to compute Laplacian ('inverse' or 'eigen').
|
||||||
|
"""
|
||||||
|
self.L_list = []
|
||||||
|
|
||||||
|
for e in range(self.E_size):
|
||||||
|
self.L_list.append(self.compute_laplacian(Y_r[e, :], normalized, method))
|
||||||
|
|
||||||
|
def TV_denoising(self, X, gamma_, n_epochs=10, verbose=True):
|
||||||
|
"""
|
||||||
|
Iterative reweighted least square solve the total variation denoising Problem.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- gamma_: torch.tensor(float64)
|
||||||
|
Penalty coefficient for denoising level
|
||||||
|
- n_epochs: Int (integer)
|
||||||
|
Number of iterations
|
||||||
|
- verbose: Bool
|
||||||
|
Display convergence logs
|
||||||
|
"""
|
||||||
|
|
||||||
|
# initialize the signal values
|
||||||
|
X_denoised = X.copy()
|
||||||
|
|
||||||
|
#################################
|
||||||
|
|
||||||
|
# loop over the energy cuts
|
||||||
|
for e in range(self.E_size)[5:]:
|
||||||
|
|
||||||
|
# Extract the energy cut values
|
||||||
|
X_l = X[e, :].detach().clone()
|
||||||
|
|
||||||
|
# set notnan indices
|
||||||
|
idx_notnan = np.where(np.isnan(X_l) == False)[0]
|
||||||
|
Y_r = X[e, idx_notnan]
|
||||||
|
|
||||||
|
# loss function
|
||||||
|
loss = 0.0
|
||||||
|
|
||||||
|
# start loop of iterative reweighted least square
|
||||||
|
for it in range(n_epochs):
|
||||||
|
|
||||||
|
# compute the laplacian
|
||||||
|
L_tmp = self.compute_laplacian(X_l, normalized=False, method='inverse')
|
||||||
|
L_tmp = np.eye(L_tmp.shape[0], dtype=self.dtype) + gamma_[e] * L_tmp # add the regularization
|
||||||
|
|
||||||
|
# compute the signal
|
||||||
|
X_tmp = np.linalg.solve(L_tmp, Y_r)
|
||||||
|
|
||||||
|
X_l[idx_notnan] = X_tmp.copy()
|
||||||
|
|
||||||
|
# set the signal values
|
||||||
|
X_denoised[e, :] = X_l.copy()
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("======= energy cut denoised: ", str(e), " ========")
|
||||||
|
|
||||||
|
return X_denoised
|
||||||
|
|
||||||
|
def L_e(self):
|
||||||
|
|
||||||
|
# Define graph Laplacian along energy axis
|
||||||
|
return laplacian_chain(self.E_size)
|
||||||
|
|
||||||
|
def set_e_design_matrix(self, mu_=1.0):
|
||||||
|
|
||||||
|
L = self.L_e()
|
||||||
|
|
||||||
|
# return I + mu L
|
||||||
|
return np.eye(L.shape[0], dtype=self.dtype) + mu_*L
|
||||||
|
|
||||||
|
def MAD_lambda(self):
|
||||||
|
"""
|
||||||
|
Compute the MAD (Median absolute deviation) to set lambda values for each energy slice.
|
||||||
|
The scaling factor k=1.4826 enables to ensure the robustness of the variance estimator.
|
||||||
|
This does not work well.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array:
|
||||||
|
Lambda values.
|
||||||
|
"""
|
||||||
|
return 1.4826 * np.nanmedian(np.abs(self.Ygrid - np.nanmedian(self.Ygrid)))
|
||||||
|
|
||||||
|
def mu_estimator(self):
|
||||||
|
r"""
|
||||||
|
Compute an estimator of $\mu = Mean(Var(Y, axis=energy))$.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
torch.scalar:
|
||||||
|
mu values.
|
||||||
|
"""
|
||||||
|
return np.nanmean(np.var(self.Ygrid, axis=0))
|
||||||
|
|
||||||
|
def denoising(self, Y_r, lambda_, beta_, mu_=1.0, n_epochs=20, verbose=True):
|
||||||
|
"""
|
||||||
|
Run coordinate descent to solve optimization Problem (3).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- Y_r: Observations as torch.tensor (float64)
|
||||||
|
- lambda_: Penalty coefficient for signal sparsity E_size x 1 (float64)
|
||||||
|
- beta_: Penalty coefficient for the background's smoothness E_size x 1 (float64)
|
||||||
|
- mu_: Penalty coefficient for the graph Laplacian regularization (float64)
|
||||||
|
- e_cut: Range of energy cuts (numpy array)
|
||||||
|
- n_epochs: Maximum number of iterations (integer)
|
||||||
|
- verbose: Display convergence logs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
# ################### Initialize variable values ###############################
|
||||||
|
# Define signal values
|
||||||
|
self.X = np.zeros_like(Y_r, dtype=self.dtype)
|
||||||
|
|
||||||
|
# Define background values
|
||||||
|
self.b = np.zeros((self.E_size, self.n_bins), dtype=self.dtype)
|
||||||
|
b_tmp = self.R_operator(self.b)
|
||||||
|
|
||||||
|
# #############################################################################
|
||||||
|
|
||||||
|
# Initialize the values of the propagated background
|
||||||
|
self.b_grid = np.zeros_like(Y_r, dtype=self.dtype)
|
||||||
|
|
||||||
|
# Compute non-zero pixels in radial background ##
|
||||||
|
self.set_radial_nans(Y_r)
|
||||||
|
|
||||||
|
# Define lists of matrices to invert for background update
|
||||||
|
Lb_lst = [self.L_b(e_cut=e) for e in range(self.E_size)]
|
||||||
|
Mb_lst = [self.set_b_design_matrix(beta_, e_cut=e) for e in range(self.E_size)]
|
||||||
|
|
||||||
|
# Define matrix to invert for signal smoothness
|
||||||
|
Le = self.L_e()
|
||||||
|
Me = self.set_e_design_matrix(mu_)
|
||||||
|
|
||||||
|
# Loss function
|
||||||
|
loss = np.zeros(n_epochs, dtype=self.dtype)
|
||||||
|
loss[1] = 1.0
|
||||||
|
k = 1
|
||||||
|
|
||||||
|
while (np.abs(loss[k] - loss[k-1]) > 1e-3) and (k < n_epochs-1):
|
||||||
|
# Compute A = Y - B by filling the nans with 0s
|
||||||
|
A = np.where(np.isnan(Y_r - b_tmp) == True, 0.0, Y_r - b_tmp)
|
||||||
|
|
||||||
|
# Update the X values
|
||||||
|
self.X = np.linalg.solve(Me, self.S_lambda(A, lambda_))
|
||||||
|
|
||||||
|
# Projection step
|
||||||
|
self.X = np.maximum(self.X, 0.0)
|
||||||
|
|
||||||
|
# Update b values with b:= (beta*L_b + R^* R)^{-1} R^*(Y-X)
|
||||||
|
b_agg = self.Rstar_operator(Y_r - self.X)
|
||||||
|
|
||||||
|
# Update b values
|
||||||
|
for e in range(self.E_size):
|
||||||
|
self.b[e, :] = np.linalg.solve(Mb_lst[e], b_agg[e, :].T).T
|
||||||
|
|
||||||
|
# Projection step
|
||||||
|
self.b = np.maximum(self.b, 0.0)
|
||||||
|
|
||||||
|
# Propagate in 3D
|
||||||
|
b_tmp = self.R_operator(self.b)
|
||||||
|
|
||||||
|
# ######################### Compute loss function ##################
|
||||||
|
loss[k] = 0.5 * np.nansum((Y_r - self.X - b_tmp) ** 2) + lambda_ * np.nansum(np.abs(self.X))
|
||||||
|
|
||||||
|
for e in range(self.E_size):
|
||||||
|
loss[k] += (beta_/2) * np.matmul(self.b[e, :], np.matmul(Lb_lst[e], self.b[e, :].T))
|
||||||
|
|
||||||
|
loss[k] += (mu_ / 2) * np.trace(np.matmul(self.X.T, np.matmul(Le, self.X)))
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(" Iteration ", str(k))
|
||||||
|
print(" Loss function: ", loss[k].item())
|
||||||
|
|
||||||
|
k += 1
|
||||||
|
|
||||||
|
# Compute the propagated background
|
||||||
|
self.b_grid = self.R_operator(self.b).copy()
|
||||||
|
self.b_grid = self.mask_nans(self.b_grid)
|
||||||
|
|
||||||
|
# Mask the nan values
|
||||||
|
self.X = self.mask_nans(self.X)
|
||||||
|
|
||||||
|
def median_bg(self, Y):
|
||||||
|
"""
|
||||||
|
Compute the median value along the wedges. Basically, we sum for each radial bin.
|
||||||
|
|
||||||
|
Args: None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- med_bg: torch.tensor (size_e x nb_radial_bins)
|
||||||
|
Background computed using the median-based approach
|
||||||
|
"""
|
||||||
|
# Define the set of radius
|
||||||
|
rX = np.sqrt(self.X2dgrid[:, 0] ** 2 + self.X2dgrid[:, 1] ** 2)
|
||||||
|
|
||||||
|
# Compute the indices of grid vector in a range of radial values
|
||||||
|
indices = np.digitize(rX, self.r_range) - 1
|
||||||
|
indices[indices == self.n_bins] -= 1
|
||||||
|
|
||||||
|
indices_tmp = indices.T.reshape(self.Qx_size, self.Qy_size, self.E_size)
|
||||||
|
indices_tmp = indices_tmp.reshape(-1, self.E_size).T
|
||||||
|
|
||||||
|
# Initiate median background
|
||||||
|
med_bg = np.zeros((self.Ygrid.shape[0], self.n_bins), dtype=self.dtype)
|
||||||
|
|
||||||
|
# Compute the median across the wedges
|
||||||
|
for i, r in enumerate(self.r_range):
|
||||||
|
if Y[:, indices == i].shape[1] > 0:
|
||||||
|
med_bg[:, i] = np.nanmedian(Y[:, indices == i], dim=1)[0]
|
||||||
|
|
||||||
|
# assign 0 to nan values (enables to plot the QvE map)
|
||||||
|
med_bg = np.nan_to_num(med_bg)
|
||||||
|
|
||||||
|
# Computed median background
|
||||||
|
return med_bg
|
||||||
|
|
||||||
|
def compute_signal_to_noise(self, b):
|
||||||
|
"""
|
||||||
|
Compute and returns the signal to noise ratio as X^2/background^2.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- b: torch.tensor (float64)
|
||||||
|
background values
|
||||||
|
|
||||||
|
Returns: None
|
||||||
|
"""
|
||||||
|
# compute the background defined on the grid
|
||||||
|
b_grid = self.R_operator(self.Ygrid, b)
|
||||||
|
|
||||||
|
# compute signal values as Yobs - B
|
||||||
|
Signal = self.Ygrid - b_grid
|
||||||
|
|
||||||
|
# compute snr
|
||||||
|
snr = Signal**2/b_grid**2
|
||||||
|
|
||||||
|
return snr
|
||||||
|
|
||||||
|
def plot_snr(self, b, e_cut=range(40, 44), fmin=0.0, fmax=0.1):
|
||||||
|
"""
|
||||||
|
Compute and returns the signal to noise ratio as X^2/background^2.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- b: torch.tensor (float64)
|
||||||
|
background values
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- snr: torch.tensor (float64)
|
||||||
|
Signal-to-noise ratio as a 3D tensor
|
||||||
|
"""
|
||||||
|
# compute the snr
|
||||||
|
snr = self.compute_signal_to_noise(b)
|
||||||
|
|
||||||
|
# For 2D case (no propagation along the energy axis)
|
||||||
|
snrplot = snr.T.reshape(self.Qx_size, self.Qy_size, self.E_size)[:, :, e_cut]
|
||||||
|
fplot = np.mean(snrplot, axis=2)
|
||||||
|
|
||||||
|
# Create a 3x2 subplot for various plots
|
||||||
|
fig0 = plt.figure(figsize=(16, 9))
|
||||||
|
|
||||||
|
# Plot 1: Observations Y
|
||||||
|
ax0 = fig0.add_subplot(1, 1, 1)
|
||||||
|
ax0.set_title('Observations Y')
|
||||||
|
im0 = ax0.pcolormesh(self.grid2dQx, self.grid2dQy, fplot, vmin=fmin, vmax=fmax)
|
||||||
|
plt.colorbar(im0, ax=ax0)
|
||||||
|
ax0.set_xlabel(r'x')
|
||||||
|
ax0.set_ylabel(r'y')
|
||||||
|
|
||||||
|
def save_arrays(self, median=True):
|
||||||
|
"""
|
||||||
|
Save radial background as numpy arrays in folder "/arrays".
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- median: Boolean
|
||||||
|
True if we want to save the median background line as "bg_med.py"
|
||||||
|
"""
|
||||||
|
if not os.path.exists('arrays'):
|
||||||
|
os.makedirs('arrays')
|
||||||
|
|
||||||
|
if self.str_dataset == "VanadiumOzide":
|
||||||
|
with open('arrays/' + self.str_dataset + '_' + self.str_option + '_bg.npy', 'wb') as f:
|
||||||
|
np.save(f, self.b.detach().numpy())
|
||||||
|
|
||||||
|
with open('arrays/' + self.str_dataset + '_' + self.str_option + '_radial_bins.npy', 'wb') as f:
|
||||||
|
np.save(f, self.r_range.detach().numpy())
|
||||||
|
|
||||||
|
if median:
|
||||||
|
med_bg = self.median_bg(self.Ygrid)
|
||||||
|
with open('arrays/' + self.str_dataset + '_' + self.str_option + '_bg_med.npy', 'wb') as f:
|
||||||
|
np.save(f, med_bg.detach().numpy())
|
||||||
|
|
||||||
|
else:
|
||||||
|
with open('arrays/' + self.str_dataset + '_bg.npy', 'wb') as f:
|
||||||
|
np.save(f, self.b.detach().numpy())
|
||||||
|
|
||||||
|
with open('arrays/' + self.str_dataset + '_radial_bins.npy', 'wb') as f:
|
||||||
|
np.save(f, self.r_range.detach().numpy())
|
||||||
|
|
||||||
|
if median:
|
||||||
|
med_bg = self.median_bg(self.Ygrid)
|
||||||
|
with open('arrays/' + self.str_dataset + '_bg_med.npy', 'wb') as f:
|
||||||
|
np.save(f, med_bg.detach().numpy())
|
||||||
|
|
||||||
|
def compute_signal_to_obs(self, b):
|
||||||
|
"""
|
||||||
|
Compute and returns the signal X^2/background^2.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- b: torch.tensor (float64)
|
||||||
|
background values
|
||||||
|
|
||||||
|
Returns: None
|
||||||
|
"""
|
||||||
|
# compute the background defined on the grid
|
||||||
|
b_grid = self.R_operator(self.Ygrid, b)
|
||||||
|
|
||||||
|
# compute signal values as Yobs - B
|
||||||
|
Signal = self.Ygrid - b_grid
|
||||||
|
|
||||||
|
# compute snr
|
||||||
|
snr = np.abs(Signal)**2 / np.abs(b_grid + 0.1)**2
|
||||||
|
|
||||||
|
return snr
|
||||||
|
|
||||||
|
def cross_validation(self, q=0.75, beta_range=np.array([1.0]), lambda_=1.0, mu_=1.0, n_epochs=15, verbose=True):
|
||||||
|
"""
|
||||||
|
Runs cross-validation procedure to get the best set of hyperparameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- q: scalar
|
||||||
|
quantile level
|
||||||
|
- lambda_range: array
|
||||||
|
potential lambda values.
|
||||||
|
- beta_range: array
|
||||||
|
potential beta values.
|
||||||
|
- mu_range: array
|
||||||
|
potential mu values.
|
||||||
|
- b_truth: array
|
||||||
|
background groundtruth.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- rmse: rmse of all possible combinations of hyperparaneter values
|
||||||
|
- lambda, alpha, beta, mu: best possible
|
||||||
|
"""
|
||||||
|
# Define test set
|
||||||
|
lambda_r = np.nanquantile(self.Ygrid, q)
|
||||||
|
|
||||||
|
# Define test set
|
||||||
|
b_test_index_upper = np.where((self.Ygrid.ravel() > lambda_r) & (np.isnan(self.Ygrid.ravel()) == False))[0]
|
||||||
|
|
||||||
|
# compute error
|
||||||
|
rmse = np.zeros(beta_range.shape[0], dtype=self.dtype)
|
||||||
|
|
||||||
|
# iterate in the beta range
|
||||||
|
for idx_beta, beta_tmp in enumerate(beta_range):
|
||||||
|
print("Test - (", beta_tmp, ")")
|
||||||
|
|
||||||
|
# run the optimization
|
||||||
|
self.denoising(self.Ygrid, lambda_, beta_tmp, mu_, n_epochs, verbose)
|
||||||
|
|
||||||
|
# compute rmse on the test set
|
||||||
|
self.b_prop = self.R_operator(self.b)
|
||||||
|
self.b_prop = self.mask_nans(self.b_prop)
|
||||||
|
|
||||||
|
# Background
|
||||||
|
B = self.b_prop.copy().ravel()
|
||||||
|
B[b_test_index_upper] = float('nan')
|
||||||
|
B = B.reshape(self.E_size, self.Qx_size * self.Qy_size)
|
||||||
|
|
||||||
|
# Test set
|
||||||
|
Y_tmp = self.Ygrid.ravel().astype(self.dtype)
|
||||||
|
Y_tmp[b_test_index_upper] = float('nan')
|
||||||
|
Y_tmp = Y_tmp.reshape(self.E_size, self.Qx_size * self.Qy_size)
|
||||||
|
|
||||||
|
# ########################### rmse ###############################
|
||||||
|
rmse[idx_beta] = np.sqrt(np.nanmean((Y_tmp - B)**2))
|
||||||
|
|
||||||
|
print("RMSE - (", lambda_, beta_tmp, mu_, ") : ", rmse[idx_beta])
|
||||||
|
|
||||||
|
return rmse
|
||||||
|
|
||||||
|
def compute_mask(self, q=0.75):
|
||||||
|
"""
|
||||||
|
Return the masked dataset where data points for which Y > quantile_{alpha}(Y) are NaN values.
|
||||||
|
This is an alternative to the mask processing function.
|
||||||
|
If no mask of the spurious intensities is provided by the user, then this function can be seen as an alternative.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- alpha: scalar value in (0,1)
|
||||||
|
quantile level to define the threshold.
|
||||||
|
- e_cut: integer
|
||||||
|
index of the energy cut
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- X_mask: torch.tensor
|
||||||
|
masked signal
|
||||||
|
"""
|
||||||
|
X_mask = self.Ygrid.copy()
|
||||||
|
X_mask[X_mask > np.nanquantile(X_mask, q)] = float('nan')
|
||||||
|
X_mask[np.isnan(self.Ygrid) == True] = float('nan')
|
||||||
|
|
||||||
|
return X_mask
|
218
src/AMBER/graph_laplacian.py
Normal file
218
src/AMBER/graph_laplacian.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# tools
|
||||||
|
import numpy as np
|
||||||
|
import scipy
|
||||||
|
import torch
|
||||||
|
from scipy.sparse import lil_matrix, block_diag, csr_array, diags, csr_matrix
|
||||||
|
|
||||||
|
|
||||||
|
def create_laplacian_matrix(nx, ny=None):
|
||||||
|
"""
|
||||||
|
Helper method to create the laplacian matrix for the laplacian
|
||||||
|
regularization
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
:param nx: height of the original image
|
||||||
|
:param ny: width of the original image
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
:rtype: scipy.sparse.csr_matrix
|
||||||
|
:return:the n x n laplacian matrix, where n = nx*ny
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
if ny is None:
|
||||||
|
ny = nx
|
||||||
|
assert (nx > 1)
|
||||||
|
assert (ny > 1)
|
||||||
|
# Blocks corresponding to the corner of the image (linking row elements)
|
||||||
|
top_block = lil_matrix((ny, ny), dtype=np.float32)
|
||||||
|
top_block.setdiag([2] + [3] * (ny - 2) + [2])
|
||||||
|
top_block.setdiag(-1, k=1)
|
||||||
|
top_block.setdiag(-1, k=-1)
|
||||||
|
# Blocks corresponding to the middle of the image (linking row elements)
|
||||||
|
mid_block = lil_matrix((ny, ny), dtype=np.float32)
|
||||||
|
mid_block.setdiag([3] + [4]*(ny - 2) + [3])
|
||||||
|
mid_block.setdiag(-1, k=1)
|
||||||
|
mid_block.setdiag(-1, k=-1)
|
||||||
|
# Construction of the diagonal of blocks
|
||||||
|
list_blocks = [top_block] + [mid_block]*(nx-2) + [top_block]
|
||||||
|
blocks = block_diag(list_blocks)
|
||||||
|
# Diagonals linking different rows
|
||||||
|
blocks.setdiag(-1, k=ny)
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def delete_from_csr(mat, row_indices=[], col_indices=[]):
|
||||||
|
"""
|
||||||
|
Remove the rows (denoted by ``row_indices``) and columns (denoted by ``col_indices``) from the CSR sparse matrix ``mat``.
|
||||||
|
WARNING: Indices of altered axes are reset in the returned matrix
|
||||||
|
"""
|
||||||
|
if not isinstance(mat, csr_matrix):
|
||||||
|
raise ValueError("works only for CSR format -- use .tocsr() first")
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
cols = []
|
||||||
|
if row_indices:
|
||||||
|
rows = list(row_indices)
|
||||||
|
if col_indices:
|
||||||
|
cols = list(col_indices)
|
||||||
|
|
||||||
|
if len(rows) > 0 and len(cols) > 0:
|
||||||
|
row_mask = np.ones(mat.shape[0], dtype=bool)
|
||||||
|
row_mask[rows] = False
|
||||||
|
col_mask = np.ones(mat.shape[1], dtype=bool)
|
||||||
|
col_mask[cols] = False
|
||||||
|
return mat[row_mask][:, col_mask]
|
||||||
|
elif len(rows) > 0:
|
||||||
|
mask = np.ones(mat.shape[0], dtype=bool)
|
||||||
|
mask[rows] = False
|
||||||
|
return mat[mask]
|
||||||
|
elif len(cols) > 0:
|
||||||
|
mask = np.ones(mat.shape[1], dtype=bool)
|
||||||
|
mask[cols] = False
|
||||||
|
return mat[:, mask]
|
||||||
|
else:
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def remove_vertex(L, lst_rows=[], lst_cols=[]):
|
||||||
|
"""
|
||||||
|
Function that removes a vertex and adjust the graph laplacian matrix.
|
||||||
|
"""
|
||||||
|
L_cut = delete_from_csr(L.tocsr(), row_indices=lst_rows, col_indices=lst_cols)
|
||||||
|
L_cut = L_cut - diags(L_cut.diagonal())
|
||||||
|
L_cut = L_cut - diags(L_cut.sum(axis=1).A1)
|
||||||
|
assert (L_cut.sum(axis=1).A1 == np.zeros(L_cut.shape[0])).all()
|
||||||
|
|
||||||
|
return L_cut
|
||||||
|
|
||||||
|
|
||||||
|
def laplacian(Y, nx, ny=None):
|
||||||
|
"""
|
||||||
|
Function that removes the vertices corresponding to Nan locations of tensor Y.
|
||||||
|
Args:
|
||||||
|
- Y: torch.tensor of the observations (Float64)
|
||||||
|
- nx,ny: dimensions of the image (Int64)
|
||||||
|
"""
|
||||||
|
# find Nan indices
|
||||||
|
Nan_indices = torch.where(torch.isnan(Y.ravel()) == True)[0]
|
||||||
|
|
||||||
|
# get list of indices
|
||||||
|
list_idx = list(Nan_indices.detach().numpy())
|
||||||
|
|
||||||
|
# create Laplacian
|
||||||
|
L = create_laplacian_matrix(nx, ny=ny)
|
||||||
|
L = remove_vertex(L, list_idx, list_idx)
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
def unnormalized_laplacian(y, nx, ny=None, method='inverse'):
|
||||||
|
"""Construct numpy array with non zeros weights and non zeros indices.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- y: np.array for observations of size (size_x*size_y)
|
||||||
|
- method: str indicating how to compute the weight of the unnormalized laplacian
|
||||||
|
"""
|
||||||
|
# create laplacian matrix
|
||||||
|
lapl_tmp = laplacian(y, nx, ny)
|
||||||
|
lapl_tmp.setdiag(np.zeros(nx*ny))
|
||||||
|
|
||||||
|
# select the non nan indices
|
||||||
|
y_tmp = y[torch.isnan(y) == False]
|
||||||
|
|
||||||
|
# store non zero indices
|
||||||
|
idx_rows = np.array(lapl_tmp.nonzero()[0])
|
||||||
|
idx_cols = np.array(lapl_tmp.nonzero()[1])
|
||||||
|
|
||||||
|
# construct the set of weights
|
||||||
|
nnz_w = np.zeros_like(idx_rows, dtype=np.float32)
|
||||||
|
|
||||||
|
# construction of the non zeros weights
|
||||||
|
if method == 'inverse':
|
||||||
|
nnz_w = 1/(np.abs(y_tmp[idx_rows] - y_tmp[idx_cols]) + 1e-4)
|
||||||
|
else:
|
||||||
|
nnz_w = np.exp(-np.abs(y_tmp[idx_rows] - y_tmp[idx_cols]))
|
||||||
|
|
||||||
|
# construct the non diagonal terms of the Laplacian
|
||||||
|
lapl_nondiag = csr_array((nnz_w, (idx_rows, idx_cols)), shape=(lapl_tmp.shape[0], lapl_tmp.shape[0]), dtype=np.float32)
|
||||||
|
|
||||||
|
# construct the diagonal terms of the Laplacian
|
||||||
|
lapl_diag = diags(lapl_nondiag.sum(axis=0))
|
||||||
|
|
||||||
|
# construct the Laplacian
|
||||||
|
L = lapl_diag - lapl_nondiag
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
def laplacian_chain(nb_vertices):
|
||||||
|
"""
|
||||||
|
Construct the Laplacian matrix of a chain.
|
||||||
|
"""
|
||||||
|
L = np.zeros((nb_vertices, nb_vertices))
|
||||||
|
|
||||||
|
# First vertex
|
||||||
|
L[0, 0] = 1
|
||||||
|
L[0, 1] = -1
|
||||||
|
|
||||||
|
# Toeplitz matrix
|
||||||
|
if nb_vertices > 2:
|
||||||
|
first_row = torch.zeros(nb_vertices)
|
||||||
|
first_row[0] = -1
|
||||||
|
first_row[1] = 2
|
||||||
|
first_row[2] = -1
|
||||||
|
|
||||||
|
first_col = torch.zeros(nb_vertices-2)
|
||||||
|
first_col[0] = -1
|
||||||
|
|
||||||
|
D = scipy.linalg.toeplitz(first_col, r=first_row)
|
||||||
|
|
||||||
|
L[1:nb_vertices-1, :] = D
|
||||||
|
|
||||||
|
# Last vertex
|
||||||
|
L[-1, -2] = -1
|
||||||
|
L[-1, -1] = 1
|
||||||
|
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
def unnormalized_laplacian_chain(y, nx, method='inverse'):
|
||||||
|
"""Construct numpy array with non zeros weights and non zeros indices.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- y: np.array for aggregated observations of size (nb_bins)
|
||||||
|
- method: str indicating how to compute the weight of the unnormalized laplacian
|
||||||
|
"""
|
||||||
|
# create laplacian matrix
|
||||||
|
lapl_tmp = csr_matrix(laplacian_chain(nx))
|
||||||
|
lapl_tmp.setdiag(np.zeros(nx*nx))
|
||||||
|
|
||||||
|
# select the non nan indices
|
||||||
|
y_tmp = np.nan_to_num(y)
|
||||||
|
|
||||||
|
# store non zero indices
|
||||||
|
idx_rows = np.array(lapl_tmp.nonzero()[0])
|
||||||
|
idx_cols = np.array(lapl_tmp.nonzero()[1])
|
||||||
|
|
||||||
|
# construct the set of weights
|
||||||
|
nnz_w = np.zeros_like(idx_rows, dtype=np.float32)
|
||||||
|
|
||||||
|
# construction of the non zeros weights
|
||||||
|
if method == 'inverse':
|
||||||
|
nnz_w = 1/(np.abs(y_tmp[idx_rows] - y_tmp[idx_cols]))
|
||||||
|
else:
|
||||||
|
nnz_w = np.exp(-np.abs(y_tmp[idx_rows] - y_tmp[idx_cols]))
|
||||||
|
|
||||||
|
# construct the non diagonal terms of the Laplacian
|
||||||
|
lapl_nondiag = csr_array((nnz_w, (idx_rows, idx_cols)), shape=(lapl_tmp.shape[0], lapl_tmp.shape[0]), dtype=np.float32)
|
||||||
|
|
||||||
|
# construct the diagonal terms of the Laplacian
|
||||||
|
lapl_diag = diags(lapl_nondiag.sum(axis=0))
|
||||||
|
|
||||||
|
# construct the Laplacian
|
||||||
|
L = lapl_diag - lapl_nondiag
|
||||||
|
|
||||||
|
return L
|
Reference in New Issue
Block a user