Spaces:
Running on Zero
Running on Zero
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| Created on Fri Nov 3 15:14:51 2023 | |
| @author: louis | |
| """ | |
| import torch | |
| import math | |
| from torchaudio.functional import fftconvolve as fftconvolve_torchaudio, filtfilt | |
| from torchaudio.functional.functional import _check_shape_compatible, _check_convolve_mode, _apply_convolve_mode | |
| import scipy | |
| import itertools | |
| from lightning.pytorch.utilities import move_data_to_device | |
| def power_to_db(x): | |
| # if x == 0: | |
| # return -torch.inf | |
| return 10 * torch.log10(x) | |
| energy_to_db = power_to_db | |
| def db_to_power(x_db): | |
| return torch.pow(10, x_db / 10) | |
| db_to_energy = db_to_power | |
| def db_to_amplitude(x_db): | |
| return db_to_power(x_db).sqrt() | |
| def signal_power_db(signal): | |
| return power_to_db((torch.abs(signal) ** 2).mean()) | |
| def complex_circular_gaussian_noise(mean=0 + 0j, std=math.sqrt(2), **kwargs): | |
| return torch.complex(torch.normal(mean.real, std, **kwargs), torch.normal(mean.imag, std, **kwargs)) | |
| def awgn(original_signal, target_snr_db): | |
| """ | |
| Adds white gaussian noise whose variance is adjusted to fit a given signal-to-noise ratio | |
| Parameters | |
| ---------- | |
| original_signal : np.array | |
| original signal. | |
| target_snr_db : float | |
| target signal-to-noise ratio (in db). | |
| Returns | |
| ------- | |
| noisy_signal : np.array | |
| noisy signal s.t. snr_db(noisy_signal, original_signal) = target_snr_db. | |
| """ | |
| Px_db = signal_power_db(original_signal) | |
| var = db_to_power(Px_db - target_snr_db) | |
| std = torch.sqrt(var) | |
| if original_signal.dtype.is_complex: | |
| return complex_circular_gaussian_noise(original_signal, std) | |
| return torch.normal(original_signal, std) | |
| def arange_last_dim_like(a): | |
| return torch.arange(a.shape[-1], dtype=a.dtype, device=a.device).expand(*a.shape[:-1], -1) | |
| def zero_pad(a, target_len, dim=-1): | |
| assert dim == -1 | |
| return torch.nn.functional.pad(a, (0, target_len - a.shape[-1]), mode="constant", value=0) | |
| def crop_or_zero_pad_to_target_len(a, target_len, dim=-1): | |
| # functional.pad seems to also be able to crop | |
| return zero_pad(a, target_len, dim=dim) | |
| def fftconvolve_complex(x: torch.Tensor, y: torch.Tensor, mode: str = "full") -> torch.Tensor: | |
| r""" | |
| Same as torchaudio.functional.fftconvolve but for complex_valued tensors | |
| """ | |
| _check_shape_compatible(x, y) | |
| _check_convolve_mode(mode) | |
| n = x.size(-1) + y.size(-1) - 1 | |
| fresult = torch.fft.fft(x, n=n) * torch.fft.fft(y, n=n) | |
| result = torch.fft.ifft(fresult, n=n) | |
| return _apply_convolve_mode(result, x.size(-1), y.size(-1), mode) | |
| def fftconvolve(x: torch.Tensor, y: torch.Tensor, mode: str = "full", dim=-1) -> torch.Tensor: | |
| """ | |
| Wrapper around torchaudio.fftconvolve if the inputs aren't real | |
| Parameters | |
| ---------- | |
| x : torch.Tensor | |
| DESCRIPTION. | |
| y : torch.Tensor | |
| DESCRIPTION. | |
| mode : str, optional | |
| DESCRIPTION. The default is "full". | |
| Returns | |
| ------- | |
| TYPE | |
| DESCRIPTION. | |
| """ | |
| x_transposed = x.transpose(-1, dim) | |
| y_transposed = y.transpose(-1, dim) | |
| if x.dtype.is_complex or y.dtype.is_complex: | |
| res = fftconvolve_complex(x_transposed, y_transposed, mode) | |
| else: | |
| res = fftconvolve_torchaudio(x_transposed, y_transposed, mode) | |
| return res.transpose(-1, dim) | |
| def correlate(x, k, **kwargs): | |
| return fftconvolve(x, k.flip(-1).conj(), **kwargs) | |
| def test_correlate(): | |
| import scipy | |
| x = torch.rand(64, dtype=torch.complex64) | |
| k = torch.rand(64, dtype=torch.complex64) | |
| print(torch.dist(correlate(x, k), torch.tensor(scipy.signal.correlate(x.numpy(), k.numpy())))) | |
| def toeplitz_transposed_product_fft(x: torch.Tensor, k: torch.Tensor, zero_pad: bool = False) -> torch.Tensor: | |
| if x.dtype.is_complex or k.dtype.is_complex: | |
| raise NotImplementedError("There might be a conjugation error. Use the correlation instead") | |
| # Performs X.T@k where X is toeplitz of coefs x | |
| res = fftconvolve((x[..., : k.shape[-1]]).flip(-1), k)[..., -k.shape[-1] :] | |
| if zero_pad: | |
| return zero_pad(res, x.shape[-1] + k.shape[-1] - 1) | |
| return res | |
| def toeplitz(x): | |
| # returns the toeplitz matrix which 1st row is x | |
| # res = torch.zeros(x.shape + (x.shape[-1],), device=x.device, dtype=x.dtype) | |
| # for i in range(x.shape[-1]): | |
| # res[..., i:] = x[..., :i] | |
| res = torch.tril(torch.stack([torch.roll(x, shifts=i, dims=-1) for i in range(x.shape[-1])], dim=-1)) | |
| return res | |
| def test_toeplitz(): | |
| import scipy | |
| x = torch.rand(512) | |
| x_toeplitz = toeplitz(x) | |
| x_scipy_toeplitz = torch.tensor(scipy.linalg.toeplitz(x.numpy(), torch.zeros_like(x).numpy())) | |
| assert torch.allclose(x_toeplitz, x_scipy_toeplitz), torch.dist(x_scipy_toeplitz, x_toeplitz) | |
| def deconvolve_corr(y, x, **kwargs_deconvolve_toeplitz): | |
| # from https://media.ed.ac.uk/media/Topic+73A+Application+of+Cross-Correlation+to+System+Identification/1_e6662yx1 | |
| x_autocorr = correlate(x, x) | |
| # x_autocorr = x_autocorr[..., x.shape[-1] - 1 : x.shape[-1] + output_len - 1] # Only in Jonathan's version | |
| # x_autocorr = crop_or_zero_pad_to_target_len(x_autocorr, output_len) | |
| # Rxx = toeplitz(x_autocorr) | |
| xy_corr = correlate(y, x) | |
| # xy_corr = xy_corr[..., x.shape[-1] - 1 : x.shape[-1] + output_len - 1] # only in Jonathan's version | |
| xy_corr = crop_or_zero_pad_to_target_len(xy_corr, x_autocorr.shape[-1]) # Only in my version | |
| # xy_corr = crop_or_zero_pad_to_target_len(xy_corr, output_len) # Only in my version | |
| # res = torch.linalg.solve_triangular(Rxx, xy_corr[..., None], upper=False)[..., 0] # Bcp moins précis, erreurs énormes | |
| return deconvolve_toeplitz(xy_corr, x_autocorr, **kwargs_deconvolve_toeplitz) | |
| def deconvolve_toeplitz(y, x, output_len=None, solver="qr"): | |
| # solve_lstsq to use least-squares | |
| if output_len is None: | |
| output_len = y.shape[-1] - x.shape[-1] + 1 | |
| x_toeplitz = toeplitz(zero_pad(x, y.size(-1))) | |
| if "qr" in solver or "lstsq" in solver or "tri" in solver: | |
| x_toeplitz = x_toeplitz[..., :output_len] | |
| if "qr" in solver: | |
| return solve_lstsq_qr(x_toeplitz, y) | |
| if "lstsq" in solver: | |
| return torch.linalg.lstsq(x_toeplitz, y).solution | |
| if "tri" in solver: | |
| raise RuntimeWarning("solve_triangular is very unstable") | |
| return torch.linalg.solve_triangular( | |
| x_toeplitz[..., :output_len, :output_len], | |
| y[..., :output_len, None], | |
| upper=False, | |
| ).squeeze(-1) | |
| if "solve" in solver: | |
| return torch.linalg.solve(x_toeplitz, y)[..., :output_len] | |
| raise ValueError("unsupported solver") | |
| # return torch.linalg.solve(toeplitz(x), y[..., : x.size(-1)])[..., : y.size(-1) - x.size(-1) + 1] | |
| def solve_lstsq_qr(A, y): | |
| # Does the same as torch.linalg.lstsq but faster backward | |
| Q, R = torch.linalg.qr(A, mode="reduced") | |
| rhs = Q.mH @ y.unsqueeze(-1) | |
| return torch.linalg.solve_triangular(R, rhs, upper=True).squeeze(-1) | |
| def deconvolve_fourier(y, x, output_len=None, epsilon=1e-8, use_autocorr=True): | |
| if output_len is None: | |
| output_len = y.shape[-1] - x.shape[-1] + 1 | |
| Y = torch.fft.fft(y, y.size(-1)) | |
| X = torch.fft.fft(x, y.size(-1)) | |
| if use_autocorr: | |
| numerator = Y * X.conj() | |
| denominator = X * X.conj() | |
| else: | |
| numerator = Y | |
| denominator = X | |
| return torch.fft.ifft(numerator / (denominator + epsilon))[..., :output_len] | |
| def test_deconvolve_fourier(): ... | |
| def batched_inner_product_3d(x, y, keepdim=False): | |
| res = torch.linalg.vecdot(x, y) | |
| if keepdim: | |
| return res[..., None] | |
| return res | |
| def white_noise_same_std(t, last_dim_new_size=None): | |
| if last_dim_new_size is None: | |
| last_dim_new_size = t.shape[-1] | |
| noise = torch.randn(t.shape[:-1] + (last_dim_new_size,), device=t.device, dtype=t.dtype) | |
| return noise * t.std(axis=-1, keepdim=True) | |
| def test_outer_with_lag(): | |
| # Used in FCP | |
| F = 257 | |
| T = 128 | |
| a = torch.rand(F, T) | |
| b = torch.rand(F, T) | |
| b_toeplitz = toeplitz(b.transpose(-1, -2)).permute((-2, -1, -3)) | |
| assert b_toeplitz.shape == (F, F, T) | |
| assert all(((b_toeplitz[..., t] == b_toeplitz[..., t].tril()).all() for t in range(b_toeplitz.shape[-1]))) | |
| res = a * b_toeplitz | |
| assert all( | |
| (res[..., f, f_prime, :] == a[..., f_prime, :] * b[..., f - f_prime, :]).all() | |
| for f in range(F) | |
| for f_prime in range(F) | |
| if f - f_prime >= 0 | |
| ) | |
| def compare_deconvolution_methods(): | |
| import itertools | |
| B, C, F, T = 1, 1, 257, 128 | |
| Tk = 64 | |
| x = torch.rand((B, C, F, T), dtype=torch.complex64) | |
| k = torch.rand((B, C, F, Tk), dtype=torch.complex64) | |
| # x = torch.arange(8).to(dtype=torch.complex64) + 1 | |
| # k = torch.arange(4).to(dtype=torch.complex64) + 2 | |
| y = fftconvolve(x, k) | |
| x = awgn(x, 10) | |
| for deconvolver, solver in itertools.product( | |
| (deconvolve_corr, deconvolve_toeplitz), | |
| [ | |
| "qr", | |
| "lstsq", | |
| # "tri", | |
| # "solve", | |
| ], | |
| ): | |
| k_hat = deconvolver(y, x, solver=solver) | |
| print(deconvolver.__name__, solver + ":", torch.dist(k_hat, k)) | |
| k_deconvolved_fourier = deconvolve_fourier(y, x) | |
| print("Fourier", torch.dist(k, k_deconvolved_fourier)) | |
| def nansum_complex(x, dim=-1, keepdim=False): | |
| # here we don't care, itshould be the case that both real and imag are nans | |
| return torch.complex(x.real.nansum(dim=dim, keepdim=keepdim), x.imag.nansum(dim=dim, keepdim=keepdim)) | |
| def tuple_to_device(t, device=torch.device("cpu")): | |
| if isinstance(t, torch.Tensor): | |
| return t.to(device=device) | |
| if isinstance(t, dict): | |
| return {k: tuple_to_device(v) for k, v in t.items()} | |
| if t is None: | |
| return t | |
| return tuple(tuple_to_device(ti, device=device) for ti in t) | |
| tuple_to_device = move_data_to_device | |
| def autocorrelation(signal, dim: int = -1, return_half: bool = True): | |
| signal_fourier = torch.fft.fft(signal, n=2 * signal.shape[-1] - 1, dim=dim) | |
| autocorr_fourier = torch.fft.ifft(signal_fourier * signal_fourier.conj(), dim=dim) | |
| if return_half: | |
| if dim != -1: | |
| raise NotImplementedError() | |
| return autocorr_fourier[..., : signal.shape[-1] + 1] | |
| return torch.fft.fftshift(autocorr_fourier, dim=dim) | |
| class Filterbank(torch.nn.Module): | |
| def __init__( | |
| self, | |
| num_evenly_spaced_filters: int | None, | |
| filter_type: str = "butter", | |
| filter_order: int = 4, | |
| fs: int = 16000, | |
| ): | |
| super().__init__() | |
| self.num_evenly_spaced_filters = num_evenly_spaced_filters | |
| self.filter_type = filter_type | |
| self.filter_order = filter_order | |
| self.fs = fs | |
| filter_cutoffs = torch.linspace(0, self.fs / 2, self.num_evenly_spaced_filters + 1) | |
| self.band_edges = list(itertools.pairwise(filter_cutoffs)) | |
| if self.filter_type.lower() != "butter": | |
| raise NotImplementedError() | |
| filter_coeffs = tuple(zip(*(self.butter_filter(lowcut, highcut) for lowcut, highcut in self.band_edges))) | |
| maxlen_a = max(len(a) for a in filter_coeffs[1]) | |
| maxlen_b = max(len(b) for b in filter_coeffs[0]) | |
| self.register_buffer( | |
| "filter_coeffs_a", torch.stack(tuple(zero_pad(torch.tensor(a), maxlen_a) for a in filter_coeffs[1])) | |
| ) | |
| self.register_buffer( | |
| "filter_coeffs_b", torch.stack(tuple(zero_pad(torch.tensor(b), maxlen_b) for b in filter_coeffs[0])) | |
| ) | |
| def butter_filter(self, lowcut, highcut): | |
| if lowcut == 0: | |
| return scipy.signal.butter(self.filter_order, highcut, btype="low", output="ba", fs=self.fs) | |
| if highcut == self.fs / 2: | |
| return scipy.signal.butter(self.filter_order, lowcut, btype="high", output="ba", fs=self.fs) | |
| return scipy.signal.butter(self.filter_order, (lowcut, highcut), btype="band", output="ba", fs=self.fs) | |
| def forward(self, signal): | |
| # [... t] -> [..., num_filt, t] | |
| return filtfilt( | |
| signal.unsqueeze(-2).expand(*signal.shape[:-1], len(self.filter_coeffs_a), -1).to(dtype=torch.double), | |
| self.filter_coeffs_a, | |
| self.filter_coeffs_b, | |
| ).to(dtype=signal.dtype) | |
| def inverse(self, filterbanked_signal): | |
| # [..., num_filt, t] -> [... t] | |
| assert filterbanked_signal.size(-2) == self.num_evenly_spaced_filters | |
| return torch.sum(filterbanked_signal, dim=-2) | |
| def plot_filters(self): | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| fig, ax1 = plt.subplots(tight_layout=True) | |
| ax1.set_title(f"Frequency Response") | |
| # ax1.axvline(self.fs, color='black', linestyle=':', linewidth=0.8) | |
| ax1.set_ylabel("Amplitude in dB") | |
| ax1.set_xlabel("f") | |
| w, frequency_response_all_filters = np.stack( | |
| [ | |
| scipy.signal.freqz(filter_coeff_b.numpy(), filter_coeff_a.numpy(), fs=self.fs, worN=4096) | |
| for (filter_coeff_a, filter_coeff_b) in zip(self.filter_coeffs_a, self.filter_coeffs_b) | |
| ], | |
| axis=-1, | |
| ) | |
| ax1.plot(w, 20 * np.log10(abs(frequency_response_all_filters))) | |
| ax1.set_ylim(-100, 10) | |
| def test_filterbank(): | |
| n = torch.randn(100, 1, 10000) | |
| fb = Filterbank(num_evenly_spaced_filters=20) | |
| n_fb = fb(n) | |
| n_hat = fb.inverse(n_fb) | |
| from torchmetrics.audio import ScaleInvariantSignalDistortionRatio | |
| sisdr = ScaleInvariantSignalDistortionRatio()(n_hat, n) | |
| print(f"Analysis-synthesis SISDR: {sisdr}") | |
| def OMP(dictionnary, y, n_iter): | |
| residual = y.clone() | |
| set_of_indices = torch.full_like(y[..., :n_iter], -1, dtype=torch.long) # -1 means that it is empty | |
| for iteration in range(n_iter): | |
| scalar_product = torch.einsum("...ti,...t->...i", dictionnary, residual) | |
| current_idx = torch.argmax(scalar_product.abs(), dim=-1) | |
| set_of_indices[..., iteration] = current_idx | |
| dict_subset = dictionnary.gather( | |
| -1, set_of_indices[..., None, : iteration + 1].expand(*set_of_indices.shape[:-1], dictionnary.size(-2), -1) | |
| ) | |
| optimal_gains = solve_lstsq_qr(dict_subset, y) | |
| residual = y - torch.einsum("...ti, ...i -> ...t", dict_subset, optimal_gains) | |
| return set_of_indices, optimal_gains | |
| def test_omp(): | |
| raise NotImplementedError() | |
| class Polynomial(torch.nn.Module): | |
| def __init__( | |
| self, | |
| coeffs: list[float] | tuple[float] | torch.Tensor | None = None, | |
| deg: int | None = None, | |
| trainable: bool = False, | |
| ) -> None: | |
| """ | |
| Parameters | |
| ---------- | |
| coeffs : list[float] | torch.Tensor | None, optional | |
| Coefficients of the polynomial. If None, defaults to identity polynomial of degree deg. The default is None. | |
| deg : int | None, optional | |
| Degree of the identity polynomial if coeffs is not specified. The default is None. | |
| trainable : bool, optional | |
| Whether the coeffs should be considered as buffers or parameters. The default is False. | |
| Raises | |
| ------ | |
| ValueError | |
| If coeffs and deg are not specified. | |
| """ | |
| super().__init__() | |
| self.trainable = trainable | |
| if coeffs is None and deg is None: | |
| raise ValueError("Please specify either coeffs or deg") | |
| if coeffs is None: | |
| coeffs = torch.zeros(deg + 1) | |
| coeffs[1] = 1.0 | |
| if self.trainable: | |
| self.register_parameter("coeffs", torch.nn.Parameter(torch.as_tensor(coeffs))) | |
| else: | |
| self.register_buffer("coeffs", torch.as_tensor(coeffs)) | |
| def deg(self): | |
| return len(self.coeffs) - 1 | |
| def degree(self): | |
| return self.deg | |
| def __repr__(self): | |
| return f"Polynomial(coeffs={self.coeffs})" | |
| def forward(self, x): | |
| if self.coeffs.ndim > 1: | |
| raise NotImplementedError( | |
| "Batching of coeffs in forward not supported yet (need to define wether pointwise or not)" | |
| ) | |
| x_expanded = x.unsqueeze(-1).expand(*(-1,) * x.ndim, *self.coeffs.shape) | |
| X = x_expanded ** arange_last_dim_like(x_expanded) | |
| y = torch.sum(X * self.coeffs[(None,) * x.ndim], dim=-1) | |
| return y | |
| def fit(self, x, y, solver="lstsq", flatten: bool = True, inplace: bool = True): | |
| coeffs = polyfit(x, y, deg=self.deg, solver=solver, flatten=flatten) | |
| if inplace: | |
| self.coeffs = coeffs | |
| return coeffs | |
| # def __add__(self, other): | |
| # return Polynomial(self.coeffs + other.coeffs, trainable=self.trainable or other.trainable) | |
| # def __iadd__(self, other): | |
| # self.coeffs += other.coeffs | |
| # def __neg__(self): | |
| # return Polynomial(-self.coeffs, trainable=self.trainable) | |
| # def __eq__(self, other): | |
| # return self.coeffs == other.coeffs | |
| def polyfit(x, y, deg: int = 2, solver="lstsq", flatten: bool = True): | |
| if solver.lower() not in ["lstsq", "qr"]: | |
| raise NotImplementedError() | |
| if flatten: | |
| x_flat = x.flatten() | |
| y_flat = y.flatten() | |
| else: | |
| x_flat = x | |
| y_flat = y | |
| x_expanded = x_flat.unsqueeze(-1).expand(*(-1,) * x_flat.ndim, deg + 1) | |
| X = x_expanded ** arange_last_dim_like(x_expanded) | |
| if "qr" in solver.lower(): | |
| return solve_lstsq_qr(X, y_flat) | |
| else: | |
| return torch.linalg.lstsq(X, y_flat).solution | |
| def test_polyfit(deg=4, snr_db=10, flatten: bool = True): | |
| import matplotlib.pyplot as plt | |
| x = 4 * torch.rand(8, 2, 1000) - 2 | |
| if flatten: | |
| x = x.flatten() | |
| coeffs = 4 * torch.randn(deg + 1) | |
| x_expanded = x.unsqueeze(-1).expand(*(-1,) * x.ndim, len(coeffs)) | |
| X = x_expanded ** arange_last_dim_like(x_expanded) | |
| y = torch.sum(X * coeffs[(None,) * x.ndim], dim=-1) | |
| y_noisy = awgn(y, target_snr_db=snr_db) | |
| estimated_coeffs = polyfit(x, y_noisy, deg=deg, flatten=flatten) | |
| if flatten: | |
| plt.figure() | |
| plt.plot(x, y, ".", label="target") | |
| plt.plot(x, y_noisy, ".", label="noisy") | |
| y_hat = torch.sum(X * estimated_coeffs[(None,) * x.ndim], dim=-1) | |
| plt.plot(x, y_hat, ".", label="estimated") | |
| plt.legend() | |
| print(f"coeffs: real: {coeffs}, estimated: {estimated_coeffs}") | |
| def test_polynomial(): | |
| import matplotlib.pyplot as plt | |
| poly = Polynomial(deg=3) | |
| x = 4 * torch.rand(4) - 2 | |
| y = poly(x) | |
| assert torch.allclose(x, y) | |
| deg = 3 | |
| coeffs = 4 * torch.randn(deg + 1) | |
| poly = Polynomial(coeffs=coeffs) | |
| print(poly) | |
| x = 4 * torch.rand(1, 1, 1000) - 2 | |
| y = poly(x) | |
| y_noisy = awgn(y, target_snr_db=10) | |
| estimated_coeffs = poly.fit(x, y_noisy) | |
| print(poly) | |
| plt.figure() | |
| plt.plot(x[(0,) * (x.ndim - 1)], y[(0,) * (y.ndim - 1)], ".", label="target") | |
| plt.plot(x[(0,) * (x.ndim - 1)], y_noisy[(0,) * (y_noisy.ndim - 1)], ".", label="noisy") | |
| poly_hat = Polynomial(coeffs=estimated_coeffs) | |
| y_hat = poly_hat(x) | |
| plt.plot(x[(0,) * (x.ndim - 1)], y_hat[(0,) * (y_hat.ndim - 1)], ".", label="estimated") | |
| if __name__ == "__main__": | |
| compare_deconvolution_methods() | |
| # test_correlate() | |
| # test_deconvolve_toeplitz() | |
| test_outer_with_lag() | |
| # test_toeplitz() | |
| # test_deconvolve_corr() | |
| test_filterbank() | |
| # test_omp() | |
| test_polyfit() | |
| test_polynomial() | |