def AD_BldGag(AD, AD_bld, chordOut=False): """ Returns the radial position of AeroDyn blade gages INPUTS: - AD: either: - a filename of a AeroDyn input file - an instance of FileCl, as returned by reading the file, AD = pyFAST.read(AD_filename) - AD_bld: either: - a filename of a AeroDyn Blade input file - an instance of FileCl, as returned by reading the file, AD_bld = pyFAST.read(AD_bld_filename) OUTPUTS: - r_gag: The radial positions of the gages, given from the blade root """ if not isinstance(AD, FASTInputFile): AD = FASTInputFile(AD) if not isinstance(AD_bld, FASTInputFile): AD_bld = FASTInputFile(AD_bld) #print(AD_bld.keys()) nOuts = AD['NBlOuts'] if nOuts <= 0: if chordOut: return np.array([]), np.array([]) else: return np.array([]) INodes = np.array(AD['BlOutNd'][:nOuts]) r_gag = AD_bld['BldAeroNodes'][INodes - 1, 0] if chordOut: chord_gag = AD_bld['BldAeroNodes'][INodes - 1, 5] return r_gag, chord_gag else: return r_gag
def ED_TwrStations(ED): """ Returns ElastoDyn Tower Station positions, useful to know where the outputs are. INPUTS: - ED: either: - a filename of a ElastoDyn input file - an instance of FileCl, as returned by reading the file, ED = pyFAST.read(ED_filename) OUTPUTS: - r_fract: fraction of the towet length were stations are defined - h_nodes: height from the *ground* of the stations (not from the Tower base) """ if not isinstance(ED, FASTInputFile): ED = FASTInputFile(ED) nTwrNodes = ED['TwrNodes'] twr_fract = np.arange(1. / nTwrNodes / 2., 1, 1. / nTwrNodes) h_nodes = twr_fract * (ED['TowerHt'] - ED['TowerBsHt']) + ED['TowerBsHt'] return twr_fract, h_nodes
def ED_BldStations(ED): """ Returns ElastoDyn Blade Station positions, useful to know where the outputs are. INPUTS: - ED: either: - a filename of a ElastoDyn input file - an instance of FileCl, as returned by reading the file, ED = pyFAST.read(ED_filename) OUTUPTS: - bld_fract: fraction of the blade length were stations are defined - r_nodes: spanwise position from the rotor apex of the Blade stations """ if not isinstance(ED, FASTInputFile): ED = FASTInputFile(ED) nBldNodes = ED['BldNodes'] bld_fract = np.arange(1. / nBldNodes / 2., 1, 1. / nBldNodes) r_nodes = bld_fract * (ED['TipRad'] - ED['HubRad']) + ED['HubRad'] return bld_fract, r_nodes
def test_001_read_all(self, DEBUG=True): nError = 0 for f in glob.glob(os.path.join(MyDir, 'FASTIn*.*')): if os.path.splitext(f)[-1] in ['.py', '.pyc' ] or f.find('_TMP') > 0: continue try: obj = FASTInputFile(f) s = type(obj).__name__.replace('file', '')[:20] if DEBUG: print('[ OK ] {:30s}\t{:20s}'.format( os.path.basename(f)[:30], s)) except: nError += 1 if DEBUG: print('[FAIL] {:30s}\tException occurred'.format( os.path.basename(f)[:30])) raise if nError > 0: raise Exception('Some tests failed')
def ED_TwrGag(ED): """ Returns the heights of ElastoDyn blade gages INPUTS: - ED: either: - a filename of a ElastoDyn input file - an instance of FileCl, as returned by reading the file, ED = pyFAST.read(ED_filename) OUTPUTS: - h_gag: The heights of the gages, given from the ground height (tower base + TowerBsHt) """ if not isinstance(ED, FASTInputFile): ED = FASTInputFile(ED) _, h_nodes = ED_TwrStations(ED) nOuts = ED['NTwGages'] if nOuts <= 0: return np.array([]) if type(ED['TwrGagNd']) is list: Inodes = np.asarray(ED['TwrGagNd']) else: Inodes = np.array([ED['TwrGagNd']]) h_gag = h_nodes[Inodes[:nOuts] - 1] return h_gag
def ED_BldGag(ED): """ Returns the radial position of ElastoDyn blade gages INPUTS: - ED: either: - a filename of a ElastoDyn input file - an instance of FileCl, as returned by reading the file, ED = pyFAST.read(ED_filename) OUTPUTS: - r_gag: The radial positions of the gages, given from the rotor apex """ if not isinstance(ED, FASTInputFile): ED = FASTInputFile(ED) _, r_nodes = ED_BldStations(ED) nOuts = ED['NBlGages'] if nOuts <= 0: return np.array([]), np.array([]) if type(ED['BldGagNd']) is list: Inodes = np.asarray(ED['BldGagNd']) else: Inodes = np.array([ED['BldGagNd']]) r_gag = r_nodes[Inodes[:nOuts] - 1] return r_gag, Inodes
def AD14_BldGag(AD): """ Returns the radial position of AeroDyn 14 blade gages (based on "print" in column 6) INPUTS: - AD: either: - a filename of a AeroDyn input file - an instance of FileCl, as returned by reading the file, AD = pyFAST.read(AD_filename) OUTPUTS: - r_gag: The radial positions of the gages, given from the blade root """ if not isinstance(AD, FASTInputFile): AD = FASTInputFile(AD) Nodes = AD['BldAeroNodes'] if Nodes.shape[1] == 6: doPrint = np.array([n.lower().find('p') == 0 for n in Nodes[:, 5]]) else: doPrint = np.array([True for n in Nodes[:, 0]]) r_gag = Nodes[doPrint, 0].astype(float) IR = np.arange(1, len(Nodes) + 1)[doPrint] return r_gag, IR
def BD_BldGag(BD): """ Returns the radial position of BeamDyn blade gages INPUTS: - BD: either: - a filename of a BeamDyn input file - an instance of FileCl, as returned by reading the file, BD = pyFAST.read(BD_filename) OUTPUTS: - r_gag: The radial positions of the gages, given from the rotor apex """ if not isinstance(BD, FASTInputFile): BD = FASTInputFile(BD) M = BD['MemberGeom'] r_nodes = M[:, 2] # NOTE: we select the z axis here, and we don't take curvilenear coord nOuts = BD['NNodeOuts'] if nOuts <= 0: nOuts = 0 if type(BD['OutNd']) is list: Inodes = np.asarray(BD['OutNd']) else: Inodes = np.array([BD['OutNd']]) r_gag = r_nodes[Inodes[:nOuts] - 1] return r_gag, Inodes, r_nodes
def CPCT_LambdaPitch( refdir, main_fastfile, Lambda=None, Pitch=np.linspace(-10, 40, 5), WS=None, Omega=None, # operating conditions TMax=20, bStiff=True, bNoGen=True, bSteadyAero=True, # simulation options reRun=True, fastExe=None, showOutputs=True, nCores=4): # execution options """ Computes CP and CT as function of tip speed ratio (lambda) and pitch. There are two main ways to define the inputs: - Option 1: provide Lambda and Pitch (deg) - Option 2: provide WS (m/s), Omega (in rpm) and Pitch (deg), in which case len(WS)==len(Omega) """ WS_default = 5 # If user does not provide a wind speed vector, wind speed used # if the user provided a full path to the main file, we scrap the directory. TODO, should be cleaner if len(os.path.dirname(main_fastfile)) > 0: main_fastfile = os.path.basename(main_fastfile) # --- Reading main fast file to get rotor radius fst = FASTInputFile(os.path.join(refdir, main_fastfile)) ed = FASTInputFile(os.path.join(refdir, fst['EDFile'].replace('"', ''))) R = ed['TipRad'] # --- Making sure we have if (Omega is not None): if (Lambda is not None): WS = np.ones(Omega.shape) * WS_default elif (WS is not None): if len(WS) != len(Omega): raise Exception( 'When providing Omega and WS, both vectors should have the same dimension' ) else: WS = np.ones(Omega.shape) * WS_default else: Omega = WS_default * Lambda / R * 60 / ( 2 * np.pi) # TODO, use more realistic combinations of WS and Omega WS = np.ones(Omega.shape) * WS_default # --- Defining flat vectors of operating conditions WS_flat = [] RPM_flat = [] Pitch_flat = [] for pitch in Pitch: for (rpm, ws) in zip(Omega, WS): WS_flat.append(ws) RPM_flat.append(rpm) Pitch_flat.append(pitch) # --- Setting up default options baseDict = { 'FAST|TMax': TMax, 'FAST|DT': 0.01, 'FAST|DT_Out': 0.1 } # NOTE: Tmax should be at least 2pi/Omega baseDict = paramsNoController(baseDict) if bStiff: baseDict = paramsStiff(baseDict) if bNoGen: baseDict = paramsNoGen(baseDict) if bSteadyAero: baseDict = paramsSteadyAero(baseDict) # --- Creating set of parameters to be changed # TODO: verify that RtAeroCp and RtAeroCt are present in AeroDyn outlist PARAMS = paramsWS_RPM_Pitch(WS_flat, RPM_flat, Pitch_flat, baseDict=baseDict, FlatInputs=True) # --- Generating all files in a workDir workDir = refdir.strip('/').strip('\\') + '_CPLambdaPitch' print('>>> Generating inputs files in {}'.format(workDir)) RemoveAllowed = reRun # If the user want to rerun, we can remove, otherwise we keep existing simulations fastFiles = templateReplace(PARAMS, refdir, workDir=workDir, RemoveRefSubFiles=True, RemoveAllowed=RemoveAllowed, main_file=main_fastfile) # --- Running fast simulations print('>>> Running {} simulations...'.format(len(fastFiles))) run_fastfiles(fastFiles, showOutputs=showOutputs, fastExe=fastExe, nCores=nCores, reRun=reRun) # --- Postpro - Computing averages at the end of the simluation print('>>> Postprocessing...') outFiles = [os.path.splitext(f)[0] + '.outb' for f in fastFiles] # outFiles = glob.glob(os.path.join(workDir,'*.outb')) ColKeepStats = [ 'RotSpeed_[rpm]', 'BldPitch1_[deg]', 'RtAeroCp_[-]', 'RtAeroCt_[-]', 'Wind1VelX_[m/s]' ] result = averagePostPro(outFiles, avgMethod='periods', avgParam=1, ColKeep=ColKeepStats, ColSort='RotSpeed_[rpm]') # print(result) # --- Adding lambda, sorting and keeping only few columns result['lambda_[-]'] = result[ 'RotSpeed_[rpm]'] * R * 2 * np.pi / 60 / result['Wind1VelX_[m/s]'] result.sort_values(['lambda_[-]', 'BldPitch1_[deg]'], ascending=[True, True], inplace=True) ColKeepFinal = [ 'lambda_[-]', 'BldPitch1_[deg]', 'RtAeroCp_[-]', 'RtAeroCt_[-]' ] result = result[ColKeepFinal] print('>>> Done') # --- Converting to a matrices CP = result['RtAeroCp_[-]'].values CT = result['RtAeroCt_[-]'].values MCP = CP.reshape((len(Lambda), len(Pitch))) MCT = CT.reshape((len(Lambda), len(Pitch))) LAMBDA, PITCH = np.meshgrid(Lambda, Pitch) # --- CP max i, j = np.unravel_index(MCP.argmax(), MCP.shape) MaxVal = { 'CP_max': MCP[i, j], 'lambda_opt': LAMBDA[j, i], 'pitch_opt': PITCH[j, i] } return MCP, MCT, Lambda, Pitch, MaxVal, result
def replaceRecurse(templatename_or_newname, FileKey, ParamKey, ParamValue, Files, strID, workDir, TemplateFiles): """ FileKey: a single key defining which file we are currently modifying e.g. :'AeroFile', 'EDFile','FVWInputFileName' ParamKey: the address key of the parameter to be changed, relative to the current FileKey e.g. 'EDFile|IntMethod' (if FileKey is '') 'IntMethod' (if FileKey is 'EDFile') ParamValue: the value to be used Files: dict of files, as returned by weio, keys are "FileKeys" """ # --- Special handling for the root if FileKey == '': FileKey = 'Root' # --- Open (or get if already open) file where a parameter needs to be changed if FileKey in Files.keys(): # The file was already opened, it's stored f = Files[FileKey] newfilename_full = f.filename newfilename = os.path.relpath(newfilename_full, workDir).replace('\\', '/') else: templatefilename = templatename_or_newname templatefilename_full = os.path.join(workDir, templatefilename) TemplateFiles.append(templatefilename_full) if FileKey == 'Root': # Root files, we start from strID ext = os.path.splitext(templatefilename)[-1] newfilename_full = os.path.join(wd, strID + ext) newfilename = strID + ext else: newfilename, newfilename_full = rebaseFileName( templatefilename, workDir, strID) #print('--------------------------------------------------------------') #print('TemplateFile :', templatefilename) #print('TemplateFileFull:', templatefilename_full) #print('NewFile :', newfilename) #print('NewFileFull :', newfilename_full) shutil.copyfile(templatefilename_full, newfilename_full) f = FASTInputFile( newfilename_full) # open the template file for that filekey Files[FileKey] = f # store it # --- Changing parameters in that file NewFileKey_or_Key, ChildrenKeys = splitAddress(ParamKey) if len(ChildrenKeys) == 0: # A simple parameter is changed Key = NewFileKey_or_Key #print('Setting', FileKey, '|',Key, 'to',ParamValue) if Key == 'OutList': OutList = f[Key] f[Key] = addToOutlist(OutList, ParamValue) else: f[Key] = ParamValue else: # Parameters needs to be changed in subfiles (children) NewFileKey = NewFileKey_or_Key ChildrenKey = '|'.join(ChildrenKeys) child_templatefilename = f[NewFileKey].strip( '"') # old filename that will be used as a template baseparent = os.path.dirname(newfilename) #print('Child templatefilename:',child_templatefilename) #print('Parent base dir :',baseparent) workDir = os.path.join(workDir, baseparent) # newchildFilename, Files = replaceRecurse(child_templatefilename, NewFileKey, ChildrenKey, ParamValue, Files, strID, workDir, TemplateFiles) #print('Setting', FileKey, '|',NewFileKey, 'to',newchildFilename) f[NewFileKey] = '"' + newchildFilename + '"' return newfilename, Files
def writeLinearizationFiles(main_fst, workDir, operatingPointsFile, nPerPeriod=36, baseDict=None, tStart=100, LinInputs=0, LinOutputs=0): """ Write FAST inputs files for linearization, to a given directory `workDir`. INPUTS: - main_fst: path to an existing .fst file This file (and the ones it refers to) will be used as templates. Values of the templates can be modified using `baseDict`. The parent directory of the main fst file will be copied to `workDir`. - workDir: directory (will be created) where the simulation files will be generated - operatingPointsFile: csv file containing operating conditions - nPerPeriod : number of linearization points per rotation (usually 12 or 36) - baseDict : a dictionary of inputs files keys to be applied to all simulations Ignored if not provided. e.g. baseDict={'DT':0.01, 'EDFile|ShftTilt':-5, 'InflowFile|PLexp':0.0} see templateReplaceGeneral. - tStart: time at which the linearization will start. When triming option is not available, this needs to be sufficiently large for the rotor to reach an equilibrium - LinInputs: linearize wrt. inputs (see OpenFAST documentation). {0,1,2, default:1} - LinOutputs: linearize wrt. outputs (see OpenFAST documentation). {0,1, default:0} OUTPUTS: - list of fst files created """ # --- Optional values if baseDict is None: baseDict = dict() # --- Checking main fst file fst = FASTInputFile(main_fst) hasTrim = 'TrimCase' in fst.keys() if fst['CompServo'] == 1: print('[WARN] For now linearization is done without controller.') baseDict['CompServo'] = 0 # --- Reading operating points OP = readOperatingPoints(operatingPointsFile) # --- Generating list of parameters that vary based on the operating conditions provided PARAMS = [] for i, op in OP.iterrows(): # Extract operating conditions (TODO, handling of missing fields) ws = op['WindSpeed_[m/s]'] rpm = op['RotorSpeed_[rpm]'] pitch = op['PitchAngle_[deg]'] filename = op['Filename_[-]'] # TODO gen trq or Tower top displacement # Determine linearization times based on RPM and nPerPeriod Omega = rpm / 60 * 2 * np.pi if abs(Omega) < 0.001: LinTimes = [tStart] Tmax = tStart + 1 else: T = 2 * np.pi / Omega LinTimes = np.linspace(tStart, tStart + T, nPerPeriod + 1)[:-1] Tmax = tStart + 1.01 * T # --- Creating "linDict", dictionary of changes to fast input files for linearization linDict = dict() linDict['__name__'] = os.path.splitext(filename)[0] # --- Main fst options linDict['TMax'] = Tmax linDict['TStart'] = 0 if abs(ws) < 0.001: linDict['CompAero'] = 0 linDict['CompInflow'] = 0 # --- Linearization options linDict['Linearize'] = True linDict['NLinTimes'] = len(LinTimes) linDict['LinTimes'] = list(LinTimes) linDict['OutFmt'] = '"ES20.12E3"' # Important for decent resolution linDict[ 'LinInputs'] = LinInputs # 0: none, 1: standard, 2: to get full linearizations linDict['LinOutputs'] = LinOutputs # 0: none, 1: based on outlist # --- New Linearization options # TrimCase - Controller parameter to be trimmed {1:yaw; 2:torque; 3:pitch} [used only when CalcSteady=True] # TrimTol - Tolerance for the rotational speed convergence [>eps] [used only when CalcSteady=True] # TrimGain - Proportional gain for the rotational speed error (rad/(rad/s) or Nm/(rad/s)) [>0] [used only when CalcSteady=True] # Twr_Kdmp - Damping factor for the tower (N/(m/s)) [>=0] [used only when CalcSteady=True] # Bld_Kdmp - Damping factor for the blade (N/(m/s)) [>=0] [used only when CalcSteady=True] # --- Mode shape vizualization options if hasTrim: linDict['WrVTK'] = 3 linDict['VTK_type'] = 1 linDict['VTK_fields'] = True linDict['VTK_fps'] = 30 else: linDict['WrVTK'] = 0 # --- Aero options linDict['AeroFile|WakeMod'] = 1 # Needed for linearization linDict['AeroFile|AFAeroMod'] = 1 # Needed for linearization linDict['AeroFile|FrozenWake'] = True # Needed for linearization # --- Inflow options linDict['InflowFile|WindType'] = 1 linDict['InflowFile|HWindSpeed'] = ws # --- ElastoDyn options linDict['EDFile|BlPitch(1)'] = pitch linDict['EDFile|BlPitch(2)'] = pitch linDict['EDFile|BlPitch(3)'] = pitch linDict['EDFile|RotSpeed'] = rpm #linDict['EDFile|TTDspFA'] = tt # --- Servo options # --- Merging linDict dictionary with user override inputs for k, v in baseDict.items(): if k in linDict and v != linDict[k]: print('Overriding key {} with value {} (previous value {})'. format(k, v, linDict[k])) linDict[k] = v PARAMS.append(linDict) # --- Generating all files in a workDir refDir = os.path.dirname(main_fst) main_file = os.path.basename(main_fst) fastfiles = templateReplace(PARAMS, refDir, outputDir=workDir, removeRefSubFiles=True, main_file=main_file) return fastfiles
def test_FASTIn(self): F = FASTInputFile(os.path.join(MyDir, 'FASTIn_BD.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['PitchK'], 2.0e+07) self.assertEqual(F['MemberGeom'][-1, 2], 61.5) self.assertEqual(F['MemberGeom'][-2, 3], 0.023000) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_BD_bld.dat')) F.test_ascii(bCompareWritesOnly=False, bDelete=True) self.assertEqual(F['DampingCoeffs'][0][0], 0.01) # TODO BeamDyn Blade properties are not really "user friendly" self.assertEqual(F['BeamProperties']['span'][1], 1.0) self.assertEqual(F['BeamProperties']['K'][1][0, 0], 1.8e+08) # K11 @ section 2 self.assertEqual(F['BeamProperties']['M'][1][0, 0], 1.2) # M11 @ section 2 F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ED.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['RotSpeed'], 0.2) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ED_bld.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['BldEdgSh(6)'], -0.6952) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ED_twr.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['AdjFASt'], 1) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_AD15.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertTrue(F['TipLoss']) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ExtPtfm_SubSef.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['StiffnessMatrix'][2, 2], 1.96653266e+09) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_HD.dat')) #F.test_ascii(bCompareWritesOnly=True,bDelete=True) # TODO self.assertEqual(F['RdtnDT'], 0.0125) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_IF_NoHead.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['Z0'], 0.03) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_SbD.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['Joints'][0, 3], -100) self.assertEqual(int(F['Members'][0, 1]), 1) self.assertEqual(int(F['Members'][0, 2]), 2) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_SD.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['PitManRat(1)'], 2) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_MD.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(float(F['LineTypes'][0, 1]), 0.02)
def test_FASTIn(self): F = FASTInputFile(os.path.join(MyDir, 'FASTIn_BD.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['PitchK'], 2.0e+07) self.assertEqual(F['MemberGeom'][-1, 2], 61.5) self.assertEqual(F['MemberGeom'][-2, 3], 0.023000) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ED.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['RotSpeed'], 0.2) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ED_bld.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['BldEdgSh(6)'], -0.6952) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ED_twr.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['AdjFASt'], 1) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_AD15.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertTrue(F['TipLoss']) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_ExtPtfm_SubSef.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['StiffnessMatrix'][2, 2], 1.96653266e+09) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_HD.dat')) #F.test_ascii(bCompareWritesOnly=True,bDelete=True) # TODO self.assertEqual(F['RdtnDT'], 0.0125) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_IF_NoHead.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['Z0'], 0.03) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_SbD.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['Joints'][0, 3], -100) self.assertEqual(int(F['Members'][0, 1]), 1) self.assertEqual(int(F['Members'][0, 2]), 2) F = FASTInputFile(os.path.join(MyDir, 'FASTIn_SD.dat')) F.test_ascii(bCompareWritesOnly=True, bDelete=True) self.assertEqual(F['PitManRat(1)'], 2)