コード例 #1
0
def plot_diffs(dataset,repeatlist,labels=None,outfolder=None,nsigma_crange=1,fignum=None,figsize=(9.6,6.7)):
    """Generate comparison and difference plots for a list of datasets (data,x,y) and a list of indices
    `repeatlist` as (index1,index2,label). An optional list of `labels` matching data in repeatlist can be provided,
    and then are used for titles of plots, otherwise numeric indices are simply used."""

    stats=[]
    result=[]
    #plot surface map, who knows why on figure 5
    fignumber(fignum,figsize=figsize)
    for i1,i2,t in repeatlist:
        d1,d2=dataset[i1],dataset[i2]
        diff=d1[0]-d2[0]
        result.append((diff,d1[1],d1[2]))
        #mng = plt.get_current_fig_manager()
        #mng.window.showMaximized()
        plt.clf()
        ax1,ax2,ax3=diff_images(d1[0],d2[0],d2[1],d2[2],fignum=0)  #x and y are taken from second data
        if labels is not None:
            ax1.set_title(labels[i1])
            ax2.set_title(labels[i2])
            stats.append([labels[i1],labels[i2],np.nanstd(diff)])
        else:
            stats.append([i1,i2,np.nanstd(diff)])
        ax3.set_title('Diff, rms= %5.3f nm'%(np.nanstd(diff)*1000) )
        plt.sca(ax3)
        plt.clim(*filtered_span(diff,nsigma=nsigma_crange,itmax=1,span=True))
        ax1.set_aspect('equal')
        ax2.set_aspect('equal')
        ax3.set_aspect('equal')
        plt.suptitle(t)
        plt.tight_layout()
        if outfolder:
            #pdb.set_trace()
            plt.savefig(os.path.join(outfolder,fn_add_subfix("diff","_%i_%i"%(i1,i2),'.jpg')))
            save_data(os.path.join(outfolder,fn_add_subfix("diff","_%i_%i"%(i1,i2),'.dat')),
                diff,d1[1],d1[2])

    if outfolder:
        np.savetxt(os.path.join(outfolder,"diff_report.txt"),result,fmt="%s")

    return result
コード例 #2
0
def getdataset(datalist,outfolder=None,fignum=None,figsize=(9.6,6.7),levelingfunc=None,
    caldata=None,psdrange=[1e-15,1e-6]):
    """for each file in a list read data and make a preview plot using `plot_surface_analysis`,
        return a list of (data,x,y). caldata are optional calibration data of same shape as data."""

    """moved here from newview_plotter. This is a similar function to calibrate samples, """

    if levelingfunc==None:
        levelingfunc= lambda x: x
    result=[]
    if outfolder is not None:
        os.makedirs(outfolder,exist_ok=True)
    fig=fignumber(fignum)
    for ff in datalist:
        plt.clf()
        wdata,x,y=matrixZygo_reader(ff,scale=(1000.,1000,1.),center=(0,0))
        if caldata is not None: wdata=wdata-caldata
        wdata,x,y=levelingfunc(wdata),x,y  #simple way to remove plane
        plot_surface_analysis(wdata,x,y,label=os.path.basename(ff),outfolder=outfolder,nsigma_crange=1,
            fignum=(fignum+1 if fignum else None),psdrange=psdrange,levelingfunc=levelingfunc,frange=[4,100])  #[1e-11,1e-6]
        maximize()
        result.append((wdata,x,y))
    return result
コード例 #3
0
def calculatePSD2(wdata,xg,yg,outname="",wfun=None,vrange=[None,None],rmsrange=None,prange=None,fignum=1,misal_deg=(1,1),leg_deg=(10,10)):
    """Updated version with subtracted 4 terms legendre."""
    """given points w, calculate and plot surface maps with different leveling (piston, tilt, sag, 10 legendre)
    use psd2d to calculate and save x and y 2d PSDs, plots only y.
    fignum window where to plot, if fignnum is 0 current figure is cleared, if None new figure is created. Default to figure 1 ."""
    ##STILL AWFUL CODE
    # misal_deg is useless, since data are expected to be already leveled.
    # if not, still can be applied, but is quite useless.

    ###AWFUL CODE, partially fixed -> fix plotting window

    ## FOUR PANEL PLOT save as _lev.png, _piston.dat, _tilt.dat, _sag.dat
    ## on fignum
    fig=fignumber(fignum)
    plt.clf()
    figManager = plt.get_current_fig_manager()
    figManager.window.showMaximized()

    #
    wdata=-fit.legendre2d(wdata,1,1)[0]
    lwdata=wdata-fit.legendre2d(wdata,*misal_deg)[0]
    lwdata2=wdata-fit.legendre2d(wdata,*leg_deg)[0]
    #
    #make plots
    ax=compare_images([wdata,lwdata,lwdata2],xg,yg,titles=['original','cone corrected','10 legendre 2D corrected'],fignum=0,commonscale=True,vmin=vrange[0],vmax=vrange[1])  #generator of axis, not sure how it can work below, missing something?
    for im in ax:
        plt.xlabel('X (mm)')
        plt.ylabel('Y (mm)')
    #
    ##return wdata,lwdata,lwdata2

    wdata,lwdata,lwdata2=leveldata(wdata,xg,yg) #calculate and make a plot with 3 panels
    if outname:
        np.savetxt(fn_add_subfix(outname,'_piston','.dat'),wdata) #doesn't  write x and y
        np.savetxt(fn_add_subfix(outname,'_%2i_%2i_misal'%misal_deg,'.dat'),lwdata)
        np.savetxt(fn_add_subfix(outname,'_%2i_%2i_leg'%leg_deg,'.dat'),lwdata2)

    leg10=wdata-fit.legendre2d(wdata,*leg_deg)[0]
    #forth panel contains legendre
    plt.subplot(224)
    plt.imshow(leg10,extent=(xg[0],xg[-1],yg[0],yg[-1]),
                interpolation='None',aspect='auto')
    plt.title('(%i,%i) 2D legendre removed'%leg_deg)
    plt.colorbar()
    #display(plt.gcf())
    if outname:
        plt.savefig(fn_add_subfix(outname,'_lev','.png'))

    ## DATA OUTPUT save as _psd.dat, psd2d creates
    #create packed data for txt output
    #containing x and y psds, with 3 different levelings
    #resulting in 6 couples freq/psd.
    # matches the order specified in header

    #header for final matrix output, define labels of psd sequence
    head='yfreq ypsd xfreq xpsd ylevfreq ylevpsd xlevfreq xlevpsd ysagfreq ysagpsd xsagfreq xsagpsd'

    #this assumes x and y are same xg and yg on all datalist
    #change this part, we are no more interested in doing psd on all possible leveling, only 4 terms removed by line
    # along the direction of interest, however to keep constant number of columns, replace piston, tilt, sag
    # with tilt, sag, 4 legendre
    datalist=[wdata,lwdata,lwdata2]
    labels=['_tilt','_mis','_leg'] #postfix for output file
    flist=[]
    psdlist=[]
    for d,l in zip(datalist,labels):
        plt.figure(3)
        #x and y psds
        fx,px=psd2d(d.T,yg,xg,wfun=wfun) #psds along x, no plot
        fy,py=plot_psd2d(d,xg,yg,outname=fn_add_subfix(outname,l),wfun=wfun,
            rmsrange=rmsrange,prange=prange,vrange=vrange) #psds along y, plot
        #display(plt.gcf())

        #note y goes first
        flist.extend([fy,fx])
        psdlist.extend([py,px])
    avgpsdlist=[avgpsd2d(p) for p in psdlist]

    #generate output array and save it with header
    outarr=np.empty((np.max([len(f) for f in flist]),len(psdlist)*2))*np.nan
    for i,(f,p) in enumerate(zip(flist,avgpsdlist)):
        outarr[:len(f),i*2]=f
        outarr[:len(p),i*2+1]=p

    if outname:
        np.savetxt(fn_add_subfix(outname,'_psd','.dat'),outarr,header=head,fmt='%f')

    ## PLOT AVG PSDs save as _psds.png
    fw1,fw1m,fw2,fw2m,fw3,fw3m=flist  #used only in plot
    pw1avg,pw1mavg,pw2avg,pw2mavg,pw3avg,pw3mavg=avgpsdlist  #average psds
    plt.figure(2)
    plt.clf()
    plt.plot(fw1,pw1avg,label='Y')
    plt.plot(fw2,pw2avg,label='Y lev.')
    plt.plot(fw3,pw3avg,label='Y sag.')
    plt.plot(fw1m,pw1mavg,'--',label='X')
    plt.plot(fw2m,pw2mavg,'--',label='X lev.')
    plt.plot(fw3m,pw3mavg,'--',label='X sag.')
    plt.loglog()
    plt.grid()
    plt.legend(loc=0)
    if outname:
        plt.savefig(fn_add_subfix(outname,'_psds','.png'))

    return outarr
コード例 #4
0
def psd2d_analysis(wdata,x,y,title=None,wfun=None,vrange=None,
    rmsrange=None,prange=None,fignum=5,rmsthr=None, 
    aspect='auto', ax2f=None, units=None,outname=None,norm=1,rmsnorm=True):
    """ Calculates 2D PSD as image obtained combining all profile PSDS calculated along vertical slices of data. Resulting image has size 
    If title is provided rms slice power is also calculated and plotted on three panels with figure and PSD.
    
    Return PSD as PSD2D object. 

    uses plot_rms_power(f,p,rmsrange=None) to calculate rms power.
    fignum window where to plot, if fignnum is 0 current figure is cleared,
    if None new figure is created. Default to figure 5.
    rmsrange is one or a list of frequency ranges for plotting integrated rms. Can contain None to use max or min.
    If axf2 is set to boolean or array of boolean, plot slice rms for the frequencies
    associated to rmsrange on second axis. rmsrange must be set accordingly (same number of elements).
    
    rmsthr sets a threshold for data inclusion. If rms is above the value, the line is considered to contain invalid data and is removed from output PSD.
    This makes it easy to average the returned PSDs.
    Corresponding data are still visualized in central panel, but are marked with a red cross at top of y axes.
    If multiple rms range intervals are provided, line is removed if any of them is over the threshold, but this might change in future, e.g. TODO: make it possible to add vector threshold with as many elements as rms range intervals.

    Set outname to empty string to plot without generating output.

    typical values:
    rmsrange=[[None,0.1],[0.1,0.2],[0.2,1],[1,None]]  #list of frequency intervals for rms
    ax2f=[0,1,1,0,0]             #   axis where to plot rms corresponding to intervals in rmsrange: 1 right, 0 left
    wfun  = np.hanning           #   type of windows for fourier transform
    units = ("mm","mm","$\mu$m") #   units of surface data from which PSD is calculated

    vrange_surf=([-0.5,0.5])  #color scale of surface map
    vrange_leg=([-0.05,0.05])   #color scale of legendre removed map
    prange=np.array((1e-8,1.e-1))#np.array((5e-8,1.e-5))  #color scale of 2d psd plot

    fs,ps=psd2d_analysis(levellegendre(y,wdata,2),x,y,outname="",wfun=np.hanning,
    rmsrange=rmsrange,prange=prange,ax2f=ax2f,units=units)
    """
    #2018/04/05
    # -critical renaming of plot_psd2d(wdata,x,y..) -> psd2d_analysis
    # and _plot_psd2d(f,p,x) -> plot_psd2d consistently with other routines.
    # -added title to determine if generating plot, with the intent of replacing outname.
    plt.pause(1)
    #print('cane',fignum) 
    if outname is not None:  #handle backcompatibility
        print ('psd2d_analysis WARNING: `title` replaced `outname` and output figure will be no more generated.'+
            'OUTNAME will be removed in next version, use title and save the plot after returning from routine.')
        if title is not None:
            print('outname should be not used together with title, it will be ignored.')
        else:
            title=outname
    
    if vrange is not None:
        if prange is None:
            print("""WARNING:  older versions of psd2d use vrange as range of psdvalue.
                    In updated version prange is range of psd and vrange is range for surface
                    plots. Update your code.""")

    #check compatibility with old interface
    if len(wdata.shape)==1:
        if len(y.shape)==2:
            print("WARNING: your code calling psd2d uses old call")
            print("psd2d(x,y,data), automaticly adjusted, but")
            print("update IMMEDIATELY your code to call")
            print("psd2d(data,x,y)")
            x,y,wdata=wdata,x,y

    f, p = psd2d(wdata, x, y, wfun=wfun, norm=norm, rmsnorm=rmsnorm)  # calculate PSD 2D
       
    # GENERATE OUTPUT
    if title is not None:

        if np.size(units)==1:
            units=np.repeat(units,3)

        if prange is None:
            if f[0] == 0:
                prange=span(p[1:,:])
            else:
                prange=span(p)
                if prange[0]<1e-20:  #arbitrary small value
                    print("WARNING: low limit detected in prange, can cause problems with log color scale.")
        if vrange is None:
            vrange=span(wdata)

        #plot surface map, who knows why on figure 5       
        fig=fignumber(fignum)
        #pdb.set_trace()
        #
        plt.clf()
        maximize()
        plt.draw()

        plt.suptitle(title)

        ################
        #plot Surface
        ax1=plt.subplot(311)
        plot_data(wdata,x,y,aspect=aspect,vmin=vrange[0],vmax=vrange[1],units=units,title='Surface',stats=True)
        plt.xlabel(None)
        plt.grid(1)

        ################
        #plot 2D PSD
        ax2=plt.subplot(312,sharex=ax1)
        plot_psd2d(f,p,x,prange=prange,units=units)
        if rmsrange is not None:      #plot horizontal lines to mark frequency ranges for rms extraction,
        #rms is tranformed to 2D array
            if len(np.array(rmsrange).shape)==1:  #if only one interval, make it 2D anyway
                rmsrange=[rmsrange]
            rmsrange=np.array(rmsrange)
            for fr in np.array(rmsrange):  #I want fr to be array type, even if I don't care about rmsrange this does the job
                ax2.hlines(fr[fr != None],*((span(x)-x.mean())*1.1+x.mean()))
        ax2.set_xlabel("")
        plt.xlabel(None)
        #plt.subplots_adjust(top=0.85)

        ################
        #plot rms slice
        ax3=plt.subplot(313,sharex=ax1)
        if f[0]==0: #ignore component of 0 frequency in rms calculation if present
            ff,pp=f[1:],p[1:]
        else:
            ff,pp=f,p

        rms=plot_rms_power(ff,pp,x,rmsrange=rmsrange,ax2f=ax2f,units=units)
        #pdb.set_trace()
        mask=np.isfinite(rms) #True if good. it is an array
        if rmsthr is not None:
            mask = mask & (rms  < rmsthr)
            ax3.hlines(rmsthr,*ax2.get_xlim()) #plot markers on rms chart
            ax2.plot(x[~mask],np.repeat(ax2.get_ylim()[1],len(x[~mask])),'rx') #plot markers on psd2d chart
        
        #pdb.set_trace()
        #questa era qui, ma dava errore perche' mask e' lineare
        #mask = np.all(mask,axis =0) # if any of mask is False -> False
        p[:,~mask]=np.nan
        ax3.grid(1)
        #plt.tight_layout(rect=(0, 0.03, 1, 0.95) if title else (0, 0, 1, 1))
        
        # replaced with more evolved interactive resize:
        #  plt.colorbar().remove() #dirty trick to adjust size to other panels
        def resize(event):
            box1 = ax1.get_position()
            box2 = ax2.get_position()
            box3 = ax3.get_position()
            ax2.set_position([box1.x0, box2.y0, box1.width , box2.height])
            ax2.set_adjustable("box",share=True)
            ax3.set_position([box1.x0, box3.y0, box1.width , box3.height])
            ax3.set_adjustable("box",share=True)

        cid = fig.canvas.mpl_connect('draw_event', resize)
        cid2 = fig.canvas.mpl_connect('resize_event', resize)

        resize(None)

        #plt.tight_layout()

        if outname:    #kept for compatibility, will be removed in next version
            plt.savefig(fn_add_subfix(outname,'_2dpsd','.png'))

    return f,p
コード例 #5
0
def xdiff_images(data1,
                 data2,
                 x=None,
                 y=None,
                 fignum=None,
                 titles=None,
                 vmin=None,
                 vmax=None,
                 commonscale=False,
                 direction=0,
                 *args,
                 **kwargs):
    """plots two data sets with common axis and their difference. Return the three axis.
    Colorbars are formatted to be same height as plot.
    """

    fig = fignumber(fignum)

    plt.clf()
    ax = None

    aspect = kwargs.pop('aspect', 'auto')
    #this is to set scale if not fixed
    d1std = [np.nanstd(data) for data in (data1, data2)]
    std = min(d1std)

    if x is None:
        x = np.arange(data1.shape[1])
    if y is None:
        y = np.arange(data1.shape[0])

    ax1 = plt.subplot(131, sharex=ax, sharey=ax)

    s = (std if commonscale else d1std[0])
    d1mean = np.nanmean(data1)
    axim = plt.imshow(data1,
                      extent=np.hstack([span(x), span(y)]),
                      interpolation='None',
                      aspect=aspect,
                      vmin=kwargs.get('vmin', d1mean - s),
                      vmax=kwargs.get('vmax', d1mean + s),
                      *args,
                      **kwargs)
    plt.colorbar(fraction=0.046, pad=0.04)

    ax2 = plt.subplot(132, sharex=ax, sharey=ax)

    s = (std if commonscale else d1std[1])
    d2mean = np.nanmean(data2)
    axim = plt.imshow(data2,
                      extent=np.hstack([span(x), span(y)]),
                      interpolation='None',
                      aspect=aspect,
                      vmin=kwargs.get('vmin', d2mean - s),
                      vmax=kwargs.get('vmax', d2mean + s),
                      *args,
                      **kwargs)
    plt.colorbar(fraction=0.046, pad=0.04)

    ax3 = plt.subplot(133, sharex=ax, sharey=ax)
    plt.title('Difference (2-1)')
    diff = data2 - data1
    axim = plt.imshow(diff,
                      extent=np.hstack([span(x), span(y)]),
                      interpolation='None',
                      aspect=aspect,
                      *args,
                      **kwargs)
    plt.colorbar(fraction=0.046, pad=0.04)

    axes = [ax1, ax2, ax3]
    if titles is not None:
        for ax, tit in zip(axes, titles):
            if tit is not None:
                ax.set_title(tit)

    return axes
コード例 #6
0
def diff_images(data1,
                data2,
                x=None,
                y=None,
                fignum=None,
                titles=None,
                vmin=None,
                vmax=None,
                commonscale=False,
                direction=0,
                *args,
                **kwargs):
    """plots two data sets with common axis and their difference. Return the three axis.
    Colorbars are formatted to be same height as plot.
    2018/06/19 use data2D routines, allowing to add parameters (e.g. stats legend).
    """

    fig = fignumber(fignum)

    plt.clf()

    aspect = kwargs.pop('aspect', 'auto')
    #this is to set scale if not fixed
    d1std = [np.nanstd(data) for data in (data1, data2)]
    std = min(d1std)

    if x is None:
        x = np.arange(data1.shape[1])
    if y is None:
        y = np.arange(data1.shape[0])

    ax1 = plt.subplot(131)
    s = (std if commonscale else d1std[0])
    d1mean = np.nanmean(data1)
    plot_data(data1,
              x,
              y,
              aspect=aspect,
              vmin=kwargs.get('vmin', d1mean - s),
              vmax=kwargs.get('vmax', d1mean + s),
              *args,
              **kwargs)

    ax2 = plt.subplot(132, sharex=ax1, sharey=ax1)
    s = (std if commonscale else d1std[1])
    d2mean = np.nanmean(data2)
    plot_data(data2,
              x,
              y,
              aspect=aspect,
              vmin=kwargs.get('vmin', d2mean - s),
              vmax=kwargs.get('vmax', d2mean + s),
              *args,
              **kwargs)

    ax3 = plt.subplot(133, sharex=ax1, sharey=ax1)
    plt.title('Difference (2-1)')
    diff = data2 - data1
    plot_data(diff, x, y, aspect=aspect, *args, **kwargs)

    axes = [ax1, ax2, ax3]
    if titles is not None:
        for ax, tit in zip(axes, titles):
            if tit is not None:
                ax.set_title(tit)

    return axes
コード例 #7
0
def compare_images(datalist,
                   x=None,
                   y=None,
                   fignum=None,
                   titles=None,
                   vmin=None,
                   vmax=None,
                   commonscale=False,
                   axis=0,
                   axmax=0,
                   *args,
                   **kwargs):
    """return a generator that plots n images in a list in subplots with shared
    zoom and axes. datalist is a list of data in format (data, x, y).
    x and y provide plot range (temporarily, ideally want to be able to 
    plot same scale).
    fignum window where to plot, if fignnum is 0 current figure is cleared,
    if None new figure is created.
    axis defines the axis with larger number of plots (default ncols >= nrows)
    axmax maximum nr of plots along shorter dim.
    """
    #modified again 2018/06/05 to accept list of data,x,y triplets. x and y are
    # accepted as range.
    # old docstring was:
    # return a generator that plots n images in a list in subplots with shared
    # zoom and axes. datalist is a list of 2d data on same x and y.
    # fignum window where to plot, if fignnum is 0 current figure is cleared,
    # if None new figure is created.

    # this was changed a couple of times in interface, for example in passing
    # ((data,x,y),...) rather than (data1,data2),x,y
    # it can be tricky in a general way if data don't have all same size,
    # at the moment it is assumed they have.
    # in any case x and y are used only to generate the extent,
    # that is then adapted to the size of data.

    gridsize = find_grid_size(len(datalist), axmax)
    if axis == 1: gridsize = gridsize[::-1]

    fig = fignumber(fignum)

    plt.clf()
    ax = None

    # this is to set scale if not fixed
    d1std = [np.nanstd(data[0]) for data in datalist]
    std = min(d1std)

    for i, d in enumerate(datalist):
        """adjust to possible input formats"""
        data, x, y = d
        if x is None:
            x = np.arange(data.shape[1])
        if y is None:
            y = np.arange(data.shape[0])
        ax = plt.subplot(gridsize[0], gridsize[1], i + 1, sharex=ax, sharey=ax)
        if titles is not None:
            print("titles is not none, it is ", titles)
            plt.title(titles[i])
        # plt.xlabel('X (mm)')
        # plt.ylabel('Y (mm)')
        s = (std if commonscale else d1std[i])
        d1mean = np.nanmean(data)
        aspect = kwargs.pop('aspect', None)
        axim = plt.imshow(data,
                          extent=np.hstack([span(x), span(y)]),
                          interpolation='none',
                          aspect=aspect,
                          vmin=kwargs.get('vmin', d1mean - s),
                          vmax=kwargs.get('vmax', d1mean + s),
                          *args,
                          **kwargs)
        plt.colorbar()
        yield ax
コード例 #8
0
def psd_variability(psdlist,psdrange=None,rmsrange=None,fignum=None,units=None,
    outname=None,figsize=(15,5)):
    # to be removed
    """given a list of 2d psd files plot spans of all of them on same plot.
    TODO: replace input using data rather than files. Move to 2DPSD"""

    from plotting.fignumber import fignumber   
    from pySurf.data2D import projection
    from pySurf.psd2d import rms_power 
    from scipy.stats import ttest_ind
    
    units=['mm','mm','$\mu$m']
    labels = [p.name for p in psdlist]

    #gestione rudimentale dell'output troppo stanco per pensare allo standard.
    if outname is not None:
        label=os.path.basename(outname)
        outfolder=os.path.dirname(outname)
        os.makedirs(outfolder,exist_ok=True)

    rms_v=[]
    fig=fignumber(fignum,figsize=figsize)
    plt.subplot(121)
    for (p,x,f),fn in zip(psdlist,labels):
        ps=projection(p,axis=1,span=True)
        rms_v.append(rms_power(f,p,rmsrange))
        c=plt.plot(f,ps[0],label=os.path.basename(fn)+' AVG')[0].get_color()
        plt.plot(f,ps[1],'--',label='point-wise min',c=c)
        plt.plot(f,ps[2],'--',label='point-wise max',c=c)
        #pdb.set_trace()
    plt.ylabel('axial PSD ('+units[2]+'$^2$ '+units[1]+')')
    plt.xlabel('Freq. ('+units[1]+'$^{-1}$)')
    plt.vlines(rmsrange,*plt.ylim(),label='rms range',linestyle=':')
    plt.ylim(psdrange)
    plt.loglog()
    plt.grid(1)
    plt.legend(loc=0)

    print("Statistics for %s, rms range [%6.3g,%6.3g]:"%(label,*rmsrange))
    for rms,fn in zip(rms_v,labels):
        print(os.path.basename(fn)+": rms=%6.3g +/- %6.3g (evaluated on %i profiles)"%
            (np.nanmean(rms),np.nanstd(rms),len(rms)))
    print ("--------------")
    print ("Distribution of rms: %6.3g +/- %6.3g"%(np.nanmean(rms_v,axis=1).mean(),np.nanmean(rms_v,axis=1).std()))
    print ("AGGREGATED: rms=%6.3g +/- %6.3g (evaluated on %i profiles)"%
            (np.nanmean(rms_v),np.nanstd(rms_v),np.size(rms_v)))
    if len(rms_v)==2:
        print ("Student T test t:%6.3g p:%6.3g"%ttest_ind(rms_v[0],rms_v[1]))

    l=[os.path.splitext(os.path.basename(f))[0]+" rms=%3.2g +/- %3.2g (N=%i)"%
        (np.nanmean(r),np.nanstd(r),len(r)) for f,r in zip(labels,rms_v)]
    plt.subplot(122)
    plt.hist([rr[np.isfinite(rr)] for rr in rms_v],density=True,bins=100,label=l)
    plt.legend(loc=0)
    plt.title(label+" distrib. of avg: %3.2g +/- %3.2g"%
        (np.nanmean(rms_v,axis=1).mean(),np.nanmean(rms_v,axis=1).std()))
    #handles = [Rectangle((0,0),1,1,color=c,ec="k") for c in [low,medium, high]]
    #labels= ["low","medium", "high"]
    #plt.legend(handles, labels)
    plt.tight_layout()
    if outname is not None:
        plt.savefig(fn_add_subfix(outname,'_stats','.jpg'))

    return rms_v