spyrit.core.meas.HadamSmatrix2d
- class spyrit.core.meas.HadamSmatrix2d(h: int, M: int = None, order: tensor = None, fast: bool = True, reshape_output: bool = False, scramble: bool = False, seed: int = 0, *, noise_model=Identity(), dtype: dtype = torch.float32, device: device = device(type='cpu'))[source]
Bases:
LinearSimulate 2D S-matrix acquisitions.
Considering the acquisition of \(M\) DMD patterns of size \(h\times h\), the class computes
\[y = \mathcal{N}\left(\mathcal{S}\left(Y\right)\right), \quad Y = \frac{\mathrm{sum}(X)\cdot J_h - H_h\,X\,H_h^T}{2},\]where \(\mathcal{N} \colon\, \mathbb{R}^M \to \mathbb{R}^M\) represents a noise operator (e.g., Gaussian), \(\mathcal{S} \colon\, \mathbb{R}^{h\times h} \to \mathbb{R}^M\) is a subsampling operator, \(J_h\) is the \(h\times h\) all-ones matrix, and \(H_h \in \{-1,+1\}^{h\times h}\) is the same 1D Walsh-ordered Hadamard matrix used by
HadamSplit2d(seeHadamSplit2d.H1d).Equivalently,
\[y = \mathcal{N}\left(\mathcal{S}(A\,\mathrm{vec}(X))\right), \quad A = \max(0,-(H_h\otimes H_h)),\]where \(A \in \{0,1\}^{h^2 \times h^2}\) is the acquisition matrix – see
A.Note
By definition, the first row and column of \(H_h \in \{-1,+1\}^{h\times h}\) are all-ones. Therefore, \(A = \max(0, -(H_h\otimes H_h))\) has an all-zero first row and an all-zero first column, which correspond to the trivial measurement and the unrecoverable top-left pixel.
Warning
Because \(H_h\)’s first row and column are both all-ones (the usual Walsh-ordering convention), \(A\) has an all-zero row and an all-zero column.
scramble, when used, only permutes columns \(1,\dots,h-1\) of \(H_h\), leaving column 0 fixed (seecolumn_perm), so both of the following hold regardless ofscramble:\(A\)’s all-zero row means the very first measurement is always exactly 0 (at natural-order index 0, the position
orderkeeps first by default), regardless of \(X\) – the “all DMD micromirrors off” pattern.\(A\)’s all-zero column means the top-left pixel, \(X[0,0]\), has no effect whatsoever on any measurement, and is therefore fundamentally unrecoverable. By convention, input images are expected to have \(X[0,0]=0\) (see
fast_measure());fast_pinv()always reconstructs it as exactly 0.
Note
orderandscrambleboth rely on permutations, but they act on different things and serve different purposes – do not confuse them:orderpermutes the rows of the (subsampled) 2D system matrix \(A\), i.e. it reorders the \(M\) measurements in the output vector \(y\) (and selects which \(M\) of the \(h^2\) possible measurements are kept, if \(M<h^2\)). It does not change what any individual measurement pattern looks like, only the sequence in which the measurements appear (e.g. by decreasing variance/significance, viaorder).scramblepermutes the columns \(1,\dots,h-1\) of \(H_h\) – i.e. of the (1D) matrix used to build the acquisition matrix, before any row reordering/subsampling happens. This changes which pixels of the image each individual measurement pattern probes, not the order in which measurements are returned.
The two options are independent and can be combined.
If
scrambleis True, columns \(1,\dots,h-1\) of \(H_h\) are randomly permuted (with a fixedseedfor reproducibility; column 0 is always left fixed, see the warning above). This is useful e.g. to decorrelate the acquisition order from the natural Walsh ordering.Note
\(A\) is not orthogonal, so the adjoint (transpose) of the measurement operator and its pseudo-inverse are genuinely different operators here. Both, however, admit a closed form expressed only in terms of \(H_h\) (and its transpose), so both remain fast and separable across rows and columns – no \(h^2\times h^2\) matrix and no explicit matrix inversion is ever needed, even when
scrambleis True (seefast_adjoint()andfast_pinv()for the exact formulas).- Args:
h(int): Image size \(h\), which must be a power of 2.M(int, optional): Number of measurements. Defaults to \(h^2\) (no subsampling).order(torch.tensor, optional): Order matrix \(O\) that defines the measurements to keep. The first component of \(y\) will correspond to the index whereorderis the highest. Permutes the rows of the system matrix \(A\) (i.e. the sequence of measurements in \(y\)). Not to be confused withscramble, which permutes the columns of \(H_h\).fast(bool, optional): Whether to use the fast, separable computation of the 2D S-transform. If False, it uses (memory-heavy) matrix-vector products with the full measurement matrix. Defaults to True.reshape_output(bool, optional): Whether to reshape the output of the adjoint and pseudo-inverse methods to images. If False, outputs are vectors.scramble(bool, optional): If True, the columns of \(H_h\) are randomly permuted. Defaults to False. Not to be confused withorder, which permutes the rows of the system matrix \(A\) (i.e. the sequence of measurements in \(y\)), applied after scrambling.seed(int, optional): Seed used to generate the random column permutation whenscrambleis True, ensuring reproducibility. Defaults to 0.noise_model(seespyrit.core.noise): Noise model \(\mathcal{N}\). Defaults to torch.nn.Identity().dtype(torch.dtype, optional): Data type of the measurement matrix. Defaults to torch.float32.device(torch.device, optional): Device of the measurement matrix. Defaults to torch.device(“cpu”).- Attributes:
H1d(torch.tensor): 1D Walsh-ordered Hadamard matrix of shape \((h,h)\), values in \(\{-1,+1\}\) (the same matrix asHadamSplit2d.H1d). Columns \(1,\dots,h-1\) are permuted ifscrambleis True (seecolumn_perm); column 0 is always left fixed.column_perm(torch.tensor, optional): The random column permutation applied to \(H_h\); column_perm[0] == 0 always. Only set as an attribute whenscrambleis True.h(int): Image size \(h\).A(torch.tensor): The 2D measurement matrix given by \(\max(0, -(H_h\otimes H_h))\), subsampled toself.Mrows. Computed on the fly (not stored) to avoid materializing a \(h^2 \times h^2\) matrix.H(torch.tensor): Alias forA, kept for compatibility with code that expects a measurement operator to expose H (seeA’s docstring).M(int): Number of measurements \(M\).N(int): Number of pixels in the image, equal to \(h^2\).meas_shape(torch.Size): Shape of the measurement patterns. Equal to \((h, h)\).meas_dims(torch.Size): Dimensions of the image the acquisition matrix applies to. Equal to (-2, -1).order(torch.tensor): Order matrix \(O\). Only affects the row order (sequence) of the measurements in \(y\); unrelated toscramble, which affects the columns of \(H_h\) instead.indices(torch.tensor): Indices used to reorder the measurement vector (derived fromorder). Used by the methodreindex().- Example 1:
Basic construction and simulated (subsampled) measurements. The very first measurement is always (up to floating-point error) 0, since it corresponds to the trivial all-zero row of \(A\) (see the warning above).
>>> h = 64 # h must be a power of two >>> meas_op = HadamSmatrix2d(h, 2000) >>> print(meas_op.H1d.shape) torch.Size([64, 64]) >>> print(meas_op.M) 2000 >>> x = torch.rand(4, h, h) >>> y = meas_op(x) >>> print(y.shape) torch.Size([4, 2000]) >>> print(torch.allclose(y[:, 0], torch.zeros(4), atol=1e-2)) True
- Example 2:
With full sampling (
M= \(h^2\), the default) and the top-left pixel set to 0 (see the warning above),fast_pinv()recovers the image exactly.>>> h = 16 >>> meas_op = HadamSmatrix2d(h) # M defaults to h**2 (full sampling) >>> print(meas_op.M == meas_op.N) True >>> x = torch.rand(2, h, h) >>> x[:, 0, 0] = 0 # convention: top-left pixel carries no information >>> y = meas_op.measure(x) >>> x_hat = meas_op.fast_pinv(y, vectorize=False) >>> print(torch.allclose(x, x_hat, atol=1e-4)) True
- Example 3:
With subsampling (
M< \(h^2\)),fast_pinv()only approximates the image (see the note infast_pinv()), unlike the exact recovery obtained above with full sampling.>>> meas_op_sub = HadamSmatrix2d(h, M=100) >>> y_sub = meas_op_sub.measure(x) >>> x_hat_sub = meas_op_sub.fast_pinv(y_sub, vectorize=False) >>> print(torch.allclose(x, x_hat_sub, atol=1e-4)) False
- Example 4:
scramblepermutes columns \(1,\dots,h-1\) ofH1d(never column 0), breaking its symmetry – but full-sampling recovery viafast_pinv()remains exact, including at the top-left pixel, exactly as in Example 2.>>> meas_op_scrambled = HadamSmatrix2d(h, scramble=True, seed=42) >>> print(torch.allclose(meas_op.H1d, meas_op.H1d.T)) # unscrambled: symmetric True >>> print(torch.allclose(meas_op_scrambled.H1d, meas_op_scrambled.H1d.T)) # scrambled: not symmetric False >>> y2 = meas_op_scrambled.measure(x) >>> x_hat2 = meas_op_scrambled.fast_pinv(y2, vectorize=False) >>> print(torch.allclose(x, x_hat2, atol=1e-4)) True
- Example 5:
orderandscrambleact independently (see note above): changingorderselects/reorders which measurements are kept, but does not affectH1ditself.>>> order = torch.rand(h, h) >>> meas_op_ordered = HadamSmatrix2d(h, M=100, order=order) >>> print(torch.equal(meas_op_ordered.H1d, meas_op.H1d)) True
Methods
adjoint(m[, unvectorize])Apply the adjoint (transpose) of the measurement matrix.
fast_adjoint(m[, unvectorize])Apply the adjoint of the measurement matrix using the separability of the 2D S-transform.
fast_measure(x)Simulate noiseless measurements using the separability of the 2D S-transform.
fast_pinv(m[, vectorize])Apply the pseudo-inverse of the measurement matrix.
forward(x)Simulate noisy measurements \(y = \mathcal{N}(\mathcal{S}(\mathrm{vec}^{-1}(A\,\mathrm{vec}(X))))\).
measure(x)Simulate noiseless measurements.
reindex(x[, axis, inverse_permutation])Sorts a tensor along a specified axis using
self.indices.set_matrix_to_inverse(matrix_name)unvectorize(input)Unflatten the measured dimensions.
vectorize(input)Flatten the measured dimensions.