def get_Kprefactor(z, cos_theta, zint=100.0, n2n1=0.95, get_hdet=False,
        **kwargs):
    """
    Returns a prefactor in the electric field integral.

    This is an internal function called by get_K. The returned prefactor
    in the integrand is independent of which integral is being called;
    it is a combination of the exp(1j*phase) and apodization.

    Parameters
    ----------
        z : numpy.ndarray
            The values of z (distance along optical axis) at which to
            calculate the prefactor. Size is unrelated to the size of
            `cos_theta`
        cos_theta : numpy.ndarray
            The values of cos(theta) (i.e. position on the incoming
            focal spherical wavefront) at which to calculate the
            prefactor. Size is unrelated to the size of `z`
        zint : Float, optional
            The position of the optical interface, in units of 1/k.
            Default is 100.
        n2n1 : Float, optional
            The ratio of the index mismatch between the optics (n1) and
            the sample (n2). Default is 0.95
        get_hdet : Bool, optional
            Set to True to calculate the detection prefactor vs the
            illumination prefactor (i.e. False to include apodization).
            Default is False

    Returns
    -------
        numpy.ndarray
            The prefactor, of size [`z.size`, `cos_theta.size`], sampled
            at the values [`z`, `cos_theta`]
    """
    phase = f_theta(cos_theta, zint, z, n2n1=n2n1, **kwargs)
    to_return = np.exp(-1j*phase)
    if not get_hdet:
        to_return *= np.outer(np.ones_like(z),np.sqrt(cos_theta))

    return to_return

def get_Kprefactor ( z , cos_theta , zint = 100.0 , n2n1 = 0.95 , get_hdet = False , ** kwargs ) : phase = f_theta ( cos_theta , zint , z , n2n1 = n2n1 , ** kwargs ) to_return = np . exp ( - 1j * phase ) if not get_hdet : to_return *= np . outer ( np . ones_like ( z ) , np . sqrt ( cos_theta ) ) return to_return

Returns a prefactor in the electric field integral .


def get_K(rho, z, alpha=1.0, zint=100.0, n2n1=0.95, get_hdet=False, K=1,
        Kprefactor=None, return_Kprefactor=False, npts=20, **kwargs):
    """
    Calculates one of three electric field integrals.

    Internal function for calculating point spread functions. Returns
    one of three electric field integrals that describe the electric
    field near the focus of a lens; these integrals appear in Hell's psf
    calculation.

    Parameters
    ----------
        rho : numpy.ndarray
            Rho in cylindrical coordinates, in units of 1/k.
        z : numpy.ndarray
            Z in cylindrical coordinates, in units of 1/k. `rho` and
            `z` must be the same shape

        alpha : Float, optional
            The acceptance angle of the lens, on (0,pi/2). Default is 1.
        zint : Float, optional
            The distance of the len's unaberrated focal point from the
            optical interface, in units of 1/k. Default is 100.
        n2n1 : Float, optional
            The ratio n2/n1 of the index mismatch between the sample
            (index n2) and the optical train (index n1). Must be on
            [0,inf) but should be near 1. Default is 0.95
        get_hdet : Bool, optional
            Set to True to get the detection portion of the psf; False
            to get the illumination portion of the psf. Default is True
        K : {1, 2, 3}, optional
            Which of the 3 integrals to evaluate. Default is 1
        Kprefactor : numpy.ndarray or None
            This array is calculated internally and optionally returned;
            pass it back to avoid recalculation and increase speed. Default
            is None, i.e. calculate it internally.
        return_Kprefactor : Bool, optional
            Set to True to also return the Kprefactor (parameter above)
            to speed up the calculation for the next values of K. Default
            is False
        npts : Int, optional
            The number of points to use for Gauss-Legendre quadrature of
            the integral. Default is 20, which is a good number for x,y,z
            less than 100 or so.

    Returns
    -------
        kint : numpy.ndarray
            The integral K_i; rho.shape numpy.array
        [, Kprefactor] : numpy.ndarray
            The prefactor that is independent of which integral is being
            calculated but does depend on the parameters; can be passed
            back to the function for speed.

    Notes
    -----
        npts=20 gives double precision (no difference between 20, 30, and
        doing all the integrals with scipy.quad). The integrals are only
        over the acceptance angle of the lens, so for moderate x,y,z they
        don't vary too rapidly. For x,y,z, zint large compared to 100, a
        higher npts might be necessary.
    """
    # Comments:
        # This is the only function that relies on rho,z being numpy.arrays,
        # and it's just in a flag that I've added.... move to psf?
    if type(rho) != np.ndarray or type(z) != np.ndarray or (rho.shape != z.shape):
        raise ValueError('rho and z must be np.arrays of same shape.')

    pts, wts = np.polynomial.legendre.leggauss(npts)
    n1n2 = 1.0/n2n1

    rr = np.ravel(rho)
    zr = np.ravel(z)

    #Getting the array of points to quad at
    cos_theta = 0.5*(1-np.cos(alpha))*pts+0.5*(1+np.cos(alpha))
    #[cos_theta,rho,z]

    if Kprefactor is None:
        Kprefactor = get_Kprefactor(z, cos_theta, zint=zint, \
            n2n1=n2n1,get_hdet=get_hdet, **kwargs)

    if K==1:
        part_1 = j0(np.outer(rr,np.sqrt(1-cos_theta**2)))*\
            np.outer(np.ones_like(rr), 0.5*(get_taus(cos_theta,n2n1=n2n1)+\
            get_taup(cos_theta,n2n1=n2n1)*csqrt(1-n1n2**2*(1-cos_theta**2))))
        integrand = Kprefactor * part_1
    elif K==2:
        part_2=j2(np.outer(rr,np.sqrt(1-cos_theta**2)))*\
            np.outer(np.ones_like(rr),0.5*(get_taus(cos_theta,n2n1=n2n1)-\
            get_taup(cos_theta,n2n1=n2n1)*csqrt(1-n1n2**2*(1-cos_theta**2))))
        integrand = Kprefactor * part_2
    elif K==3:
        part_3=j1(np.outer(rho,np.sqrt(1-cos_theta**2)))*\
            np.outer(np.ones_like(rr), n1n2*get_taup(cos_theta,n2n1=n2n1)*\
            np.sqrt(1-cos_theta**2))
        integrand = Kprefactor * part_3
    else:
        raise ValueError('K=1,2,3 only...')

    big_wts=np.outer(np.ones_like(rr), wts)
    kint = (big_wts*integrand).sum(axis=1) * 0.5*(1-np.cos(alpha))

    if return_Kprefactor:
        return kint.reshape(rho.shape), Kprefactor
    else:
        return kint.reshape(rho.shape)

Calculates one of three electric field integrals .


def get_hsym_asym(rho, z, get_hdet=False, include_K3_det=True, **kwargs):
    """
    Calculates the symmetric and asymmetric portions of a confocal PSF.

    Parameters
    ----------
        rho : numpy.ndarray
            Rho in cylindrical coordinates, in units of 1/k.
        z : numpy.ndarray
            Z in cylindrical coordinates, in units of 1/k. Must be the
            same shape as `rho`
        get_hdet : Bool, optional
            Set to True to get the detection portion of the psf; False
            to get the illumination portion of the psf. Default is True
        include_K3_det : Bool, optional.
            Flag to not calculate the `K3' component for the detection
            PSF, corresponding to (I think) a low-aperature focusing
            lens and no z-polarization of the focused light. Default
            is True, i.e. calculates the K3 component as if the focusing
            lens is high-aperture

    Other Parameters
    ----------------
        alpha : Float, optional
            The acceptance angle of the lens, on (0,pi/2). Default is 1.
        zint : Float, optional
            The distance of the len's unaberrated focal point from the
            optical interface, in units of 1/k. Default is 100.
        n2n1 : Float, optional
            The ratio n2/n1 of the index mismatch between the sample
            (index n2) and the optical train (index n1). Must be on
            [0,inf) but should be near 1. Default is 0.95

    Returns
    -------
        hsym : numpy.ndarray
            `rho`.shape numpy.array of the symmetric portion of the PSF
        hasym : numpy.ndarray
            `rho`.shape numpy.array of the asymmetric portion of the PSF
    """

    K1, Kprefactor = get_K(rho, z, K=1, get_hdet=get_hdet, Kprefactor=None,
            return_Kprefactor=True, **kwargs)
    K2 = get_K(rho, z, K=2, get_hdet=get_hdet, Kprefactor=Kprefactor,
            return_Kprefactor=False, **kwargs)

    if get_hdet and not include_K3_det:
        K3 = 0*K1
    else:
        K3 = get_K(rho, z, K=3, get_hdet=get_hdet, Kprefactor=Kprefactor,
            return_Kprefactor=False, **kwargs)

    hsym = K1*K1.conj() + K2*K2.conj() + 0.5*(K3*K3.conj())
    hasym= K1*K2.conj() + K2*K1.conj() + 0.5*(K3*K3.conj())

    return hsym.real, hasym.real

def get_hsym_asym ( rho , z , get_hdet = False , include_K3_det = True , ** kwargs ) : K1 , Kprefactor = get_K ( rho , z , K = 1 , get_hdet = get_hdet , Kprefactor = None , return_Kprefactor = True , ** kwargs ) K2 = get_K ( rho , z , K = 2 , get_hdet = get_hdet , Kprefactor = Kprefactor , return_Kprefactor = False , ** kwargs ) if get_hdet and not include_K3_det : K3 = 0 * K1 else : K3 = get_K ( rho , z , K = 3 , get_hdet = get_hdet , Kprefactor = Kprefactor , return_Kprefactor = False , ** kwargs ) hsym = K1 * K1 . conj ( ) + K2 * K2 . conj ( ) + 0.5 * ( K3 * K3 . conj ( ) ) hasym = K1 * K2 . conj ( ) + K2 * K1 . conj ( ) + 0.5 * ( K3 * K3 . conj ( ) ) return hsym . real , hasym . real

Calculates the symmetric and asymmetric portions of a confocal PSF .


def get_polydisp_pts_wts(kfki, sigkf, dist_type='gaussian', nkpts=3):
    """
    Calculates a set of Gauss quadrature points & weights for polydisperse
    light.

    Returns a list of points and weights of the final wavevector's distri-
    bution, in units of the initial wavevector.

    Parameters
    ----------
        kfki : Float
            The mean of the polydisperse outgoing wavevectors.
        sigkf : Float
            The standard dev. of the polydisperse outgoing wavevectors.
        dist_type : {`gaussian`, `gamma`}, optional
            The distribution, gaussian or gamma, of the wavevectors.
            Default is `gaussian`
        nkpts : Int, optional
            The number of quadrature points to use. Default is 3
    Returns
    -------
        kfkipts : numpy.ndarray
            The Gauss quadrature points at which to calculate kfki.
        wts : numpy.ndarray
            The associated Gauss quadrature weights.
    """
    if dist_type.lower() == 'gaussian':
        pts, wts = np.polynomial.hermite.hermgauss(nkpts)
        kfkipts = np.abs(kfki + sigkf*np.sqrt(2)*pts)
    elif dist_type.lower() == 'laguerre' or dist_type.lower() == 'gamma':
        k_scale = sigkf**2/kfki
        associated_order = kfki**2/sigkf**2 - 1
        #Associated Laguerre with alpha >~170 becomes numerically unstable, so:
        max_order=150
        if associated_order > max_order or associated_order < (-1+1e-3):
            warnings.warn('Numerically unstable sigk, clipping', RuntimeWarning)
            associated_order = np.clip(associated_order, -1+1e-3, max_order)
        kfkipts, wts = la_roots(nkpts, associated_order)
        kfkipts *= k_scale
    else:
        raise ValueError('dist_type must be either gaussian or laguerre')
    return kfkipts, wts/wts.sum()

Calculates a set of Gauss quadrature points & weights for polydisperse light .


def calculate_linescan_ilm_psf(y,z, polar_angle=0., nlpts=1,
        pinhole_width=1, use_laggauss=False, **kwargs):
    """
    Calculates the illumination PSF for a line-scanning confocal with the
    confocal line oriented along the x direction.

    Parameters
    ----------
        y : numpy.ndarray
            The y points (in-plane, perpendicular to the line direction)
            at which to evaluate the illumination PSF, in units of 1/k.
            Arbitrary shape.
        z : numpy.ndarray
            The z points (optical axis) at which to evaluate the illum-
            ination PSF, in units of 1/k. Must be the same shape as `y`
        polar_angle : Float, optional
            The angle of the illuminating light's polarization with
            respect to the line's orientation along x. Default is 0.
        pinhole_width : Float, optional
            The width of the geometric image of the line projected onto
            the sample, in units of 1/k. Default is 1. The perfect line
            image is assumed to be a Gaussian. If `nlpts` is set to 1,
            the line will always be of zero width.
        nlpts : Int, optional
            The number of points to use for Hermite-gauss quadrature over
            the line's width. Default is 1, corresponding to a zero-width
            line.
        use_laggauss : Bool, optional
            Set to True to use a more-accurate sinh'd Laguerre-Gauss
            quadrature for integration over the line's length (more accurate
            in the same amount of time). Default is False for backwards
            compatibility.  FIXME what did we do here?

    Other Parameters
    ----------------
        alpha : Float, optional
            The acceptance angle of the lens, on (0,pi/2). Default is 1.
        zint : Float, optional
            The distance of the len's unaberrated focal point from the
            optical interface, in units of 1/k. Default is 100.
        n2n1 : Float, optional
            The ratio n2/n1 of the index mismatch between the sample
            (index n2) and the optical train (index n1). Must be on
            [0,inf) but should be near 1. Default is 0.95

    Returns
    -------
        hilm : numpy.ndarray
            The line illumination, of the same shape as y and z.
    """
    if use_laggauss:
        x_vals, wts = calc_pts_lag()
    else:
        x_vals, wts = calc_pts_hg()

    #I'm assuming that y,z are already some sort of meshgrid
    xg, yg, zg = [np.zeros( list(y.shape) + [x_vals.size] ) for a in range(3)]
    hilm = np.zeros(xg.shape)

    for a in range(x_vals.size):
        xg[...,a] = x_vals[a]
        yg[...,a] = y.copy()
        zg[...,a] = z.copy()

    y_pinhole, wts_pinhole = np.polynomial.hermite.hermgauss(nlpts)
    y_pinhole *= np.sqrt(2)*pinhole_width
    wts_pinhole /= np.sqrt(np.pi)

    #Pinhole hermgauss first:
    for yp, wp in zip(y_pinhole, wts_pinhole):
        rho = np.sqrt(xg*xg + (yg-yp)*(yg-yp))
        phi = np.arctan2(yg,xg)

        hsym, hasym = get_hsym_asym(rho,zg,get_hdet = False, **kwargs)
        hilm += wp*(hsym + np.cos(2*(phi-polar_angle))*hasym)

    #Now line hermgauss
    for a in range(x_vals.size):
        hilm[...,a] *= wts[a]

    return hilm.sum(axis=-1)*2.

def calculate_linescan_ilm_psf ( y , z , polar_angle = 0. , nlpts = 1 , pinhole_width = 1 , use_laggauss = False , ** kwargs ) : if use_laggauss : x_vals , wts = calc_pts_lag ( ) else : x_vals , wts = calc_pts_hg ( ) xg , yg , zg = [ np . zeros ( list ( y . shape ) + [ x_vals . size ] ) for a in range ( 3 ) ] hilm = np . zeros ( xg . shape ) for a in range ( x_vals . size ) : xg [ ... , a ] = x_vals [ a ] yg [ ... , a ] = y . copy ( ) zg [ ... , a ] = z . copy ( ) y_pinhole , wts_pinhole = np . polynomial . hermite . hermgauss ( nlpts ) y_pinhole *= np . sqrt ( 2 ) * pinhole_width wts_pinhole /= np . sqrt ( np . pi ) for yp , wp in zip ( y_pinhole , wts_pinhole ) : rho = np . sqrt ( xg * xg + ( yg - yp ) * ( yg - yp ) ) phi = np . arctan2 ( yg , xg ) hsym , hasym = get_hsym_asym ( rho , zg , get_hdet = False , ** kwargs ) hilm += wp * ( hsym + np . cos ( 2 * ( phi - polar_angle ) ) * hasym ) for a in range ( x_vals . size ) : hilm [ ... , a ] *= wts [ a ] return hilm . sum ( axis = - 1 ) * 2.

Calculates the illumination PSF for a line - scanning confocal with the confocal line oriented along the x direction .


def calculate_linescan_psf(x, y, z, normalize=False, kfki=0.889, zint=100.,
        polar_angle=0., wrap=True, **kwargs):
    """
    Calculates the point spread function of a line-scanning confocal.

    Make x,y,z  __1D__ numpy.arrays, with x the direction along the
    scan line. (to make the calculation faster since I dont' need the line
    ilm for each x).

    Parameters
    ----------
        x : numpy.ndarray
            _One_dimensional_ array of the x grid points (along the line
            illumination) at which to evaluate the psf. In units of
            1/k_incoming.
        y : numpy.ndarray
            _One_dimensional_ array of the y grid points (in plane,
            perpendicular to the line illumination) at which to evaluate
            the psf. In units of 1/k_incoming.
        z : numpy.ndarray
            _One_dimensional_ array of the z grid points (along the
            optical axis) at which to evaluate the psf. In units of
            1/k_incoming.
        normalize : Bool, optional
            Set to True to include the effects of PSF normalization on
            the image intensity. Default is False.
        kfki : Float, optional
            The ratio of the final light's wavevector to the incoming.
            Default is 0.889
        zint : Float, optional
            The position of the optical interface, in units of 1/k_incoming
            Default is 100.
        wrap : Bool, optional
            If True, wraps the psf calculation for speed, assuming that
            the input x, y are regularly-spaced points. If x,y are not
            regularly spaced then `wrap` must be set to False. Default is True.
        polar_angle : Float, optional
            The polarization angle of the light (radians) with respect to
            the line direction (x). Default is 0.

    Other Parameters
    ----------------
        alpha : Float
            The opening angle of the lens. Default is 1.
        n2n1 : Float
            The ratio of the index in the 2nd medium to that in the first.
            Default is 0.95

    Returns
    -------
        numpy.ndarray
            A 3D- numpy.array of the point-spread function. Indexing is
            psf[x,y,z]; shape is [x.size, y,size, z.size]
    """

    #0. Set up vecs
    if wrap:
        xpts = vec_to_halfvec(x)
        ypts = vec_to_halfvec(y)
        x3, y3, z3 = np.meshgrid(xpts, ypts, z, indexing='ij')
    else:
        x3,y3,z3 = np.meshgrid(x, y, z, indexing='ij')
    rho3 = np.sqrt(x3*x3 + y3*y3)

    #1. Hilm
    if wrap:
        y2,z2 = np.meshgrid(ypts, z, indexing='ij')
        hilm0 = calculate_linescan_ilm_psf(y2, z2, zint=zint,
                polar_angle=polar_angle, **kwargs)
        if ypts[0] == 0:
            hilm = np.append(hilm0[-1:0:-1], hilm0, axis=0)
        else:
            hilm = np.append(hilm0[::-1], hilm0, axis=0)
    else:
        y2,z2 = np.meshgrid(y, z, indexing='ij')
        hilm = calculate_linescan_ilm_psf(y2, z2, zint=zint,
                polar_angle=polar_angle, **kwargs)

    #2. Hdet
    if wrap:
        #Lambda function that ignores its args but still returns correct values
        func = lambda *args: get_hsym_asym(rho3*kfki, z3*kfki, zint=kfki*zint,
                    get_hdet=True, **kwargs)[0]
        hdet = wrap_and_calc_psf(xpts, ypts, z, func)
    else:
        hdet, toss = get_hsym_asym(rho3*kfki, z3*kfki, zint=kfki*zint,
                get_hdet=True, **kwargs)

    if normalize:
        hilm /= hilm.sum()
        hdet /= hdet.sum()

    for a in range(x.size):
        hdet[a] *= hilm

    return hdet if normalize else hdet / hdet.sum()

def calculate_linescan_psf ( x , y , z , normalize = False , kfki = 0.889 , zint = 100. , polar_angle = 0. , wrap = True , ** kwargs ) : if wrap : xpts = vec_to_halfvec ( x ) ypts = vec_to_halfvec ( y ) x3 , y3 , z3 = np . meshgrid ( xpts , ypts , z , indexing = 'ij' ) else : x3 , y3 , z3 = np . meshgrid ( x , y , z , indexing = 'ij' ) rho3 = np . sqrt ( x3 * x3 + y3 * y3 ) if wrap : y2 , z2 = np . meshgrid ( ypts , z , indexing = 'ij' ) hilm0 = calculate_linescan_ilm_psf ( y2 , z2 , zint = zint , polar_angle = polar_angle , ** kwargs ) if ypts [ 0 ] == 0 : hilm = np . append ( hilm0 [ - 1 : 0 : - 1 ] , hilm0 , axis = 0 ) else : hilm = np . append ( hilm0 [ : : - 1 ] , hilm0 , axis = 0 ) else : y2 , z2 = np . meshgrid ( y , z , indexing = 'ij' ) hilm = calculate_linescan_ilm_psf ( y2 , z2 , zint = zint , polar_angle = polar_angle , ** kwargs ) if wrap : func = lambda * args : get_hsym_asym ( rho3 * kfki , z3 * kfki , zint = kfki * zint , get_hdet = True , ** kwargs ) [ 0 ] hdet = wrap_and_calc_psf ( xpts , ypts , z , func ) else : hdet , toss = get_hsym_asym ( rho3 * kfki , z3 * kfki , zint = kfki * zint , get_hdet = True , ** kwargs ) if normalize : hilm /= hilm . sum ( ) hdet /= hdet . sum ( ) for a in range ( x . size ) : hdet [ a ] *= hilm return hdet if normalize else hdet / hdet . sum ( )

Calculates the point spread function of a line - scanning confocal .


def calculate_polychrome_linescan_psf(x, y, z, normalize=False, kfki=0.889,
        sigkf=0.1, zint=100., nkpts=3, dist_type='gaussian', wrap=True,
        **kwargs):
    """
    Calculates the point spread function of a line-scanning confocal with
    polydisperse dye emission.

    Make x,y,z  __1D__ numpy.arrays, with x the direction along the
    scan line. (to make the calculation faster since I dont' need the line
    ilm for each x).

    Parameters
    ----------
        x : numpy.ndarray
            _One_dimensional_ array of the x grid points (along the line
            illumination) at which to evaluate the psf. In units of
            1/k_incoming.
        y : numpy.ndarray
            _One_dimensional_ array of the y grid points (in plane,
            perpendicular to the line illumination) at which to evaluate
            the psf. In units of 1/k_incoming.
        z : numpy.ndarray
            _One_dimensional_ array of the z grid points (along the
            optical axis) at which to evaluate the psf. In units of
            1/k_incoming.
        normalize : Bool, optional
            Set to True to include the effects of PSF normalization on
            the image intensity. Default is False.
        kfki : Float, optional
            The mean of the ratio of the final light's wavevector to the
            incoming. Default is 0.889
        sigkf : Float, optional
            The standard deviation of the ratio of the final light's
            wavevector to the incoming. Default is 0.1
        zint : Float, optional
            The position of the optical interface, in units of 1/k_incoming
            Default is 100.
        dist_type : {`gaussian`, `gamma`}, optional
            The distribution of the outgoing light. If 'gaussian' the
            resulting k-values are taken in absolute value. Default
            is `gaussian`
        wrap : Bool, optional
            If True, wraps the psf calculation for speed, assuming that
            the input x, y are regularly-spaced points. If x,y are not
            regularly spaced then `wrap` must be set to False. Default is True.

    Other Parameters
    ----------------
        polar_angle : Float, optional
            The polarization angle of the light (radians) with respect to
            the line direction (x). Default is 0.
        alpha : Float
            The opening angle of the lens. Default is 1.
        n2n1 : Float
            The ratio of the index in the 2nd medium to that in the first.
            Default is 0.95

    Returns
    -------
        numpy.ndarray
            A 3D- numpy.array of the point-spread function. Indexing is
            psf[x,y,z]; shape is [x.size, y,size, z.size]
    Notes
    -----
        Neither distribution type is perfect. If sigkf/k0 is big (>0.5ish)
        then part of the Gaussian is negative. To avoid issues an abs() is
        taken, but then the actual mean and variance are not what is
        supplied. Conversely, if sigkf/k0 is small (<0.0815), then the
        requisite associated Laguerre quadrature becomes unstable. To
        prevent this sigkf/k0 is effectively clipped to be > 0.0815.
    """
    kfkipts, wts = get_polydisp_pts_wts(kfki, sigkf, dist_type=dist_type,
            nkpts=nkpts)

    #0. Set up vecs
    if wrap:
        xpts = vec_to_halfvec(x)
        ypts = vec_to_halfvec(y)
        x3, y3, z3 = np.meshgrid(xpts, ypts, z, indexing='ij')
    else:
        x3,y3,z3 = np.meshgrid(x, y, z, indexing='ij')
    rho3 = np.sqrt(x3*x3 + y3*y3)

    #1. Hilm
    if wrap:
        y2,z2 = np.meshgrid(ypts, z, indexing='ij')
        hilm0 = calculate_linescan_ilm_psf(y2, z2, zint=zint, **kwargs)
        if ypts[0] == 0:
            hilm = np.append(hilm0[-1:0:-1], hilm0, axis=0)
        else:
            hilm = np.append(hilm0[::-1], hilm0, axis=0)
    else:
        y2,z2 = np.meshgrid(y, z, indexing='ij')
        hilm = calculate_linescan_ilm_psf(y2, z2, zint=zint, **kwargs)

    #2. Hdet
    if wrap:
        #Lambda function that ignores its args but still returns correct values
        func = lambda x,y,z, kfki=1.: get_hsym_asym(rho3*kfki, z3*kfki,
                zint=kfki*zint, get_hdet=True, **kwargs)[0]
        hdet_func = lambda kfki: wrap_and_calc_psf(xpts,ypts,z, func, kfki=kfki)
    else:
        hdet_func = lambda kfki: get_hsym_asym(rho3*kfki, z3*kfki,
                zint=kfki*zint, get_hdet=True, **kwargs)[0]
    #####
    inner = [wts[a] * hdet_func(kfkipts[a]) for a in range(nkpts)]
    hdet = np.sum(inner, axis=0)

    if normalize:
        hilm /= hilm.sum()
        hdet /= hdet.sum()
    for a in range(x.size):
        hdet[a] *= hilm

    return hdet if normalize else hdet / hdet.sum()

def calculate_polychrome_linescan_psf ( x , y , z , normalize = False , kfki = 0.889 , sigkf = 0.1 , zint = 100. , nkpts = 3 , dist_type = 'gaussian' , wrap = True , ** kwargs ) : kfkipts , wts = get_polydisp_pts_wts ( kfki , sigkf , dist_type = dist_type , nkpts = nkpts ) if wrap : xpts = vec_to_halfvec ( x ) ypts = vec_to_halfvec ( y ) x3 , y3 , z3 = np . meshgrid ( xpts , ypts , z , indexing = 'ij' ) else : x3 , y3 , z3 = np . meshgrid ( x , y , z , indexing = 'ij' ) rho3 = np . sqrt ( x3 * x3 + y3 * y3 ) if wrap : y2 , z2 = np . meshgrid ( ypts , z , indexing = 'ij' ) hilm0 = calculate_linescan_ilm_psf ( y2 , z2 , zint = zint , ** kwargs ) if ypts [ 0 ] == 0 : hilm = np . append ( hilm0 [ - 1 : 0 : - 1 ] , hilm0 , axis = 0 ) else : hilm = np . append ( hilm0 [ : : - 1 ] , hilm0 , axis = 0 ) else : y2 , z2 = np . meshgrid ( y , z , indexing = 'ij' ) hilm = calculate_linescan_ilm_psf ( y2 , z2 , zint = zint , ** kwargs ) if wrap : func = lambda x , y , z , kfki = 1. : get_hsym_asym ( rho3 * kfki , z3 * kfki , zint = kfki * zint , get_hdet = True , ** kwargs ) [ 0 ] hdet_func = lambda kfki : wrap_and_calc_psf ( xpts , ypts , z , func , kfki = kfki ) else : hdet_func = lambda kfki : get_hsym_asym ( rho3 * kfki , z3 * kfki , zint = kfki * zint , get_hdet = True , ** kwargs ) [ 0 ] inner = [ wts [ a ] * hdet_func ( kfkipts [ a ] ) for a in range ( nkpts ) ] hdet = np . sum ( inner , axis = 0 ) if normalize : hilm /= hilm . sum ( ) hdet /= hdet . sum ( ) for a in range ( x . size ) : hdet [ a ] *= hilm return hdet if normalize else hdet / hdet . sum ( )

Calculates the point spread function of a line - scanning confocal with polydisperse dye emission .


def wrap_and_calc_psf(xpts, ypts, zpts, func, **kwargs):
    """
    Wraps a point-spread function in x and y.

    Speeds up psf calculations by a factor of 4 for free / some broadcasting
    by exploiting the x->-x, y->-y symmetry of a psf function. Pass x and y
    as the positive (say) values of the coordinates at which to evaluate func,
    and it will return the function sampled at [x[::-1]] + x. Note it is not
    wrapped in z.

    Parameters
    ----------
        xpts : numpy.ndarray
            1D N-element numpy.array of the x-points to evaluate func at.
        ypts : numpy.ndarray
            y-points to evaluate func at.
        zpts : numpy.ndarray
            z-points to evaluate func at.
        func : function
            The function to evaluate and wrap around. Syntax must be
            func(x,y,z, **kwargs)
        **kwargs : Any parameters passed to the function.

    Outputs
    -------
        to_return : numpy.ndarray
            The wrapped and calculated psf, of shape
            [2*x.size - x0, 2*y.size - y0, z.size], where x0=1 if x[0]=0, etc.

    Notes
    -----
    The coordinates should be something like numpy.arange(start, stop, diff),
    with start near 0. If x[0]==0, all of x is calcualted but only x[1:]
    is wrapped (i.e. it works whether or not x[0]=0).

    This doesn't work directly for a linescan psf because the illumination
    portion is not like a grid. However, the illumination and detection
    are already combined with wrap_and_calc in calculate_linescan_psf etc.
    """
    #1. Checking that everything is hunky-dory:
    for t in [xpts,ypts,zpts]:
        if len(t.shape) != 1:
            raise ValueError('xpts,ypts,zpts must be 1D.')

    dx = 1 if xpts[0]==0 else 0
    dy = 1 if ypts[0]==0 else 0

    xg,yg,zg = np.meshgrid(xpts,ypts,zpts, indexing='ij')
    xs, ys, zs = [ pts.size for pts in [xpts,ypts,zpts] ]
    to_return = np.zeros([2*xs-dx, 2*ys-dy, zs])

    #2. Calculate:
    up_corner_psf = func(xg,yg,zg, **kwargs)

    to_return[xs-dx:,ys-dy:,:] = up_corner_psf.copy()                     #x>0, y>0
    if dx == 0:
        to_return[:xs-dx,ys-dy:,:] = up_corner_psf[::-1,:,:].copy()       #x<0, y>0
    else:
        to_return[:xs-dx,ys-dy:,:] = up_corner_psf[-1:0:-1,:,:].copy()    #x<0, y>0
    if dy == 0:
        to_return[xs-dx:,:ys-dy,:] = up_corner_psf[:,::-1,:].copy()       #x>0, y<0
    else:
        to_return[xs-dx:,:ys-dy,:] = up_corner_psf[:,-1:0:-1,:].copy()    #x>0, y<0
    if (dx == 0) and (dy == 0):
        to_return[:xs-dx,:ys-dy,:] = up_corner_psf[::-1,::-1,:].copy()    #x<0,y<0
    elif (dx == 0) and (dy != 0):
        to_return[:xs-dx,:ys-dy,:] = up_corner_psf[::-1,-1:0:-1,:].copy() #x<0,y<0
    elif (dy == 0) and (dx != 0):
        to_return[:xs-dx,:ys-dy,:] = up_corner_psf[-1:0:-1,::-1,:].copy() #x<0,y<0
    else: #dx==1 and dy==1
        to_return[:xs-dx,:ys-dy,:] = up_corner_psf[-1:0:-1,-1:0:-1,:].copy()#x<0,y<0

    return to_return

def
wrap_and_calc_psf
(
xpts
,
ypts
,
zpts
,
func
,
**
kwargs
)
:
for
t
in
[
xpts
,
ypts
,
zpts
]
:
if
len
(
t
.
shape
)
!=
1
:
raise
ValueError
(
'xpts,ypts,zpts must be 1D.'
)
dx
=
1
if
xpts
[
0
]
==
0
else
0
dy
=
1
if
ypts
[
0
]
==
0
else
0
xg
,
yg
,
zg
=
np
.
meshgrid
(
xpts
,
ypts
,
zpts
,
indexing
=
'ij'
)
xs
,
ys
,
zs
=
[
pts
.
size
for
pts
in
[
xpts
,
ypts
,
zpts
]
]
to_return
=
np
.
zeros
(
[
2
*
xs
dx
,
2
*
ys
dy
,
zs
]
)
up_corner_psf
=
func
(
xg
,
yg
,
zg
,
**
kwargs
)
to_return
[
xs
dx
:
,
ys
dy
:
,
:
]
=
up_corner_psf
.
copy
(
)
if
dx
==
0
:
to_return
[
:
xs
dx
,
ys
dy
:
,
:
]
=
up_corner_psf
[
:
:
1
,
:
,
:
]
.
copy
(
)
else
:
to_return
[
:
xs
dx
,
ys
dy
:
,
:
]
=
up_corner_psf
[
1
:
0
:
1
,
:
,
:
]
.
copy
(
)
if
dy
==
0
:
to_return
[
xs
dx
:
,
:
ys
dy
,
:
]
=
up_corner_psf
[
:
,
:
:
1
,
:
]
.
copy
(
)
else
:
to_return
[
xs
dx
:
,
:
ys
dy
,
:
]
=
up_corner_psf
[
:
,
1
:
0
:
1
,
:
]
.
copy
(
)
if
(
dx
==
0
)
and
(
dy
==
0
)
:
to_return
[
:
xs
dx
,
:
ys
dy
,
:
]
=
up_corner_psf
[
:
:
1
,
:
:
1
,
:
]
.
copy
(
)
elif
(
dx
==
0
)
and
(
dy
!=
0
)
:
to_return
[
:
xs
dx
,
:
ys
dy
,
:
]
=
up_corner_psf
[
:
:
1
,
1
:
0
:
1
,
:
]
.
copy
(
)
elif
(
dy
==
0
)
and
(
dx
!=
0
)
:
to_return
[
:
xs
dx
,
:
ys
dy
,
:
]
=
up_corner_psf
[
1
:
0
:
1
,
:
:
1
,
:
]
.
copy
(
)
else
:
to_return
[
:
xs
dx
,
:
ys
dy
,
:
]
=
up_corner_psf
[
1
:
0
:
1
,
1
:
0
:
1
,
:
]
.
copy
(
)
return
to_return

Wraps a point - spread function in x and y .


def listify(a):
    """
    Convert a scalar ``a`` to a list and all iterables to list as well.

    Examples
    --------
    >>> listify(0)
    [0]

    >>> listify([1,2,3])
    [1, 2, 3]

    >>> listify('a')
    ['a']

    >>> listify(np.array([1,2,3]))
    [1, 2, 3]

    >>> listify('string')
    ['string']
    """
    if a is None:
        return []
    elif not isinstance(a, (tuple, list, np.ndarray)):
        return [a]
    return list(a)

def listify ( a ) : if a is None : return [ ] elif not isinstance ( a , ( tuple , list , np . ndarray ) ) : return [ a ] return list ( a )

Convert a scalar a to a list and all iterables to list as well .


def delistify(a, b=None):
    """
    If a single element list, extract the element as an object, otherwise
    leave as it is.

    Examples
    --------
    >>> delistify('string')
    'string'

    >>> delistify(['string'])
    'string'

    >>> delistify(['string', 'other'])
    ['string', 'other']

    >>> delistify(np.array([1.0]))
    1.0

    >>> delistify([1, 2, 3])
    [1, 2, 3]
    """
    if isinstance(b, (tuple, list, np.ndarray)):
        if isinstance(a, (tuple, list, np.ndarray)):
            return type(b)(a)
        return type(b)([a])
    else:
        if isinstance(a, (tuple, list, np.ndarray)) and len(a) == 1:
            return a[0]
        return a
    return a

def delistify ( a , b = None ) : if isinstance ( b , ( tuple , list , np . ndarray ) ) : if isinstance ( a , ( tuple , list , np . ndarray ) ) : return type ( b ) ( a ) return type ( b ) ( [ a ] ) else : if isinstance ( a , ( tuple , list , np . ndarray ) ) and len ( a ) == 1 : return a [ 0 ] return a return a

If a single element list extract the element as an object otherwise leave as it is .


def aN(a, dim=3, dtype='int'):
    """
    Convert an integer or iterable list to numpy array of length dim. This func
    is used to allow other methods to take both scalars non-numpy arrays with
    flexibility.

    Parameters
    ----------
    a : number, iterable, array-like
        The object to convert to numpy array

    dim : integer
        The length of the resulting array

    dtype : string or np.dtype
        Type which the resulting array should be, e.g. 'float', np.int8

    Returns
    -------
    arr : numpy array
        Resulting numpy array of length ``dim`` and type ``dtype``

    Examples
    --------
    >>> aN(1, dim=2, dtype='float')
    array([ 1.,  1.])

    >>> aN(1, dtype='int')
    array([1, 1, 1])

    >>> aN(np.array([1,2,3]), dtype='float')
    array([ 1.,  2.,  3.])
    """
    if not hasattr(a, '__iter__'):
        return np.array([a]*dim, dtype=dtype)
    return np.array(a).astype(dtype)

def aN ( a , dim = 3 , dtype = 'int' ) : if not hasattr ( a , '__iter__' ) : return np . array ( [ a ] * dim , dtype = dtype ) return np . array ( a ) . astype ( dtype )

Convert an integer or iterable list to numpy array of length dim . This func is used to allow other methods to take both scalars non - numpy arrays with flexibility .


def patch_docs(subclass, superclass):
    """
    Apply the documentation from ``superclass`` to ``subclass`` by filling
    in all overridden member function docstrings with those from the
    parent class
    """
    funcs0 = inspect.getmembers(subclass, predicate=inspect.ismethod)
    funcs1 = inspect.getmembers(superclass, predicate=inspect.ismethod)

    funcs1 = [f[0] for f in funcs1]

    for name, func in funcs0:
        if name.startswith('_'):
            continue

        if name not in funcs1:
            continue

        if func.__doc__ is None:
            func = getattr(subclass, name)
            func.__func__.__doc__ = getattr(superclass, name).__func__.__doc__

def patch_docs ( subclass , superclass ) : funcs0 = inspect . getmembers ( subclass , predicate = inspect . ismethod ) funcs1 = inspect . getmembers ( superclass , predicate = inspect . ismethod ) funcs1 = [ f [ 0 ] for f in funcs1 ] for name , func in funcs0 : if name . startswith ( '_' ) : continue if name not in funcs1 : continue if func . __doc__ is None : func = getattr ( subclass , name ) func . __func__ . __doc__ = getattr ( superclass , name ) . __func__ . __doc__

Apply the documentation from superclass to subclass by filling in all overridden member function docstrings with those from the parent class


def slicer(self):
        """
        Array slicer object for this tile

        >>> Tile((2,3)).slicer
        (slice(0, 2, None), slice(0, 3, None))

        >>> np.arange(10)[Tile((4,)).slicer]
        array([0, 1, 2, 3])
        """
        return tuple(np.s_[l:r] for l,r in zip(*self.bounds))

def slicer ( self ) : return tuple ( np . s_ [ l : r ] for l , r in zip ( * self . bounds ) )


def oslicer(self, tile):
        """ Opposite slicer, the outer part wrt to a field """
        mask = None
        vecs = tile.coords(form='meshed')
        for v in vecs:
            v[self.slicer] = -1
            mask = mask & (v > 0) if mask is not None else (v>0)
        return tuple(np.array(i).astype('int') for i in zip(*[v[mask] for v in vecs]))

def oslicer ( self , tile ) : mask = None vecs = tile . coords ( form = 'meshed' ) for v in vecs : v [ self . slicer ] = - 1 mask = mask & ( v > 0 ) if mask is not None else ( v > 0 ) return tuple ( np . array ( i ) . astype ( 'int' ) for i in zip ( * [ v [ mask ] for v in vecs ] ) )

Opposite slicer the outer part wrt to a field


def corners(self):
        """
        Iterate the vector of all corners of the hyperrectangles

        >>> Tile(3, dim=2).corners
        array([[0, 0],
               [0, 3],
               [3, 0],
               [3, 3]])
        """
        corners = []
        for ind in itertools.product(*((0,1),)*self.dim):
            ind = np.array(ind)
            corners.append(self.l + ind*self.r)
        return np.array(corners)

def corners ( self ) : corners = [ ] for ind in itertools . product ( * ( ( 0 , 1 ) , ) * self . dim ) : ind = np . array ( ind ) corners . append ( self . l + ind * self . r ) return np . array ( corners )


def _format_vector(self, vecs, form='broadcast'):
        """
        Format a 3d vector field in certain ways, see `coords` for a description
        of each formatting method.
        """
        if form == 'meshed':
            return np.meshgrid(*vecs, indexing='ij')
        elif form == 'vector':
            vecs = np.meshgrid(*vecs, indexing='ij')
            return np.rollaxis(np.array(np.broadcast_arrays(*vecs)),0,self.dim+1)
        elif form == 'flat':
            return vecs
        else:
            return [v[self._coord_slicers[i]] for i,v in enumerate(vecs)]

def _format_vector ( self , vecs , form = 'broadcast' ) : if form == 'meshed' : return np . meshgrid ( * vecs , indexing = 'ij' ) elif form == 'vector' : vecs = np . meshgrid ( * vecs , indexing = 'ij' ) return np . rollaxis ( np . array ( np . broadcast_arrays ( * vecs ) ) , 0 , self . dim + 1 ) elif form == 'flat' : return vecs else : return [ v [ self . _coord_slicers [ i ] ] for i , v in enumerate ( vecs ) ]

Format a 3d vector field in certain ways see coords for a description of each formatting method .


def coords(self, norm=False, form='broadcast'):
        """
        Returns the coordinate vectors associated with the tile.

        Parameters
        -----------
        norm : boolean
            can rescale the coordinates for you. False is no rescaling, True is
            rescaling so that all coordinates are from 0 -> 1.  If a scalar,
            the same norm is applied uniformally while if an iterable, each
            scale is applied to each dimension.

        form : string
            In what form to return the vector array. Can be one of:
                'broadcast' -- return 1D arrays that are broadcasted to be 3D

                'flat' -- return array without broadcasting so each component
                    is 1D and the appropriate length as the tile

                'meshed' -- arrays are explicitly broadcasted and so all have
                    a 3D shape, each the size of the tile.

                'vector' -- array is meshed and combined into one array with
                    the vector components along last dimension [Nz, Ny, Nx, 3]

        Examples
        --------
        >>> Tile(3, dim=2).coords(form='meshed')[0]
        array([[ 0.,  0.,  0.],
               [ 1.,  1.,  1.],
               [ 2.,  2.,  2.]])

        >>> Tile(3, dim=2).coords(form='meshed')[1]
        array([[ 0.,  1.,  2.],
               [ 0.,  1.,  2.],
               [ 0.,  1.,  2.]])

        >>> Tile([4,5]).coords(form='vector').shape
        (4, 5, 2)

        >>> [i.shape for i in Tile((4,5), dim=2).coords(form='broadcast')]
        [(4, 1), (1, 5)]
        """
        if norm is False:
            norm = 1
        if norm is True:
            norm = np.array(self.shape)
        norm = aN(norm, self.dim, dtype='float')

        v = list(np.arange(self.l[i], self.r[i]) / norm[i] for i in range(self.dim))
        return self._format_vector(v, form=form)

def coords ( self , norm = False , form = 'broadcast' ) : if norm is False : norm = 1 if norm is True : norm = np . array ( self . shape ) norm = aN ( norm , self . dim , dtype = 'float' ) v = list ( np . arange ( self . l [ i ] , self . r [ i ] ) / norm [ i ] for i in range ( self . dim ) ) return self . _format_vector ( v , form = form )

Returns the coordinate vectors associated with the tile .


def kvectors(self, norm=False, form='broadcast', real=False, shift=False):
        """
        Return the kvectors associated with this tile, given the standard form
        of -0.5 to 0.5. `norm` and `form` arguments arethe same as that passed to
        `Tile.coords`.

        Parameters
        -----------
        real : boolean
            whether to return kvectors associated with the real fft instead
        """
        if norm is False:
            norm = 1
        if norm is True:
            norm = np.array(self.shape)
        norm = aN(norm, self.dim, dtype='float')

        v = list(np.fft.fftfreq(self.shape[i])/norm[i] for i in range(self.dim))

        if shift:
            v = list(np.fft.fftshift(t) for t in v)

        if real:
            v[-1] = v[-1][:(self.shape[-1]+1)//2]

        return self._format_vector(v, form=form)

def kvectors ( self , norm = False , form = 'broadcast' , real = False , shift = False ) : if norm is False : norm = 1 if norm is True : norm = np . array ( self . shape ) norm = aN ( norm , self . dim , dtype = 'float' ) v = list ( np . fft . fftfreq ( self . shape [ i ] ) / norm [ i ] for i in range ( self . dim ) ) if shift : v = list ( np . fft . fftshift ( t ) for t in v ) if real : v [ - 1 ] = v [ - 1 ] [ : ( self . shape [ - 1 ] + 1 ) // 2 ] return self . _format_vector ( v , form = form )

Return the kvectors associated with this tile given the standard form of - 0 . 5 to 0 . 5 . norm and form arguments arethe same as that passed to Tile . coords .


def contains(self, items, pad=0):
        """
        Test whether coordinates are contained within this tile.

        Parameters
        ----------
        items : ndarray [3] or [N, 3]
            N coordinates to check are within the bounds of the tile

        pad : integer or ndarray [3]
            anisotropic padding to apply in the contain test

        Examples
        --------
        >>> Tile(5, dim=2).contains([[-1, 0], [2, 3], [2, 6]])
        array([False,  True, False], dtype=bool)
        """
        o = ((items >= self.l-pad) & (items < self.r+pad))
        if len(o.shape) == 2:
            o = o.all(axis=-1)
        elif len(o.shape) == 1:
            o = o.all()
        return o

def contains ( self , items , pad = 0 ) : o = ( ( items >= self . l - pad ) & ( items < self . r + pad ) ) if len ( o . shape ) == 2 : o = o . all ( axis = - 1 ) elif len ( o . shape ) == 1 : o = o . all ( ) return o

Test whether coordinates are contained within this tile .


def intersection(tiles, *args):
        """
        Intersection of tiles, returned as a tile

        >>> Tile.intersection(Tile([0, 1], [5, 4]), Tile([1, 0], [4, 5]))
        Tile [1, 1] -> [4, 4] ([3, 3])
        """
        tiles = listify(tiles) + listify(args)

        if len(tiles) < 2:
            return tiles[0]

        tile = tiles[0]
        l, r = tile.l.copy(), tile.r.copy()
        for tile in tiles[1:]:
            l = amax(l, tile.l)
            r = amin(r, tile.r)
        return Tile(l, r, dtype=l.dtype)

def intersection ( tiles , * args ) : tiles = listify ( tiles ) + listify ( args ) if len ( tiles ) < 2 : return tiles [ 0 ] tile = tiles [ 0 ] l , r = tile . l . copy ( ) , tile . r . copy ( ) for tile in tiles [ 1 : ] : l = amax ( l , tile . l ) r = amin ( r , tile . r ) return Tile ( l , r , dtype = l . dtype )

Intersection of tiles returned as a tile


def translate(self, dr):
        """
        Translate a tile by an amount dr

        >>> Tile(5).translate(1)
        Tile [1, 1, 1] -> [6, 6, 6] ([5, 5, 5])
        """
        tile = self.copy()
        tile.l += dr
        tile.r += dr
        return tile

def translate ( self , dr ) : tile = self . copy ( ) tile . l += dr tile . r += dr return tile


def pad(self, pad):
        """
        Pad this tile by an equal amount on each side as specified by pad

        >>> Tile(10).pad(2)
        Tile [-2, -2, -2] -> [12, 12, 12] ([14, 14, 14])

        >>> Tile(10).pad([1,2,3])
        Tile [-1, -2, -3] -> [11, 12, 13] ([12, 14, 16])
        """
        tile = self.copy()
        tile.l -= pad
        tile.r += pad
        return tile

def pad ( self , pad ) : tile = self . copy ( ) tile . l -= pad tile . r += pad return tile


def filtered_image(self, im):
        """Returns a filtered image after applying the Fourier-space filters"""
        q = np.fft.fftn(im)
        for k,v in self.filters:
            q[k] -= v
        return np.real(np.fft.ifftn(q))

def filtered_image ( self , im ) : q = np . fft . fftn ( im ) for k , v in self . filters : q [ k ] -= v return np . real ( np . fft . ifftn ( q ) )

Returns a filtered image after applying the Fourier - space filters


def set_filter(self, slices, values):
        """
        Sets Fourier-space filters for the image. The image is filtered by
        subtracting values from the image at slices.

        Parameters
        ----------
        slices : List of indices or slice objects.
            The q-values in Fourier space to filter.
        values : np.ndarray
            The complete array of Fourier space peaks to subtract off.  values
            should be the same size as the FFT of the image; only the portions
            of values at slices will be removed.

        Examples
        --------
        To remove a two Fourier peaks in the data at q=(10, 10, 10) &
        (245, 245, 245), where im is the residuals of a model:

            * slices = [(10,10,10), (245, 245, 245)]
            * values = np.fft.fftn(im)
            * im.set_filter(slices, values)
        """
        self.filters = [[sl,values[sl]] for sl in slices]

def set_filter ( self , slices , values ) : self . filters = [ [ sl , values [ sl ] ] for sl in slices ]

Sets Fourier - space filters for the image . The image is filtered by subtracting values from the image at slices .


def load_image(self):
        """ Read the file and perform any transforms to get a loaded image """
        try:
            image = initializers.load_tiff(self.filename)
            image = initializers.normalize(
                image, invert=self.invert, scale=self.exposure,
                dtype=self.float_precision
            )
        except IOError as e:
            log.error("Could not find image '%s'" % self.filename)
            raise e

        return image

def
load_image
(
self
)
:
try
:
image
=
initializers
.
load_tiff
(
self
.
filename
)
image
=
initializers
.
normalize
(
image
,
invert
=
self
.
invert
,
scale
=
self
.
exposure
,
dtype
=
self
.
float_precision
)
except
IOError
as
e
:
log
.
error
(
"Could not find image '%s'"
%
self
.
filename
)
raise
e
return
image


def get_scale_from_raw(raw, scaled):
        """
        When given a raw image and the scaled version of the same image, it
        extracts the ``exposure`` parameters associated with those images.
        This is useful when

        Parameters
        ----------
        raw : array_like
            The image loaded fresh from a file

        scaled : array_like
            Image scaled using :func:`peri.initializers.normalize`

        Returns
        -------
        exposure : tuple of numbers
            Returns the exposure parameters (emin, emax) which get mapped to
            (0, 1) in the scaled image. Can be passed to
            :func:`~peri.util.RawImage.__init__`
        """
        t0, t1 = scaled.min(), scaled.max()
        r0, r1 = float(raw.min()), float(raw.max())

        rmin = (t1*r0 - t0*r1) / (t1 - t0)
        rmax = (r1 - r0) / (t1 - t0) + rmin
        return (rmin, rmax)

def get_scale_from_raw ( raw , scaled ) : t0 , t1 = scaled . min ( ) , scaled . max ( ) r0 , r1 = float ( raw . min ( ) ) , float ( raw . max ( ) ) rmin = ( t1 * r0 - t0 * r1 ) / ( t1 - t0 ) rmax = ( r1 - r0 ) / ( t1 - t0 ) + rmin return ( rmin , rmax )

When given a raw image and the scaled version of the same image it extracts the exposure parameters associated with those images . This is useful when


def _draw(self):
        """ Interal draw method, simply prints to screen """
        if self.display:
            print(self._formatstr.format(**self.__dict__), end='')
            sys.stdout.flush()

def _draw ( self ) : if self . display : print ( self . _formatstr . format ( ** self . __dict__ ) , end = '' ) sys . stdout . flush ( )

Interal draw method simply prints to screen


def update(self, value=0):
        """
        Update the value of the progress and update progress bar.

        Parameters
        -----------
        value : integer
            The current iteration of the progress
        """
        self._deltas.append(time.time())

        self.value = value
        self._percent = 100.0 * self.value / self.num

        if self.bar:
            self._bars = self._bar_symbol*int(np.round(self._percent / 100. * self._barsize))

        if (len(self._deltas) < 2) or (self._deltas[-1] - self._deltas[-2]) > 1e-1:
            self._estimate_time()
            self._draw()

        if self.value == self.num:
            self.end()

def update ( self , value = 0 ) : self . _deltas . append ( time . time ( ) ) self . value = value self . _percent = 100.0 * self . value / self . num if self . bar : self . _bars = self . _bar_symbol * int ( np . round ( self . _percent / 100. * self . _barsize ) ) if ( len ( self . _deltas ) < 2 ) or ( self . _deltas [ - 1 ] - self . _deltas [ - 2 ] ) > 1e-1 : self . _estimate_time ( ) self . _draw ( ) if self . value == self . num : self . end ( )

Update the value of the progress and update progress bar .


def check_consistency(self):
        """
        Make sure that the required comps are included in the list of
        components supplied by the user. Also check that the parameters are
        consistent across the many components.
        """
        error = False
        regex = re.compile('([a-zA-Z_][a-zA-Z0-9_]*)')

        # there at least must be the full model, not necessarily partial updates
        if 'full' not in self.modelstr:
            raise ModelError(
                'Model must contain a `full` key describing '
                'the entire image formation'
            )

        # Check that the two model descriptors are consistent
        for name, eq in iteritems(self.modelstr):
            var = regex.findall(eq)
            for v in var:
                # remove the derivative signs if there (dP -> P)
                v = re.sub(r"^d", '', v)
                if v not in self.varmap:
                    log.error(
                        "Variable '%s' (eq. '%s': '%s') not found in category map %r" %
                        (v, name, eq, self.varmap)
                    )
                    error = True

        if error:
            raise ModelError('Inconsistent varmap and modelstr descriptions')

Make sure that the required comps are included in the list of components supplied by the user . Also check that the parameters are consistent across the many components .


def check_inputs(self, comps):
        """
        Check that the list of components `comp` is compatible with both the
        varmap and modelstr for this Model
        """
        error = False
        compcats = [c.category for c in comps]

        # Check that the components are all provided, given the categories
        for k, v in iteritems(self.varmap):
            if k not in self.modelstr['full']:
                log.warn('Component (%s : %s) not used in model.' % (k,v))

            if v not in compcats:
                log.error('Map component (%s : %s) not found in list of components.' % (k,v))
                error = True

        if error:
            raise ModelError('Component list incomplete or incorrect')

Check that the list of components comp is compatible with both the varmap and modelstr for this Model


def lbl(axis, label, size=22):
    """ Put a figure label in an axis """
    at = AnchoredText(label, loc=2, prop=dict(size=size), frameon=True)
    at.patch.set_boxstyle("round,pad=0.,rounding_size=0.0")
    #bb = axis.get_yaxis_transform()
    #at = AnchoredText(label,
    #        loc=3, prop=dict(size=18), frameon=True,
    #        bbox_to_anchor=(-0.5,1),#(-.255, 0.90),
    #        bbox_transform=bb,#axis.transAxes
    #    )
    axis.add_artist(at)

def lbl ( axis , label , size = 22 ) : at = AnchoredText ( label , loc = 2 , prop = dict ( size = size ) , frameon = True ) at . patch . set_boxstyle ( "round,pad=0.,rounding_size=0.0" ) axis . add_artist ( at )


def examine_unexplained_noise(state, bins=1000, xlim=(-10,10)):
    """
    Compares a state's residuals in real and Fourier space with a Gaussian.

    Point out that Fourier space should always be Gaussian and white

    Parameters
    ----------
        state : `peri.states.State`
            The state to examine.
        bins : int or sequence of scalars or str, optional
            The number of bins in the histogram, as passed to numpy.histogram
            Default is 1000
        xlim : 2-element tuple, optional
            The range, in sigma, of the x-axis on the plot. Default (-10,10).

    Returns
    -------
        list
            The axes handles for the real and Fourier space subplots.
    """
    r = state.residuals
    q = np.fft.fftn(r)
    #Get the expected values of `sigma`:
    calc_sig = lambda x: np.sqrt(np.dot(x,x) / x.size)
    rh, xr = np.histogram(r.ravel() / calc_sig(r.ravel()), bins=bins,
            density=True)
    bigq = np.append(q.real.ravel(), q.imag.ravel())
    qh, xq = np.histogram(bigq / calc_sig(q.real.ravel()), bins=bins,
            density=True)
    xr = 0.5*(xr[1:] + xr[:-1])
    xq = 0.5*(xq[1:] + xq[:-1])

    gauss = lambda t : np.exp(-t*t*0.5) / np.sqrt(2*np.pi)

    plt.figure(figsize=[16,8])
    axes = []
    for a, (x, r, lbl) in enumerate([[xr, rh, 'Real'], [xq, qh, 'Fourier']]):
        ax = plt.subplot(1,2,a+1)
        ax.semilogy(x, r, label='Data')
        ax.plot(x, gauss(x), label='Gauss Fit', scalex=False, scaley=False)
        ax.set_xlabel('Residuals value $r/\sigma$')
        ax.set_ylabel('Probability $P(r/\sigma)$')
        ax.legend(loc='upper right')
        ax.set_title('{}-Space'.format(lbl))
        ax.set_xlim(xlim)
        axes.append(ax)
    return axes

def
examine_unexplained_noise
(
state
,
bins
=
1000
,
xlim
=
(
10
,
10
)
)
:
r
=
state
.
residuals
q
=
np
.
fft
.
fftn
(
r
)
calc_sig
=
lambda
x
:
np
.
sqrt
(
np
.
dot
(
x
,
x
)
/
x
.
size
)
rh
,
xr
=
np
.
histogram
(
r
.
ravel
(
)
/
calc_sig
(
r
.
ravel
(
)
)
,
bins
=
bins
,
density
=
True
)
bigq
=
np
.
append
(
q
.
real
.
ravel
(
)
,
q
.
imag
.
ravel
(
)
)
qh
,
xq
=
np
.
histogram
(
bigq
/
calc_sig
(
q
.
real
.
ravel
(
)
)
,
bins
=
bins
,
density
=
True
)
xr
=
0.5
*
(
xr
[
1
:
]
+
xr
[
:
1
]
)
xq
=
0.5
*
(
xq
[
1
:
]
+
xq
[
:
1
]
)
gauss
=
lambda
t
:
np
.
exp
(
t
*
t
*
0.5
)
/
np
.
sqrt
(
2
*
np
.
pi
)
plt
.
figure
(
figsize
=
[
16
,
8
]
)
axes
=
[
]
for
a
,
(
x
,
r
,
lbl
)
in
enumerate
(
[
[
xr
,
rh
,
'Real'
]
,
[
xq
,
qh
,
'Fourier'
]
]
)
:
ax
=
plt
.
subplot
(
1
,
2
,
a
+
1
)
ax
.
semilogy
(
x
,
r
,
label
=
'Data'
)
ax
.
plot
(
x
,
gauss
(
x
)
,
label
=
'Gauss Fit'
,
scalex
=
False
,
scaley
=
False
)
ax
.
set_xlabel
(
'Residuals value $r/\sigma$'
)
ax
.
set_ylabel
(
'Probability $P(r/\sigma)$'
)
ax
.
legend
(
loc
=
'upper right'
)
ax
.
set_title
(
'{}-Space'
.
format
(
lbl
)
)
ax
.
set_xlim
(
xlim
)
axes
.
append
(
ax
)
return
axes

Compares a state s residuals in real and Fourier space with a Gaussian .


def compare_data_model_residuals(s, tile, data_vmin='calc', data_vmax='calc',
         res_vmin=-0.1, res_vmax=0.1, edgepts='calc', do_imshow=True,
         data_cmap=plt.cm.bone, res_cmap=plt.cm.RdBu):
    """
    Compare the data, model, and residuals of a state.

    Makes an image of any 2D slice of a state that compares the data,
    model, and residuals. The upper left portion of the image is the raw
    data, the central portion the model, and the lower right portion the
    image. Either plots the image using plt.imshow() or returns a
    np.ndarray of the image pixels for later use.

    Parameters
    ----------
        st : peri.ImageState object
            The state to plot.
        tile : peri.util.Tile object
            The slice of the image to plot. Can be any xy, xz, or yz
            projection, but it must return a valid 2D slice (the slice is
            squeezed internally).

        data_vmin : {Float, `calc`}, optional
            vmin for the imshow for the data and generative model (shared).
            Default is 'calc' = 0.5(data.min() + model.min())
        data_vmax : {Float, `calc`}, optional
            vmax for the imshow for the data and generative model (shared).
            Default is 'calc' = 0.5(data.max() + model.max())
        res_vmin : Float, optional
            vmin for the imshow for the residuals. Default is -0.1
            Default is 'calc' = 0.5(data.min() + model.min())
        res_vmax : Float, optional
            vmax for the imshow for the residuals. Default is +0.1
        edgepts : {Nested list-like, Float, 'calc'}, optional.
            The vertices of the triangles which determine the splitting of
            the image. The vertices are at (image corner, (edge, y), and
            (x,edge), where edge is the appropriate edge of the image.
                edgepts[0] : (x,y) points for the upper edge
                edgepts[1] : (x,y) points for the lower edge
            where `x` is the coordinate along the image's 0th axis and `y`
            along the images 1st axis. Default is 'calc,' which calculates
            edge points by splitting the image into 3 regions of equal
            area. If edgepts is a float scalar, calculates the edge points
            based on a constant fraction of distance from the edge.
        do_imshow : Bool
            If True, imshow's and returns the returned handle.
            If False, returns the array as a [M,N,4] array.
        data_cmap : matplotlib colormap instance
            The colormap to use for the data and model.
        res_cmap : matplotlib colormap instance
            The colormap to use for the residuals.

    Returns
    -------
        image : {matplotlib.pyplot.AxesImage, numpy.ndarray}
            If `do_imshow` == True, the returned handle from imshow.
            If `do_imshow` == False, an [M,N,4] np.ndarray of the image
            pixels.
    """
    # This could be modified to alpha the borderline... or to embiggen
    # the image and slice it more finely
    residuals = s.residuals[tile.slicer].squeeze()
    data = s.data[tile.slicer].squeeze()
    model = s.model[tile.slicer].squeeze()
    if data.ndim != 2:
        raise ValueError('tile does not give a 2D slice')

    im = np.zeros([data.shape[0], data.shape[1], 4])
    if data_vmin == 'calc':
        data_vmin = 0.5*(data.min() + model.min())
    if data_vmax == 'calc':
        data_vmax = 0.5*(data.max() + model.max())

    #1. Get masks:
    upper_mask, center_mask, lower_mask = trisect_image(im.shape, edgepts)

    #2. Get colorbar'd images
    gm = data_cmap(center_data(model, data_vmin, data_vmax))
    dt = data_cmap(center_data(data, data_vmin, data_vmax))
    rs = res_cmap(center_data(residuals, res_vmin, res_vmax))

    for a in range(4):
        im[:,:,a][upper_mask] = rs[:,:,a][upper_mask]
        im[:,:,a][center_mask] = gm[:,:,a][center_mask]
        im[:,:,a][lower_mask] = dt[:,:,a][lower_mask]
    if do_imshow:
        return plt.imshow(im)
    else:
        return im

def
compare_data_model_residuals
(
s
,
tile
,
data_vmin
=
'calc'
,
data_vmax
=
'calc'
,
res_vmin
=
0.1
,
res_vmax
=
0.1
,
edgepts
=
'calc'
,
do_imshow
=
True
,
data_cmap
=
plt
.
cm
.
bone
,
res_cmap
=
plt
.
cm
.
RdBu
)
:
residuals
=
s
.
residuals
[
tile
.
slicer
]
.
squeeze
(
)
data
=
s
.
data
[
tile
.
slicer
]
.
squeeze
(
)
model
=
s
.
model
[
tile
.
slicer
]
.
squeeze
(
)
if
data
.
ndim
!=
2
:
raise
ValueError
(
'tile does not give a 2D slice'
)
im
=
np
.
zeros
(
[
data
.
shape
[
0
]
,
data
.
shape
[
1
]
,
4
]
)
if
data_vmin
==
'calc'
:
data_vmin
=
0.5
*
(
data
.
min
(
)
+
model
.
min
(
)
)
if
data_vmax
==
'calc'
:
data_vmax
=
0.5
*
(
data
.
max
(
)
+
model
.
max
(
)
)
upper_mask
,
center_mask
,
lower_mask
=
trisect_image
(
im
.
shape
,
edgepts
)
gm
=
data_cmap
(
center_data
(
model
,
data_vmin
,
data_vmax
)
)
dt
=
data_cmap
(
center_data
(
data
,
data_vmin
,
data_vmax
)
)
rs
=
res_cmap
(
center_data
(
residuals
,
res_vmin
,
res_vmax
)
)
for
a
in
range
(
4
)
:
im
[
:
,
:
,
a
]
[
upper_mask
]
=
rs
[
:
,
:
,
a
]
[
upper_mask
]
im
[
:
,
:
,
a
]
[
center_mask
]
=
gm
[
:
,
:
,
a
]
[
center_mask
]
im
[
:
,
:
,
a
]
[
lower_mask
]
=
dt
[
:
,
:
,
a
]
[
lower_mask
]
if
do_imshow
:
return
plt
.
imshow
(
im
)
else
:
return
im

Compare the data model and residuals of a state .


def trisect_image(imshape, edgepts='calc'):
    """
    Returns 3 masks that trisect an image into 3 triangular portions.

    Parameters
    ----------
        imshape : 2-element list-like of ints
            The shape of the image. Elements after the first 2 are ignored.

        edgepts : Nested list-like, float, or `calc`, optional.
            The vertices of the triangles which determine the splitting of
            the image. The vertices are at (image corner, (edge, y), and
            (x,edge), where edge is the appropriate edge of the image.
                edgepts[0] : (x,y) points for the upper edge
                edgepts[1] : (x,y) points for the lower edge
            where `x` is the coordinate along the image's 0th axis and `y`
            along the images 1st axis. Default is 'calc,' which calculates
            edge points by splitting the image into 3 regions of equal
            area. If edgepts is a float scalar, calculates the edge points
            based on a constant fraction of distance from the edge.

    Returns
    -------
        upper_mask : numpy.ndarray
            Boolean array; True in the image's upper  region.
        center_mask : numpy.ndarray
            Boolean array; True in the image's center region.
        lower_mask : numpy.ndarray
            Boolean array; True in the image's lower  region.
    """
    im_x, im_y = np.meshgrid(np.arange(imshape[0]), np.arange(imshape[1]),
            indexing='ij')
    if np.size(edgepts) == 1:
        #Gets equal-area sections, at sqrt(2/3) of the sides
        f = np.sqrt(2./3.) if edgepts == 'calc' else edgepts
        # f = np.sqrt(2./3.)
        lower_edge = (imshape[0] * (1-f),  imshape[1] * f)
        upper_edge = (imshape[0] * f,      imshape[1] * (1-f))
    else:
        upper_edge, lower_edge = edgepts

    #1. Get masks
    lower_slope = lower_edge[1] / max(float(imshape[0] - lower_edge[0]), 1e-9)
    upper_slope = (imshape[1] - upper_edge[1]) / float(upper_edge[0])
    #and the edge points are the x or y intercepts
    lower_intercept = -lower_slope * lower_edge[0]
    upper_intercept = upper_edge[1]
    lower_mask = im_y < (im_x * lower_slope + lower_intercept)
    upper_mask = im_y > (im_x * upper_slope + upper_intercept)

    center_mask= -(lower_mask | upper_mask)
    return upper_mask, center_mask, lower_mask

def trisect_image ( imshape , edgepts = 'calc' ) : im_x , im_y = np . meshgrid ( np . arange ( imshape [ 0 ] ) , np . arange ( imshape [ 1 ] ) , indexing = 'ij' ) if np . size ( edgepts ) == 1 : f = np . sqrt ( 2. / 3. ) if edgepts == 'calc' else edgepts lower_edge = ( imshape [ 0 ] * ( 1 - f ) , imshape [ 1 ] * f ) upper_edge = ( imshape [ 0 ] * f , imshape [ 1 ] * ( 1 - f ) ) else : upper_edge , lower_edge = edgepts lower_slope = lower_edge [ 1 ] / max ( float ( imshape [ 0 ] - lower_edge [ 0 ] ) , 1e-9 ) upper_slope = ( imshape [ 1 ] - upper_edge [ 1 ] ) / float ( upper_edge [ 0 ] ) lower_intercept = - lower_slope * lower_edge [ 0 ] upper_intercept = upper_edge [ 1 ] lower_mask = im_y < ( im_x * lower_slope + lower_intercept ) upper_mask = im_y > ( im_x * upper_slope + upper_intercept ) center_mask = - ( lower_mask | upper_mask ) return upper_mask , center_mask , lower_mask

Returns 3 masks that trisect an image into 3 triangular portions .


def sim_crb_diff(std0, std1, N=10000):
    """ each element of std0 should correspond with the element of std1 """
    a = std0*np.random.randn(N, len(std0))
    b = std1*np.random.randn(N, len(std1))
    return a - b

def sim_crb_diff ( std0 , std1 , N = 10000 ) : a = std0 * np . random . randn ( N , len ( std0 ) ) b = std1 * np . random . randn ( N , len ( std1 ) ) return a - b


def twoslice(field, center=None, size=6.0, cmap='bone_r', vmin=0, vmax=1,
        orientation='vertical', figpad=1.09, off=0.01):
    """
    Plot two parts of the ortho view, the two sections given by ``orientation``.
    """
    center = center or [i//2 for i in field.shape]
    slices = []
    for i,c in enumerate(center):
        blank = [np.s_[:]]*len(center)
        blank[i] = c
        slices.append(tuple(blank))

    z,y,x = [float(i) for i in field.shape]
    w = float(x + z)
    h = float(y + z)

    def show(field, ax, slicer, transpose=False):
        tmp = field[slicer] if not transpose else field[slicer].T
        ax.imshow(
            tmp, cmap=cmap, interpolation='nearest',
            vmin=vmin, vmax=vmax
        )
        ax.set_xticks([])
        ax.set_yticks([])
        ax.grid('off')

    if orientation.startswith('v'):
        # rect = l,b,w,h
        log.info('{} {} {} {} {} {}'.format(x, y, z, w, h, x/h))
        r = x/h
        q = y/h
        f = 1 / (1 + 3*off)
        fig = pl.figure(figsize=(size*r, size*f))
        ax1 = fig.add_axes((off, f*(1-q)+2*off, f, f*q))
        ax2 = fig.add_axes((off, off,           f, f*(1-q)))

        show(field, ax1, slices[0])
        show(field, ax2, slices[1])
    else:
        # rect = l,b,w,h
        r = y/w
        q = x/w
        f = 1 / (1 + 3*off)
        fig = pl.figure(figsize=(size*f, size*r))
        ax1 = fig.add_axes((off,    off,   f*q, f))
        ax2 = fig.add_axes((2*off+f*q, off, f*(1-q), f))

        show(field, ax1, slices[0])
        show(field, ax2, slices[2], transpose=True)

    return fig, ax1, ax2

def
twoslice
(
field
,
center
=
,
size
=
6.0
,
cmap
=
'bone_r'
,
vmin
=
0
,
vmax
=
1
,
orientation
=
'vertical'
,
figpad
=
1.09
,
off
=
0.01
)
:
center
=
center
or
[
i
//
2
for
i
in
field
.
shape
]
slices
=
[
]
for
i
,
c
in
enumerate
(
center
)
:
blank
=
[
np
.
s_
[
:
]
]
*
len
(
center
)
blank
[
i
]
=
c
slices
.
append
(
tuple
(
blank
)
)
z
,
y
,
x
=
[
float
(
i
)
for
i
in
field
.
shape
]
w
=
float
(
x
+
z
)
h
=
float
(
y
+
z
)
def
show
(
field
,
ax
,
slicer
,
transpose
=
False
)
:
tmp
=
field
[
slicer
]
if
not
transpose
else
field
[
slicer
]
.
T
ax
.
imshow
(
tmp
,
cmap
=
cmap
,
interpolation
=
'nearest'
,
vmin
=
vmin
,
vmax
=
vmax
)
ax
.
set_xticks
(
[
]
)
ax
.
set_yticks
(
[
]
)
ax
.
grid
(
'off'
)
if
orientation
.
startswith
(
'v'
)
:
log
.
info
(
'{} {} {} {} {} {}'
.
format
(
x
,
y
,
z
,
w
,
h
,
x
/
h
)
)
r
=
x
/
h
q
=
y
/
h
f
=
1
/
(
1
+
3
*
off
)
fig
=
pl
.
figure
(
figsize
=
(
size
*
r
,
size
*
f
)
)
ax1
=
fig
.
add_axes
(
(
off
,
f
*
(
1
q
)
+
2
*
off
,
f
,
f
*
q
)
)
ax2
=
fig
.
add_axes
(
(
off
,
off
,
f
,
f
*
(
1
q
)
)
)
show
(
field
,
ax1
,
slices
[
0
]
)
show
(
field
,
ax2
,
slices
[
1
]
)
else
:
r
=
y
/
w
q
=
x
/
w
f
=
1
/
(
1
+
3
*
off
)
fig
=
pl
.
figure
(
figsize
=
(
size
*
f
,
size
*
r
)
)
ax1
=
fig
.
add_axes
(
(
off
,
off
,
f
*
q
,
f
)
)
ax2
=
fig
.
add_axes
(
(
2
*
off
+
f
*
q
,
off
,
f
*
(
1
q
)
,
f
)
)
show
(
field
,
ax1
,
slices
[
0
]
)
show
(
field
,
ax2
,
slices
[
2
]
,
transpose
=
True
)
return
fig
,
ax1
,
ax2

Plot two parts of the ortho view the two sections given by orientation .


def circles(st, layer, axis, ax=None, talpha=1.0, cedge='white', cface='white'):
    """
    Plots a set of circles corresponding to a slice through the platonic
    structure. Copied from twoslice_overlay with comments, standaloneness.

    Inputs
    ------
        pos : array of particle positions; [N,3]
        rad : array of particle radii; [N]
        ax : plt.axis instance
        layer : Which layer of the slice to use.
        axis : The slice of the image, 0, 1, or 2.
        cedge : edge color
        cface : face color
        talpha : Alpha of the thing
    """
    pos = st.obj_get_positions()
    rad = st.obj_get_radii()
    shape = st.ishape.shape.tolist()
    shape.pop(axis) #shape is now the shape of the image
    if ax is None:
        fig = plt.figure()
        axisbg = 'white' if cface == 'black' else 'black'
        sx, sy = ((1,shape[1]/float(shape[0])) if shape[0] > shape[1] else
                (shape[0]/float(shape[1]), 1))
        ax = fig.add_axes((0,0, sx, sy), axisbg=axisbg)
    # get the index of the particles we want to include
    particles = np.arange(len(pos))[np.abs(pos[:,axis] - layer) < rad]

    # for each of these particles display the effective radius
    # in the proper place
    scale = 1.0 #np.max(shape).astype('float')
    for i in particles:
        p = pos[i].copy()
        r = 2*np.sqrt(rad[i]**2 - (p[axis] - layer)**2)
        #CIRCLE IS IN FIGURE COORDINATES!!!
        if axis==0:
            ix = 1; iy = 2
        elif axis == 1:
            ix = 0; iy = 2
        elif axis==2:
            ix = 0; iy = 1
        c = Circle((p[ix]/scale, p[iy]/scale), radius=r/2/scale, fc=cface,
                ec=cedge, alpha=talpha)
        ax.add_patch(c)
    # plt.axis([0,1,0,1])
    plt.axis('equal') #circles not ellipses
    return ax

def circles ( st , layer , axis , ax = None , talpha = 1.0 , cedge = 'white' , cface = 'white' ) : pos = st . obj_get_positions ( ) rad = st . obj_get_radii ( ) shape = st . ishape . shape . tolist ( ) shape . pop ( axis ) if ax is None : fig = plt . figure ( ) axisbg = 'white' if cface == 'black' else 'black' sx , sy = ( ( 1 , shape [ 1 ] / float ( shape [ 0 ] ) ) if shape [ 0 ] > shape [ 1 ] else ( shape [ 0 ] / float ( shape [ 1 ] ) , 1 ) ) ax = fig . add_axes ( ( 0 , 0 , sx , sy ) , axisbg = axisbg ) particles = np . arange ( len ( pos ) ) [ np . abs ( pos [ : , axis ] - layer ) < rad ] scale = 1.0 for i in particles : p = pos [ i ] . copy ( ) r = 2 * np . sqrt ( rad [ i ] ** 2 - ( p [ axis ] - layer ) ** 2 ) if axis == 0 : ix = 1 iy = 2 elif axis == 1 : ix = 0 iy = 2 elif axis == 2 : ix = 0 iy = 1 c = Circle ( ( p [ ix ] / scale , p [ iy ] / scale ) , radius = r / 2 / scale , fc = cface , ec = cedge , alpha = talpha ) ax . add_patch ( c ) plt . axis ( 'equal' ) return ax

Plots a set of circles corresponding to a slice through the platonic structure . Copied from twoslice_overlay with comments standaloneness .


def missing_particle(separation=0.0, radius=RADIUS, SNR=20):
    """ create a two particle state and compare it to featuring using a single particle guess """
    # create a base image of one particle
    s = init.create_two_particle_state(imsize=6*radius+4, axis='x', sigma=1.0/SNR,
            delta=separation, radius=radius, stateargs={'varyn': True}, psfargs={'error': 1e-6})
    s.obj.typ[1] = 0.
    s.reset()

    return s, s.obj.pos.copy()

def missing_particle ( separation = 0.0 , radius = RADIUS , SNR = 20 ) : s = init . create_two_particle_state ( imsize = 6 * radius + 4 , axis = 'x' , sigma = 1.0 / SNR , delta = separation , radius = radius , stateargs = { 'varyn' : True } , psfargs = { 'error' : 1e-6 } ) s . obj . typ [ 1 ] = 0. s . reset ( ) return s , s . obj . pos . copy ( )


def name_globals(s, remove_params=None):
    """
    Returns a list of the global parameter names.

    Parameters
    ----------
        s : :class:`peri.states.ImageState`
            The state to name the globals of.
        remove_params : Set or None
            A set of unique additional parameters to remove from the globals
            list.

    Returns
    -------
        all_params : list
            The list of the global parameter names, with each of
            remove_params removed.
    """
    all_params = s.params
    for p in s.param_particle(np.arange(s.obj_get_positions().shape[0])):
        all_params.remove(p)
    if remove_params is not None:
        for p in set(remove_params):
            all_params.remove(p)
    return all_params

def name_globals ( s , remove_params = None ) : all_params = s . params for p in s . param_particle ( np . arange ( s . obj_get_positions ( ) . shape [ 0 ] ) ) : all_params . remove ( p ) if remove_params is not None : for p in set ( remove_params ) : all_params . remove ( p ) return all_params

Returns a list of the global parameter names .


def get_num_px_jtj(s, nparams, decimate=1, max_mem=1e9, min_redundant=20):
    """
    Calculates the number of pixels to use for J at a given memory usage.

    Tries to pick a number of pixels as (size of image / `decimate`).
    However, clips this to a maximum size and minimum size to ensure that
    (1) too much memory isn't used and (2) J has enough elements so that
    the inverse of JTJ will be well-conditioned.

    Parameters
    ----------
        s : :class:`peri.states.State`
            The state on which to calculate J.
        nparams : Int
            The number of parameters that will be included in J.
        decimate : Int, optional
            The amount to decimate the number of pixels in the image by,
            i.e. tries to pick num_px = size of image / decimate.
            Default is 1
        max_mem : Numeric, optional
            The maximum allowed memory, in bytes, for J to occupy at
            double-precision. Default is 1e9.
        min_redundant : Int, optional
            The number of pixels must be at least `min_redundant` *
            `nparams`. If not, an error is raised. Default is 20

    Returns
    -------
        num_px : Int
            The number of pixels at which to calcualte J.
    """
    #1. Max for a given max_mem:
    px_mem = int(max_mem // 8 // nparams) #1 float = 8 bytes
    #2. num_pix for a given redundancy
    px_red = min_redundant*nparams
    #3. And # desired for decimation
    px_dec = s.residuals.size//decimate

    if px_red > px_mem:
        raise RuntimeError('Insufficient max_mem for desired redundancy.')
    num_px = np.clip(px_dec, px_red, px_mem).astype('int')
    return num_px

Calculates the number of pixels to use for J at a given memory usage .


def vectorize_damping(params, damping=1.0, increase_list=[['psf-', 1e4]]):
    """
    Returns a non-constant damping vector, allowing certain parameters to be
    more strongly damped than others.

    Parameters
    ----------
        params : List
            The list of parameter names, in order.
        damping : Float
            The default value of the damping.
        increase_list: List
            A nested 2-element list of the params to increase and their
            scale factors. All parameters containing the string
            increase_list[i][0] are increased by a factor increase_list[i][1].
    Returns
    -------
        damp_vec : np.ndarray
            The damping vector to use.
    """
    damp_vec = np.ones(len(params)) * damping
    for nm, fctr in increase_list:
        for a in range(damp_vec.size):
            if nm in params[a]:
                damp_vec[a] *= fctr
    return damp_vec

def vectorize_damping ( params , damping = 1.0 , increase_list = [ [ 'psf-' , 1e4 ] ] ) : damp_vec = np . ones ( len ( params ) ) * damping for nm , fctr in increase_list : for a in range ( damp_vec . size ) : if nm in params [ a ] : damp_vec [ a ] *= fctr return damp_vec

Returns a non - constant damping vector allowing certain parameters to be more strongly damped than others .


def find_particles_in_tile(positions, tile):
    """
    Finds the particles in a tile, as numpy.ndarray of ints.

    Parameters
    ----------
        positions : `numpy.ndarray`
            [N,3] array of the particle positions to check in the tile
        tile : :class:`peri.util.Tile` instance
            Tile of the region inside which to check for particles.

    Returns
    -------
        numpy.ndarray, int
            The indices of the particles in the tile.
    """
    bools = tile.contains(positions)
    return np.arange(bools.size)[bools]

def find_particles_in_tile ( positions , tile ) : bools = tile . contains ( positions ) return np . arange ( bools . size ) [ bools ]

Finds the particles in a tile as numpy . ndarray of ints .


def separate_particles_into_groups(s, region_size=40, bounds=None,
        doshift=False):
    """
    Separates particles into convenient groups for optimization.

    Given a state, returns a list of groups of particles. Each group of
    particles are located near each other in the image. Every particle
    located in the desired region is contained in exactly 1 group.

    Parameters
    ----------
    s : :class:`peri.states.ImageState`
        The peri state to find particles in.
    region_size : Int or 3-element list-like of ints, optional
        The size of the box. Groups particles into boxes of shape
        (region_size[0], region_size[1], region_size[2]). If region_size
        is a scalar, the box is a cube of length region_size.
        Default is 40.
    bounds : 2-element list-like of 3-element lists, optional
        The sub-region of the image over which to look for particles.
            bounds[0]: The lower-left  corner of the image region.
            bounds[1]: The upper-right corner of the image region.
        Default (None -> ([0,0,0], s.oshape.shape)) is a box of the entire
        image size, i.e. the default places every particle in the image
        somewhere in the groups.
    doshift : {True, False, `'rand'`}, optional
        Whether or not to shift the tile boxes by half a region size, to
        prevent the same particles to be chosen every time. If `'rand'`,
        randomly chooses either True or False. Default is False

    Returns
    -------
    particle_groups : List
        Each element of particle_groups is an int numpy.ndarray of the
        group of nearby particles. Only contains groups with a nonzero
        number of particles, so the elements don't necessarily correspond
        to a given image region.
    """
    imtile = s.oshape.translate(-s.pad)
    bounding_tile = (imtile if bounds is None else Tile(bounds[0], bounds[1]))
    rs = (np.ones(bounding_tile.dim, dtype='int')*region_size if
            np.size(region_size) == 1 else np.array(region_size))

    n_translate = np.ceil(bounding_tile.shape.astype('float')/rs).astype('int')
    particle_groups = []
    tile = Tile(left=bounding_tile.l, right=bounding_tile.l + rs)
    if doshift == 'rand':
        doshift = np.random.choice([True, False])
    if doshift:
        shift = rs // 2
        n_translate += 1
    else:
        shift = 0
    deltas = np.meshgrid(*[np.arange(i) for i in n_translate])
    positions = s.obj_get_positions()
    if bounds is None:
        # FIXME this (deliberately) masks a problem where optimization
        # places particles outside the image. However, it ensures that
        # all particles are in at least one group when `bounds is None`,
        # which is the use case within opt. The 1e-3 is to ensure that
        # they are inside the box and not on the edge.
        positions = np.clip(positions, imtile.l+1e-3, imtile.r-1e-3)
    groups = list(map(lambda *args: find_particles_in_tile(positions,
            tile.translate( np.array(args) * rs - shift)), *[d.ravel()
            for d in deltas]))

    for i in range(len(groups)-1, -1, -1):
        if groups[i].size == 0:
            groups.pop(i)
    assert _check_groups(s, groups)
    return groups

def separate_particles_into_groups ( s , region_size = 40 , bounds = None , doshift = False ) : imtile = s . oshape . translate ( - s . pad ) bounding_tile = ( imtile if bounds is None else Tile ( bounds [ 0 ] , bounds [ 1 ] ) ) rs = ( np . ones ( bounding_tile . dim , dtype = 'int' ) * region_size if np . size ( region_size ) == 1 else np . array ( region_size ) ) n_translate = np . ceil ( bounding_tile . shape . astype ( 'float' ) / rs ) . astype ( 'int' ) particle_groups = [ ] tile = Tile ( left = bounding_tile . l , right = bounding_tile . l + rs ) if doshift == 'rand' : doshift = np . random . choice ( [ True , False ] ) if doshift : shift = rs // 2 n_translate += 1 else : shift = 0 deltas = np . meshgrid ( * [ np . arange ( i ) for i in n_translate ] ) positions = s . obj_get_positions ( ) if bounds is None : positions = np . clip ( positions , imtile . l + 1e-3 , imtile . r - 1e-3 ) groups = list ( map ( lambda * args : find_particles_in_tile ( positions , tile . translate ( np . array ( args ) * rs - shift ) ) , * [ d . ravel ( ) for d in deltas ] ) ) for i in range ( len ( groups ) - 1 , - 1 , - 1 ) : if groups [ i ] . size == 0 : groups . pop ( i ) assert _check_groups ( s , groups ) return groups

Separates particles into convenient groups for optimization .


def _check_groups(s, groups):
    """Ensures that all particles are included in exactly 1 group"""
    ans = []
    for g in groups:
        ans.extend(g)
    if np.unique(ans).size != np.size(ans):
        return False
    elif np.unique(ans).size != s.obj_get_positions().shape[0]:
        return False
    else:
        return (np.arange(s.obj_get_radii().size) == np.sort(ans)).all()

def _check_groups ( s , groups ) : ans = [ ] for g in groups : ans . extend ( g ) if np . unique ( ans ) . size != np . size ( ans ) : return False elif np . unique ( ans ) . size != s . obj_get_positions ( ) . shape [ 0 ] : return False else : return ( np . arange ( s . obj_get_radii ( ) . size ) == np . sort ( ans ) ) . all ( )


def calc_particle_group_region_size(s, region_size=40, max_mem=1e9, **kwargs):
    """
    Finds the biggest region size for LM particle optimization with a
    given memory constraint.

    Input Parameters
    ----------------
        s : :class:`peri.states.ImageState`
            The state with the particles
        region_size : Int or 3-element list-like of ints, optional.
            The initial guess for the region size. Default is 40
        max_mem : Numeric, optional
            The maximum memory for the optimizer to take. Default is 1e9

    Other Parameters
    ----------------
        bounds: 2-element list-like of 3-element lists.
            The sub-region of the image over which to look for particles.
                bounds[0]: The lower-left  corner of the image region.
                bounds[1]: The upper-right corner of the image region.
            Default (None -> ([0,0,0], s.oshape.shape)) is a box of the entire
            image size, i.e. the default places every particle in the image
            somewhere in the groups.
    Returns
    -------
        region_size : numpy.ndarray of ints of the region size.
    """
    region_size = np.array(region_size).astype('int')

    def calc_mem_usage(region_size):
        rs = np.array(region_size)
        particle_groups = separate_particles_into_groups(s, region_size=
                rs.tolist(), **kwargs)
        # The actual mem usage is the max of the memory usage of all the
        # particle groups. However this is too slow. So instead we use the
        # max of the memory of the biggest 5 particle groups:
        numpart = [np.size(g) for g in particle_groups]
        biggroups = [particle_groups[i] for i in np.argsort(numpart)[-5:]]
        def get_tile_jsize(group):
            nms = s.param_particle(group)
            tile = s.get_update_io_tiles(nms, s.get_values(nms))[2]
            return tile.shape.prod() * len(nms)
        mems = [8*get_tile_jsize(g) for g in biggroups]  # 8 for bytes/float64
        return np.max(mems)

    im_shape = s.oshape.shape
    if calc_mem_usage(region_size) > max_mem:
        while ((calc_mem_usage(region_size) > max_mem) and
                np.any(region_size > 2)):
            region_size = np.clip(region_size-1, 2, im_shape)
    else:
        while ((calc_mem_usage(region_size) < max_mem) and
                np.any(region_size < im_shape)):
            region_size = np.clip(region_size+1, 2, im_shape)
        region_size -= 1 #need to be < memory, so we undo 1 iteration

    return region_size

def calc_particle_group_region_size ( s , region_size = 40 , max_mem = 1e9 , ** kwargs ) : region_size = np . array ( region_size ) . astype ( 'int' ) def calc_mem_usage ( region_size ) : rs = np . array ( region_size ) particle_groups = separate_particles_into_groups ( s , region_size = rs . tolist ( ) , ** kwargs ) numpart = [ np . size ( g ) for g in particle_groups ] biggroups = [ particle_groups [ i ] for i in np . argsort ( numpart ) [ - 5 : ] ] def get_tile_jsize ( group ) : nms = s . param_particle ( group ) tile = s . get_update_io_tiles ( nms , s . get_values ( nms ) ) [ 2 ] return tile . shape . prod ( ) * len ( nms ) mems = [ 8 * get_tile_jsize ( g ) for g in biggroups ] return np . max ( mems ) im_shape = s . oshape . shape if calc_mem_usage ( region_size ) > max_mem : while ( ( calc_mem_usage ( region_size ) > max_mem ) and np . any ( region_size > 2 ) ) : region_size = np . clip ( region_size - 1 , 2 , im_shape ) else : while ( ( calc_mem_usage ( region_size ) < max_mem ) and np . any ( region_size < im_shape ) ) : region_size = np . clip ( region_size + 1 , 2 , im_shape ) region_size -= 1 return region_size

Finds the biggest region size for LM particle optimization with a given memory constraint .


def get_residuals_update_tile(st, padded_tile):
    """
    Translates a tile in the padded image to the unpadded image.

    Given a state and a tile that corresponds to the padded image, returns
    a tile that corresponds to the the corresponding pixels of the difference
    image

    Parameters
    ----------
        st : :class:`peri.states.State`
            The state
        padded_tile : :class:`peri.util.Tile`
            The tile in the padded image.

    Returns
    -------
        :class:`peri.util.Tile`
            The tile corresponding to padded_tile in the unpadded image.
    """
    inner_tile = st.ishape.intersection([st.ishape, padded_tile])
    return inner_tile.translate(-st.pad)

def get_residuals_update_tile ( st , padded_tile ) : inner_tile = st . ishape . intersection ( [ st . ishape , padded_tile ] ) return inner_tile . translate ( - st . pad )

Translates a tile in the padded image to the unpadded image .


def find_best_step(err_vals):
    """
    Returns the index of the lowest of the passed values. Catches nans etc.
    """
    if np.all(np.isnan(err_vals)):
        raise ValueError('All err_vals are nans!')
    return np.nanargmin(err_vals)

def
find_best_step
(
err_vals
)
:
if
np
.
all
(
np
.
isnan
(
err_vals
)
)
:
raise
ValueError
(
'All err_vals are nans!'
)
return
np
.
nanargmin
(
err_vals
)

Returns the index of the lowest of the passed values . Catches nans etc .


def do_levmarq(s, param_names, damping=0.1, decrease_damp_factor=10.,
        run_length=6, eig_update=True, collect_stats=False, rz_order=0,
        run_type=2, **kwargs):
    """
    Runs Levenberg-Marquardt optimization on a state.

    Convenience wrapper for LMGlobals. Same keyword args, but the defaults
    have been set to useful values for optimizing globals.
    See LMGlobals and LMEngine for documentation.

    See Also
    --------
        do_levmarq_particles : Levenberg-Marquardt optimization of a
            specified set of particles.

        do_levmarq_all_particle_groups : Levenberg-Marquardt optimization
            of all the particles in the state.

        LMGlobals : Optimizer object; the workhorse of do_levmarq.

        LMEngine : Engine superclass for all the optimizers.
    """
    if rz_order > 0:
        aug = AugmentedState(s, param_names, rz_order=rz_order)
        lm = LMAugmentedState(aug, damping=damping, run_length=run_length,
                decrease_damp_factor=decrease_damp_factor, eig_update=
                eig_update, **kwargs)
    else:
        lm = LMGlobals(s, param_names, damping=damping, run_length=run_length,
                decrease_damp_factor=decrease_damp_factor, eig_update=
                eig_update, **kwargs)
    if run_type == 2:
        lm.do_run_2()
    elif run_type == 1:
        lm.do_run_1()
    else:
        raise ValueError('run_type=1,2 only')
    if collect_stats:
        return lm.get_termination_stats()

def
do_levmarq
(
s
,
param_names
,
damping
=
0.1
,
decrease_damp_factor
=
10.
,
run_length
=
6
,
eig_update
=
True
,
collect_stats
=
False
,
rz_order
=
0
,
run_type
=
2
,
**
kwargs
)
:
if
rz_order
>
0
:
aug
=
AugmentedState
(
s
,
param_names
,
rz_order
=
rz_order
)
lm
=
LMAugmentedState
(
aug
,
damping
=
damping
,
run_length
=
run_length
,
decrease_damp_factor
=
decrease_damp_factor
,
eig_update
=
eig_update
,
**
kwargs
)
else
:
lm
=
LMGlobals
(
s
,
param_names
,
damping
=
damping
,
run_length
=
run_length
,
decrease_damp_factor
=
decrease_damp_factor
,
eig_update
=
eig_update
,
**
kwargs
)
if
run_type
==
2
:
lm
.
do_run_2
(
)
elif
run_type
==
1
:
lm
.
do_run_1
(
)
else
:
raise
ValueError
(
'run_type=1,2 only'
)
if
collect_stats
:
return
lm
.
get_termination_stats
(
)

Runs Levenberg - Marquardt optimization on a state .


def do_levmarq_particles(s, particles, damping=1.0, decrease_damp_factor=10.,
        run_length=4, collect_stats=False, max_iter=2, **kwargs):
    """
    Levenberg-Marquardt optimization on a set of particles.

    Convenience wrapper for LMParticles. Same keyword args, but the
    defaults have been set to useful values for optimizing particles.
    See LMParticles and LMEngine for documentation.

    See Also
    --------
        do_levmarq_all_particle_groups : Levenberg-Marquardt optimization
            of all the particles in the state.

        do_levmarq : Levenberg-Marquardt optimization of the entire state;
            useful for optimizing global parameters.

        LMParticles : Optimizer object; the workhorse of do_levmarq_particles.

        LMEngine : Engine superclass for all the optimizers.
    """
    lp = LMParticles(s, particles, damping=damping, run_length=run_length,
            decrease_damp_factor=decrease_damp_factor, max_iter=max_iter,
            **kwargs)
    lp.do_run_2()
    if collect_stats:
        return lp.get_termination_stats()

def do_levmarq_particles ( s , particles , damping = 1.0 , decrease_damp_factor = 10. , run_length = 4 , collect_stats = False , max_iter = 2 , ** kwargs ) : lp = LMParticles ( s , particles , damping = damping , run_length = run_length , decrease_damp_factor = decrease_damp_factor , max_iter = max_iter , ** kwargs ) lp . do_run_2 ( ) if collect_stats : return lp . get_termination_stats ( )

Levenberg - Marquardt optimization on a set of particles .


def do_levmarq_all_particle_groups(s, region_size=40, max_iter=2, damping=1.0,
        decrease_damp_factor=10., run_length=4, collect_stats=False, **kwargs):
    """
    Levenberg-Marquardt optimization for every particle in the state.

    Convenience wrapper for LMParticleGroupCollection. Same keyword args,
    but I've set the defaults to what I've found to be useful values for
    optimizing particles. See LMParticleGroupCollection for documentation.

    See Also
    --------
        do_levmarq_particles : Levenberg-Marquardt optimization of a
            specified set of particles.

        do_levmarq : Levenberg-Marquardt optimization of the entire state;
            useful for optimizing global parameters.

        LMParticleGroupCollection : The workhorse of do_levmarq.

        LMEngine : Engine superclass for all the optimizers.
    """
    lp = LMParticleGroupCollection(s, region_size=region_size, damping=damping,
            run_length=run_length, decrease_damp_factor=decrease_damp_factor,
            get_cos=collect_stats, max_iter=max_iter, **kwargs)
    lp.do_run_2()
    if collect_stats:
        return lp.stats

def do_levmarq_all_particle_groups ( s , region_size = 40 , max_iter = 2 , damping = 1.0 , decrease_damp_factor = 10. , run_length = 4 , collect_stats = False , ** kwargs ) : lp = LMParticleGroupCollection ( s , region_size = region_size , damping = damping , run_length = run_length , decrease_damp_factor = decrease_damp_factor , get_cos = collect_stats , max_iter = max_iter , ** kwargs ) lp . do_run_2 ( ) if collect_stats : return lp . stats

Levenberg - Marquardt optimization for every particle in the state .


def do_levmarq_n_directions(s, directions, max_iter=2, run_length=2,
        damping=1e-3, collect_stats=False, marquardt_damping=True, **kwargs):
    """
    Optimization of a state along a specific set of directions in parameter
    space.

    Parameters
    ----------
        s : :class:`peri.states.State`
            The state to optimize
        directions : np.ndarray
            [n,d] element numpy.ndarray of the n directions in the d-
            dimensional space to optimize along. `directions` is trans-
            formed to a unit vector internally
    Other Parameters
    ----------------
        Any parameters passed to LMEngine.
    """
    # normal = direction / np.sqrt(np.dot(direction, direction))
    normals = np.array([d/np.sqrt(np.dot(d,d)) for d in directions])
    if np.isnan(normals).any():
        raise ValueError('`directions` must not be 0s or contain nan')
    obj = OptState(s, normals)
    lo = LMOptObj(obj, max_iter=max_iter, run_length=run_length, damping=
            damping, marquardt_damping=marquardt_damping, **kwargs)
    lo.do_run_1()
    if collect_stats:
        return lo.get_termination_stats()

Optimization of a state along a specific set of directions in parameter space .


def finish(s, desc='finish', n_loop=4, max_mem=1e9, separate_psf=True,
        fractol=1e-7, errtol=1e-3, dowarn=True):
    """
    Crawls slowly to the minimum-cost state.

    Blocks the global parameters into small enough sections such that each
    can be optimized separately while including all the pixels (i.e. no
    decimation). Optimizes the globals, then the psf separately if desired,
    then particles, then a line minimization along the step direction to
    speed up convergence.

    Parameters
    ----------
        s : :class:`peri.states.ImageState`
            The state to optimize
        desc : string, optional
            Description to append to the states.save() call every loop.
            Set to `None` to avoid saving. Default is `'finish'`.
        n_loop : Int, optional
            The number of times to loop over in the optimizer. Default is 4.
        max_mem : Numeric, optional
            The maximum amount of memory allowed for the optimizers' J's,
            for both particles & globals. Default is 1e9.
        separate_psf : Bool, optional
            If True, does the psf optimization separately from the rest of
            the globals, since the psf has a more tortuous fit landscape.
            Default is True.
        fractol : Float, optional
            Fractional change in error at which to terminate. Default 1e-4
        errtol : Float, optional
            Absolute change in error at which to terminate. Default 1e-2
        dowarn : Bool, optional
            Whether to log a warning if termination results from finishing
            loops rather than from convergence. Default is True.

    Returns
    -------
        dictionary
            Information about the optimization. Has two keys: ``'converged'``,
            a Bool which of whether optimization stopped due to convergence
            (True) or due to max number of iterations (False), and
            ``'loop_values'``, a [n_loop+1, N] ``numpy.ndarray`` of the
            state's values, at the start of optimization and at the end of
            each loop, before the line minimization.
    """
    values = [np.copy(s.state[s.params])]
    remove_params = s.get('psf').params if separate_psf else None
    # FIXME explicit params
    global_params = name_globals(s, remove_params=remove_params)
    #FIXME this could be done much better, since much of the globals such
    #as the ilm are local. Could be done with sparse matrices and/or taking
    #nearby globals in a group and using the update tile only as the slicer,
    #rather than the full residuals.
    gs = np.floor(max_mem / s.residuals.nbytes).astype('int')
    groups = [global_params[a:a+gs] for a in range(0, len(global_params), gs)]
    CLOG.info('Start  ``finish``:\t{}'.format(s.error))
    for a in range(n_loop):
        start_err = s.error
        #1. Min globals:
        for g in groups:
            do_levmarq(s, g, damping=0.1, decrease_damp_factor=20.,
                    max_iter=1, max_mem=max_mem, eig_update=False)
        if separate_psf:
            do_levmarq(s, remove_params, max_mem=max_mem, max_iter=4,
                    eig_update=False)
        CLOG.info('Globals,   loop {}:\t{}'.format(a, s.error))
        if desc is not None:
            states.save(s, desc=desc)
        #2. Min particles
        do_levmarq_all_particle_groups(s, max_iter=1, max_mem=max_mem)
        CLOG.info('Particles, loop {}:\t{}'.format(a, s.error))
        if desc is not None:
            states.save(s, desc=desc)
        #3. Append vals, line min:
        values.append(np.copy(s.state[s.params]))
        # dv = (np.array(values[1:]) - np.array(values[0]))[-3:]
        # do_levmarq_n_directions(s, dv, damping=1e-2, max_iter=2, errtol=3e-4)
        # CLOG.info('Line min., loop {}:\t{}'.format(a, s.error))
        # if desc is not None:
            # states.save(s, desc=desc)
        #4. terminate?
        new_err = s.error
        derr = start_err - new_err
        dobreak = (derr/new_err < fractol) or (derr < errtol)
        if dobreak:
            break

    if dowarn and (not dobreak):
        CLOG.warn('finish() did not converge; consider re-running')
    return {'converged':dobreak, 'loop_values':np.array(values)}

Crawls slowly to the minimum - cost state .