def check_invpascal(n, kind, exact): ip = invpascal(n, kind=kind, exact=exact) p = pascal(n, kind=kind, exact=exact) # Matrix-multiply ip and p, and check that we get the identity matrix. # We can't use the simple expression e = ip.dot(p), because when # n < 35 and exact is True, p.dtype is np.uint64 and ip.dtype is # np.int64. The product of those dtypes is np.float64, which loses # precision when n is greater than 18. Instead we'll cast both to # object arrays, and then multiply. e = ip.astype(object).dot(p.astype(object)) assert_array_equal(e, eye(n), err_msg="n=%d kind=%r exact=%r" % (n, kind, exact))
def cossin(cls, m, n): r""" Product of cosine and sine powers: ``Angular.cossin(m, n)`` means :math:`\cos^m\theta \cdot \sin^n\theta` (*n* must be even). """ if not isinstance(m, int) or m < 0: raise ValueError('Cosine power must be positive integer.') if not isinstance(n, int) or n < 0 or n % 2: raise ValueError('Sine power must be even positive integer.') c = np.zeros(1 + m + n) # binomial coefficients of (1 - cos^2)^(n/2) c[m::2] = invpascal(1 + n // 2, 'lower', False)[-1, ::-1] return cls(c)
def polyrecenter(pols, x, yerr, center): n, pols, x, yerr = len(pols), np.array(pols)[::-1], np.array(x), np.array( yerr) P = invpascal(n, kind='lower').T A = np.zeros([n, n]) for i in range(n): for j in range(n): if (j - i) < 0: continue A[i, j] = P[i, j] * center**(j - i) new_pols = (np.linalg.inv(A) @ pols)[::-1] alpha = [] for j, a1 in enumerate(new_pols): alpha.append([]) for k, a2 in enumerate(new_pols): alpha[j].append( sum((yerr**-2) * ((x - center)**j) * ((x - center)**k))) error_matrix = np.linalg.inv(alpha) new_sds = list(np.sqrt(np.diag(error_matrix)))[::-1] return new_pols, new_sds
def time_invpascal(self, size): sl.invpascal(size)
def get_bs_cached(Rmax, order=2, odd=False, direction='inverse', reg=None, valid=None, basis_dir=None, verbose=False): """ Internal function. Gets the basis set (from cache or runs computations and caches them) and calculates the transform matrix. Loaded/calculated matrices are also cached in memory. Parameters ---------- Rmax : int largest radius to be transformed order : int highest angular order odd : bool include odd angular orders direction : str: ``'forward'`` or ``'inverse'`` type of Abel transform to be performed reg : None or str or tuple (str, float) regularization type and strength for inverse transform valid : None or bool array flags to exclude invalid radii from transform basis_dir : str, optional path to the directory for saving / loading the basis set. If ``None``, the basis set will not be saved to disk. verbose : bool print some debug information Returns ------- A : list of 2D numpy arrays (**Rmax** + 1) × (**Rmax** + 1) matrices of the Abel transform (forward or inverse) for each angular order """ global _bs_prm, _bs, _trf, _tri_full, _tri_prm, _tri new_bs = False # new basis set computed (for saving to disk) prm = [Rmax, order, odd] if _bs is None or _bs_prm != prm: _bs_prm = prm # try to load basis set and maybe inverse-transform matrices _bs, _tri_full = _load_bs(basis_dir, Rmax, order, odd, direction == 'inverse' and reg is None, verbose) if _bs is None: if verbose: print('Computing basis set...') _bs = _bs_rbasex(Rmax, order, odd) new_bs = True # reset transforms _trf = None _tri_prm = None _tri = None else: if verbose: print('Using cached basis set') if valid is None or valid.all(): invalid = None else: invalid = np.logical_not(valid) def mask(A): # Zero rows for output radii without data (columns do not need to be # zeroed, since input profiles already have zeros there). # Array is modified; to preserve the original — pass a copy. if invalid is not None: A[invalid] = 0 return A def Af(): # Make optionally masked forward-transform matrices. if invalid is None: return [Pn.T for Pn in _bs] else: return [mask(Pn.T.copy()) for Pn in _bs] if direction == 'forward': if _trf is None: if new_bs: _save_bs(basis_dir, Rmax, order, odd, _bs, None, verbose) if verbose: print('Creating forward-transform matrices...') _trf = Af() return _trf else: # 'inverse' if _tri_prm != [reg]: _tri_prm = [reg] if reg is None: # calculate full inverse matrices, if not yet if _tri_full is None: if verbose: print('Calculating inverse-transform matrices...') # P[n] are triangular, thus can be inverted faster than # general matrices, however, NumPy/SciPy do not have such # functions; nevertheless, solve_triangular() is ~twice # faster than inv() (and ...(Pn, I, lower=True).T is faster # than ...(Pn.T, I)) I = np.eye(Rmax + 1) _tri_full = [ solve_triangular(Pn, I, lower=True).T for Pn in _bs ] new_bs = True # mask invalid radii _tri = [mask(An.copy()) for An in _tri_full] elif reg == 'pos': # non-negative cos sin if verbose: print('Preparing matrices for NNLS equations...') # Construct forward transform matrix cossin → cos projections. # Notes: # 1. By reversing orders, it also could be made triangular for # more effective inversion, but nnls() does not care. # 2. This code is not optimized, but its execution time is # still negligible compared to nnls(). if odd: if order > 1: raise ValueError('reg="pos" is not implemented for ' 'odd orders > 1') # use (1 ± cos) / 2 A0, A1 = Af() A = [[A0, A0], [A1, -A1]] else: # even only N = 1 + order // 2 # cossin → cos transform C = np.flip(invpascal(N, 'upper')) # blocks for each order combination A = [[C[n, m] * An for m in range(N)] for n, An in enumerate(Af())] # make single matrix from blocks _tri = np.block(A) elif np.ndim(reg) == 0: # not sequence type raise ValueError( 'Wrong regularization format "{}"'.format(reg)) elif reg[0] == 'L2': # Tikhonov L2 norm if verbose: print('Calculating L2-regularized transform matrices...') E = np.diag([reg[1]] * (Rmax + 1)) # regularized inverse for each angular order _tri = [An.T.dot(inv((An).dot(An.T) + E)) for An in Af()] elif reg[0] == 'diff': # Tikhonov derivative if verbose: print('Calculating diff-regularized transform matrices...') # GTG = reg D^T D, where D is 1st-order difference operator GTG = 2 * np.eye(Rmax + 1) - \ np.eye(Rmax + 1, k=-1) - \ np.eye(Rmax + 1, k=1) GTG[0, 0] = 1 GTG[-1, -1] = 1 GTG *= reg[1] # regularized inverse for each angular order _tri = [An.T.dot(inv((An).dot(An.T) + GTG)) for An in Af()] elif reg[0] == 'SVD': if verbose: print('Calculating SVD-regularized transform matrices...') if reg[1] > 1: raise ValueError( 'Wrong SVD truncation factor {} > 1'.format(reg[1])) # truncation index (smallest SV of P -> largest SV of inverse) smax = int((1 - reg[1]) * Rmax) + 1 _tri = [] # loop over angular orders for An in Af(): U, s, Vh = svd(An.T) # truncate matrices U = U[:, :smax] s = 1 / s[:smax] # inverse Vh = Vh[:smax] # regularized inverse for this angular order _tri.append((U * s).dot(Vh)) else: raise ValueError('Wrong regularization type "{}"'.format( reg[0])) if new_bs: _save_bs(basis_dir, Rmax, order, odd, _bs, _tri_full, verbose) return _tri
def rbasex_transform(IM, origin='center', rmax='MIN', order=2, odd=False, weights=None, direction='inverse', reg=None, out='same', basis_dir=None, verbose=False): r""" This function takes the input image and outputs its forward or inverse Abel transform as an image and its radial distributions. The **origin**, **rmax**, **order**, **odd** and **weights** parameters are passed to :class:`abel.tools.vmi.Distributions`, so see its documentation for their detailed descriptions. Parameters ---------- IM : m × n numpy array the image to be transformed origin : tuple of int or str image origin, explicit in the (row, column) format, or as a location string (by default, the image center) rmax : int or string largest radius to include in the transform (by default, the largest radius with at least one full quadrant of data) order : int highest angular order present in the data, ≥ 0 (by default, 2) odd : bool include odd angular orders (by default is `False`, but is enabled automatically if **order** is odd) weights : m × n numpy array, optional weighting factors for each pixel. The array shape must match the image shape. Parts of the image can be excluded from analysis by assigning zero weights to their pixels. By default is `None`, which applies equal weight to all pixels. direction : str: ``'forward'`` or ``'inverse'`` type of Abel transform to be performed (by default, inverse) reg : None or str or tuple (str, float), optional regularization to use for inverse Abel transform. ``None`` (default) means no regularization, a string selects a non-parameterized regularization method, and parameterized methods are selected by a tuple (`method`, `strength`). Available methods are: ``('L2', strength)``: Tikhonov :math:`L_2` regularization with `strength` as the square of the Tikhonov factor. This is the same as “Tikhonov regularization” used in BASEX, with almost identical effects on the radial distributions. ``('diff', strength)``: Tikhonov regularization with the difference operator (approximation of the derivative) multiplied by the square root of `strength` as the Tikhonov matrix. This tends to produce less blurring, but more negative overshoots than ``'L2'``. ``('SVD', strength)``: truncated SVD (singular value decomposition) with N = `strength` × **rmax** largest singular values removed for each angular order. This mimics the approach proposed (but in fact not used) in pBasex. `Not recommended` due to generally poor results. ``'pos'``: non-parameterized method, finds the best (in the least-squares sense) solution with non-negative :math:`\cos^n\theta \sin^m\theta` terms (see :meth:`~abel.tools.vmi.Distributions.Results.cossin`). For **order** = 0, 1, and 2 (with **odd** = `False`) this is equivalent to :math:`I(r, \theta) \geqslant 0`; for higher orders this assumption is stronger than :math:`I \geqslant 0` and corresponds to no interference between different multiphoton channels. Not implemented for odd orders > 1. Notice that this method is nonlinear, which also means that it is considerably slower than the linear methods and the transform operator cannot be cached. In all cases, `strength` = 0 provides no regularization. For the Tikhonov methods, `strength` ~ 100 is a reasonable value for megapixel images. For truncated SVD, `strength` must be < 1; `strength` ~ 0.1 is a reasonable value; `strength` ~ 0.5 can produce noticeable ringing artifacts. See the :ref:`full description <rBasexmathreg>` and examples there. out : str or None shape of the output image: ``'same'`` (default): same shape and origin as the input ``'fold'`` (fastest): Q0 (upper right) quadrant (for ``odd=False``) or right half (for ``odd=True``) up to **rmax**, but limited to the largest input-image quadrant (or half) ``'unfold'``: like ``'fold'``, but symmetrically “unfolded” to all 4 quadrants ``'full'``: all pixels with radii up to **rmax** ``'full-unique'``: the unique part of ``'full'``: Q0 (upper right) quadrant for ``odd=False``, right half for ``odd=True`` ``None``: no image (**recon** will be ``None``). Can be useful to avoid unnecessary calculations when only the transformed radial distributions (**distr**) are needed. basis_dir : str, optional path to the directory for saving / loading the basis set (useful only for the inverse transform without regularization; time savings in other cases are small and might be negated by the disk-access overhead). If ``None`` (default), the basis set will not be loaded from or saved to disk. verbose : bool print information about processing (for debugging), disabled by default Returns ------- recon : 2D numpy array or None the transformed image. Is centered and might have different dimensions than the input image. distr : Distributions.Results object the object from which various distributions for the transformed image can be retrieved, see :class:`abel.tools.vmi.Distributions.Results` """ if order == 0: odd = False # (to eliminate additional checks) elif order % 2: odd = True # enable automatically for odd orders # extract radial profiles from input image p = _profiles(IM, origin, rmax, order, odd, weights, verbose) # (caches Distributions as _dst) Rmax = _dst.rmax # get appropriate transform matrices A = get_bs_cached(Rmax, order, odd, direction, reg, _dst.valid, basis_dir, verbose) # transform radial profiles if reg == 'pos': if verbose: print('Solving NNLS equations...') N = len(p) p = np.hstack(p) cs = nnls(A, p)[0] cs = np.split(cs, N) if odd: # (1 ± cos) / 2 → cos^0, cos^1 c = [cs[0] + cs[1], cs[0] - cs[1]] else: # cossin → cos transform C = np.flip(invpascal(N, 'upper')) c = C.dot(cs) else: if verbose: print('Applying radial transforms...') c = [An.dot(pn) for An, pn in zip(A, p)] # construct output (transformed) distributions distr = Distributions.Results(np.arange(Rmax + 1), np.array(c), order, odd, _dst.valid) if out is None: return None, distr # output size if out == 'same': height = _dst.shape[0] if odd else _dst.VER + 1 width = _dst.HOR + 1 row = _dst.row if odd else 0 elif out in ['fold', 'unfold']: height = _dst.Qheight width = _dst.Qwidth row = _dst.row if odd else 0 elif out in ['full', 'full-unique']: height = 2 * Rmax + 1 if odd else Rmax + 1 width = Rmax + 1 row = Rmax if odd else 0 else: raise ValueError('Wrong output shape "{}"'.format(out)) # construct output image from transformed radial profiles if verbose: print('Constructing output image...') # bottom right quadrant or right half recon = _image(height, width, row, c, verbose) if odd: if out not in ['fold', 'full-unique']: # combine with left half (mirrored without central column) recon = np.hstack((recon[:, :0:-1], recon)) else: # even only recon = recon[::-1] # flip to Q0 if out not in ['fold', 'full-unique']: # assemble full image recon = put_image_quadrants((recon, recon, recon, recon), (2 * height - 1, 2 * width - 1)) if out == 'same': # crop as needed row = 0 if odd else _dst.VER - _dst.row col = _dst.HOR - _dst.col H, W = IM.shape recon = recon[row:row + H, col:col + W] return recon, distr
numalign='right')) if latex: print(r'}\end{table}') print('\n') if __name__ == '__main__': LATEX_DISPLAY = False tests = OrderedDict() eps = np.finfo(np.float).eps rcond = None # Pascal matrices if not LATEX_DISPLAY: print('Treating Pascal matrices:') n_values = (4, 6, 8, 10) matrices = [scla.pascal(n).astype(int) for n in n_values] pinv_ref = [scla.invpascal(n).astype(int) for n in n_values] tests['Pascal'] = PinvStabilityTester(matrices, pinv_ref) tests['Pascal'].compute_all() if LATEX_DISPLAY: tests['Pascal'].print_all(parameters=OrderedDict( {'$n$': [r'${:d}$'.format(n) for n in n_values]}), latex=True, label='Pascal', caption='Pascal matrices $P(n)$') else: tests['Pascal'].print_all(parameters=OrderedDict({'n': n_values})) # N(0,1) random matrices if not LATEX_DISPLAY: print('Treating N(0,1) random matrices:') n_values = (4, 6, 8, 10) matrices = [reset_randn(3 * n, n).astype(np.float) for n in n_values]