|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +import re |
| 4 | +import numpy as np |
| 5 | +import scipy |
| 6 | +import scipy.optimize |
| 7 | +from base.tools import status |
| 8 | + |
| 9 | +machine_eps = eps = np.finfo(float).eps |
| 10 | +dotplace = lambda n : re.compile(r'(\d)0+$').sub(r'\1',"%3.5f"%float(n)).rjust(8) |
| 11 | + |
| 12 | +""" |
| 13 | +Plotting functions for creating undulation spectra. |
| 14 | +""" |
| 15 | + |
| 16 | +def fftwrap(dat): |
| 17 | + """ |
| 18 | + Wrapper function for 2D Fourier transform. |
| 19 | + """ |
| 20 | + return np.fft.fft2(np.array(dat)) |
| 21 | + |
| 22 | +def perfect_collapser(xs,ys,trim=False,return_indices=False): |
| 23 | + """ |
| 24 | + Return two arrays for a "perfect" collapse. |
| 25 | + """ |
| 26 | + xsort = np.array(np.sort(list(set(xs)))) |
| 27 | + if trim: xsort = xsort[1:] |
| 28 | + inds = np.argmax(np.array([xs==xsort[i] for i in range(len(xsort))]),axis=0) |
| 29 | + #---! future warning below |
| 30 | + if type(ys)==type(None): col = None |
| 31 | + else: col = np.array([np.mean(ys[np.where(inds==i)]) for i in range(len(xsort))]) |
| 32 | + if return_indices: return xsort,col,inds |
| 33 | + else: return xsort,col |
| 34 | + |
| 35 | +def blurry_binner_deprecated(xs,ys,bin_width=0.05,trim=True,return_mapping=False): |
| 36 | + """ |
| 37 | + Group wavevectors by bins. |
| 38 | + Retired this because it was confusing! |
| 39 | + """ |
| 40 | + blurred = (xs/bin_width).astype(int) |
| 41 | + xsort = np.array(np.sort(list(set(blurred)))) |
| 42 | + if trim: xsort = xsort[1:] |
| 43 | + inds = np.argmax(np.array([blurred==xsort[i] for i in range(len(xsort))]),axis=0) |
| 44 | + if type(ys)!=np.ndarray: coly = None |
| 45 | + else: coly = np.array([np.mean(ys[np.where(inds==i)]) for i in range(len(xsort))]) |
| 46 | + colx = np.array([np.mean(xs[np.where(inds==i)]) for i in range(len(xsort))]) |
| 47 | + if not return_mapping: return colx,coly,inds |
| 48 | + else: |
| 49 | + #---! this takes a moment |
| 50 | + mapping = [np.where(blurred==i)[0] for i in np.sort(blurred)] |
| 51 | + return colx,coly,inds,mapping |
| 52 | + |
| 53 | +def blurry_binner(xs,ys,bin_width=0.05,trim=True,return_mapping=False,mode='snapped'): |
| 54 | + """ |
| 55 | + Improved over original with sensible binning and interpolation. |
| 56 | + Removed the index return strategy in favor of handling things internally. |
| 57 | + """ |
| 58 | + #---get the range of the independent variable |
| 59 | + x0,x1 = xs.min(),xs.max() |
| 60 | + #---snap to the bin width |
| 61 | + x0b,x1b = bin_width*np.floor(x0/bin_width),bin_width*np.ceil(x1/bin_width) |
| 62 | + #---develop canonical bin markers |
| 63 | + bins = np.arange(x0b,x1b+bin_width,bin_width) |
| 64 | + #---bins are represented in the middle |
| 65 | + xmid = (bins[1:]+bins[:-1])/2. |
| 66 | + #---get the bin positions |
| 67 | + xbin = np.floor(xs/bin_width).astype(int) - np.floor(xs.min()/bin_width).astype(int) |
| 68 | + #---check that everything is in the right bin |
| 69 | + try: |
| 70 | + if not (np.all(xs<=bins[1:][xbin]) and np.all(xs>=bins[:-1][xbin])): |
| 71 | + import ipdb;ipdb.set_trace() |
| 72 | + raise Exception('binning failure!') |
| 73 | + except: |
| 74 | + import ipdb;ipdb.set_trace() |
| 75 | + #---reindex each position into bins |
| 76 | + indices = np.unique(xbin) |
| 77 | + reindex = [np.where(xbin==i)[0] for i in indices] |
| 78 | + #---snapped mode just uses the middle of each bin and takes the average of the constituents |
| 79 | + if mode=='snapped': |
| 80 | + y_out = np.array([ys[r].mean() for r in reindex]) |
| 81 | + x_out = xmid[indices] |
| 82 | + #---! recommend developing an interpolation method here to shift laterally if off-center constituents |
| 83 | + else: raise |
| 84 | + #---for each point, return the indices of other points in the same bin |
| 85 | + #---...note that this is essential for developing weights in the blurry_explicit |
| 86 | + #---...where the weights end up: weights = 1./np.array([len(i) for i in q_mapping])[band] |
| 87 | + if return_mapping: |
| 88 | + mapping = [np.where(xbin==i)[0] for i in np.sort(xbin)] |
| 89 | + return x_out,y_out,mapping |
| 90 | + else: return x_out,y_out |
| 91 | + |
| 92 | +def undulation_fitter(q_raw,hqs,area,initial_conditions=(20.0,0.0),residual_form='linear'): |
| 93 | + """Like bath fitter, but for undulations.""" |
| 94 | + |
| 95 | + if residual_form == 'log': |
| 96 | + def residual(values): |
| 97 | + return np.sum(np.log10(values.clip(min=machine_eps))**2)/float(len(values)) |
| 98 | + elif residual_form == 'linear': |
| 99 | + def residual(values): |
| 100 | + return np.sum((values-1.0)**2)/float(len(values)) |
| 101 | + else: raise Exception('unclear residual form %s'%residual_form) |
| 102 | + |
| 103 | + def multipliers(x,y): |
| 104 | + """Multiplying complex matrices in the list of terms that contribute to the energy.""" |
| 105 | + return x*np.conjugate(y) |
| 106 | + |
| 107 | + def callback(args): |
| 108 | + """Watch the optimization.""" |
| 109 | + global Nfeval |
| 110 | + name_groups = ['kappa','gamma'] |
| 111 | + text = ' step = %d '%Nfeval+' '.join([name+' = '+dotplace(val) |
| 112 | + for name,val in zip(name_groups,args)+[('error',objective(args))]]) |
| 113 | + status('searching! '+text,tag='optimize') |
| 114 | + Nfeval += 1 |
| 115 | + |
| 116 | + def objective(args,mode='residual'): |
| 117 | + kappa,gamma = args |
| 118 | + termlist = [multipliers(x,y) for x,y in [(hqs,hqs)]] |
| 119 | + gamma = 0.0 |
| 120 | + ratio = kappa/2.0*area*(termlist[0]*q_raw**4)+gamma/2.0*area*(termlist[0]*q_raw**2) |
| 121 | + if mode=='residual': return residual(ratio) |
| 122 | + elif mode=='ratio': return ratio |
| 123 | + else: raise Exception('invalid mode %s'%mode) |
| 124 | + |
| 125 | + global Nfeval |
| 126 | + Nfeval = 0 |
| 127 | + test_ans = objective(initial_conditions) |
| 128 | + if not isinstance(test_ans,np.floating): |
| 129 | + raise Exception('objective_residual function must return a scalar') |
| 130 | + fit = scipy.optimize.minimize(objective, |
| 131 | + #---note Newton-CG requires Jacobian, coblya is not strong enough, usw |
| 132 | + x0=tuple(initial_conditions),method='BFGS',callback=callback) |
| 133 | + return dict(fit,kappa=fit.x[0],gamma=fit.x[1]) |
| 134 | + |
| 135 | +def calculate_undulations(surf,vecs,fit_style=None,chop_last=False,lims=(0,1.0), |
| 136 | + perfect=False,raw=False,midplane_method=None,custom_heights=None,residual_form='log',fit_tension=False): |
| 137 | + """ |
| 138 | + Compute undulation spectrum. |
| 139 | + """ |
| 140 | + nframes,m,n = surf.shape |
| 141 | + frames = np.arange(nframes) |
| 142 | + Lx,Ly = np.mean(vecs,axis=0)[:2] |
| 143 | + lenscale = 1. |
| 144 | + qmagsshift = lenscale*np.array([[np.sqrt( |
| 145 | + ((i-m*(i>m/2))/((Lx)/1.)*2*np.pi)**2+ |
| 146 | + ((j-n*(j>n/2))/((Ly)/1.)*2*np.pi)**2) |
| 147 | + for j in range(0,n)] for i in range(0,m)]) |
| 148 | + area = (Lx*Ly/lenscale**2) |
| 149 | + |
| 150 | + #---remove the supposed "average" structure |
| 151 | + if midplane_method=='flat' or midplane_method==None: |
| 152 | + surf = surf-np.mean(surf) |
| 153 | + elif midplane_method=='average': |
| 154 | + surf = surf-np.mean(surf,axis=0) |
| 155 | + elif midplane_method=='average_normal' and type(custom_heights)==type(None): |
| 156 | + raise Exception('send custom_heights for average_normal') |
| 157 | + elif midplane_method=='average_normal': surf = custom_heights |
| 158 | + else: raise Exception('invalid midplane_method %s'%midplane_method) |
| 159 | + |
| 160 | + #---perform the FFT and line up the results |
| 161 | + hqs = np.array([fftwrap(surf[fr])/lenscale/np.double((m*n)) for fr in range(nframes)]) |
| 162 | + y = np.reshape(np.mean(np.abs(hqs)**2,axis=0),-1) |
| 163 | + x = np.reshape(qmagsshift,-1) |
| 164 | + |
| 165 | + #---default method |
| 166 | + if fit_style==None: fit_style = 'band,perfect,simple' |
| 167 | + |
| 168 | + packed = {} |
| 169 | + #---choose a binning method, range method, and fitting method |
| 170 | + if fit_style in ['band,perfect,simple','band,perfect,basic', |
| 171 | + 'band,perfect,fit','band,perfect,curvefit','band,blurry,curvefit','band,perfect,curvefit-crossover']: |
| 172 | + |
| 173 | + if lims==None: raise Exception('fit_style %s requires lims'%fit_style) |
| 174 | + #---collapse, perfectly |
| 175 | + x2,y2 = perfect_collapser(x,x)[1],perfect_collapser(x,y)[1] |
| 176 | + goodslice = np.where(np.all((x2>lims[0],x2<lims[1]),axis=0)) |
| 177 | + #---sample in the band and drop the zero mode |
| 178 | + #---! where is the zero mode dropped? |
| 179 | + x3,y3 = x2[goodslice],y2[goodslice] |
| 180 | + |
| 181 | + #---apply binning method |
| 182 | + if fit_style=='band,blurry,curvefit': |
| 183 | + x3,y3 = blurry_binner(x3,y3) |
| 184 | + |
| 185 | + |
| 186 | + #---perform the fit |
| 187 | + if fit_style=='band,perfect,simple': |
| 188 | + #---the simple method is a crude way to do the fit, by fixing the exponent and then using the fact |
| 189 | + #---...that the y-axis centroid of the positions must determine the |
| 190 | + #---we recommend against using this method for further calculaiton. useful as a comparison only. |
| 191 | + if fit_tension: raise Exception('cannot do simple with fit_tension') |
| 192 | + kappa,gamma = 2.0*np.mean(1/((y3*x3**4)*Lx*Ly/lenscale**2)),0.0 |
| 193 | + elif fit_style=='band,perfect,basic': |
| 194 | + if fit_tension: raise Exception('cannot do basic with fit_tension') |
| 195 | + #---in this method we use polyfit to do a linear fit in the log space |
| 196 | + #---note that exponent is free for this method and we have no specific residual form |
| 197 | + c0,c1 = np.polyfit(np.log10(x3),np.log10(y3),1) |
| 198 | + #---fit would be: 10**c1*x3**(c0) |
| 199 | + #---! just spitballing here |
| 200 | + kappa,gamma = 1./(10**c1*area)*2.0,0.0 |
| 201 | + #---save for debugging |
| 202 | + packed['linear_fit_in_log'] = dict(c0=c0,c1=c1) |
| 203 | + elif fit_style in ['band,perfect,curvefit','band,blurry,curvefit']: |
| 204 | + #---in this method we use the scipy curve_fit function |
| 205 | + exponent = 4.0 |
| 206 | + kwargs = {} |
| 207 | + kwargs.update(bounds=((5.0,-1.0),(10**2.0,1.0))) |
| 208 | + kwargs.update(maxfev=10**5) |
| 209 | + if residual_form=='linear': |
| 210 | + def hqhq(q_raw,kappa,sigma): |
| 211 | + if not fit_tension: sigma = 0.0 |
| 212 | + return 1.0/(area/2.0*(kappa*q_raw**(exponent)+sigma*q_raw**2)) |
| 213 | + fit = scipy.optimize.curve_fit(hqhq,x3,y3,**kwargs) |
| 214 | + kappa,gamma = fit[0][0],fit[0][1] |
| 215 | + print(gamma) |
| 216 | + elif residual_form=='log': |
| 217 | + #---! deprecated but works |
| 218 | + if False: |
| 219 | + def hqhq(q_raw,kappa,sigma): |
| 220 | + sigma = 0.0 |
| 221 | + return np.log10(2.0/area/kappa)+-4.0*q_raw |
| 222 | + fit = scipy.optimize.curve_fit(hqhq,np.log10(x3),np.log10(y3),**kwargs) |
| 223 | + #---new method has the full expression fit in the log space |
| 224 | + else: |
| 225 | + def hqhq(q_raw,kappa,sigma): |
| 226 | + if not fit_tension: sigma = 0.0 |
| 227 | + return np.log10(1.0/(area/2.0*(kappa*q_raw**(exponent)+sigma*q_raw**2))) |
| 228 | + fit = scipy.optimize.curve_fit(hqhq,x3,np.log10(y3),**kwargs) |
| 229 | + kappa,gamma = fit[0][0],fit[0][1] |
| 230 | + else: raise Exception('invalid residual_form %s'%residual_form) |
| 231 | + |
| 232 | + elif fit_style=='band,perfect,curvefit-crossover': |
| 233 | + #---in this method we use the scipy curve_fit function |
| 234 | + exponent = 4.0 |
| 235 | + kwargs = {} |
| 236 | + kwargs.update(bounds=((5.0,-1.0,lims[0]+0.001),(10**2.0,1.0,lims[1]-0.001))) |
| 237 | + kwargs.update(maxfev=10**5) |
| 238 | + if residual_form!='log': raise Exception('crossover only works with residual_form log') |
| 239 | + if fit_tension==False: raise Exception('crossover only works with fit_tension') |
| 240 | + def hqhq(q_raw,kappa,sigma,crossover): |
| 241 | + if sigma==0.0: sigma = eps |
| 242 | + regime_tension = q_raw<=crossover |
| 243 | + regime_bending = ~regime_tension |
| 244 | + heights_tension = np.log10(1.0/(area/2.0*(sigma*q_raw[regime_tension]**2.0))) |
| 245 | + heights_bending = np.log10(1.0/(area/2.0*(kappa*q_raw[regime_bending]**4.0))) |
| 246 | + return np.concatenate((heights_tension,heights_bending)) |
| 247 | + fit = scipy.optimize.curve_fit(hqhq,x3,np.log10(y3),(20.0,0.001,0.02),**kwargs) |
| 248 | + kappa,gamma,crossover = fit[0][0],fit[0][1],fit[0][2] |
| 249 | + packed.update(crossover=crossover) |
| 250 | + elif fit_style=='band,perfect,fit': |
| 251 | + #---the most advanced method uses scipy.optimmize in a separate function |
| 252 | + fit = undulation_fitter(x3,y3,area,residual_form=residual_form, |
| 253 | + initial_conditions=(20.0,0.0)) |
| 254 | + kappa,gamma = fit['kappa'],fit['gamma'] |
| 255 | + else: raise Exception('invalid fit_style %s'%fit_style) |
| 256 | + #---return the data |
| 257 | + packed.update(kappa=kappa,points=np.transpose((x3,y3)),sigma=gamma, |
| 258 | + q_raw=x,energy_raw=y,q_binned=x2,energy_binned=y2,area=area) |
| 259 | + |
| 260 | + else: raise Exception('invalid fit_style %s'%fit_style) |
| 261 | + return packed |
0 commit comments