Source code for boutpy.boutdata.map_pfile2grid

"""Map 1D experimental profiles to netcdf grid generated by hypernotoad.pro
"""
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)

__all__ = ["map_1d4grid",
           "map_pfile2grid",
           "map_nc2grid",
           ]

__date__ = '02072018'
__version__ = '0.2.1'
__author__ = 'J.G. Chen'
__email__ = 'cjgls@pku.edu.cn'

import os
import re
from shutil import copyfile

import numpy as np
import matplotlib.pyplot as plt
import scipy.interpolate as si
from scipy import ndimage, integrate

import boutpy.boututils.pfile
from boutpy.boututils.fileio import file_import, save2nc
from boutpy.boututils.functions import get_yesno
from boutpy.boutdata.boutgrid import boutgrid


[docs]def map_1d4grid(x, y, gridfile, index=None, ext='const', kind='cubic', sigma=8, mode='nearest', wtitle='', Er=None, vfilter=None, units=None, smooth='1st', w=None, sfactor=None, verbose=False, check_positive=False, **kwargs): """Map 1D-profile to grid 1. basic rule: constant in y-direction 2. map :math:`E_r`: - E_r = er*Rxy*Bpxy/(Rxy*Bpxy)_omp, - E_r = omgeb*Rxy*Bpxy 3. leg regions - SOL region: same as the nearest line which has core part - private flux region: constant values which are same as separatrix .. warning:: The smooth method(`smooth=gaussian`: gaussian_filter1d) will slightly make the gradient at boundary closer to 0. Parameters ---------- x : (N,) array_like 1-D array of independent input data. Must be increasing. y : (N,) array_like 1-D array of dependent input data, of the same length as x. psin : (M,) array_like 1-D array of independent data. index : int array_like or int, optional, default: None ignoring data points at `index` when fitting data. if `index` is a integer, then ignoring `index` data points at both 'x' boundary for smoothing purposes. smooth : ['gaussian', 'spline', '1st', '2nd'], optional, default: '1st' smooth method if not `None` - 'gaussian': scipy.ndimage.gaussian_filter1d - 'spline': scipy.interpolate.UnivariantSpline - '1st': smooth 1st order derivative - '2nd': smooth 2nd order derivative w : (M,) array_like, optional, default: None weight for smoothing if 'spline' method used sfactor : float, optional, default: None smooth_factor for 'spline' method ext : float, (float, float) or 'const', optional, default: 'const' Controls the extrapolation mode for elements not in the interval defined by the knot sequence. - float : slope for the profiles outside the interval - (float, float): slopes for both boundary - 'const': constant value outside the interval sigma : scalar, default: 8 standard deviation for Gaussian kernel mode : ['reflect', 'constant', 'nearest', 'mirror', 'wrap'], optional The `mode` parameter determines how the array borders are handled, where `cval` is the value when mode is equal to 'constant'. Default is 'nearest'. wtitle : str, optional, default: ''. addtional title string for plot window units : str, optional, default: None additional info for the profile: units. It is shown in the fig title. TODO: convert units in here? Er : ['er', 'omgeb'], optional, default: None - ``er``: map 'er' to 'E_r', E_r = er*Rxy*Bpxy/(Rxy*Bpxy)_omp - ``omgeb``: map 'omgeb' to 'E_r', E_r = omgeb*Rxy*Bpxy - ``None``: constant in y-direction. vfilter : int|float, optional, default: None Set the data where are less than `vfilter` to the one closest to `vfilter`. Not used by default. It's recommended for electron temperature, especially when the Spitzer resistivity, which is propotional to Te^{-1.5}, is used in the code. check_positive : boel, optional, default: False check that the profile only contains positive value. Raise ValueError if it contains nagetive value or 0. verbose : bool, optional, default: False output more details Other keywords arguments are passed to smooth method. Returns ------- data_2d : 2D-array_like 2D data after mapping 1D to grid See Also -------- scipy.ndimage.gaussian_filter1d, scipy.ndimage.gaussian_filter, scipy.interpolate.UnivariateSpline """ grid = boutgrid(gridfile) # get psin at outer mid-plane psin, yind_imp, yind_omp = grid.get_psin( yind='omp', index=True, verbose=False) ind_min = np.abs(x - psin[0]).argmin() ind_max = np.abs(x - psin[-1]).argmin() # convert unit to Latex format if units: # ^20 --> ^{20} pat = re.compile(r'\^(-*\d+)') for i in pat.findall(units): units = units.replace('^{}'.format(i), '^{{{}}}'.format(i)) # _20 --> _{20} pat = re.compile(r'_(-*\d+)') for i in pat.findall(units): units = units.replace('_{}'.format(i), '_{{{}}}'.format(i)) plt.figure('oned4grid-'+wtitle) plt.clf() fig, ax = plt.subplots(2, 2, sharex=True, num='oned4grid-'+wtitle) ax = ax.flatten() plt.suptitle(wtitle + ("(${}$)".format(units) if units is not None else '')) ax[0].plot(x[ind_min:ind_max], y[ind_min:ind_max], 'k*-', markevery=5, label='original') grad_1st_tmp = np.gradient(y[ind_min:ind_max], x[ind_min:ind_max]) grad_2nd_tmp = np.gradient(grad_1st_tmp, x[ind_min:ind_max]) ax[1].plot(x[ind_min:ind_max], grad_1st_tmp, 'k*-', markevery=5, label='original') ax[2].plot(x[ind_min:ind_max], grad_2nd_tmp, 'k*-', markevery=5, label='original') # extend profile to outside the interval data_1d = si.interp1d(x, y, kind='cubic', bounds_error=False, fill_value=(y[0], y[-1]))(psin) data_orignal = data_1d.copy() # data_original[xind_l:xind_r+1] is experimental data xind_l = 0 xind_r = len(psin) - 1 if x[0] > psin[0] or x[-1] < psin[-1]: if x[0] > psin[0]: # left boundary needs extension xind_l = np.where(psin >= x[0])[0][0] if x[-1] < psin[-1]: # right boundary needs extension xind_r = np.where(psin <= x[-1])[0][-1] if isinstance(ext, basestring) and ext == 'const': # constant outside the boundary print("NOTE: set constant value outside the [{}, {}]!!!!!" .format(x[0], x[-1])) else: if isinstance(ext, float): slope_l = slope_r = ext elif isinstance(ext, (list, tuple, np.ndarray)) and len(ext) == 2: slope_l, slope_r = ext else: raise ValueError( "option `ext`: a float value, 2-elements tuple of float " "or 'const' required") if xind_l != 0: data_1d[:xind_l] = (slope_l * (psin[:xind_l] - psin[xind_l]) + data_1d[xind_l]) if xind_r != len(psin) - 1: data_1d[xind_r:] = (slope_r * (psin[xind_r:] - psin[xind_r]) + data_1d[xind_r]) # using weight to remove some data ind_keep = np.ones_like(psin) xind_lpan = xind_rpan = None if isinstance(index, (list, tuple, np.ndarray)): ind_keep[np.asarray(index)] = 0 elif isinstance(index, int): if index > psin.size / 8: raise ValueError("option `index`: Too Large!") if xind_l != 0: xind_lpan = [max(xind_l-index, 1), min(xind_l+index, psin.size)] ind_keep[xind_lpan[0]:xind_lpan[1]] = 0 if xind_r != len(psin) - 1: xind_rpan = [max(xind_r-index, 1), min(xind_r+index, psin.size)] ind_keep[xind_rpan[0]:xind_rpan[1]] = 0 if verbose: print("[xind_l, xind_r] = [{}, {}]".format(xind_l, xind_r)) print("xind_lpan: ", xind_lpan) print("xind_rpan: ", xind_rpan) if (ind_keep == 0).any(): # some data points are removed data_1d = si.interp1d(psin[np.where(ind_keep)], data_1d[np.where(ind_keep)], kind=kind)(psin) if (isinstance(index, (tuple, list, np.ndarray)) and (np.diff(index) == 1).all()): ax[0].axvspan(psin[index[0]], psin[index[-1]], alpha=0.5) ax[1].axvspan(psin[index[0]], psin[index[-1]], alpha=0.5) ax[2].axvspan(psin[index[0]], psin[index[-1]], alpha=0.5) ax[3].axvspan(psin[index[0]], psin[index[-1]], alpha=0.5) elif isinstance(index, int): if xind_lpan is not None: ax[0].axvspan(psin[xind_lpan[0]], psin[xind_lpan[-1]-1], alpha=0.5) ax[1].axvspan(psin[xind_lpan[0]], psin[xind_lpan[-1]-1], alpha=0.5) ax[2].axvspan(psin[xind_lpan[0]], psin[xind_lpan[-1]-1], alpha=0.5) ax[3].axvspan(psin[xind_lpan[0]], psin[xind_lpan[-1]-1], alpha=0.5) if xind_rpan is not None: ax[0].axvspan(psin[xind_rpan[0]], psin[xind_rpan[-1]-1], alpha=0.5) ax[1].axvspan(psin[xind_rpan[0]], psin[xind_rpan[-1]-1], alpha=0.5) ax[2].axvspan(psin[xind_rpan[0]], psin[xind_rpan[-1]-1], alpha=0.5) ax[3].axvspan(psin[xind_rpan[0]], psin[xind_rpan[-1]-1], alpha=0.5) ax[0].plot(psin, data_1d, 'r+-', label='interp1d', markevery=7) ax[1].plot(psin, np.gradient(data_1d, psin), 'r+-', label='interp1d', markevery=7) ax[2].plot(psin, np.gradient(np.gradient(data_1d, psin), psin), 'r+-', label='interp1d', markevery=7) ax[3].plot(psin[xind_l:xind_r+1], ((data_1d-data_orignal)/data_orignal)[xind_l:xind_r+1]*100, 'r+-', label='interp1d', markevery=7) if isinstance(vfilter, (float, int)) and (data_1d < vfilter).any(): print("WARNING: set data where is less than {0} to {0}" .format(vfilter)) ind = np.abs(data_1d - vfilter).argmin() data_1d[np.where(data_1d < vfilter)] = data_1d[ind] # weight for 'scipy.interpolate.UnivariateSpline()' if w is None: w = ind_keep # smooth data, same psin range grad_1st = np.gradient(data_1d, psin) grad_2nd = np.gradient(grad_1st, psin) if smooth == 'gaussian': print("WARNING: gradient at boundary may be changed a lot!") data_1d = ndimage.gaussian_filter1d(data_1d, sigma=sigma, mode=mode, **kwargs) elif smooth == 'spline': spl = si.UnivariateSpline(psin, data_1d, w=w, **kwargs) if sfactor: spl.set_smoothing_factor(sfactor) data_1d = spl(psin) elif smooth == '1st': # smooth 1st order derivative s_grad_1st = ndimage.gaussian_filter1d(grad_1st, sigma=sigma, mode=mode, **kwargs) int1 = integrate.cumtrapz(s_grad_1st, psin, initial=0) data_1d = int1 + (data_1d - int1).mean() elif smooth == '2nd': s_grad_2nd = ndimage.gaussian_filter1d(grad_2nd, sigma=sigma, mode=mode, **kwargs) int2_tmp = integrate.cumtrapz(s_grad_2nd, psin, initial=0) int2_tmp += (grad_1st - int2_tmp).mean() int2 = integrate.cumtrapz(int2_tmp, psin, initial=0) data_1d = int2 + (data_1d - int2).mean() if smooth: ax[0].plot(psin, data_1d, 'b-', label=smooth) ax[1].plot(psin, np.gradient(data_1d, psin), 'b-', label=smooth) ax[2].plot(psin, np.gradient(np.gradient(data_1d, psin), psin), 'b-', label=smooth) ax[3].plot(psin[xind_l:xind_r+1], ((data_1d-data_orignal)/data_orignal)[xind_l:xind_r+1]*100, 'b-', label=smooth) ax[0].legend() ax[-1].set_xlabel('$\psi_n$') ax[-2].set_xlabel('$\psi_n$') # ax[1].text(0.8, 0.8, 'df/d$\psi_n$', transform=ax[1].transAxes) ax[1].legend([], title='df/d$\psi_n$') ax[2].legend([], title='$d^2f/d\psi_n^2$') ax[3].legend([], title='rel tol(%)') if check_positive and (data_1d <= 0).any(): raise ValueError("negative value or 0 in profile!") nx, ny = grid['nx'], grid['ny'] # psin[xsep] is larger than but nearest to 1.0?? xsep, xsep2 = grid.get('ixseps1', nx), grid.get('ixseps2', nx) xsep = nx - 1 if xsep < 0 or xsep >= nx else xsep xsep2 = nx - 1 if xsep2 < 0 or xsep2 >= nx else xsep2 assert xsep <= xsep2 nsol1 = nx - xsep nsol2 = nx - xsep2 ny_inner = grid['ny_inner'] Rxy = grid['Rxy'] Bpxy = grid['Bpxy'] jyseps1_1 = grid['jyseps1_1'] jyseps2_1 = grid['jyseps2_1'] jyseps1_2 = grid['jyseps1_2'] jyseps2_2 = grid['jyseps2_2'] # initiate variable data_2d = np.zeros([nx, ny]) # split yind yind_leg1_1 = np.r_[0: jyseps1_1 + 1] yind_leg2_2 = np.r_[jyseps2_2 + 1: ny] if jyseps2_1 == jyseps1_2: yind_leg2_1 = np.array([], dtype=int) yind_leg1_2 = np.array([], dtype=int) else: yind_leg2_1 = np.r_[jyseps2_1 + 1: ny_inner] yind_leg1_2 = np.r_[ny_inner: jyseps1_2 + 1] yind_core = np.r_[jyseps1_1 + 1: jyseps2_1 + 1, jyseps1_2 + 1: jyseps2_2 + 1] yind_legd = np.r_[yind_leg1_1, yind_leg2_2] yind_legu = np.r_[yind_leg2_1, yind_leg1_2] yind_leg = np.r_[yind_legd, yind_legu] if verbose >= 2: print("yind for each region\n" + "=" * 18) # print("yind_core: ", yind_core) print("yind_legd: ", yind_legd) print("yind_legu: ", yind_legu) # print("yind_leg : ", yind_leg) assert (len(yind_core) + len(yind_leg) == ny) assert (len(yind_legd) + len(yind_legu) == len(yind_leg)) # start mapping # ------------- # region 1: core + SOL # constant in y-direction, except for E_r if Er is None: data_2d[:, yind_core] = data_1d.reshape(nx, 1) elif Er == 'er': print("NOTE: map er to E_r\n" " E_r = er*Rxy*Bpxy/(Rxy*Bpxy)_omp\n") data_2d[:, yind_core] = (data_1d.reshape(nx, 1) * Rxy[:, yind_core] * Bpxy[:, yind_core] / Rxy[:, yind_omp].reshape(nx, 1) / Bpxy[:, yind_omp].reshape(nx, 1) ) elif Er == 'omgeb': print("NOTE: map omgeb to E_r\n" " E_r = omgeb*Rxy*Bpxy\n") data_2d[:, yind_core] = (data_1d.reshape(nx, 1) * Rxy[:, yind_core] * Bpxy[:, yind_core]) else: raise ValueError("ERROR: unrecognized value for option `Er`!!!") # region 2: SOL regions of leg # constant value at leg SOL region in y-direction # set to the value nearest to region 1 respectively. if Er is None: data_2d[xsep:, yind_legd] = data_1d[xsep:].reshape(nsol1, 1) data_2d[xsep2:, yind_legu] = data_1d[xsep2:].reshape(nsol2, 1) else: # E_r: data_2d[xsep:, yind_leg1_1] = ( data_2d[xsep:, jyseps1_1 + 1].reshape(nsol1, 1)) data_2d[xsep:, yind_leg2_2] = ( data_2d[xsep:, jyseps2_2].reshape(nsol1, 1)) data_2d[xsep2:, yind_leg2_1] = ( data_2d[xsep2:, jyseps2_1].reshape(nsol2, 1)) data_2d[xsep2:, yind_leg1_2] = ( data_2d[xsep2:, jyseps1_2 + 1].reshape(nsol2, 1)) # region 3: private flux region # constant: same value as separatrix data_2d[:xsep, yind_legd] = data_2d[xsep, yind_legd] data_2d[:xsep2, yind_legu] = data_2d[xsep2, yind_legu] return data_2d
[docs]def map_pfile2grid(gridfile, pfile, output=None, Er='omgeb', verbose=False, **kwargs): """Map pfile to netcdf grid generated by hypnotoad.pro Parameters ---------- gridfile : str path to gridfile pfile : str path to pfile output : str, None, default: None output data to file `output` which is a copy of `gridfile`, if `output` is None, the suffix of filename is '_exp_constSOL_er' (Er='er') or '_exp_constSOL_omgeb'(Er='omgeb'), Er : ['er'|'omgeb'], default: 'omgeb' - ``er``: map 'er' to 'E_r', E_r = er*Rxy*Bpxy/(Rxy*Bpxy)_omp - ``omgeb``: map 'omgeb' to 'E_r', E_r = omgeb*Rxy*Bpxy verbose : bool, optional, default: False output more information. Other keywords arguments are passed to map_1d4grid() Returns ------- None """ # load original data grid = boutgrid(gridfile) print("=" * 50) psin, yind_imp, yind_omp = grid.get_psin(yind='omp', index=True) xsep = np.where(psin >= 1.0)[0][0] if grid.topology(): # with xpoint assert xsep == grid['ixseps1'] else: # no xpoint assert xsep == psin.size # interpolate all profiles in pfile to same psin # try: # pdata = file_import(pfile) # except: pfile_obj = boutpy.boututils.pfile(pfile) pdata = pfile_obj.interpolate() units = pfile_obj.units profiles = {} name = {'ti': 'Tiexp', 'te': 'Teexp', 'ni': 'Niexp', 'ne': 'Neexp', 'er': 'E_r', 'pressure_s': 'pressure_s' } print("smooth around separatrix area ...") for i in ['ti', 'ni', 'ne']: print("===> mapping {} ...".format(i)) profiles[i] = map_1d4grid( pdata['psin'], pdata[i], grid, wtitle=i, units=units[i], **kwargs) # map te, filter out < 0.01 keV if verbose >= 1: print("===> mapping {} ...".format('te')) profiles['te'] = map_1d4grid( pdata['psin'], pdata['te'], grid, wtitle='te', vfilter=0.05, units=units['te'], **kwargs) if verbose >= 1: print("===> mapping {} ...".format(Er)) profiles['er'] = map_1d4grid( pdata['psin'], pdata[Er], grid, wtitle=Er, Er=Er, units=units[Er], **kwargs) if verbose >= 1: print("===> mapping {} ...".format('pressure')) profiles['pressure_s'] = map_1d4grid( psin, grid['pressure'][:, yind_omp], grid, wtitle='pressure', units='Pa', **kwargs) if verbose >= 1: print("===> Unit conversion ...\n" " er: kV/m --> V/m\n" " ti, te: keV --> eV") profiles['er'] *= 1000.0 profiles['ti'] *= 1000.0 profiles['te'] *= 1000.0 if isinstance(output, basestring): output = output if output.endswith('.nc') else (output + '.nc') else: output = gridfile[:-3] + '_exp_constSOL_' + Er + '.nc' if os.path.exists(output): if not get_yesno('gridfile:\n {}\n'.format(output) + 'Already exists, Overwrite the file?'): raise IOError("{} already exists!!!!".format(output)) # saving data to grid ... print("save data to {} ...".format(output)) copyfile(gridfile, output) save2nc(output, 'a', **{name[i]: profiles[i] for i in profiles.keys()})
[docs]def map_nc2grid(gridfile, ncfile, output=None, Er='omgeb', verbose=False, **kwargs): """Map 1D profiles in `ncfile` to netcdf grid generated by hypnotoad.pro Parameters ---------- gridfile : str path to gridfile ncfile : str path to ncfile, which includes 1D profiles. output : str, None, default: None output data to file `output` which is a copy of `gridfile`, if `output` is None, the suffix of filename is '_exp_constSOL_er' (Er='er') or '_exp_constSOL_omgeb'(Er='omgeb'), Er : ['er'|'omgeb'], default: 'omgeb' 'er': map 'er' to 'E_r', E_r = er*Rxy*Bpxy/(Rxy*Bpxy)_omp 'omgeb': map 'omgeb' to 'E_r', E_r = omgeb*Rxy*Bpxy Other keywords arguments are passed to map_1d4grid() Returns ------- None """ # load original data grid = boutgrid(gridfile) # check topology of grid nxpoint = grid.topology() print("=" * 50) psin, yind_imp, yind_omp = grid.get_psin(yind='omp', index=True) xsep = np.where(psin >= 1.0)[0][0] assert xsep == grid['ixseps1'] # interpolate all profiles in pfile to same psin pdata = file_import(ncfile) if (Er == 'omgeb') and (not pdata.has_key('omgeb')): print("WARNING: `omgeb' not in {}".format(ncfile)) print(" Mapping Er using er") Er = 'er' if pdata['psin'][0] > psin[0]: raise ValueError("psin range error") units = {'ti': 'eV', 'te': 'eV', 'ne': '10^{20}/m^3', 'nee': '10^{20}/m^3', 'ni': '10^{20}/m^3', 'p0': 'Pa', 'er': 'V/m', 'omgeb': 'Rad/s', } profiles = {} name = {'ti': 'Tiexp', # 'name in pdata': 'name in grid' 'te': 'Teexp', 'ni': 'Niexp', 'ne': 'Neexp', 'nee': 'Neexp', 'er': 'E_r', 'p0': 'pressure_s', } if verbose >= 1: print("smooth around separatrix area ...") for i in ['ti', 'ni', 'ne', 'nee', 'p0']: if i not in pdata.keys(): continue print("===> mapping {} ...".format(i)) profiles[i] = map_1d4grid(pdata['psin'], pdata[i], grid, wtitle=i, units=units[i], **kwargs) # map te, filter out < 0.01 if verbose >= 1: print("===> mapping {} ...".format('te')) profiles['te'] = map_1d4grid(pdata['psin'], pdata['te'], grid, wtitle='te', vfilter=0.01, units=units['te'], **kwargs) if verbose >= 1: print("===> mapping {} ...".format(Er)) profiles['er'] = map_1d4grid( pdata['psin'], pdata[Er], grid, wtitle=Er, Er=Er, units=units[Er], **kwargs) if verbose >= 1: print("===> mapping {} ...".format('pressure')) if not profiles.has_key('p0'): profiles['p0'] = map_1d4grid( psin, grid['pressure'][:, yind_omp], grid, wtitle='pressure', units='Pa', **kwargs) if verbose >= 1: print("===> Unit conversion ...") if get_yesno("er: kV/m --> V/m?"): profiles['er'] *= 1000.0 plt.figure('oned4grid-er') # the plot use original data, so change to olddata plt.title('er(kV/m)') if get_yesno("omgeb: kRad/s --> Rad/s?"): profiles['er'] *= 1000.0 plt.figure('oned4grid-omgeb') plt.title('omgeb(kRad/s)') if get_yesno("ti, te: keV --> eV?"): profiles['ti'] *= 1000.0 profiles['te'] *= 1000.0 plt.figure('oned4grid-ti') plt.title('ti(keV)') if get_yesno("p0: kPa --> Pa?"): profiles['p0'] *= 1000.0 plt.figure('oned4grid-pessure') plt.title('pressure(kPa)') if get_yesno("ne, ni: 10^19/m^3 --> 10^20/m^3?"): profiles['nee'] /= 10.0 profiles['ni'] /= 10.0 plt.figure('oned4grid-nee') plt.title('nee($10^{19}/m^3$)') plt.figure('oned4grid-ni') plt.title('ni($10^{19}/m^3$)') if isinstance(output, basestring): output = output if output.endswith('.nc') else (output + '.nc') else: if pdata['psin'][-1] < psin[-1]: output = gridfile[:-3] + '_exp_constSOL_' + Er + '.nc' else: output = gridfile[:-3] + '_exp_' + Er + '.nc' if os.path.exists(output): if not get_yesno('gridfile:\n {}\n'.format(output) + 'Already exists, Overwrite the file?'): raise IOError("{} already exists!!!".format(output)) # saving data to grid ... print("save data to {} ...".format(output)) copyfile(gridfile, output) save2nc(output, 'a', **{name[i]: profiles[i] for i in profiles.keys()})