diff --git a/Scripts/mapSpecAngleScan_v4.2.py b/Scripts/mapSpecAngleScan_v4.2.py new file mode 100644 index 0000000..065c4b3 --- /dev/null +++ b/Scripts/mapSpecAngleScan_v4.2.py @@ -0,0 +1,382 @@ +''' + +Script to rebuild reciprocal space map +Try to handle the mapping data from the specfile as it is taken. +Check the file as it goes, analysis the data + + Copyright (c) 2017, UChicago Argonne, LLC + See LICENSE file. + + Modified from earlier version and Mike's code -- ZZ 2022/10/07 + + And try to move the inputs to the external json file: config.json. + ========== + User parameters are loaded from the rsmconfig.json file, preferably in the + same folder as this script. If not, the full path and filename need to be + given as an argument, e.g. : + python mapSpecAngleScan_v2.py path/to/rsmconfig.json + ========== + + +Script to process a dataset using the Sector33SpecDataSource + +Change list: + + 2022/10/12 (ZZ): + - consolidated earlier version and generated v3 (input in the py file) + and v4 (use external json file for input). + + 2022/10/14 (ZZ): + - try to check the scan is done or not on realtime option with the + EPICS PV flag. Just implemented at 33-ID-D. + + 2024/01/03 (ZZ): + - add the grid range in the json file as entry "grid_range", in + the format of [h_min, h_max, k_min, k_max, l_min, l_max] + - accordingly, modify the code here to use the range. + - if it is value "null", use the default full range. + +To DO: + + - SPEC file reindex check file time stamp? No need to redo if not changed. + +''' + +import os +import numpy as np +import sys +import datetime +import time +import json +import argparse +from pathlib import Path +from spec2nexus import spec + +from rsMap3D.datasource.Sector33SpecDataSource import Sector33SpecDataSource +from rsMap3D.datasource.DetectorGeometryForXrayutilitiesReader import DetectorGeometryForXrayutilitiesReader as detReader +from rsMap3D.utils.srange import srange +from rsMap3D.config.rsmap3dconfigparser import RSMap3DConfigParser +from rsMap3D.constants import ENERGY_WAVELENGTH_CONVERT_FACTOR +from rsMap3D.mappers.gridmapper import QGridMapper +from rsMap3D.gui.rsm3dcommonstrings import BINARY_OUTPUT +from rsMap3D.transforms.unitytransform3d import UnityTransform3D +from rsMap3D.mappers.output.vtigridwriter import VTIGridWriter +from epics import PV + +def updateDataSourceProgress(value1, value2): + logger.info("\t\tDataLoading Progress -- Current set: %.3f%%/%s%%" % (value1, value2)) + +def updateMapperProgress(value1): + logger.info("\t\tMapper Progress -- Current volume: %.3f%%" % (value1)) + +def reindex_specfile(fullFilename): + logger.info('=============================') + logger.info(' Re-indexing the SPEC file...') + spec_data = spec.SpecDataFile(fullFilename) + scansInFile_list = spec_data.getScanNumbers() + logger.info(' Done. The lastest scan # is %s' % scansInFile_list[-1]) + return scansInFile_list, spec_data + +def parseArgs(): + parser = argparse.ArgumentParser(description='Default MPI run script. Expects a json config file pointed at the data.') + parser.add_argument('configPath', + nargs='?', + default=os.path.join(os.getcwd(), 'rsmconfig.json'), + help='Path to config file. If none supplied, directs to a config.json located in CWD') + return parser.parse_args() + +def generateScanLists(inputScanList): + # How many conditions/cycles in total + num_cycles = inputScanList["cycles"] + # Total number of scans at one condition, say each temperature + scans_in_1_cycle = inputScanList["scans_per_cycle"] + SetsOfRSM = inputScanList["rsm_sets"] + + scanListTop = [] + for i in range(0, num_cycles): + for oneSetRSM in SetsOfRSM: + scan_s = oneSetRSM["start"] + scan_e = oneSetRSM["end"] + scans_in_1_rsm = scan_e - scan_s + 1 + scanListTop = scanListTop + \ + ( [[f"{x}" if scans_in_1_rsm==1 else f"{x}-{x+scans_in_1_rsm-1}"] for \ + x in range(scan_s+i*scans_in_1_cycle, scan_e+i*scans_in_1_cycle+1, scans_in_1_rsm)] ) + return scanListTop + +def _is_scan_done(specfile_full, curr_scan=1): + specfile_pv = PV("33idSIS:spec:SPECFileName") + scann_pv = PV("33idSIS:spec:SCANNUM") + scanDone_pv = PV("33idSIS:spec:ISSCANDONE") + + # Check if PVs are there. If one cant be reached, stop there. + if (not scann_pv.wait_for_connection(timeout=2)) \ + or (not scanDone_pv.wait_for_connection(timeout=2)) \ + or (not specfile_pv.wait_for_connection(timeout=2)): + return -1 + + # If it is not the current file, this one is irrelavent. + if specfile_full != specfile_pv.char_value: + return 1 + # If scan number is lower than the current, go ahead + if (curr_scan < scann_pv.value): + return 1 + elif (curr_scan == scann_pv.value): + if scanDone_pv.value == 1: + return 1 + else: + return 0 + else: + return 0 + +def checkGridRange(gridRange, fullRange): + if (gridRange is None): + tmp = fullRange + elif (len(gridRange) != 6): + tmp = fullRange + else: + h_min = max(fullRange[0], min(gridRange[0], gridRange[1])) + h_max = min(fullRange[1], max(gridRange[0], gridRange[1])) + k_min = max(fullRange[2], min(gridRange[2], gridRange[3])) + k_max = min(fullRange[3], max(gridRange[2], gridRange[3])) + l_min = max(fullRange[4], min(gridRange[4], gridRange[5])) + l_max = min(fullRange[5], max(gridRange[4], gridRange[5])) + tmp = h_min, h_max, k_min, k_max, l_min, l_max + return tmp + +# Rocord the starting run time +startTime = datetime.datetime.now() +with open('time.log', 'a') as time_log: + time_log.write(f'Start: {startTime}\n') + +#================================================ +# try use the logger to do console display +import logging + +# create root_logger +root_logger = logging.getLogger() +root_logger.setLevel(logging.INFO) + +# create console handler and set level to debug +ch = logging.StreamHandler() +# create formatter +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +# add formatter to ch +ch.setFormatter(formatter) +# add ch to logger +root_logger.addHandler(ch) + +# create logger +logger = logging.getLogger('mapSpecAngleScan_RSM3D') +logger.setLevel(logging.INFO) +#================================================ + +#$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ +# Get the input from the input file +args = parseArgs() +with open(args.configPath, 'r') as config_f: + config = json.load(config_f) + +# workpath and config file path +projectDir = config["project_dir"] +configDir = config["config_dir"] + +if configDir == None: + configDir = projectDir + +# Config files +detectorConfigName = os.path.join(configDir, config["detector_config"]) +instConfigName = os.path.join(configDir, config["instrument_config"]) +badPixelFile = os.path.join(configDir, config["badpixel_file"]) +flatfieldFile = config["flat_field"] + +# Detector settngs +detectorName = config["detector_name"] +bin = config["binning"] +roi = config["roi_setting"] +# Output grid number +nx = config["nx"] +ny = config["ny"] +nz = config["nz"] +# Output grid range +gridRange_input = config["grid_range"] +# Output selction +mapHKL = config["use_HKL"] +# Do realtime or not +realtime_flag = config["real_time"] + +datasets = config["datasets"] +one_scan_time = config["maxTime_1Scan"] + +#$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ + +##################################### +# checking for validity of the inputs +if not os.path.exists(detectorConfigName): + raise Exception("Detector Config file does not exist: %s" % + detectorConfigName) +if not os.path.exists(instConfigName): + raise Exception("Instrument Config file does not exist: %s" % + instConfigName) +if not os.path.exists(badPixelFile): + raise Exception("Bad Pixel file does not exist: %s" % + badPixelFile) +if not (flatfieldFile is None): + flatfieldFile = os.path.join(configDir, flatfieldFile) + if not os.path.exists(flatfieldFile): + raise Exception("Flat field file does not exist: %s" % + flatfieldFile) + +# If no ROI set, get info from the config +if roi is None: + detector = dReader.getDetectorById(detectorName) + nPixels = dReader.getNpixels(detector) + roi = [1, nPixels[0], 1, nPixels[1]] +logger.info("ROI: %s " % roi) +# End checking for inputs +##################################### + +dReader = detReader(detectorConfigName) + +#===================================== +# Outer-most loop, iterate over different SPEC files +for idx, dataset in enumerate(datasets, 1): + # SPEC file name and scan numbers + specFile = dataset["spec_file"] + scanListTop = dataset["scan_list"] + + if scanListTop == None: + inputScanList = dataset["scan_range"] + scanListTop = generateScanLists(inputScanList) + + specfile_full = os.path.join(projectDir, specFile) + # Check if the SPEC file exists. Quit if not. + if not os.path.exists( specfile_full ): + print(f"File not found. Please check the path and name {specFile} is correct.") + print(f"Moving on...in a couple of seconds. ") + time.sleep(2) + continue + + specName, specExt = os.path.splitext(specFile) + # generate the output file folder now + outputFilePath = os.path.join(projectDir, \ + "analysis_runtime", specName) + Path(outputFilePath).mkdir(parents=True, exist_ok=True) + + logger.info('=============================') + logger.info(f"SPEC file #{idx}: {specfile_full}") + logger.info(f"Generated Scan List #{idx}: {scanListTop}") + + # (Re)index the SPEC file + scansInFile_list, spec_data = reindex_specfile(specfile_full) + + for scanList1 in scanListTop: + outputFileName = os.path.join(outputFilePath, \ + f"{specName}_{str(scanList1[0])}") + + if mapHKL == True: + outputFileName += '_hkl.vti' + else: + outputFileName += '_Qxyz.vti' + + appConfig = RSMap3DConfigParser() + maxImageMemory = appConfig.getMaxImageMemory() + + scanRange = [] + for scans in scanList1: + scanRange += srange(scans).list() + logger.info(f" --------------------------------") + logger.info("scanRange %s" % scanRange) + #logger.info("specName, specExt: %s, %s" % (specName, specExt)) + + #======================= + # Find the largest scan number in this set + curr_scan = max(scanRange) + # waiting time factor + _accu = 1 + last_len = 0 + while (realtime_flag): + # add my local way to check if scan is done + _scanDone_flag = _is_scan_done(specfile_full, curr_scan) + if _scanDone_flag == 1: + break + elif _scanDone_flag == 0: + sleep_time = 5 + else: + # check if the largest scan number is available yet. + if not (str(curr_scan) in scansInFile_list): + sleep_time = _accu*5 + logger.info(' Scan #%d not available yet. \ + Wait %d seconds' % (curr_scan, sleep_time)) + elif scansInFile_list.index(str(curr_scan)) == (len(scansInFile_list) - 1): + # Try to figure out the scan is done or not -- no good way as I see it. + scan = spec_data.getScan(curr_scan) + #scan.interpret() + if(len(scan.data)>0): + curr_len = len(scan.data[scan.L[0]]) + else: + curr_len = 0 + logger.info(' Data points current in the scan #%d is %d' \ + % (curr_scan, curr_len) ) + if(curr_len == 0): + pass + elif(curr_len != last_len): + last_len = curr_len + else: + if(_accu>4): + break + sleep_time = 5*_accu + logger.info(' Scan #%d may not be finished. Wait %d seconds...' \ + % (curr_scan, sleep_time) ) + else: + logger.info('\t--------------------------------') + logger.info(' Scan #%d ready.\n' % curr_scan) + break + _accu += 1 + + scansInFile_list, spec_data = \ + reindex_specfile(specfile_full) + + # this is in the outer loop, when the scan does not exist yet, wait sometime before resume loops + time.sleep(sleep_time) + + ds = Sector33SpecDataSource(projectDir, specName, specExt, + instConfigName, detectorConfigName, roi=roi, + pixelsToAverage=bin, scanList= scanRange, + badPixelFile=badPixelFile, + flatFieldFile=flatfieldFile, + appConfig=appConfig) + ds.setCurrentDetector(detectorName) + ds.setProgressUpdater(updateDataSourceProgress) + ds.loadSource(mapHKL=mapHKL) + + fullRange = ds.getOverallRanges() + if gridRange_input is None: + gridRange = fullRange + else: + gridRange = checkGridRange(gridRange_input, fullRange) + + ds.setRangeBounds(gridRange) + + imageToBeUsed = ds.getImageToBeUsed() + #print("imageToBeUsed %s" % imageToBeUsed) + imageSize = np.prod(ds.getDetectorDimensions()) + logger.info(' --------------------------------') + + gridMapper = QGridMapper(ds, + outputFileName, + outputType=BINARY_OUTPUT, + nx=nx, ny=ny, nz=nz, + transform=UnityTransform3D(), + gridWriter=VTIGridWriter(), + appConfig=appConfig) + + gridMapper.setProgressUpdater(updateMapperProgress) + gridMapper.doMap() + + with open('time.log', 'a') as time_log: + endTime = datetime.datetime.now() + time_log.write(f'End: {endTime}\n') + time_log.write(f'Diff: {endTime - startTime}\n') + + logger.info(' --------------------------------') +logger.info('=============================') diff --git a/Scripts/paraviewPlot.py b/Scripts/paraviewPlot.py new file mode 100644 index 0000000..5806145 --- /dev/null +++ b/Scripts/paraviewPlot.py @@ -0,0 +1,230 @@ +#### import the simple module from the paraview +from paraview.simple import * +import numpy as np +import os +#### disable automatic camera reset on 'Show' +paraview.simple._DisableFirstRenderCameraReset() + +# create a new 'XML Image Data Reader' +#PathName = 'X:\\data\\caoyue\\20240304_NSLS_ISR\\woods\\analysis_runtime' +PathName = 'Z:\\Data\\ChoiMinju\\202404_NSLSII\\analysis_runtime' +# this is the sample name, typically SPEC filename without the suffix +SampleName = 'Continuous_Pd_film_on_STO_001' +# scan numbers exactly as they are in the .vti file name. +scans = ['27-28'] +# Is the grid based on "hkl" or "qxyz"? +grid_type = "hkl" + +# Setup some variables to use for plot slices and tile them along one main axis direction. +# this is the center of the RSM volume and where the slice would go through +slice_Origin = [0, 0, 1.9] +# normal direction of the slice +slice_normals = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] +# clipping the RSM3D volume so that we have better control of the tiling. This is the 'box' size here +clip_size = [0.4, 0.4, 0.6] + +# view from this direction in reciprocal space +view_direction= slice_normals[0] +# color scale low and high limits +color_scale_low = 1.2 +color_scale_high = 5.2 + +# Check the existence of the .vti files first; skip those that nonexist +FileNames = [] +for ScanN in scans: + # build vti file name here for each RSM volume + FileName = SampleName + f'_{ScanN}_{grid_type}.vti' + thisFileName = os.path.join(PathName, SampleName, FileName) + if os.path.isfile(thisFileName): + FileNames.append(FileName) + else: + print(f"Make sure the filename {thisFileName} is valid. ") + +_ind0 = 1 +for _ind1, FileName in enumerate(FileNames): + # build vti file name here for each RSM volume + thisFileName = os.path.join(PathName, SampleName, FileName) + vti_data = XMLImageDataReader(FileName=thisFileName) + + # get active source. + xMLImageDataReader = GetActiveSource() + + # rename source object + RenameSource(FileName, xMLImageDataReader) + + # get active view + renderView1 = GetActiveViewOrCreate('RenderView') + # uncomment following to set a specific view size + # renderView1.ViewSize = [1217, 918] + + # show data in view + vti_dataDisplay = Show(vti_data, renderView1) + # trace defaults for the display properties. + vti_dataDisplay.Representation = 'Outline' + + # reset view to fit data + renderView1.ResetCamera() + + # update the view to ensure updated data information + renderView1.Update() + + # create a new 'Calculator' + calculator1 = Calculator(Input=vti_data) + # Properties modified on calculator1 + calculator1.Function = 'log10(Scalars_)' + # show data in view + calculator1Display = Show(calculator1, renderView1) + # trace defaults for the display properties. + calculator1Display.Representation = 'Outline' + # hide data in view + Hide(vti_data, renderView1) + # hide data in view + Hide(calculator1, renderView1) + + # update the view to ensure updated data information + renderView1.Update() + + # create a new 'Clip' + clip1 = Clip(registrationName=f'Clip{_ind1+1}', Input=calculator1) + # Properties modified on clip1 + clip1.ClipType = 'Box' + # Properties modified on clip1.ClipType + clip1.ClipType.Position = list(np.array(slice_Origin) - np.array(clip_size)/2) + clip1.ClipType.Length = clip_size + # get active view + renderView1 = GetActiveViewOrCreate('RenderView') + # show data in view + clip1Display = Show(clip1, renderView1, 'UnstructuredGridRepresentation') + # get color transfer function/color map for 'Result' + resultLUT = GetColorTransferFunction('Result') + + # hide data in view + Hide(clip1, renderView1) + + # create a new 'Slice' + for slice_normal in slice_normals: + slice1 = Slice(registrationName=f'Slice{_ind0}', Input=clip1) + slice1.SliceType = 'Plane' + slice1.HyperTreeGridSlicer = 'Plane' + slice1.SliceOffsetValues = [0.0] + + # Properties modified on slice1.SliceType + slice1.SliceType.Origin = slice_Origin + slice1.SliceType.Normal = slice_normal + + # show data in view + slice1Display = Show(slice1, renderView1) + # trace defaults for the display properties. + slice1Display.Representation = 'Surface' + + # show color bar/color legend + slice1Display.SetScalarBarVisibility(renderView1, True) + + # update the view to ensure updated data information + renderView1.Update() + _ind0 += 1 + + # init the 'GridAxesRepresentation' selected for 'DataAxesGrid' + slice1Display.DataAxesGrid.GridColor = [0.0, 0.6666666666666666, 1.0] + slice1Display.DataAxesGrid.ShowTicks = 0 + slice1Display.DataAxesGrid.AxesToLabel = 0 + slice1Display.DataAxesGrid.ShowEdges = 0 + # settings for font and size for labels and titles + slice1Display.DataAxesGrid.XTitle = 'H (rlu)' + slice1Display.DataAxesGrid.YTitle = 'K (rlu)' + slice1Display.DataAxesGrid.ZTitle = 'L (rlu)' + slice1Display.DataAxesGrid.XTitleColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.XTitleFontFamily = 'Times' + slice1Display.DataAxesGrid.XTitleFontSize = 28 + slice1Display.DataAxesGrid.XLabelColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.XLabelFontFamily = 'Times' + slice1Display.DataAxesGrid.XLabelFontSize = 20 + slice1Display.DataAxesGrid.XAxisPrecision = 0 + slice1Display.DataAxesGrid.YTitleColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.YTitleFontFamily = 'Times' + slice1Display.DataAxesGrid.YTitleFontSize = 28 + slice1Display.DataAxesGrid.YLabelColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.YLabelFontFamily = 'Times' + slice1Display.DataAxesGrid.YLabelFontSize = 20 + slice1Display.DataAxesGrid.YAxisPrecision = 0 + slice1Display.DataAxesGrid.ZTitleColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.ZTitleFontFamily = 'Times' + slice1Display.DataAxesGrid.ZTitleFontSize = 28 + slice1Display.DataAxesGrid.ZLabelColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.ZLabelFontFamily = 'Times' + slice1Display.DataAxesGrid.ZLabelFontSize = 20 + slice1Display.DataAxesGrid.ZAxisPrecision = 0 + + _labelFlag = np.array([1, 2, 4]) + + # figure out what the label values would be on each axis + _tmp1 = slice_Origin[2] + _tmp2 = clip_size[2]/2 + slice1Display.DataAxesGrid.ZAxisLabels = [_tmp1-_tmp2, _tmp1, _tmp1+_tmp2] + _tmp1 = slice_Origin[0] + _tmp2 = clip_size[0]/2 + slice1Display.DataAxesGrid.XAxisLabels = [_tmp1-_tmp2, _tmp1, _tmp1+_tmp2] + _tmp1 = slice_Origin[1] + _tmp2 = clip_size[1]/2 + slice1Display.DataAxesGrid.YAxisLabels = [_tmp1-_tmp2, _tmp1, _tmp1+_tmp2] + # display all 3 lower axis side labels/titles + slice1Display.DataAxesGrid.AxesToLabel = 7 + # show axis labels + slice1Display.DataAxesGrid.GridAxesVisibility = 1 + + # show color bar/color legend + slice1Display.SetScalarBarVisibility(renderView1, True) + + # toggle 3D widget visibility (only when running from the GUI) + Hide3DWidgets(proxy=slice1.SliceType) + +# update the view to ensure updated data information +renderView1.Update() + +# Apply a preset using its name. Note this may not work as expected when presets have duplicate names. +resultLUT.ApplyPreset('Jet', True) + +#paraview.simple._DisableFirstRenderCameraReset() +# +# get active view +renderView1 = GetActiveViewOrCreate('RenderView') +# uncomment following to set a specific view size +# renderView1.ViewSize = [1217, 918] + +# reset view to fit data +renderView1.ResetCamera() + +# get color transfer function/color map for 'Result' +resultLUT = GetColorTransferFunction('Result') + +# Rescale transfer function +resultLUT.RescaleTransferFunction(color_scale_low, color_scale_high) + +# get opacity transfer function/opacity map for 'Result' +resultPWF = GetOpacityTransferFunction('Result') + +# Rescale transfer function +resultPWF.RescaleTransferFunction(color_scale_low, color_scale_high) + +# Properties modified on renderView1 +renderView1.UseColorPaletteForBackground = 0 +# Properties modified on renderView1 +renderView1.Background = [1.0, 1.0, 1.0] + +#### saving camera placements for all active views + +# current camera placement for renderView1 +renderView1.CameraPosition = list(np.array(slice_Origin) + slice_normals[0]) +renderView1.CameraFocalPoint = slice_Origin +#renderView1.CameraPosition = [0.0249742791056633, -1.6546449800997225, 3.9885722398757935] +#renderView1.CameraFocalPoint = [0.0249742791056633, 0.00796423852443695, 3.9885722398757935] +renderView1.CameraViewUp = [0.0, 0.0, 1.0] +renderView1.CameraParallelScale = 0.5 + + + + + +#### uncomment the following to render all views +# RenderAllViews() +# alternatively, if you want to write images, you can use SaveScreenshot(...). diff --git a/Scripts/paraviewPlot_tile.py b/Scripts/paraviewPlot_tile.py new file mode 100644 index 0000000..78bc70d --- /dev/null +++ b/Scripts/paraviewPlot_tile.py @@ -0,0 +1,301 @@ +#### import the simple module from the paraview +from paraview.simple import * +import numpy as np +import os +#### disable automatic camera reset on 'Show' +paraview.simple._DisableFirstRenderCameraReset() + +# create a new 'XML Image Data Reader' +#PathName = 'X:\\data\\caoyue\\20240304_NSLS_ISR\\woods\\analysis_runtime' +PathName = 'Z:\\Data\\ChoiMinju\\202404_NSLSII\\analysis_runtime' +# this is the sample name, typically SPEC filename without the suffix +SampleName_list = ['Continuous_Pd_film_on_STO_001', + ] +# scan numbers exactly as they are in the .vti file name. +#scans = ['104-106', '117-119', '129-131', '141-143', '153-155', '165-167', '177-179', '189-191', '201-203', '213-215', '331-333'] +scans_list = [['27-28', '34-35', '44-45', '49-50', '55-56', '59-60']] +# Is the grid based on "hkl" or "qxyz"? +grid_type = "hkl" + +# Setup some variables to use for plot slices and tile them along one main axis direction. +# this is the center of the RSM volume and where the slice would go through +slice_Origin = [0, 0, 1.93] +# normal direction of the slice +slice_normal = [0, 0, 1] +# tiling multiple images along this direction; and default viewing direction would be another in-plane one +slice_offSet_direction = [1, 0, 0] +# clipping the RSM3D volume so that we have better control of the tiling. This is the 'box' size here +clip_size = [0.4, 0.4, 0.6] +# use the 'box' dimension in this direction as the tiling offset +offset_unit = clip_size[slice_offSet_direction.index(1)] + +# view from this direction in reciprocal space +view_direction= slice_normal +# color scale low and high limits +color_scale_low = 1.2 +color_scale_high = 5.2 + +# Check the existence of the .vti files first +FileNames = [] +LabelNames = [] +for (scans, SampleName) in zip(scans_list, SampleName_list): + for ScanN in scans: + # build vti file name here for each RSM volume + FileName = SampleName + f'_{ScanN}_{grid_type}.vti' + thisFileName = os.path.join(PathName, SampleName, FileName) + if os.path.isfile(thisFileName): + LabelNames.append(FileName) + FileNames.append(thisFileName) + else: + print(f"Make sure the filename {thisFileName} is valid. ") + +# How many slices +num_plot = len(FileNames) +# the largest offset value +offset_max = offset_unit*int(num_plot/2) + +for _ind1, (FileName, LabelName) in enumerate(zip(FileNames, LabelNames)): + # build vti file name here for each RSM volume + #thisFileName = os.path.join(PathName, SampleName, FileName) + vti_data = XMLImageDataReader(FileName=FileName) + + # offset value for this slice + _this_offset = -offset_max + offset_unit * _ind1 + # get active source. + xMLImageDataReader = GetActiveSource() + + # rename source object + RenameSource(LabelName, xMLImageDataReader) + + # get active view + renderView1 = GetActiveViewOrCreate('RenderView') + + # create a new 'Calculator' + calculator1 = Calculator(Input=vti_data) + + # Properties modified on calculator1 + calculator1.Function = 'log10(Scalars_)' + + # hide data in view + Hide(vti_data, renderView1) + + # hide data in view + Hide(calculator1, renderView1) + + # update the view to ensure updated data information + renderView1.Update() + + # create a new 'Clip' + clip1 = Clip(registrationName=f'Clip{_ind1+1}', Input=calculator1) + + # Properties modified on clip1 + clip1.ClipType = 'Box' + + # Properties modified on clip1.ClipType + clip1.ClipType.Position = list(np.array(slice_Origin) - np.array(clip_size)/2) + clip1.ClipType.Length = clip_size + + # get active view + renderView1 = GetActiveViewOrCreate('RenderView') + + # show data in view + clip1Display = Show(clip1, renderView1, 'UnstructuredGridRepresentation') + + # get color transfer function/color map for 'Result' + resultLUT = GetColorTransferFunction('Result') + + # hide data in view + Hide(clip1, renderView1) + + # update the view to ensure updated data information + renderView1.Update() + + # reset view to fit data + renderView1.ResetCamera(False) + + # create a new 'Slice' + slice1 = Slice(registrationName=f'Slice{_ind1+1}', Input=clip1) + slice1.SliceType = 'Plane' + slice1.HyperTreeGridSlicer = 'Plane' + slice1.SliceOffsetValues = [0.0] + + # init the 'Plane' selected for 'SliceType' + slice1.SliceType.Origin = slice_Origin + slice1.SliceType.Normal = slice_normal + + # init the 'Plane' selected for 'HyperTreeGridSlicer' + slice1.HyperTreeGridSlicer.Origin = slice_Origin + + # show data in view + slice1Display = Show(slice1, renderView1, 'GeometryRepresentation') + + # trace defaults for the display properties. + slice1Display.Representation = 'Surface' + slice1Display.ColorArrayName = ['POINTS', 'Result'] + slice1Display.LookupTable = resultLUT + slice1Display.SelectTCoordArray = 'None' + slice1Display.SelectNormalArray = 'None' + slice1Display.SelectTangentArray = 'None' + slice1Display.OSPRayScaleArray = 'Result' + slice1Display.OSPRayScaleFunction = 'PiecewiseFunction' + slice1Display.SelectOrientationVectors = 'None' + slice1Display.ScaleFactor = 0.02399998903274536 + slice1Display.SelectScaleArray = 'Result' + slice1Display.GlyphType = 'Arrow' + slice1Display.GlyphTableIndexArray = 'Result' + slice1Display.GaussianRadius = 0.0011999994516372682 + slice1Display.SetScaleArray = ['POINTS', 'Result'] + slice1Display.ScaleTransferFunction = 'PiecewiseFunction' + slice1Display.OpacityArray = ['POINTS', 'Result'] + slice1Display.OpacityTransferFunction = 'PiecewiseFunction' + slice1Display.DataAxesGrid = 'GridAxesRepresentation' + slice1Display.PolarAxes = 'PolarAxesRepresentation' + + # init the 'PiecewiseFunction' selected for 'ScaleTransferFunction' + slice1Display.ScaleTransferFunction.Points = [0.0, 0.0, 0.5, 0.0, 8.617164190771732, 1.0, 0.5, 0.0] + + # init the 'PiecewiseFunction' selected for 'OpacityTransferFunction' + slice1Display.OpacityTransferFunction.Points = [0.0, 0.0, 0.5, 0.0, 8.617164190771732, 1.0, 0.5, 0.0] + + # init the 'GridAxesRepresentation' selected for 'DataAxesGrid' + slice1Display.DataAxesGrid.GridColor = [0.0, 0.6666666666666666, 1.0] + slice1Display.DataAxesGrid.ShowTicks = 0 + slice1Display.DataAxesGrid.AxesToLabel = 0 + slice1Display.DataAxesGrid.ShowEdges = 0 + # settings for font and size for labels and titles + slice1Display.DataAxesGrid.XTitle = 'H (rlu)' + slice1Display.DataAxesGrid.YTitle = 'K (rlu)' + slice1Display.DataAxesGrid.ZTitle = 'L (rlu)' + slice1Display.DataAxesGrid.XTitleColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.XTitleFontFamily = 'Times' + slice1Display.DataAxesGrid.XTitleFontSize = 28 + slice1Display.DataAxesGrid.XLabelColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.XLabelFontFamily = 'Times' + slice1Display.DataAxesGrid.XLabelFontSize = 20 + slice1Display.DataAxesGrid.XAxisPrecision = 0 + slice1Display.DataAxesGrid.YTitleColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.YTitleFontFamily = 'Times' + slice1Display.DataAxesGrid.YTitleFontSize = 28 + slice1Display.DataAxesGrid.YLabelColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.YLabelFontFamily = 'Times' + slice1Display.DataAxesGrid.YLabelFontSize = 20 + slice1Display.DataAxesGrid.YAxisPrecision = 0 + slice1Display.DataAxesGrid.ZTitleColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.ZTitleFontFamily = 'Times' + slice1Display.DataAxesGrid.ZTitleFontSize = 28 + slice1Display.DataAxesGrid.ZLabelColor = [0.0, 0.0, 0.0] + slice1Display.DataAxesGrid.ZLabelFontFamily = 'Times' + slice1Display.DataAxesGrid.ZLabelFontSize = 20 + slice1Display.DataAxesGrid.ZAxisPrecision = 0 + + # OK, this below might not be correct, i don't quite understand + # how it works. Maybe it is not fixed? + # which axis label to display: 1 - z_min; 2 - y_min; 4 - x_min; + # 8 - z_max; 16 - y_max; 32 - x_max + #_labelFlag = np.array([4, 2, 1]) + _labelFlag = np.array([1, 2, 4]) + + # figure out what the label values would be on each axis + _tmp1 = slice_Origin[2] + _tmp2 = clip_size[2]/2 + slice1Display.DataAxesGrid.ZAxisLabels = [_tmp1-_tmp2, _tmp1, _tmp1+_tmp2] + _tmp1 = slice_Origin[0] + _tmp2 = clip_size[0]/2 + slice1Display.DataAxesGrid.XAxisLabels = [_tmp1-_tmp2, _tmp1, _tmp1+_tmp2] + _tmp1 = slice_Origin[1] + _tmp2 = clip_size[1]/2 + slice1Display.DataAxesGrid.YAxisLabels = [_tmp1-_tmp2, _tmp1, _tmp1+_tmp2] + # try to figure out which axes to label, probably not working well + if _ind1 == 0: + # label the axis-min on the lowest side of offset direction + _tmp1 = np.array(slice_offSet_direction)!=0 + _axesToLable = sum( np.multiply(_tmp1, _labelFlag) ) + slice1Display.DataAxesGrid.AxesToLabel = _axesToLable + elif _this_offset==0: + # lable the axis-min orthorgonal to both offset and slice normal direction. + _tmp1 = ( np.array(slice_offSet_direction) + np.array(slice_normal) ) == 0 + _axesToLable = sum( np.multiply(_tmp1, _labelFlag) ) + slice1Display.DataAxesGrid.AxesToLabel = _axesToLable + + # show axis labels + slice1Display.DataAxesGrid.GridAxesVisibility = 1 + + # show color bar/color legend + slice1Display.SetScalarBarVisibility(renderView1, True) + + # update the view to ensure updated data information + renderView1.Update() + + # toggle 3D widget visibility (only when running from the GUI) + Hide3DWidgets(proxy=slice1.SliceType) + + # set active source + SetActiveSource(slice1) + + # show data in view + slice1Display = Show(slice1, renderView1, 'GeometryRepresentation') + + # show color bar/color legend + slice1Display.SetScalarBarVisibility(renderView1, True) + + # Calculate how much offset this slice needs + this_slice_offset = list( np.array(slice_offSet_direction) * (_this_offset) ) + + # Properties modified on slice1Display + slice1Display.Position = this_slice_offset + + # Properties modified on slice1Display.DataAxesGrid + slice1Display.DataAxesGrid.Position = this_slice_offset + + # Properties modified on slice1Display.PolarAxes + slice1Display.PolarAxes.Translation = this_slice_offset + + # reset view to fit data + renderView1.ResetCamera(False) + + # update the view to ensure updated data information + renderView1.Update() + + +# Apply a preset using its name. Note this may not work as expected when presets have duplicate names. +resultLUT.ApplyPreset('Jet', True) + +# reset view to fit data +renderView1.ResetCamera() + +# get color transfer function/color map for 'Result' +resultLUT = GetColorTransferFunction('Result') + +# Rescale transfer function +resultLUT.RescaleTransferFunction(color_scale_low, color_scale_high) + +# Properties modified on renderView1 +renderView1.UseColorPaletteForBackground = 0 + +# Properties modified on renderView1 +renderView1.Background = [1.0, 1.0, 1.0] + +""" +# get opacity transfer function/opacity map for 'Result' +resultPWF = GetOpacityTransferFunction('Result') + +# Rescale transfer function +resultPWF.RescaleTransferFunction(color_scale_low, color_scale_high) +""" +#### saving camera placements for all active views + +# current camera placement for renderView1 +renderView1.CameraPosition = list(np.array(slice_Origin) + view_direction) +renderView1.CameraFocalPoint = slice_Origin +#renderView1.CameraPosition = [0.0249742791056633, -1.6546449800997225, 3.9885722398757935] +#renderView1.CameraFocalPoint = [0.0249742791056633, 0.00796423852443695, 3.9885722398757935] +renderView1.CameraViewUp = [0.0, 0.0, 1.0] +renderView1.CameraParallelScale = 0.25 + + + + + +#### uncomment the following to render all views +# RenderAllViews() +# alternatively, if you want to write images, you can use SaveScreenshot(...). diff --git a/Scripts/rsmconfig_v4.2_lsfo_IntegerPeaks.json b/Scripts/rsmconfig_v4.2_lsfo_IntegerPeaks.json new file mode 100644 index 0000000..5e49304 --- /dev/null +++ b/Scripts/rsmconfig_v4.2_lsfo_IntegerPeaks.json @@ -0,0 +1,32 @@ +{ + "project_dir":"C:\\work\\Users\\YCao\\LSFO\\", + "config_dir":null, + "detector_config":"NSLS_4ID_Eiger500k.xml", + "instrument_config":"NSLS_4ID_fourc.xml", + "badpixel_file":"badpixels_ISREiger500k.txt", + "flat_field":null, + "use_HKL":true, + "real_time":false, + "detector_name":"Eiger500k", + "binning":[1, 1], + "roi_setting":[10, 1020, 10, 504], + "nx":400, + "ny":400, + "nz":200, + "grid_range":null, + "maxTime_1Scan":150, + "datasets":[ + { + "spec_file":"LSFO_001_Cryo.spec", + "scan_list":[["316-318"]], + "scan_range": + { + "cycles":1, + "scans_per_cycle":30, + "rsm_sets":[ + {"start":117, "end":207} + ] + } + } + ] +} diff --git a/Scripts/rsmconfig_v4.2_lsfo_IntegerPeaks_batch.json b/Scripts/rsmconfig_v4.2_lsfo_IntegerPeaks_batch.json new file mode 100644 index 0000000..6cc8822 --- /dev/null +++ b/Scripts/rsmconfig_v4.2_lsfo_IntegerPeaks_batch.json @@ -0,0 +1,62 @@ +{ + "project_dir":"C:\\work\\Users\\YCao\\LSFO\\", + "config_dir":null, + "detector_config":"NSLS_4ID_Eiger500k.xml", + "instrument_config":"NSLS_4ID_fourc.xml", + "badpixel_file":"badpixels_ISREiger500k.txt", + "flat_field":null, + "use_HKL":true, + "real_time":false, + "detector_name":"Eiger500k", + "binning":[1, 1], + "roi_setting":[10, 1020, 10, 504], + "nx":200, + "ny":200, + "nz":200, + "grid_range":null, + "maxTime_1Scan":150, + "datasets":[ + { + "spec_file":"LSFO_001_Cryo.spec", + "scan_list":null, + "scan_range": + { + "cycles":1, + "scans_per_cycle":12, + "rsm_sets":[ + {"start":101, "end":103}, + {"start":104, "end":106}, + {"start":107, "end":109} + ] + } + }, + { + "spec_file":"LSFO_001_Cryo.spec", + "scan_list":null, + "scan_range": + { + "cycles":16, + "scans_per_cycle":12, + "rsm_sets":[ + {"start":114, "end":116}, + {"start":117, "end":119}, + {"start":120, "end":122} + ] + } + }, + { + "spec_file":"LSFO_001_Cryo.spec", + "scan_list":null, + "scan_range": + { + "cycles":3, + "scans_per_cycle":12, + "rsm_sets":[ + {"start":304, "end":306}, + {"start":307, "end":309}, + {"start":310, "end":312} + ] + } + } + ] +} diff --git a/rsMap3D/__init__.py b/rsMap3D/__init__.py index 63b9f6b..16fa8ca 100644 --- a/rsMap3D/__init__.py +++ b/rsMap3D/__init__.py @@ -1,3 +1,3 @@ # import gui # import datasource -__version__ = '1.3.0' \ No newline at end of file +__version__ = '1.3.1' diff --git a/rsMap3D/datasource/NSLSIISector4SpecDataSource.py b/rsMap3D/datasource/NSLSIISector4SpecDataSource.py new file mode 100644 index 0000000..ca29e9d --- /dev/null +++ b/rsMap3D/datasource/NSLSIISector4SpecDataSource.py @@ -0,0 +1,566 @@ +''' + Copyright (c) 2012, UChicago Argonne, LLC + See LICENSE file. +''' +import os +from spec2nexus.spec import SpecDataFile +from rsMap3D.exception.rsmap3dexception import RSMap3DException,\ + ScanDataMissingException +from rsMap3D.gui.rsm3dcommonstrings import CANCEL_STR +from rsMap3D.datasource.pilatusbadpixelfile import PilatusBadPixelFile +from rsMap3D.mappers.abstractmapper import ProcessCanceledException +from rsMap3D.datasource.specxmldrivendatasource import SpecXMLDrivenDataSource +import numpy as np +import xrayutilities as xu +import time +import sys,traceback +import logging +import importlib +import h5py +import hdf5plugin +try: + from PIL import Image +except ImportError: + import Image +logger = logging.getLogger(__name__) + + +# +++++++++++++++ +# Make changes to work with HDF5 from NSLSII 4-ID data, brutal forced +# I do need to generate folder structure simular to our Tiff case +# i.e. $(PROJECT_DIR)/images/$(SAMPLE_NAME)/Snnn/*.h5 +#IMAGE_DIR_MERGE_STR = "images/%s" +IMAGE_DIR_MERGE_STR = "hdf5s/%s" +SCAN_NUMBER_MERGE_STR = "S%03d" +TIFF_FILE_MERGE_STR = "S%%03d/%s_S%%03d_%%05d.tif" +#hdf5 filename format string +HDF5_FILE_MERGE_STR = "S%%03d/%s_%%03d_%%03d_data_000001.h5" +# +++++++++++++++ + +class NSLSIISector4SpecDataSource(SpecXMLDrivenDataSource): + ''' + Class to load data from spec file and configuration xml files from + for the way that data is collected at sector 33. + :members + ''' + + + def __init__(self, + projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs): + ''' + Constructor + :param projectDir: Directory holding the project file to open + :param projectName: First part of file name for the project + :param projectExt: File extension for the project file. + :param instConfigFile: Full path to Instrument configuration file. + :param detConfigFile: Full path to the detector configuration file + :param kwargs: Assorted keyword arguments + + :rtype: NSLSIISector4SpecDataSource + ''' + super(NSLSIISector4SpecDataSource, self).__init__(projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs) + + def _calc_eulerian_from_kappa(self, primaryAngles=None, + referenceAngles = None): + """ + Calculate the eulerian sample angles from the kappa stage angles. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + """ + + keta = np.deg2rad(referenceAngles[:,0]) + kappa = np.deg2rad(referenceAngles[:,1]) + kphi = np.deg2rad(referenceAngles[:,2]) + kappaParam = self.instConfig.getSampleAngleMappingParameter('kappa') + + try: + if kappaParam != None: + self.kalpha = np.deg2rad(float(kappaParam)) + else: + self.kalpha = np.deg2rad(50.000) + kappaInvertedParam = \ + self.instConfig.getSampleAngleMappingParameter('kappaInverted') + if kappaInvertedParam != None: + self.kappa_inverted = self.to_bool(kappaInvertedParam) + else: + self.kappa_inverted = False + except Exception as ex: + raise RSMap3DException("Error trying to get parameter for " + \ + "sampleAngleMappingFunction " + \ + "_calc_eulerian_from_kappa in inst config " + \ + "file\n" + \ + str(ex)) + + _t1 = np.arctan(np.tan(kappa / 2.0) * np.cos(self.kalpha)) + if self.kappa_inverted: + eta = np.rad2deg(keta + _t1) + phi = np.rad2deg(kphi + _t1) + else: + eta = np.rad2deg(keta - _t1) + phi = np.rad2deg(kphi - _t1) + chi = 2.0 * np.rad2deg(np.arcsin(np.sin(kappa / 2.0) * \ + np.sin(self.kalpha))) + + return eta, chi, phi + + def _calc_replace_angle_values(self , primaryAngles=None, + referenceAngles=None): + ''' + Fix a situation were some constant angle values have been + recorded incorrectly and need to be fixed. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + ''' + logger.info( "Running " + __name__) + angles = [] + logger.debug("referenceAngles " + str(referenceAngles)) + mappingAngles = self.instConfig.getSampleAngleMappingReferenceAngles() + + logger.debug( "mappingAngles" + str(mappingAngles)) + for ii in range(len(referenceAngles)): + replaceVal = \ + float(self.instConfig.getSampleAngleMappingReferenceAngleAttrib( \ + number= str(mappingAngles[ii]), \ + attribName='replaceValue')) + logger.debug( "primary Angles" + str(referenceAngles)) + angles.append(replaceVal* np.ones(len(referenceAngles[:,ii]),)) + logger.debug("Angles" + str( angles)) + return angles + + def fixGeoAngles(self, scan, angles): + ''' + Fix the angles using a user selected function. + :param scan: scan to set the angles for + :param angles: Array of angles to set for this scan + ''' + logger.debug( "starting " + __name__) + needToCorrect = False + refAngleNames = self.instConfig.getSampleAngleMappingReferenceAngles() + for refAngleName in refAngleNames: + alwaysFix = self.instConfig.getSampleAngleMappingAlwaysFix() + if refAngleName in scan.L or alwaysFix: + needToCorrect = True + + if needToCorrect: + logger.debug( __name__ + ": Fixing angles") + refAngles = self.getScanAngles(scan, refAngleNames) + primaryAngles = self.instConfig.getSampleAngleMappingPrimaryAngles() + functionName = self.instConfig.getSampleAngleMappingFunctionName() + functionModuleName = self.instConfig.getSampleAngleMappingFunctionModule() + logger.debug("sampleMappingFunction moduleName %s" % functionModuleName) + #Call a defined method to calculate angles from the reference angles. + moduleSource = self + if functionModuleName != None: + functionModule = importlib.import_module(functionModuleName) + logger.debug("dir(functionModule)" % dir(functionModule)) + moduleSource = functionModule + method = getattr(moduleSource, functionName) + fixedAngles = method(primaryAngles=primaryAngles, + referenceAngles=refAngles) + logger.debug ("fixed Angles: " + str(fixedAngles)) + for i in range(len(primaryAngles)): + logger.debug ("Fixing primaryAngles: %d " % primaryAngles[i]) + angles[:,primaryAngles[i]-1] = fixedAngles[i] + + def getGeoAngles(self, scan, angleNames): + """ + This function returns all of the geometry angles for the + for the scan as a N-by-num_geo array, where N is the number of scan + points and num_geo is the number of geometry motors. + """ +# scan = self.sd[scanNo] + geoAngles = self.getScanAngles(scan, angleNames) + if not (self.instConfig.getSampleAngleMappingFunctionName() == ""): + tb = None + try: + self.fixGeoAngles(scan, geoAngles) + except Exception as ex: + tb = traceback.format_exc() + raise RSMap3DException("Handling exception in getGeoAngles." + \ + "\n" + \ + str(ex) + \ + "\n" + \ + str(tb)) + logger.debug("getGeoAngles:\n" + str(geoAngles)) + return geoAngles + + def getUBMatrix(self, scan): + """ + Read UB matrix from the #G3 line from the spec file. + """ + try: + g3 = scan.G["G3"].strip().split() + g3 = np.array(list(map(float, g3))) + ub = g3.reshape(-1,3) + logger.debug("ub " +str(ub)) + return ub + except: + logger.error("Unable to read UB Matrix from G3") + logger.error( '-'*60) + traceback.print_exc(file=sys.stdout) + logger.error('-'*60) + + + def hotpixelkill(self, areaData): + """ + function to remove hot pixels from CCD frames + ADD REMOVE VALUES IF NEEDED! + :param areaData: area detector data + """ + + for pixel in self.getBadPixels(): + badLoc = pixel.getBadLocation() + replaceLoc = pixel.getReplacementLocation() + areaData[badLoc[0],badLoc[1]] = \ + areaData[replaceLoc[0],replaceLoc[1]] + + return areaData + + def loadSource(self, mapHKL=False): + ''' + This method does the work of loading data from the files. This has been + split off from the constructor to allow this to be threaded and later + canceled. + :param mapHKL: boolean to mark if the data should be mapped to HKL + ''' + # Load up the instrument configuration file + self.loadInstrumentXMLConfig() + #Load up the detector configuration file + self.loadDetectorXMLConfig() + + self.specFile = os.path.join(self.projectDir, self.projectName + \ + self.projectExt) + imageDir = os.path.join(self.projectDir, \ + IMAGE_DIR_MERGE_STR % self.projectName) + + # +++++++++++++++ + # for hdf5 file,use the same property but different format + self.imageFileTmp = os.path.join(imageDir, \ + HDF5_FILE_MERGE_STR % + (self.projectName)) + # +++++++++++++++ + # if needed load up the bad pixel file. + if not (self.badPixelFile is None): + + badPixelFile = PilatusBadPixelFile(self.badPixelFile) + self.badPixels = badPixelFile.getBadPixels() + + # id needed load the flat field file + if not (self.flatFieldFile is None): + self.flatFieldData = np.array(Image.open(self.flatFieldFile)).T + # Load scan information from the spec file + try: + self.sd = SpecDataFile(self.specFile) + self.mapHKL = mapHKL + maxScan = int(self.sd.getMaxScanNumber()) + logger.debug("Number of Scans" + str(maxScan)) + if self.scans is None: + self.scans = range(1, maxScan+1) + imagePath = os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName) + + self.imageBounds = {} + self.imageToBeUsed = {} + self.availableScans = [] + self.incidentEnergy = {} + self.ubMatrix = {} + self.scanType = {} + self.progress = 0 + self.progressInc = 1 + #======= ZZ, 2020/02/19, add initialization + self.progressMax = 100 + #======= + # Zero the progress bar at the beginning. + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + for scan in self.scans: + if (self.cancelLoad): + self.cancelLoad = False + raise LoadCanceledException(CANCEL_STR) + + else: + if (os.path.exists(os.path.join(imagePath, \ + SCAN_NUMBER_MERGE_STR % scan))): + try: + curScan = self.sd.scans[str(scan)] + self.scanType[scan] = \ + self.sd.scans[str(scan)].scanCmd.split()[0] + angles = self.getGeoAngles(curScan, self.angleNames) + self.availableScans.append(scan) + if self.mapHKL==True: + self.ubMatrix[scan] = self.getUBMatrix(curScan) + if self.ubMatrix[scan] is None: + raise Sector33SpecFileException("UB matrix " + \ + "not found.") + else: + self.ubMatrix[scan] = None + self.incidentEnergy[scan] = 12398.4 /float(curScan.G['G4'].split()[3]) + _start_time = time.time() + self.imageBounds[scan] = \ + self.findImageQs(angles, \ + self.ubMatrix[scan], \ + self.incidentEnergy[scan]) + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + logger.info (('Elapsed time for Finding qs for scan %d: ' + + '%.3f seconds') % \ + (scan, (time.time() - _start_time))) + except ScanDataMissingException: + logger.error( "Scan " + str(scan) + " has no data") + #Make sure to show 100% completion + if self.progressUpdater is not None: + self.progressUpdater(self.progressMax, self.progressMax) + except IOError: + raise IOError( "Cannot open file " + str(self.specFile)) + if len(self.getAvailableScans()) == 0: + raise ScanDataMissingException("Could not find scan data for " + \ + "input file \n" + self.specFile + \ + "\nOne possible reason for this " + \ + "is that the image files are " + \ + "missing. Images are assumed " + \ + "to be in " + \ + os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName)) + + self.availableScanTypes = set(self.scanType.values()) + + + + def to_bool(self, value): + """ + Note this method found in answer to: + http://stackoverflow.com/questions/715417/converting-from-a-string-to-boolean-in-python + Converts 'something' to boolean. Raises exception if it gets a string + it doesn't handle. + Case is ignored for strings. These string values are handled: + True: 'True', "1", "TRue", "yes", "y", "t" + False: "", "0", "faLse", "no", "n", "f" + Non-string values are passed to bool. + """ + if type(value) == type(''): + if value.lower() in ("yes", "y", "true", "t", "1"): + return True + if value.lower() in ("no", "n", "false", "f", "0", ""): + return False + raise Exception('Invalid value for boolean conversion: ' + value) + return bool(value) + + def rawmap(self,scans, angdelta=[0,0,0,0,0], + adframes=None, mask = None): + """ + read ad frames and and convert them in reciprocal space + angular coordinates are taken from the spec file + or read from the edf file header when no scan number is given (scannr=None) + """ + + if mask is None: + mask_was_none = True + #mask = [True] * len(self.getImageToBeUsed()[scans[0]]) + else: + mask_was_none = False + #sd = spec.SpecDataFile(self.specFile) + intensity = np.array([]) + + # fourc goniometer in fourc coordinates + # convention for coordinate system: + # x: upwards; + # y: along the incident beam; + # z: "outboard" (makes coordinate system right-handed). + # QConversion will set up the goniometer geometry. + # So the first argument describes the sample rotations, the second the + # detector rotations and the third the primary beam direction. + qconv = xu.experiment.QConversion(self.getSampleCircleDirections(), \ + self.getDetectorCircleDirections(), \ + self.getPrimaryBeamDirection()) + + # define experimental class for angle conversion + # + # ipdir: inplane reference direction (ipdir points into the primary beam + # direction at zero angles) + # ndir: surface normal of your sample (ndir points in a direction + # perpendicular to the primary beam and the innermost detector + # rotation axis) + en = self.getIncidentEnergy() + hxrd = xu.HXRD(self.getInplaneReferenceDirection(), \ + self.getSampleSurfaceNormalDirection(), \ + en=en[self.getAvailableScans()[0]], \ + qconv=qconv) + + + # initialize area detector properties + if (self.getDetectorPixelWidth() != None ) and \ + (self.getDistanceToDetector() != None): + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + pwidth1=self.getDetectorPixelWidth()[0], \ + pwidth2=self.getDetectorPixelWidth()[1], \ + distance=self.getDistanceToDetector(), \ + Nav=self.getNumPixelsToAverage(), \ + roi=self.getDetectorROI()) + else: + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + chpdeg1=self.getDetectorChannelsPerDegree()[0], \ + chpdeg2=self.getDetectorChannelsPerDegree()[1], \ + Nav=self.getNumPixelsToAverage(), + roi=self.getDetectorROI()) + + angleNames = self.getAngles() + scanAngle = {} + for i in range(len(angleNames)): + scanAngle[i] = np.array([]) + + offset = 0 + imageToBeUsed = self.getImageToBeUsed() + monitorName = self.getMonitorName() + monitorScaleFactor = self.getMonitorScaleFactor() + filterName = self.getFilterName() + filterScaleFactor = self.getFilterScaleFactor() + for scannr in scans: + if self.haltMap: + raise ProcessCanceledException("Process Canceled") + scan = self.sd.scans[str(scannr)] + angles = self.getGeoAngles(scan, angleNames) + scanAngle1 = {} + scanAngle2 = {} + for i in range(len(angleNames)): + scanAngle1[i] = angles[:,i] + scanAngle2[i] = [] + if monitorName != None: + monitor_data = scan.data.get(monitorName) + if monitor_data is None: + raise IOError("Did not find Monitor source '" + \ + monitorName + \ + "' in the Spec file. Make sure " + \ + "monitorName is correct in the " + \ + "instrument Config file") + if filterName != None: + filter_data = scan.data.get(filterName) + if filter_data is None: + raise IOError("Did not find filter source '" + \ + filterName + \ + "' in the Spec file. Make sure " + \ + "filterName is correct in the " + \ + "instrument Config file") + # read in the image data + arrayInitializedForScan = False + foundIndex = 0 + + if mask_was_none: + mask = [True] * len(self.getImageToBeUsed()[scannr]) + + for ind in range(len(scan.data[list(scan.data.keys())[0]])): + if imageToBeUsed[scannr][ind] and mask[ind]: + # +++++++++++++++ + # Read HDF5 file here + #im = Image.open(self.imageFileTmp % (scannr, scannr, ind)) + #img = np.array(im.getdata()).reshape(im.size[1],im.size[0]).T + h5file = self.imageFileTmp % (scannr, scannr, ind) + h5data = h5py.File(h5file, "r") + im = h5data["entry"]["data"]["data"][0,:,:] + img = np.array(im).T + # +++++++++++++++ + + img = self.hotpixelkill(img) + ff_data = self.getFlatFieldData() + if not (ff_data is None): + img = img * ff_data + # reduce data siz + img2 = xu.blockAverage2D(img, + self.getNumPixelsToAverage()[0], \ + self.getNumPixelsToAverage()[1], \ + roi=self.getDetectorROI()) + + # apply intensity corrections + if monitorName != None: + img2 = img2 / monitor_data[ind] * monitorScaleFactor + if filterName != None: + img2 = img2 / filter_data[ind] * filterScaleFactor + + # initialize data array + if not arrayInitializedForScan: + imagesToProcess = [imageToBeUsed[scannr][i] and mask[i] for i in range(len(imageToBeUsed[scannr]))] + if not intensity.shape[0]: + intensity = np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape) + arrayInitializedForScan = True + else: + offset = intensity.shape[0] + intensity = np.concatenate( + (intensity, + (np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape))), + axis=0) + arrayInitializedForScan = True + # add data to intensity array + intensity[foundIndex+offset,:,:] = img2 + for i in range(len(angleNames)): +# logger.debug("appending angles to angle2 " + +# str(scanAngle1[i][ind])) + scanAngle2[i].append(scanAngle1[i][ind]) + foundIndex += 1 + if len(scanAngle2[0]) > 0: + for i in range(len(angleNames)): + scanAngle[i] = \ + np.concatenate((scanAngle[i], np.array(scanAngle2[i])), \ + axis=0) + # transform scan angles to reciprocal space coordinates for all detector pixels + angleList = [] + for i in range(len(angleNames)): + angleList.append(scanAngle[i]) + if self.ubMatrix[scans[0]] is None: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage()) + else: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage(), \ + UB = self.ubMatrix[scans[0]]) + + + # apply selected transform + qxTrans, qyTrans, qzTrans = \ + self.transform.do3DTransform(qx, qy, qz) + + + return qxTrans, qyTrans, qzTrans, intensity + +class LoadCanceledException(RSMap3DException): + ''' + Exception Thrown when loading data is canceled. + ''' + def __init__(self, message): + super(LoadCanceledException, self).__init__(message) + +class Sector33SpecFileException(RSMap3DException): + ''' + Exception class to be raised if there is a problem loading information + from a spec file + file + ''' + def __init__(self, message): + super(Sector33SpecFileException, self).__init__(message) + + + diff --git a/rsMap3D/datasource/Sector12NSLSIISpecDataSource.py b/rsMap3D/datasource/Sector12NSLSIISpecDataSource.py new file mode 100644 index 0000000..eb78fa3 --- /dev/null +++ b/rsMap3D/datasource/Sector12NSLSIISpecDataSource.py @@ -0,0 +1,562 @@ +''' + Copyright (c) 2012, UChicago Argonne, LLC + See LICENSE file. +''' +import os +from spec2nexus.spec import SpecDataFile +from rsMap3D.exception.rsmap3dexception import RSMap3DException,\ + ScanDataMissingException +from rsMap3D.gui.rsm3dcommonstrings import CANCEL_STR +from rsMap3D.datasource.pilatusbadpixelfile import PilatusBadPixelFile +from rsMap3D.mappers.abstractmapper import ProcessCanceledException +from rsMap3D.datasource.specxmldrivendatasource import SpecXMLDrivenDataSource +import numpy as np +import xrayutilities as xu +import time +import sys,traceback +import logging +import importlib +try: + from PIL import Image +except ImportError: + import Image +logger = logging.getLogger(__name__) + + + +IMAGE_DIR_MERGE_STR = "images/%s" +SCAN_NUMBER_MERGE_STR = "%s_%04d" +#TIFF_FILE_MERGE_STR = "S%%03d/%s_S%%03d_%%05d.tif" +#TIFF_FILE_MERGE_STR = "%s_%%04d/pil_%%05d.tif" +# This seems changed at some point, at least for data at NSLS-II 4ID ones. +TIFF_FILE_MERGE_STR = "%s_%%04d/%s_%%04d_%%05d.tif" +# Some messed up SPEC macro started the point # at 116 instead of 0 +# to account for that, here we have: +MESSYSCANS = [27, 28, 34, 45, 59] + +class Sector12NSLSIISpecDataSource(SpecXMLDrivenDataSource): + ''' + Class to load data from spec file and configuration xml files from + for the way that data is collected at sector 12. + :members + ''' + + + def __init__(self, + projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs): + ''' + Constructor + :param projectDir: Directory holding the project file to open + :param projectName: First part of file name for the project + :param projectExt: File extension for the project file. + :param instConfigFile: Full path to Instrument configuration file. + :param detConfigFile: Full path to the detector configuration file + :param kwargs: Assorted keyword arguments + + :rtype: Sector12NSLSIISpecDataSource + ''' + super(Sector12NSLSIISpecDataSource, self).__init__(projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs) + + def _calc_eulerian_from_kappa(self, primaryAngles=None, + referenceAngles = None): + """ + Calculate the eulerian sample angles from the kappa stage angles. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + """ + + keta = np.deg2rad(referenceAngles[:,0]) + kappa = np.deg2rad(referenceAngles[:,1]) + kphi = np.deg2rad(referenceAngles[:,2]) + kappaParam = self.instConfig.getSampleAngleMappingParameter('kappa') + + try: + if kappaParam != None: + self.kalpha = np.deg2rad(float(kappaParam)) + else: + self.kalpha = np.deg2rad(50.000) + kappaInvertedParam = \ + self.instConfig.getSampleAngleMappingParameter('kappaInverted') + if kappaInvertedParam != None: + self.kappa_inverted = self.to_bool(kappaInvertedParam) + else: + self.kappa_inverted = False + except Exception as ex: + raise RSMap3DException("Error trying to get parameter for " + \ + "sampleAngleMappingFunction " + \ + "_calc_eulerian_from_kappa in inst config " + \ + "file\n" + \ + str(ex)) + + _t1 = np.arctan(np.tan(kappa / 2.0) * np.cos(self.kalpha)) + if self.kappa_inverted: + eta = np.rad2deg(keta + _t1) + phi = np.rad2deg(kphi + _t1) + else: + eta = np.rad2deg(keta - _t1) + phi = np.rad2deg(kphi - _t1) + chi = 2.0 * np.rad2deg(np.arcsin(np.sin(kappa / 2.0) * \ + np.sin(self.kalpha))) + + return eta, chi, phi + + def _calc_replace_angle_values(self , primaryAngles=None, + referenceAngles=None): + ''' + Fix a situation were some constant angle values have been + recorded incorrectly and need to be fixed. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + ''' + logger.info( "Running " + __name__) + angles = [] + logger.debug("referenceAngles " + str(referenceAngles)) + mappingAngles = self.instConfig.getSampleAngleMappingReferenceAngles() + + logger.debug( "mappingAngles" + str(mappingAngles)) + for ii in range(len(referenceAngles)): + replaceVal = \ + float(self.instConfig.getSampleAngleMappingReferenceAngleAttrib( \ + number= str(mappingAngles[ii]), \ + attribName='replaceValue')) + logger.debug( "primary Angles" + str(referenceAngles)) + angles.append(replaceVal* np.ones(len(referenceAngles[:,ii]),)) + logger.debug("Angles" + str( angles)) + return angles + + def fixGeoAngles(self, scan, angles): + ''' + Fix the angles using a user selected function. + :param scan: scan to set the angles for + :param angles: Array of angles to set for this scan + ''' + logger.debug( "starting " + __name__) + needToCorrect = False + refAngleNames = self.instConfig.getSampleAngleMappingReferenceAngles() + for refAngleName in refAngleNames: + alwaysFix = self.instConfig.getSampleAngleMappingAlwaysFix() + if refAngleName in scan.L or alwaysFix: + needToCorrect = True + + if needToCorrect: + logger.debug( __name__ + ": Fixing angles") + refAngles = self.getScanAngles(scan, refAngleNames) + primaryAngles = self.instConfig.getSampleAngleMappingPrimaryAngles() + functionName = self.instConfig.getSampleAngleMappingFunctionName() + functionModuleName = self.instConfig.getSampleAngleMappingFunctionModule() + logger.debug("sampleMappingFunction moduleName %s" % functionModuleName) + #Call a defined method to calculate angles from the reference angles. + moduleSource = self + if functionModuleName != None: + functionModule = importlib.import_module(functionModuleName) + logger.debug("dir(functionModule)" % dir(functionModule)) + moduleSource = functionModule + method = getattr(moduleSource, functionName) + fixedAngles = method(primaryAngles=primaryAngles, + referenceAngles=refAngles) + logger.debug ("fixed Angles: " + str(fixedAngles)) + for i in range(len(primaryAngles)): + logger.debug ("Fixing primaryAngles: %d " % primaryAngles[i]) + angles[:,primaryAngles[i]-1] = fixedAngles[i] + + def getGeoAngles(self, scan, angleNames): + """ + This function returns all of the geometry angles for the + for the scan as a N-by-num_geo array, where N is the number of scan + points and num_geo is the number of geometry motors. + """ +# scan = self.sd[scanNo] + geoAngles = self.getScanAngles(scan, angleNames) + if not (self.instConfig.getSampleAngleMappingFunctionName() == ""): + tb = None + try: + self.fixGeoAngles(scan, geoAngles) + except Exception as ex: + tb = traceback.format_exc() + raise RSMap3DException("Handling exception in getGeoAngles." + \ + "\n" + \ + str(ex) + \ + "\n" + \ + str(tb)) + logger.debug("getGeoAngles:\n" + str(geoAngles)) + return geoAngles + + def getUBMatrix(self, scan): + """ + Read UB matrix from the #G3 line from the spec file. + """ + try: + g3 = scan.G["G3"].strip().split() + g3 = np.array(list(map(float, g3))) + ub = g3.reshape(-1,3) + logger.debug("ub " +str(ub)) + return ub + except: + logger.error("Unable to read UB Matrix from G3") + logger.error( '-'*60) + traceback.print_exc(file=sys.stdout) + logger.error('-'*60) + + + def hotpixelkill(self, areaData): + """ + function to remove hot pixels from CCD frames + ADD REMOVE VALUES IF NEEDED! + :param areaData: area detector data + """ + + for pixel in self.getBadPixels(): + badLoc = pixel.getBadLocation() + replaceLoc = pixel.getReplacementLocation() + areaData[badLoc[0],badLoc[1]] = \ + areaData[replaceLoc[0],replaceLoc[1]] + + return areaData + + def loadSource(self, mapHKL=False): + ''' + This method does the work of loading data from the files. This has been + split off from the constructor to allow this to be threaded and later + canceled. + :param mapHKL: boolean to mark if the data should be mapped to HKL + ''' + # Load up the instrument configuration file + self.loadInstrumentXMLConfig() + #Load up the detector configuration file + self.loadDetectorXMLConfig() + + self.specFile = os.path.join(self.projectDir, self.projectName + \ + self.projectExt) + imageDir = os.path.join(self.projectDir, \ + IMAGE_DIR_MERGE_STR % self.projectName) + self.imageFileTmp = os.path.join(imageDir, \ + TIFF_FILE_MERGE_STR % + (self.projectName, self.projectName)) + # if needed load up the bad pixel file. + if not (self.badPixelFile is None): + + badPixelFile = PilatusBadPixelFile(self.badPixelFile) + self.badPixels = badPixelFile.getBadPixels() + + # id needed load the flat field file + if not (self.flatFieldFile is None): + self.flatFieldData = np.array(Image.open(self.flatFieldFile)).T + # Load scan information from the spec file + try: + self.sd = SpecDataFile(self.specFile) + self.mapHKL = mapHKL + maxScan = int(self.sd.getMaxScanNumber()) + logger.debug("Number of Scans" + str(maxScan)) + if self.scans is None: + self.scans = range(1, maxScan+1) + imagePath = os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName) + + self.imageBounds = {} + self.imageToBeUsed = {} + self.availableScans = [] + self.incidentEnergy = {} + self.ubMatrix = {} + self.scanType = {} + self.progress = 0 + self.progressInc = 1 + #======= ZZ, 2020/02/19, add initialization + self.progressMax = 100 + #======= + # Zero the progress bar at the beginning. + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + for scan in self.scans: + if (self.cancelLoad): + self.cancelLoad = False + raise LoadCanceledException(CANCEL_STR) + + else: + if (os.path.exists(os.path.join(imagePath, \ + SCAN_NUMBER_MERGE_STR % (self.projectName, scan) ))): + try: + curScan = self.sd.scans[str(scan)] + self.scanType[scan] = \ + self.sd.scans[str(scan)].scanCmd.split()[0] + angles = self.getGeoAngles(curScan, self.angleNames) + self.availableScans.append(scan) + if self.mapHKL==True: + self.ubMatrix[scan] = self.getUBMatrix(curScan) + if self.ubMatrix[scan] is None: + raise Sector12SpecFileException("UB matrix " + \ + "not found.") + else: + self.ubMatrix[scan] = None + self.incidentEnergy[scan] = 12398.4 /float(curScan.G['G4'].split()[3]) + _start_time = time.time() + self.imageBounds[scan] = \ + self.findImageQs(angles, \ + self.ubMatrix[scan], \ + self.incidentEnergy[scan]) + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + logger.info (('Elapsed time for Finding qs for scan %d: ' + + '%.3f seconds') % \ + (scan, (time.time() - _start_time))) + except ScanDataMissingException: + logger.error( "Scan " + str(scan) + " has no data") + #Make sure to show 100% completion + if self.progressUpdater is not None: + self.progressUpdater(self.progressMax, self.progressMax) + except IOError: + raise IOError( "Cannot open file " + str(self.specFile)) + if len(self.getAvailableScans()) == 0: + raise ScanDataMissingException("Could not find scan data for " + \ + "input file \n" + self.specFile + \ + "\nOne possible reason for this " + \ + "is that the image files are " + \ + "missing. Images are assumed " + \ + "to be in " + \ + os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName)) + + self.availableScanTypes = set(self.scanType.values()) + + + + def to_bool(self, value): + """ + Note this method found in answer to: + http://stackoverflow.com/questions/715417/converting-from-a-string-to-boolean-in-python + Converts 'something' to boolean. Raises exception if it gets a string + it doesn't handle. + Case is ignored for strings. These string values are handled: + True: 'True', "1", "TRue", "yes", "y", "t" + False: "", "0", "faLse", "no", "n", "f" + Non-string values are passed to bool. + """ + if type(value) == type(''): + if value.lower() in ("yes", "y", "true", "t", "1"): + return True + if value.lower() in ("no", "n", "false", "f", "0", ""): + return False + raise Exception('Invalid value for boolean conversion: ' + value) + return bool(value) + + def rawmap(self,scans, angdelta=[0,0,0,0,0], + adframes=None, mask = None): + """ + read ad frames and and convert them in reciprocal space + angular coordinates are taken from the spec file + or read from the edf file header when no scan number is given (scannr=None) + """ + + if mask is None: + mask_was_none = True + #mask = [True] * len(self.getImageToBeUsed()[scans[0]]) + else: + mask_was_none = False + #sd = spec.SpecDataFile(self.specFile) + intensity = np.array([]) + + # fourc goniometer in fourc coordinates + # convention for coordinate system: + # x: upwards; + # y: along the incident beam; + # z: "outboard" (makes coordinate system right-handed). + # QConversion will set up the goniometer geometry. + # So the first argument describes the sample rotations, the second the + # detector rotations and the third the primary beam direction. + qconv = xu.experiment.QConversion(self.getSampleCircleDirections(), \ + self.getDetectorCircleDirections(), \ + self.getPrimaryBeamDirection()) + + # define experimental class for angle conversion + # + # ipdir: inplane reference direction (ipdir points into the primary beam + # direction at zero angles) + # ndir: surface normal of your sample (ndir points in a direction + # perpendicular to the primary beam and the innermost detector + # rotation axis) + en = self.getIncidentEnergy() + hxrd = xu.HXRD(self.getInplaneReferenceDirection(), \ + self.getSampleSurfaceNormalDirection(), \ + en=en[self.getAvailableScans()[0]], \ + qconv=qconv) + + + # initialize area detector properties + if (self.getDetectorPixelWidth() != None ) and \ + (self.getDistanceToDetector() != None): + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + pwidth1=self.getDetectorPixelWidth()[0], \ + pwidth2=self.getDetectorPixelWidth()[1], \ + distance=self.getDistanceToDetector(), \ + Nav=self.getNumPixelsToAverage(), \ + roi=self.getDetectorROI()) + else: + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + chpdeg1=self.getDetectorChannelsPerDegree()[0], \ + chpdeg2=self.getDetectorChannelsPerDegree()[1], \ + Nav=self.getNumPixelsToAverage(), + roi=self.getDetectorROI()) + + angleNames = self.getAngles() + scanAngle = {} + for i in range(len(angleNames)): + scanAngle[i] = np.array([]) + + offset = 0 + imageToBeUsed = self.getImageToBeUsed() + monitorName = self.getMonitorName() + monitorScaleFactor = self.getMonitorScaleFactor() + filterName = self.getFilterName() + filterScaleFactor = self.getFilterScaleFactor() + for scannr in scans: + if self.haltMap: + raise ProcessCanceledException("Process Canceled") + scan = self.sd.scans[str(scannr)] + angles = self.getGeoAngles(scan, angleNames) + scanAngle1 = {} + scanAngle2 = {} + for i in range(len(angleNames)): + scanAngle1[i] = angles[:,i] + scanAngle2[i] = [] + if monitorName != None: + monitor_data = scan.data.get(monitorName) + if monitor_data is None: + raise IOError("Did not find Monitor source '" + \ + monitorName + \ + "' in the Spec file. Make sure " + \ + "monitorName is correct in the " + \ + "instrument Config file") + if filterName != None: + filter_data = scan.data.get(filterName) + if filter_data is None: + raise IOError("Did not find filter source '" + \ + filterName + \ + "' in the Spec file. Make sure " + \ + "filterName is correct in the " + \ + "instrument Config file") + # read in the image data + arrayInitializedForScan = False + foundIndex = 0 + + if mask_was_none: + mask = [True] * len(self.getImageToBeUsed()[scannr]) + + for ind in range(len(scan.data[list(scan.data.keys())[0]])): + if imageToBeUsed[scannr][ind] and mask[ind]: + # read tif image + # Now here is something really messy for Hua's data: + # to handle the filename sequence number not starting at 0 + # This is hard coded in, it does not worth the trouble to + # change the class to be more generic. + #------------------- + if scannr in MESSYSCANS: + POINT_NUM_OFFSET = 116 + else: + POINT_NUM_OFFSET = 0 + im = Image.open(self.imageFileTmp % (scannr, scannr, ind+POINT_NUM_OFFSET)) + #------------------- + img = np.array(im.getdata()).reshape(im.size[1],im.size[0]).T + img = self.hotpixelkill(img) + ff_data = self.getFlatFieldData() + if not (ff_data is None): + img = img * ff_data + # reduce data siz + img2 = xu.blockAverage2D(img, + self.getNumPixelsToAverage()[0], \ + self.getNumPixelsToAverage()[1], \ + roi=self.getDetectorROI()) + + # apply intensity corrections + if monitorName != None: + img2 = img2 / monitor_data[ind] * monitorScaleFactor + if filterName != None: + img2 = img2 / filter_data[ind] * filterScaleFactor + + # initialize data array + if not arrayInitializedForScan: + imagesToProcess = [imageToBeUsed[scannr][i] and mask[i] for i in range(len(imageToBeUsed[scannr]))] + if not intensity.shape[0]: + intensity = np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape) + arrayInitializedForScan = True + else: + offset = intensity.shape[0] + intensity = np.concatenate( + (intensity, + (np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape))), + axis=0) + arrayInitializedForScan = True + # add data to intensity array + intensity[foundIndex+offset,:,:] = img2 + for i in range(len(angleNames)): +# logger.debug("appending angles to angle2 " + +# str(scanAngle1[i][ind])) + scanAngle2[i].append(scanAngle1[i][ind]) + foundIndex += 1 + if len(scanAngle2[0]) > 0: + for i in range(len(angleNames)): + scanAngle[i] = \ + np.concatenate((scanAngle[i], np.array(scanAngle2[i])), \ + axis=0) + # transform scan angles to reciprocal space coordinates for all detector pixels + angleList = [] + for i in range(len(angleNames)): + angleList.append(scanAngle[i]) + if self.ubMatrix[scans[0]] is None: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage()) + else: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage(), \ + UB = self.ubMatrix[scans[0]]) + + + # apply selected transform + qxTrans, qyTrans, qzTrans = \ + self.transform.do3DTransform(qx, qy, qz) + + + return qxTrans, qyTrans, qzTrans, intensity + +class LoadCanceledException(RSMap3DException): + ''' + Exception Thrown when loading data is canceled. + ''' + def __init__(self, message): + super(LoadCanceledException, self).__init__(message) + +class Sector12SpecFileException(RSMap3DException): + ''' + Exception class to be raised if there is a problem loading information + from a spec file + file + ''' + def __init__(self, message): + super(Sector12SpecFileException, self).__init__(message) + + + diff --git a/rsMap3D/datasource/Sector28SpecDataSource.py b/rsMap3D/datasource/Sector28SpecDataSource.py new file mode 100644 index 0000000..00eea03 --- /dev/null +++ b/rsMap3D/datasource/Sector28SpecDataSource.py @@ -0,0 +1,585 @@ +''' + Copyright (c) 2012, UChicago Argonne, LLC + See LICENSE file. +''' +import os +from spec2nexus.spec import SpecDataFile +from rsMap3D.exception.rsmap3dexception import RSMap3DException,\ + ScanDataMissingException +from rsMap3D.gui.rsm3dcommonstrings import CANCEL_STR +from rsMap3D.datasource.pilatusbadpixelfile import PilatusBadPixelFile +from rsMap3D.mappers.abstractmapper import ProcessCanceledException +from rsMap3D.datasource.specxmldrivendatasource import SpecXMLDrivenDataSource +import numpy as np +import xrayutilities as xu +import time +import sys,traceback +import logging +import importlib +import h5py +import hdf5plugin +try: + from PIL import Image +except ImportError: + import Image +logger = logging.getLogger(__name__) + + +# +++++++++++++++ +# Zhan Zhang (2026-03-26) +# Make changes to work with HDF5 file generated at CHEX 28-ID beamline. +# I do need to generate folder structure different to the Tiff case, +# which is : +# $(PROJECT_DIR)/images/$(SAMPLE_NAME)/Snnn/$(SAMPLE_NAME)_Sxxx_nnnnn.tiff +# Here it is : +# $(PROJECT_DIR)/images/$(SAMPLE_NAME)/$(SAMPLE_NAME)_Sxxx_00000.h5 +IMAGE_DIR_MERGE_STR = "images/%s" +#IMAGE_DIR_MERGE_STR = "hdf5s/%s" +SCAN_NUMBER_MERGE_STR = "S%03d" +#TIFF_FILE_MERGE_STR = "S%%03d/%s_S%%03d_%%05d.tif" +#hdf5 filename format string: one h5 file per scan here +# This one is when we still had Sxxx/ folder in place. +#HDF5_FILE_MERGE_STR = "S%%03d/%s_S%%03d_00000.h5" +HDF5_FILE_MERGE_STR = "%s_S%%03d_00000.h5" +# +++++++++++++++ + +class Sector28SpecDataSource(SpecXMLDrivenDataSource): + ''' + Class to load data from spec file and configuration xml files from + for the way that data is collected at sector 33. + :members + ''' + + + def __init__(self, + projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs): + ''' + Constructor + :param projectDir: Directory holding the project file to open + :param projectName: First part of file name for the project + :param projectExt: File extension for the project file. + :param instConfigFile: Full path to Instrument configuration file. + :param detConfigFile: Full path to the detector configuration file + :param kwargs: Assorted keyword arguments + + :rtype: Sector28SpecDataSource + ''' + super(Sector28SpecDataSource, self).__init__(projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs) + + def _calc_eulerian_from_kappa(self, primaryAngles=None, + referenceAngles = None): + """ + Calculate the eulerian sample angles from the kappa stage angles. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + """ + + keta = np.deg2rad(referenceAngles[:,0]) + kappa = np.deg2rad(referenceAngles[:,1]) + kphi = np.deg2rad(referenceAngles[:,2]) + kappaParam = self.instConfig.getSampleAngleMappingParameter('kappa') + + try: + if kappaParam != None: + self.kalpha = np.deg2rad(float(kappaParam)) + else: + self.kalpha = np.deg2rad(50.000) + kappaInvertedParam = \ + self.instConfig.getSampleAngleMappingParameter('kappaInverted') + if kappaInvertedParam != None: + self.kappa_inverted = self.to_bool(kappaInvertedParam) + else: + self.kappa_inverted = False + except Exception as ex: + raise RSMap3DException("Error trying to get parameter for " + \ + "sampleAngleMappingFunction " + \ + "_calc_eulerian_from_kappa in inst config " + \ + "file\n" + \ + str(ex)) + + _t1 = np.arctan(np.tan(kappa / 2.0) * np.cos(self.kalpha)) + if self.kappa_inverted: + eta = np.rad2deg(keta + _t1) + phi = np.rad2deg(kphi + _t1) + else: + eta = np.rad2deg(keta - _t1) + phi = np.rad2deg(kphi - _t1) + chi = 2.0 * np.rad2deg(np.arcsin(np.sin(kappa / 2.0) * \ + np.sin(self.kalpha))) + + return eta, chi, phi + + def _calc_replace_angle_values(self , primaryAngles=None, + referenceAngles=None): + ''' + Fix a situation were some constant angle values have been + recorded incorrectly and need to be fixed. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + ''' + logger.info( "Running " + __name__) + angles = [] + logger.debug("referenceAngles " + str(referenceAngles)) + mappingAngles = self.instConfig.getSampleAngleMappingReferenceAngles() + + logger.debug( "mappingAngles" + str(mappingAngles)) + for ii in range(len(referenceAngles)): + replaceVal = \ + float(self.instConfig.getSampleAngleMappingReferenceAngleAttrib( \ + number= str(mappingAngles[ii]), \ + attribName='replaceValue')) + logger.debug( "primary Angles" + str(referenceAngles)) + angles.append(replaceVal* np.ones(len(referenceAngles[:,ii]),)) + logger.debug("Angles" + str( angles)) + return angles + + def fixGeoAngles(self, scan, angles): + ''' + Fix the angles using a user selected function. + :param scan: scan to set the angles for + :param angles: Array of angles to set for this scan + ''' + logger.debug( "starting " + __name__) + needToCorrect = False + refAngleNames = self.instConfig.getSampleAngleMappingReferenceAngles() + for refAngleName in refAngleNames: + alwaysFix = self.instConfig.getSampleAngleMappingAlwaysFix() + if refAngleName in scan.L or alwaysFix: + needToCorrect = True + + if needToCorrect: + logger.debug( __name__ + ": Fixing angles") + refAngles = self.getScanAngles(scan, refAngleNames) + primaryAngles = self.instConfig.getSampleAngleMappingPrimaryAngles() + functionName = self.instConfig.getSampleAngleMappingFunctionName() + functionModuleName = self.instConfig.getSampleAngleMappingFunctionModule() + logger.debug("sampleMappingFunction moduleName %s" % functionModuleName) + #Call a defined method to calculate angles from the reference angles. + moduleSource = self + if functionModuleName != None: + functionModule = importlib.import_module(functionModuleName) + logger.debug("dir(functionModule)" % dir(functionModule)) + moduleSource = functionModule + method = getattr(moduleSource, functionName) + fixedAngles = method(primaryAngles=primaryAngles, + referenceAngles=refAngles) + logger.debug ("fixed Angles: " + str(fixedAngles)) + for i in range(len(primaryAngles)): + logger.debug ("Fixing primaryAngles: %d " % primaryAngles[i]) + angles[:,primaryAngles[i]-1] = fixedAngles[i] + + def getGeoAngles(self, scan, angleNames): + """ + This function returns all of the geometry angles for the + for the scan as a N-by-num_geo array, where N is the number of scan + points and num_geo is the number of geometry motors. + """ +# scan = self.sd[scanNo] + geoAngles = self.getScanAngles(scan, angleNames) + if not (self.instConfig.getSampleAngleMappingFunctionName() == ""): + tb = None + try: + self.fixGeoAngles(scan, geoAngles) + except Exception as ex: + tb = traceback.format_exc() + raise RSMap3DException("Handling exception in getGeoAngles." + \ + "\n" + \ + str(ex) + \ + "\n" + \ + str(tb)) + logger.debug("getGeoAngles:\n" + str(geoAngles)) + return geoAngles + + def getUBMatrix(self, scan): + """ + Read UB matrix from the #G3 line from the spec file. + """ + try: + g3 = scan.G["G3"].strip().split() + g3 = np.array(list(map(float, g3))) + ub = g3.reshape(-1,3) + logger.debug("ub " +str(ub)) + return ub + except: + logger.error("Unable to read UB Matrix from G3") + logger.error( '-'*60) + traceback.print_exc(file=sys.stdout) + logger.error('-'*60) + + + def hotpixelkill(self, areaData): + """ + function to remove hot pixels from CCD frames + ADD REMOVE VALUES IF NEEDED! + :param areaData: area detector data + """ + + for pixel in self.getBadPixels(): + badLoc = pixel.getBadLocation() + replaceLoc = pixel.getReplacementLocation() + areaData[badLoc[0],badLoc[1]] = \ + areaData[replaceLoc[0],replaceLoc[1]] + + return areaData + + def loadSource(self, mapHKL=False): + ''' + This method does the work of loading data from the files. This has been + split off from the constructor to allow this to be threaded and later + canceled. + :param mapHKL: boolean to mark if the data should be mapped to HKL + ''' + # Load up the instrument configuration file + self.loadInstrumentXMLConfig() + #Load up the detector configuration file + self.loadDetectorXMLConfig() + + self.specFile = os.path.join(self.projectDir, self.projectName + \ + self.projectExt) + imageDir = os.path.join(self.projectDir, \ + IMAGE_DIR_MERGE_STR % self.projectName) + + # +++++++++++++++ + # for hdf5 file,use the same property but different format + self.imageFileTmp = os.path.join(imageDir, \ + HDF5_FILE_MERGE_STR % + (self.projectName)) + # +++++++++++++++ + # if needed load up the bad pixel file. + if not (self.badPixelFile is None): + + badPixelFile = PilatusBadPixelFile(self.badPixelFile) + self.badPixels = badPixelFile.getBadPixels() + + # id needed load the flat field file + if not (self.flatFieldFile is None): + self.flatFieldData = np.array(Image.open(self.flatFieldFile)).T + # Load scan information from the spec file + try: + self.sd = SpecDataFile(self.specFile) + self.mapHKL = mapHKL + maxScan = int(self.sd.getMaxScanNumber()) + logger.debug("Number of Scans" + str(maxScan)) + if self.scans is None: + self.scans = range(1, maxScan+1) + imagePath = os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName) + + self.imageBounds = {} + self.imageToBeUsed = {} + self.availableScans = [] + self.incidentEnergy = {} + self.ubMatrix = {} + self.scanType = {} + self.progress = 0 + self.progressInc = 1 + #======= ZZ, 2020/02/19, add initialization + self.progressMax = 100 + #======= + # Zero the progress bar at the beginning. + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + for scan in self.scans: + if (self.cancelLoad): + self.cancelLoad = False + raise LoadCanceledException(CANCEL_STR) + + else: + # Ok, this needs to be changed, too, if we get rid of Sxxx/ folder. + # I don't think it looks for the images in this section, but just to + # make sure they do exsit. + # ZZ 2026-03-26 + #if (os.path.exists(os.path.join(imagePath, \ + # SCAN_NUMBER_MERGE_STR % scan))): + if (os.path.exists( self.imageFileTmp % (scan) )): + try: + curScan = self.sd.scans[str(scan)] + self.scanType[scan] = \ + self.sd.scans[str(scan)].scanCmd.split()[0] + angles = self.getGeoAngles(curScan, self.angleNames) + self.availableScans.append(scan) + if self.mapHKL==True: + self.ubMatrix[scan] = self.getUBMatrix(curScan) + if self.ubMatrix[scan] is None: + raise Sector28SpecFileException("UB matrix " + \ + "not found.") + else: + self.ubMatrix[scan] = None + self.incidentEnergy[scan] = 12398.4 /float(curScan.G['G4'].split()[3]) + _start_time = time.time() + self.imageBounds[scan] = \ + self.findImageQs(angles, \ + self.ubMatrix[scan], \ + self.incidentEnergy[scan]) + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + logger.info (('Elapsed time for Finding qs for scan %d: ' + + '%.3f seconds') % \ + (scan, (time.time() - _start_time))) + except ScanDataMissingException: + logger.error( "Scan " + str(scan) + " has no data") + #Make sure to show 100% completion + if self.progressUpdater is not None: + self.progressUpdater(self.progressMax, self.progressMax) + except IOError: + raise IOError( "Cannot open file " + str(self.specFile)) + if len(self.getAvailableScans()) == 0: + raise ScanDataMissingException("Could not find scan data for " + \ + "input file \n" + self.specFile + \ + "\nOne possible reason for this " + \ + "is that the image files are " + \ + "missing. Images are assumed " + \ + "to be in " + \ + os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName)) + + self.availableScanTypes = set(self.scanType.values()) + + + + def to_bool(self, value): + """ + Note this method found in answer to: + http://stackoverflow.com/questions/715417/converting-from-a-string-to-boolean-in-python + Converts 'something' to boolean. Raises exception if it gets a string + it doesn't handle. + Case is ignored for strings. These string values are handled: + True: 'True', "1", "TRue", "yes", "y", "t" + False: "", "0", "faLse", "no", "n", "f" + Non-string values are passed to bool. + """ + if type(value) == type(''): + if value.lower() in ("yes", "y", "true", "t", "1"): + return True + if value.lower() in ("no", "n", "false", "f", "0", ""): + return False + raise Exception('Invalid value for boolean conversion: ' + value) + return bool(value) + + def rawmap(self,scans, angdelta=[0,0,0,0,0], + adframes=None, mask = None): + """ + read ad frames and and convert them in reciprocal space + angular coordinates are taken from the spec file + or read from the edf file header when no scan number is given (scannr=None) + """ + + if mask is None: + mask_was_none = True + #mask = [True] * len(self.getImageToBeUsed()[scans[0]]) + else: + mask_was_none = False + #sd = spec.SpecDataFile(self.specFile) + intensity = np.array([]) + + # fourc goniometer in fourc coordinates + # convention for coordinate system: + # x: upwards; + # y: along the incident beam; + # z: "outboard" (makes coordinate system right-handed). + # QConversion will set up the goniometer geometry. + # So the first argument describes the sample rotations, the second the + # detector rotations and the third the primary beam direction. + qconv = xu.experiment.QConversion(self.getSampleCircleDirections(), \ + self.getDetectorCircleDirections(), \ + self.getPrimaryBeamDirection()) + + # define experimental class for angle conversion + # + # ipdir: inplane reference direction (ipdir points into the primary beam + # direction at zero angles) + # ndir: surface normal of your sample (ndir points in a direction + # perpendicular to the primary beam and the innermost detector + # rotation axis) + en = self.getIncidentEnergy() + hxrd = xu.HXRD(self.getInplaneReferenceDirection(), \ + self.getSampleSurfaceNormalDirection(), \ + en=en[self.getAvailableScans()[0]], \ + qconv=qconv) + + + # initialize area detector properties + if (self.getDetectorPixelWidth() != None ) and \ + (self.getDistanceToDetector() != None): + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + pwidth1=self.getDetectorPixelWidth()[0], \ + pwidth2=self.getDetectorPixelWidth()[1], \ + distance=self.getDistanceToDetector(), \ + Nav=self.getNumPixelsToAverage(), \ + roi=self.getDetectorROI()) + else: + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + chpdeg1=self.getDetectorChannelsPerDegree()[0], \ + chpdeg2=self.getDetectorChannelsPerDegree()[1], \ + Nav=self.getNumPixelsToAverage(), + roi=self.getDetectorROI()) + + angleNames = self.getAngles() + scanAngle = {} + for i in range(len(angleNames)): + scanAngle[i] = np.array([]) + + offset = 0 + imageToBeUsed = self.getImageToBeUsed() + monitorName = self.getMonitorName() + monitorScaleFactor = self.getMonitorScaleFactor() + filterName = self.getFilterName() + filterScaleFactor = self.getFilterScaleFactor() + for scannr in scans: + if self.haltMap: + raise ProcessCanceledException("Process Canceled") + scan = self.sd.scans[str(scannr)] + angles = self.getGeoAngles(scan, angleNames) + scanAngle1 = {} + scanAngle2 = {} + for i in range(len(angleNames)): + scanAngle1[i] = angles[:,i] + scanAngle2[i] = [] + if monitorName != None: + monitor_data = scan.data.get(monitorName) + if monitor_data is None: + raise IOError("Did not find Monitor source '" + \ + monitorName + \ + "' in the Spec file. Make sure " + \ + "monitorName is correct in the " + \ + "instrument Config file") + if filterName != None: + filter_data = scan.data.get(filterName) + if filter_data is None: + raise IOError("Did not find filter source '" + \ + filterName + \ + "' in the Spec file. Make sure " + \ + "filterName is correct in the " + \ + "instrument Config file") + # read in the image data + arrayInitializedForScan = False + foundIndex = 0 + + if mask_was_none: + mask = [True] * len(self.getImageToBeUsed()[scannr]) + + # +++++++++++++++ + # Read HDF5 file here, brutal force. Need better handle. + h5file = self.imageFileTmp % (scannr) + h5data = h5py.File(h5file, "r") + scan_images = h5data["entry"]["data"]["data"] + # +++++++++++++++ + + + for ind in range(len(scan.data[list(scan.data.keys())[0]])): + if imageToBeUsed[scannr][ind] and mask[ind]: + # +++++++++++++++ + # Read HDF5 file here + #im = Image.open(self.imageFileTmp % (scannr, scannr, ind)) + #img = np.array(im.getdata()).reshape(im.size[1],im.size[0]).T + #h5file = self.imageFileTmp % (scannr, scannr, ind) + #h5data = h5py.File(h5file, "r") + #im = h5data["entry"]["data"]["data"][0,:,:] + img = np.array(scan_images[ind,:,:]).T + # +++++++++++++++ + + img = self.hotpixelkill(img) + ff_data = self.getFlatFieldData() + if not (ff_data is None): + img = img * ff_data + # reduce data siz + img2 = xu.blockAverage2D(img, + self.getNumPixelsToAverage()[0], \ + self.getNumPixelsToAverage()[1], \ + roi=self.getDetectorROI()) + + # apply intensity corrections + if monitorName != None: + img2 = img2 / monitor_data[ind] * monitorScaleFactor + if filterName != None: + img2 = img2 / filter_data[ind] * filterScaleFactor + + # initialize data array + if not arrayInitializedForScan: + imagesToProcess = [imageToBeUsed[scannr][i] and mask[i] for i in range(len(imageToBeUsed[scannr]))] + if not intensity.shape[0]: + intensity = np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape) + arrayInitializedForScan = True + else: + offset = intensity.shape[0] + intensity = np.concatenate( + (intensity, + (np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape))), + axis=0) + arrayInitializedForScan = True + # add data to intensity array + intensity[foundIndex+offset,:,:] = img2 + for i in range(len(angleNames)): +# logger.debug("appending angles to angle2 " + +# str(scanAngle1[i][ind])) + scanAngle2[i].append(scanAngle1[i][ind]) + foundIndex += 1 + if len(scanAngle2[0]) > 0: + for i in range(len(angleNames)): + scanAngle[i] = \ + np.concatenate((scanAngle[i], np.array(scanAngle2[i])), \ + axis=0) + # transform scan angles to reciprocal space coordinates for all detector pixels + angleList = [] + for i in range(len(angleNames)): + angleList.append(scanAngle[i]) + if self.ubMatrix[scans[0]] is None: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage()) + else: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage(), \ + UB = self.ubMatrix[scans[0]]) + + + # apply selected transform + qxTrans, qyTrans, qzTrans = \ + self.transform.do3DTransform(qx, qy, qz) + + + return qxTrans, qyTrans, qzTrans, intensity + +class LoadCanceledException(RSMap3DException): + ''' + Exception Thrown when loading data is canceled. + ''' + def __init__(self, message): + super(LoadCanceledException, self).__init__(message) + +class Sector28SpecFileException(RSMap3DException): + ''' + Exception class to be raised if there is a problem loading information + from a spec file + file + ''' + def __init__(self, message): + super(Sector28SpecFileException, self).__init__(message) + + + diff --git a/rsMap3D/datasource/s28waxpcsSpecDataSource.py b/rsMap3D/datasource/s28waxpcsSpecDataSource.py new file mode 100644 index 0000000..e1c94a0 --- /dev/null +++ b/rsMap3D/datasource/s28waxpcsSpecDataSource.py @@ -0,0 +1,585 @@ +''' + Copyright (c) 2012, UChicago Argonne, LLC + See LICENSE file. +''' +import os +from spec2nexus.spec import SpecDataFile +from rsMap3D.exception.rsmap3dexception import RSMap3DException,\ + ScanDataMissingException +from rsMap3D.gui.rsm3dcommonstrings import CANCEL_STR +from rsMap3D.datasource.pilatusbadpixelfile import PilatusBadPixelFile +from rsMap3D.mappers.abstractmapper import ProcessCanceledException +from rsMap3D.datasource.specxmldrivendatasource import SpecXMLDrivenDataSource +import numpy as np +import xrayutilities as xu +import time +import sys,traceback +import logging +import importlib +import h5py +import hdf5plugin +try: + from PIL import Image +except ImportError: + import Image +logger = logging.getLogger(__name__) + + +# +++++++++++++++ +# Zhan Zhang (2026-03-26) +# Make changes to work with HDF5 file generated at CHEX 28-ID beamline. +# I do need to generate folder structure different to the Tiff case, +# which is : +# $(PROJECT_DIR)/images/$(SAMPLE_NAME)/Snnn/$(SAMPLE_NAME)_Sxxx_nnnnn.tiff +# Here it is : +# $(PROJECT_DIR)/images/$(SAMPLE_NAME)/$(SAMPLE_NAME)_Sxxx_00000.h5 +IMAGE_DIR_MERGE_STR = "images/%s" +#IMAGE_DIR_MERGE_STR = "hdf5s/%s" +SCAN_NUMBER_MERGE_STR = "S%03d" +#TIFF_FILE_MERGE_STR = "S%%03d/%s_S%%03d_%%05d.tif" +#hdf5 filename format string: one h5 file per scan here +# This one is when we still had Sxxx/ folder in place. +#HDF5_FILE_MERGE_STR = "S%%03d/%s_S%%03d_00000.h5" +HDF5_FILE_MERGE_STR = "%s_S%%03d_00000.h5" +# +++++++++++++++ + +class s28waxpcsSpecDataSource(SpecXMLDrivenDataSource): + ''' + Class to load data from spec file and configuration xml files from + for the way that data is collected at sector 33. + :members + ''' + + + def __init__(self, + projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs): + ''' + Constructor + :param projectDir: Directory holding the project file to open + :param projectName: First part of file name for the project + :param projectExt: File extension for the project file. + :param instConfigFile: Full path to Instrument configuration file. + :param detConfigFile: Full path to the detector configuration file + :param kwargs: Assorted keyword arguments + + :rtype: s28waxpcsSpecDataSource + ''' + super(s28waxpcsSpecDataSource, self).__init__(projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs) + + def _calc_eulerian_from_kappa(self, primaryAngles=None, + referenceAngles = None): + """ + Calculate the eulerian sample angles from the kappa stage angles. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + """ + + keta = np.deg2rad(referenceAngles[:,0]) + kappa = np.deg2rad(referenceAngles[:,1]) + kphi = np.deg2rad(referenceAngles[:,2]) + kappaParam = self.instConfig.getSampleAngleMappingParameter('kappa') + + try: + if kappaParam != None: + self.kalpha = np.deg2rad(float(kappaParam)) + else: + self.kalpha = np.deg2rad(50.000) + kappaInvertedParam = \ + self.instConfig.getSampleAngleMappingParameter('kappaInverted') + if kappaInvertedParam != None: + self.kappa_inverted = self.to_bool(kappaInvertedParam) + else: + self.kappa_inverted = False + except Exception as ex: + raise RSMap3DException("Error trying to get parameter for " + \ + "sampleAngleMappingFunction " + \ + "_calc_eulerian_from_kappa in inst config " + \ + "file\n" + \ + str(ex)) + + _t1 = np.arctan(np.tan(kappa / 2.0) * np.cos(self.kalpha)) + if self.kappa_inverted: + eta = np.rad2deg(keta + _t1) + phi = np.rad2deg(kphi + _t1) + else: + eta = np.rad2deg(keta - _t1) + phi = np.rad2deg(kphi - _t1) + chi = 2.0 * np.rad2deg(np.arcsin(np.sin(kappa / 2.0) * \ + np.sin(self.kalpha))) + + return eta, chi, phi + + def _calc_replace_angle_values(self , primaryAngles=None, + referenceAngles=None): + ''' + Fix a situation were some constant angle values have been + recorded incorrectly and need to be fixed. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + ''' + logger.info( "Running " + __name__) + angles = [] + logger.debug("referenceAngles " + str(referenceAngles)) + mappingAngles = self.instConfig.getSampleAngleMappingReferenceAngles() + + logger.debug( "mappingAngles" + str(mappingAngles)) + for ii in range(len(referenceAngles)): + replaceVal = \ + float(self.instConfig.getSampleAngleMappingReferenceAngleAttrib( \ + number= str(mappingAngles[ii]), \ + attribName='replaceValue')) + logger.debug( "primary Angles" + str(referenceAngles)) + angles.append(replaceVal* np.ones(len(referenceAngles[:,ii]),)) + logger.debug("Angles" + str( angles)) + return angles + + def fixGeoAngles(self, scan, angles): + ''' + Fix the angles using a user selected function. + :param scan: scan to set the angles for + :param angles: Array of angles to set for this scan + ''' + logger.debug( "starting " + __name__) + needToCorrect = False + refAngleNames = self.instConfig.getSampleAngleMappingReferenceAngles() + for refAngleName in refAngleNames: + alwaysFix = self.instConfig.getSampleAngleMappingAlwaysFix() + if refAngleName in scan.L or alwaysFix: + needToCorrect = True + + if needToCorrect: + logger.debug( __name__ + ": Fixing angles") + refAngles = self.getScanAngles(scan, refAngleNames) + primaryAngles = self.instConfig.getSampleAngleMappingPrimaryAngles() + functionName = self.instConfig.getSampleAngleMappingFunctionName() + functionModuleName = self.instConfig.getSampleAngleMappingFunctionModule() + logger.debug("sampleMappingFunction moduleName %s" % functionModuleName) + #Call a defined method to calculate angles from the reference angles. + moduleSource = self + if functionModuleName != None: + functionModule = importlib.import_module(functionModuleName) + logger.debug("dir(functionModule)" % dir(functionModule)) + moduleSource = functionModule + method = getattr(moduleSource, functionName) + fixedAngles = method(primaryAngles=primaryAngles, + referenceAngles=refAngles) + logger.debug ("fixed Angles: " + str(fixedAngles)) + for i in range(len(primaryAngles)): + logger.debug ("Fixing primaryAngles: %d " % primaryAngles[i]) + angles[:,primaryAngles[i]-1] = fixedAngles[i] + + def getGeoAngles(self, scan, angleNames): + """ + This function returns all of the geometry angles for the + for the scan as a N-by-num_geo array, where N is the number of scan + points and num_geo is the number of geometry motors. + """ +# scan = self.sd[scanNo] + geoAngles = self.getScanAngles(scan, angleNames) + if not (self.instConfig.getSampleAngleMappingFunctionName() == ""): + tb = None + try: + self.fixGeoAngles(scan, geoAngles) + except Exception as ex: + tb = traceback.format_exc() + raise RSMap3DException("Handling exception in getGeoAngles." + \ + "\n" + \ + str(ex) + \ + "\n" + \ + str(tb)) + logger.debug("getGeoAngles:\n" + str(geoAngles)) + return geoAngles + + def getUBMatrix(self, scan): + """ + Read UB matrix from the #G3 line from the spec file. + """ + try: + g3 = scan.G["G3"].strip().split() + g3 = np.array(list(map(float, g3))) + ub = g3.reshape(-1,3) + logger.debug("ub " +str(ub)) + return ub + except: + logger.error("Unable to read UB Matrix from G3") + logger.error( '-'*60) + traceback.print_exc(file=sys.stdout) + logger.error('-'*60) + + + def hotpixelkill(self, areaData): + """ + function to remove hot pixels from CCD frames + ADD REMOVE VALUES IF NEEDED! + :param areaData: area detector data + """ + + for pixel in self.getBadPixels(): + badLoc = pixel.getBadLocation() + replaceLoc = pixel.getReplacementLocation() + areaData[badLoc[0],badLoc[1]] = \ + areaData[replaceLoc[0],replaceLoc[1]] + + return areaData + + def loadSource(self, mapHKL=False): + ''' + This method does the work of loading data from the files. This has been + split off from the constructor to allow this to be threaded and later + canceled. + :param mapHKL: boolean to mark if the data should be mapped to HKL + ''' + # Load up the instrument configuration file + self.loadInstrumentXMLConfig() + #Load up the detector configuration file + self.loadDetectorXMLConfig() + + self.specFile = os.path.join(self.projectDir, self.projectName + \ + self.projectExt) + imageDir = os.path.join(self.projectDir, \ + IMAGE_DIR_MERGE_STR % self.projectName) + + # +++++++++++++++ + # for hdf5 file,use the same property but different format + self.imageFileTmp = os.path.join(imageDir, \ + HDF5_FILE_MERGE_STR % + (self.projectName)) + # +++++++++++++++ + # if needed load up the bad pixel file. + if not (self.badPixelFile is None): + + badPixelFile = PilatusBadPixelFile(self.badPixelFile) + self.badPixels = badPixelFile.getBadPixels() + + # id needed load the flat field file + if not (self.flatFieldFile is None): + self.flatFieldData = np.array(Image.open(self.flatFieldFile)).T + # Load scan information from the spec file + try: + self.sd = SpecDataFile(self.specFile) + self.mapHKL = mapHKL + maxScan = int(self.sd.getMaxScanNumber()) + logger.debug("Number of Scans" + str(maxScan)) + if self.scans is None: + self.scans = range(1, maxScan+1) + imagePath = os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName) + + self.imageBounds = {} + self.imageToBeUsed = {} + self.availableScans = [] + self.incidentEnergy = {} + self.ubMatrix = {} + self.scanType = {} + self.progress = 0 + self.progressInc = 1 + #======= ZZ, 2020/02/19, add initialization + self.progressMax = 100 + #======= + # Zero the progress bar at the beginning. + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + for scan in self.scans: + if (self.cancelLoad): + self.cancelLoad = False + raise LoadCanceledException(CANCEL_STR) + + else: + # Ok, this needs to be changed, too, if we get rid of Sxxx/ folder. + # I don't think it looks for the images in this section, but just to + # make sure they do exsit. + # ZZ 2026-03-26 + #if (os.path.exists(os.path.join(imagePath, \ + # SCAN_NUMBER_MERGE_STR % scan))): + if (os.path.exists( self.imageFileTmp % (scan) )): + try: + curScan = self.sd.scans[str(scan)] + self.scanType[scan] = \ + self.sd.scans[str(scan)].scanCmd.split()[0] + angles = self.getGeoAngles(curScan, self.angleNames) + self.availableScans.append(scan) + if self.mapHKL==True: + self.ubMatrix[scan] = self.getUBMatrix(curScan) + if self.ubMatrix[scan] is None: + raise s28waxpcsSpecFileException("UB matrix " + \ + "not found.") + else: + self.ubMatrix[scan] = None + self.incidentEnergy[scan] = 12398.4 /float(curScan.G['G4'].split()[3]) + _start_time = time.time() + self.imageBounds[scan] = \ + self.findImageQs(angles, \ + self.ubMatrix[scan], \ + self.incidentEnergy[scan]) + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + logger.info (('Elapsed time for Finding qs for scan %d: ' + + '%.3f seconds') % \ + (scan, (time.time() - _start_time))) + except ScanDataMissingException: + logger.error( "Scan " + str(scan) + " has no data") + #Make sure to show 100% completion + if self.progressUpdater is not None: + self.progressUpdater(self.progressMax, self.progressMax) + except IOError: + raise IOError( "Cannot open file " + str(self.specFile)) + if len(self.getAvailableScans()) == 0: + raise ScanDataMissingException("Could not find scan data for " + \ + "input file \n" + self.specFile + \ + "\nOne possible reason for this " + \ + "is that the image files are " + \ + "missing. Images are assumed " + \ + "to be in " + \ + os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName)) + + self.availableScanTypes = set(self.scanType.values()) + + + + def to_bool(self, value): + """ + Note this method found in answer to: + http://stackoverflow.com/questions/715417/converting-from-a-string-to-boolean-in-python + Converts 'something' to boolean. Raises exception if it gets a string + it doesn't handle. + Case is ignored for strings. These string values are handled: + True: 'True', "1", "TRue", "yes", "y", "t" + False: "", "0", "faLse", "no", "n", "f" + Non-string values are passed to bool. + """ + if type(value) == type(''): + if value.lower() in ("yes", "y", "true", "t", "1"): + return True + if value.lower() in ("no", "n", "false", "f", "0", ""): + return False + raise Exception('Invalid value for boolean conversion: ' + value) + return bool(value) + + def rawmap(self,scans, angdelta=[0,0,0,0,0], + adframes=None, mask = None): + """ + read ad frames and and convert them in reciprocal space + angular coordinates are taken from the spec file + or read from the edf file header when no scan number is given (scannr=None) + """ + + if mask is None: + mask_was_none = True + #mask = [True] * len(self.getImageToBeUsed()[scans[0]]) + else: + mask_was_none = False + #sd = spec.SpecDataFile(self.specFile) + intensity = np.array([]) + + # fourc goniometer in fourc coordinates + # convention for coordinate system: + # x: upwards; + # y: along the incident beam; + # z: "outboard" (makes coordinate system right-handed). + # QConversion will set up the goniometer geometry. + # So the first argument describes the sample rotations, the second the + # detector rotations and the third the primary beam direction. + qconv = xu.experiment.QConversion(self.getSampleCircleDirections(), \ + self.getDetectorCircleDirections(), \ + self.getPrimaryBeamDirection()) + + # define experimental class for angle conversion + # + # ipdir: inplane reference direction (ipdir points into the primary beam + # direction at zero angles) + # ndir: surface normal of your sample (ndir points in a direction + # perpendicular to the primary beam and the innermost detector + # rotation axis) + en = self.getIncidentEnergy() + hxrd = xu.HXRD(self.getInplaneReferenceDirection(), \ + self.getSampleSurfaceNormalDirection(), \ + en=en[self.getAvailableScans()[0]], \ + qconv=qconv) + + + # initialize area detector properties + if (self.getDetectorPixelWidth() != None ) and \ + (self.getDistanceToDetector() != None): + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + pwidth1=self.getDetectorPixelWidth()[0], \ + pwidth2=self.getDetectorPixelWidth()[1], \ + distance=self.getDistanceToDetector(), \ + Nav=self.getNumPixelsToAverage(), \ + roi=self.getDetectorROI()) + else: + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + chpdeg1=self.getDetectorChannelsPerDegree()[0], \ + chpdeg2=self.getDetectorChannelsPerDegree()[1], \ + Nav=self.getNumPixelsToAverage(), + roi=self.getDetectorROI()) + + angleNames = self.getAngles() + scanAngle = {} + for i in range(len(angleNames)): + scanAngle[i] = np.array([]) + + offset = 0 + imageToBeUsed = self.getImageToBeUsed() + monitorName = self.getMonitorName() + monitorScaleFactor = self.getMonitorScaleFactor() + filterName = self.getFilterName() + filterScaleFactor = self.getFilterScaleFactor() + for scannr in scans: + if self.haltMap: + raise ProcessCanceledException("Process Canceled") + scan = self.sd.scans[str(scannr)] + angles = self.getGeoAngles(scan, angleNames) + scanAngle1 = {} + scanAngle2 = {} + for i in range(len(angleNames)): + scanAngle1[i] = angles[:,i] + scanAngle2[i] = [] + if monitorName != None: + monitor_data = scan.data.get(monitorName) + if monitor_data is None: + raise IOError("Did not find Monitor source '" + \ + monitorName + \ + "' in the Spec file. Make sure " + \ + "monitorName is correct in the " + \ + "instrument Config file") + if filterName != None: + filter_data = scan.data.get(filterName) + if filter_data is None: + raise IOError("Did not find filter source '" + \ + filterName + \ + "' in the Spec file. Make sure " + \ + "filterName is correct in the " + \ + "instrument Config file") + # read in the image data + arrayInitializedForScan = False + foundIndex = 0 + + if mask_was_none: + mask = [True] * len(self.getImageToBeUsed()[scannr]) + + # +++++++++++++++ + # Read HDF5 file here, brutal force. Need better handle. + h5file = self.imageFileTmp % (scannr) + h5data = h5py.File(h5file, "r") + scan_images = h5data["entry"]["data"]["data"] + # +++++++++++++++ + + + for ind in range(len(scan.data[list(scan.data.keys())[0]])): + if imageToBeUsed[scannr][ind] and mask[ind]: + # +++++++++++++++ + # Read HDF5 file here + #im = Image.open(self.imageFileTmp % (scannr, scannr, ind)) + #img = np.array(im.getdata()).reshape(im.size[1],im.size[0]).T + #h5file = self.imageFileTmp % (scannr, scannr, ind) + #h5data = h5py.File(h5file, "r") + #im = h5data["entry"]["data"]["data"][0,:,:] + img = np.array(scan_images[ind,:,:]).T + # +++++++++++++++ + + img = self.hotpixelkill(img) + ff_data = self.getFlatFieldData() + if not (ff_data is None): + img = img * ff_data + # reduce data siz + img2 = xu.blockAverage2D(img, + self.getNumPixelsToAverage()[0], \ + self.getNumPixelsToAverage()[1], \ + roi=self.getDetectorROI()) + + # apply intensity corrections + if monitorName != None: + img2 = img2 / monitor_data[ind] * monitorScaleFactor + if filterName != None: + img2 = img2 / filter_data[ind] * filterScaleFactor + + # initialize data array + if not arrayInitializedForScan: + imagesToProcess = [imageToBeUsed[scannr][i] and mask[i] for i in range(len(imageToBeUsed[scannr]))] + if not intensity.shape[0]: + intensity = np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape) + arrayInitializedForScan = True + else: + offset = intensity.shape[0] + intensity = np.concatenate( + (intensity, + (np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape))), + axis=0) + arrayInitializedForScan = True + # add data to intensity array + intensity[foundIndex+offset,:,:] = img2 + for i in range(len(angleNames)): +# logger.debug("appending angles to angle2 " + +# str(scanAngle1[i][ind])) + scanAngle2[i].append(scanAngle1[i][ind]) + foundIndex += 1 + if len(scanAngle2[0]) > 0: + for i in range(len(angleNames)): + scanAngle[i] = \ + np.concatenate((scanAngle[i], np.array(scanAngle2[i])), \ + axis=0) + # transform scan angles to reciprocal space coordinates for all detector pixels + angleList = [] + for i in range(len(angleNames)): + angleList.append(scanAngle[i]) + if self.ubMatrix[scans[0]] is None: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage()) + else: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage(), \ + UB = self.ubMatrix[scans[0]]) + + + # apply selected transform + qxTrans, qyTrans, qzTrans = \ + self.transform.do3DTransform(qx, qy, qz) + + + return qxTrans, qyTrans, qzTrans, intensity + +class LoadCanceledException(RSMap3DException): + ''' + Exception Thrown when loading data is canceled. + ''' + def __init__(self, message): + super(LoadCanceledException, self).__init__(message) + +class s28waxpcsSpecFileException(RSMap3DException): + ''' + Exception class to be raised if there is a problem loading information + from a spec file + file + ''' + def __init__(self, message): + super(s28waxpcsSpecFileException, self).__init__(message) + + + diff --git a/rsMap3D/datasource/s8waxpcsSpecDataSource.py b/rsMap3D/datasource/s8waxpcsSpecDataSource.py new file mode 100644 index 0000000..bde8dbd --- /dev/null +++ b/rsMap3D/datasource/s8waxpcsSpecDataSource.py @@ -0,0 +1,574 @@ +''' + Copyright (c) 2012, UChicago Argonne, LLC + See LICENSE file. +''' +import os +from spec2nexus.spec import SpecDataFile +from rsMap3D.exception.rsmap3dexception import RSMap3DException,\ + ScanDataMissingException +from rsMap3D.gui.rsm3dcommonstrings import CANCEL_STR +from rsMap3D.datasource.pilatusbadpixelfile import PilatusBadPixelFile +from rsMap3D.mappers.abstractmapper import ProcessCanceledException +from rsMap3D.datasource.specxmldrivendatasource import SpecXMLDrivenDataSource +import numpy as np +import xrayutilities as xu +import time +import sys,traceback +import logging +import importlib +import h5py +import hdf5plugin +try: + from PIL import Image +except ImportError: + import Image +logger = logging.getLogger(__name__) + + +# +++++++++++++++ +# Make changes to work with HDF5 from 8-ID-E WA-XPCS data, brutal forced +# I do need to generate folder structure simular to our Tiff case +# i.e. $(PROJECT_DIR)/images/$(SAMPLE_NAME)/Snnn/*.h5 +IMAGE_DIR_MERGE_STR = "images/%s" +#IMAGE_DIR_MERGE_STR = "hdf5s/%s" +SCAN_NUMBER_MERGE_STR = "S%03d" +#TIFF_FILE_MERGE_STR = "S%%03d/%s_S%%03d_%%05d.tif" +#hdf5 filename format string: one h5 file per scan here +HDF5_FILE_MERGE_STR = "S%%03d/%s_S%%03d_00000.h5" +# +++++++++++++++ + +class s8waxpcsSpecDataSource(SpecXMLDrivenDataSource): + ''' + Class to load data from spec file and configuration xml files from + for the way that data is collected at sector 33. + :members + ''' + + + def __init__(self, + projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs): + ''' + Constructor + :param projectDir: Directory holding the project file to open + :param projectName: First part of file name for the project + :param projectExt: File extension for the project file. + :param instConfigFile: Full path to Instrument configuration file. + :param detConfigFile: Full path to the detector configuration file + :param kwargs: Assorted keyword arguments + + :rtype: s8waxpcsSpecDataSource + ''' + super(s8waxpcsSpecDataSource, self).__init__(projectDir, + projectName, + projectExtension, + instConfigFile, + detConfigFile, + **kwargs) + + def _calc_eulerian_from_kappa(self, primaryAngles=None, + referenceAngles = None): + """ + Calculate the eulerian sample angles from the kappa stage angles. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + """ + + keta = np.deg2rad(referenceAngles[:,0]) + kappa = np.deg2rad(referenceAngles[:,1]) + kphi = np.deg2rad(referenceAngles[:,2]) + kappaParam = self.instConfig.getSampleAngleMappingParameter('kappa') + + try: + if kappaParam != None: + self.kalpha = np.deg2rad(float(kappaParam)) + else: + self.kalpha = np.deg2rad(50.000) + kappaInvertedParam = \ + self.instConfig.getSampleAngleMappingParameter('kappaInverted') + if kappaInvertedParam != None: + self.kappa_inverted = self.to_bool(kappaInvertedParam) + else: + self.kappa_inverted = False + except Exception as ex: + raise RSMap3DException("Error trying to get parameter for " + \ + "sampleAngleMappingFunction " + \ + "_calc_eulerian_from_kappa in inst config " + \ + "file\n" + \ + str(ex)) + + _t1 = np.arctan(np.tan(kappa / 2.0) * np.cos(self.kalpha)) + if self.kappa_inverted: + eta = np.rad2deg(keta + _t1) + phi = np.rad2deg(kphi + _t1) + else: + eta = np.rad2deg(keta - _t1) + phi = np.rad2deg(kphi - _t1) + chi = 2.0 * np.rad2deg(np.arcsin(np.sin(kappa / 2.0) * \ + np.sin(self.kalpha))) + + return eta, chi, phi + + def _calc_replace_angle_values(self , primaryAngles=None, + referenceAngles=None): + ''' + Fix a situation were some constant angle values have been + recorded incorrectly and need to be fixed. + :param primaryAngles: list of sample axis numbers to be handled by + the conversion + :param referenceAngles: list of reference angles to be used in angle + conversion + ''' + logger.info( "Running " + __name__) + angles = [] + logger.debug("referenceAngles " + str(referenceAngles)) + mappingAngles = self.instConfig.getSampleAngleMappingReferenceAngles() + + logger.debug( "mappingAngles" + str(mappingAngles)) + for ii in range(len(referenceAngles)): + replaceVal = \ + float(self.instConfig.getSampleAngleMappingReferenceAngleAttrib( \ + number= str(mappingAngles[ii]), \ + attribName='replaceValue')) + logger.debug( "primary Angles" + str(referenceAngles)) + angles.append(replaceVal* np.ones(len(referenceAngles[:,ii]),)) + logger.debug("Angles" + str( angles)) + return angles + + def fixGeoAngles(self, scan, angles): + ''' + Fix the angles using a user selected function. + :param scan: scan to set the angles for + :param angles: Array of angles to set for this scan + ''' + logger.debug( "starting " + __name__) + needToCorrect = False + refAngleNames = self.instConfig.getSampleAngleMappingReferenceAngles() + for refAngleName in refAngleNames: + alwaysFix = self.instConfig.getSampleAngleMappingAlwaysFix() + if refAngleName in scan.L or alwaysFix: + needToCorrect = True + + if needToCorrect: + logger.debug( __name__ + ": Fixing angles") + refAngles = self.getScanAngles(scan, refAngleNames) + primaryAngles = self.instConfig.getSampleAngleMappingPrimaryAngles() + functionName = self.instConfig.getSampleAngleMappingFunctionName() + functionModuleName = self.instConfig.getSampleAngleMappingFunctionModule() + logger.debug("sampleMappingFunction moduleName %s" % functionModuleName) + #Call a defined method to calculate angles from the reference angles. + moduleSource = self + if functionModuleName != None: + functionModule = importlib.import_module(functionModuleName) + logger.debug("dir(functionModule)" % dir(functionModule)) + moduleSource = functionModule + method = getattr(moduleSource, functionName) + fixedAngles = method(primaryAngles=primaryAngles, + referenceAngles=refAngles) + logger.debug ("fixed Angles: " + str(fixedAngles)) + for i in range(len(primaryAngles)): + logger.debug ("Fixing primaryAngles: %d " % primaryAngles[i]) + angles[:,primaryAngles[i]-1] = fixedAngles[i] + + def getGeoAngles(self, scan, angleNames): + """ + This function returns all of the geometry angles for the + for the scan as a N-by-num_geo array, where N is the number of scan + points and num_geo is the number of geometry motors. + """ +# scan = self.sd[scanNo] + geoAngles = self.getScanAngles(scan, angleNames) + if not (self.instConfig.getSampleAngleMappingFunctionName() == ""): + tb = None + try: + self.fixGeoAngles(scan, geoAngles) + except Exception as ex: + tb = traceback.format_exc() + raise RSMap3DException("Handling exception in getGeoAngles." + \ + "\n" + \ + str(ex) + \ + "\n" + \ + str(tb)) + logger.debug("getGeoAngles:\n" + str(geoAngles)) + return geoAngles + + def getUBMatrix(self, scan): + """ + Read UB matrix from the #G3 line from the spec file. + """ + try: + g3 = scan.G["G3"].strip().split() + g3 = np.array(list(map(float, g3))) + ub = g3.reshape(-1,3) + logger.debug("ub " +str(ub)) + return ub + except: + logger.error("Unable to read UB Matrix from G3") + logger.error( '-'*60) + traceback.print_exc(file=sys.stdout) + logger.error('-'*60) + + + def hotpixelkill(self, areaData): + """ + function to remove hot pixels from CCD frames + ADD REMOVE VALUES IF NEEDED! + :param areaData: area detector data + """ + + for pixel in self.getBadPixels(): + badLoc = pixel.getBadLocation() + replaceLoc = pixel.getReplacementLocation() + areaData[badLoc[0],badLoc[1]] = \ + areaData[replaceLoc[0],replaceLoc[1]] + + return areaData + + def loadSource(self, mapHKL=False): + ''' + This method does the work of loading data from the files. This has been + split off from the constructor to allow this to be threaded and later + canceled. + :param mapHKL: boolean to mark if the data should be mapped to HKL + ''' + # Load up the instrument configuration file + self.loadInstrumentXMLConfig() + #Load up the detector configuration file + self.loadDetectorXMLConfig() + + self.specFile = os.path.join(self.projectDir, self.projectName + \ + self.projectExt) + imageDir = os.path.join(self.projectDir, \ + IMAGE_DIR_MERGE_STR % self.projectName) + + # +++++++++++++++ + # for hdf5 file,use the same property but different format + self.imageFileTmp = os.path.join(imageDir, \ + HDF5_FILE_MERGE_STR % + (self.projectName)) + # +++++++++++++++ + # if needed load up the bad pixel file. + if not (self.badPixelFile is None): + + badPixelFile = PilatusBadPixelFile(self.badPixelFile) + self.badPixels = badPixelFile.getBadPixels() + + # id needed load the flat field file + if not (self.flatFieldFile is None): + self.flatFieldData = np.array(Image.open(self.flatFieldFile)).T + # Load scan information from the spec file + try: + self.sd = SpecDataFile(self.specFile) + self.mapHKL = mapHKL + maxScan = int(self.sd.getMaxScanNumber()) + logger.debug("Number of Scans" + str(maxScan)) + if self.scans is None: + self.scans = range(1, maxScan+1) + imagePath = os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName) + + self.imageBounds = {} + self.imageToBeUsed = {} + self.availableScans = [] + self.incidentEnergy = {} + self.ubMatrix = {} + self.scanType = {} + self.progress = 0 + self.progressInc = 1 + #======= ZZ, 2020/02/19, add initialization + self.progressMax = 100 + #======= + # Zero the progress bar at the beginning. + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + for scan in self.scans: + if (self.cancelLoad): + self.cancelLoad = False + raise LoadCanceledException(CANCEL_STR) + + else: + if (os.path.exists(os.path.join(imagePath, \ + SCAN_NUMBER_MERGE_STR % scan))): + try: + curScan = self.sd.scans[str(scan)] + self.scanType[scan] = \ + self.sd.scans[str(scan)].scanCmd.split()[0] + angles = self.getGeoAngles(curScan, self.angleNames) + self.availableScans.append(scan) + if self.mapHKL==True: + self.ubMatrix[scan] = self.getUBMatrix(curScan) + if self.ubMatrix[scan] is None: + raise s8waxpcsSpecFileException("UB matrix " + \ + "not found.") + else: + self.ubMatrix[scan] = None + self.incidentEnergy[scan] = 12398.4 /float(curScan.G['G4'].split()[3]) + _start_time = time.time() + self.imageBounds[scan] = \ + self.findImageQs(angles, \ + self.ubMatrix[scan], \ + self.incidentEnergy[scan]) + if self.progressUpdater is not None: + self.progressUpdater(self.progress, self.progressMax) + logger.info (('Elapsed time for Finding qs for scan %d: ' + + '%.3f seconds') % \ + (scan, (time.time() - _start_time))) + except ScanDataMissingException: + logger.error( "Scan " + str(scan) + " has no data") + #Make sure to show 100% completion + if self.progressUpdater is not None: + self.progressUpdater(self.progressMax, self.progressMax) + except IOError: + raise IOError( "Cannot open file " + str(self.specFile)) + if len(self.getAvailableScans()) == 0: + raise ScanDataMissingException("Could not find scan data for " + \ + "input file \n" + self.specFile + \ + "\nOne possible reason for this " + \ + "is that the image files are " + \ + "missing. Images are assumed " + \ + "to be in " + \ + os.path.join(self.projectDir, + IMAGE_DIR_MERGE_STR % self.projectName)) + + self.availableScanTypes = set(self.scanType.values()) + + + + def to_bool(self, value): + """ + Note this method found in answer to: + http://stackoverflow.com/questions/715417/converting-from-a-string-to-boolean-in-python + Converts 'something' to boolean. Raises exception if it gets a string + it doesn't handle. + Case is ignored for strings. These string values are handled: + True: 'True', "1", "TRue", "yes", "y", "t" + False: "", "0", "faLse", "no", "n", "f" + Non-string values are passed to bool. + """ + if type(value) == type(''): + if value.lower() in ("yes", "y", "true", "t", "1"): + return True + if value.lower() in ("no", "n", "false", "f", "0", ""): + return False + raise Exception('Invalid value for boolean conversion: ' + value) + return bool(value) + + def rawmap(self,scans, angdelta=[0,0,0,0,0], + adframes=None, mask = None): + """ + read ad frames and and convert them in reciprocal space + angular coordinates are taken from the spec file + or read from the edf file header when no scan number is given (scannr=None) + """ + + if mask is None: + mask_was_none = True + #mask = [True] * len(self.getImageToBeUsed()[scans[0]]) + else: + mask_was_none = False + #sd = spec.SpecDataFile(self.specFile) + intensity = np.array([]) + + # fourc goniometer in fourc coordinates + # convention for coordinate system: + # x: upwards; + # y: along the incident beam; + # z: "outboard" (makes coordinate system right-handed). + # QConversion will set up the goniometer geometry. + # So the first argument describes the sample rotations, the second the + # detector rotations and the third the primary beam direction. + qconv = xu.experiment.QConversion(self.getSampleCircleDirections(), \ + self.getDetectorCircleDirections(), \ + self.getPrimaryBeamDirection()) + + # define experimental class for angle conversion + # + # ipdir: inplane reference direction (ipdir points into the primary beam + # direction at zero angles) + # ndir: surface normal of your sample (ndir points in a direction + # perpendicular to the primary beam and the innermost detector + # rotation axis) + en = self.getIncidentEnergy() + hxrd = xu.HXRD(self.getInplaneReferenceDirection(), \ + self.getSampleSurfaceNormalDirection(), \ + en=en[self.getAvailableScans()[0]], \ + qconv=qconv) + + + # initialize area detector properties + if (self.getDetectorPixelWidth() != None ) and \ + (self.getDistanceToDetector() != None): + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + pwidth1=self.getDetectorPixelWidth()[0], \ + pwidth2=self.getDetectorPixelWidth()[1], \ + distance=self.getDistanceToDetector(), \ + Nav=self.getNumPixelsToAverage(), \ + roi=self.getDetectorROI()) + else: + hxrd.Ang2Q.init_area(self.getDetectorPixelDirection1(), \ + self.getDetectorPixelDirection2(), \ + cch1=self.getDetectorCenterChannel()[0], \ + cch2=self.getDetectorCenterChannel()[1], \ + Nch1=self.getDetectorDimensions()[0], \ + Nch2=self.getDetectorDimensions()[1], \ + chpdeg1=self.getDetectorChannelsPerDegree()[0], \ + chpdeg2=self.getDetectorChannelsPerDegree()[1], \ + Nav=self.getNumPixelsToAverage(), + roi=self.getDetectorROI()) + + angleNames = self.getAngles() + scanAngle = {} + for i in range(len(angleNames)): + scanAngle[i] = np.array([]) + + offset = 0 + imageToBeUsed = self.getImageToBeUsed() + monitorName = self.getMonitorName() + monitorScaleFactor = self.getMonitorScaleFactor() + filterName = self.getFilterName() + filterScaleFactor = self.getFilterScaleFactor() + for scannr in scans: + if self.haltMap: + raise ProcessCanceledException("Process Canceled") + scan = self.sd.scans[str(scannr)] + angles = self.getGeoAngles(scan, angleNames) + scanAngle1 = {} + scanAngle2 = {} + for i in range(len(angleNames)): + scanAngle1[i] = angles[:,i] + scanAngle2[i] = [] + if monitorName != None: + monitor_data = scan.data.get(monitorName) + if monitor_data is None: + raise IOError("Did not find Monitor source '" + \ + monitorName + \ + "' in the Spec file. Make sure " + \ + "monitorName is correct in the " + \ + "instrument Config file") + if filterName != None: + filter_data = scan.data.get(filterName) + if filter_data is None: + raise IOError("Did not find filter source '" + \ + filterName + \ + "' in the Spec file. Make sure " + \ + "filterName is correct in the " + \ + "instrument Config file") + # read in the image data + arrayInitializedForScan = False + foundIndex = 0 + + if mask_was_none: + mask = [True] * len(self.getImageToBeUsed()[scannr]) + + # +++++++++++++++ + # Read HDF5 file here + h5file = self.imageFileTmp % (scannr, scannr) + h5data = h5py.File(h5file, "r") + scan_images = h5data["entry"]["data"]["data"] + # +++++++++++++++ + + + for ind in range(len(scan.data[list(scan.data.keys())[0]])): + if imageToBeUsed[scannr][ind] and mask[ind]: + # +++++++++++++++ + # Read HDF5 file here + #im = Image.open(self.imageFileTmp % (scannr, scannr, ind)) + #img = np.array(im.getdata()).reshape(im.size[1],im.size[0]).T + #h5file = self.imageFileTmp % (scannr, scannr, ind) + #h5data = h5py.File(h5file, "r") + #im = h5data["entry"]["data"]["data"][0,:,:] + img = np.array(scan_images[ind,:,:]).T + # +++++++++++++++ + + img = self.hotpixelkill(img) + ff_data = self.getFlatFieldData() + if not (ff_data is None): + img = img * ff_data + # reduce data siz + img2 = xu.blockAverage2D(img, + self.getNumPixelsToAverage()[0], \ + self.getNumPixelsToAverage()[1], \ + roi=self.getDetectorROI()) + + # apply intensity corrections + if monitorName != None: + img2 = img2 / monitor_data[ind] * monitorScaleFactor + if filterName != None: + img2 = img2 / filter_data[ind] * filterScaleFactor + + # initialize data array + if not arrayInitializedForScan: + imagesToProcess = [imageToBeUsed[scannr][i] and mask[i] for i in range(len(imageToBeUsed[scannr]))] + if not intensity.shape[0]: + intensity = np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape) + arrayInitializedForScan = True + else: + offset = intensity.shape[0] + intensity = np.concatenate( + (intensity, + (np.zeros((np.count_nonzero(imagesToProcess),) + img2.shape))), + axis=0) + arrayInitializedForScan = True + # add data to intensity array + intensity[foundIndex+offset,:,:] = img2 + for i in range(len(angleNames)): +# logger.debug("appending angles to angle2 " + +# str(scanAngle1[i][ind])) + scanAngle2[i].append(scanAngle1[i][ind]) + foundIndex += 1 + if len(scanAngle2[0]) > 0: + for i in range(len(angleNames)): + scanAngle[i] = \ + np.concatenate((scanAngle[i], np.array(scanAngle2[i])), \ + axis=0) + # transform scan angles to reciprocal space coordinates for all detector pixels + angleList = [] + for i in range(len(angleNames)): + angleList.append(scanAngle[i]) + if self.ubMatrix[scans[0]] is None: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage()) + else: + qx, qy, qz = hxrd.Ang2Q.area(*angleList, \ + roi=self.getDetectorROI(), + Nav=self.getNumPixelsToAverage(), \ + UB = self.ubMatrix[scans[0]]) + + + # apply selected transform + qxTrans, qyTrans, qzTrans = \ + self.transform.do3DTransform(qx, qy, qz) + + + return qxTrans, qyTrans, qzTrans, intensity + +class LoadCanceledException(RSMap3DException): + ''' + Exception Thrown when loading data is canceled. + ''' + def __init__(self, message): + super(LoadCanceledException, self).__init__(message) + +class s8waxpcsSpecFileException(RSMap3DException): + ''' + Exception class to be raised if there is a problem loading information + from a spec file + file + ''' + def __init__(self, message): + super(s8waxpcsSpecFileException, self).__init__(message) + + + diff --git a/rsMap3D/gui/input/fileinputcontroller.py b/rsMap3D/gui/input/fileinputcontroller.py index f55d7c4..739e175 100644 --- a/rsMap3D/gui/input/fileinputcontroller.py +++ b/rsMap3D/gui/input/fileinputcontroller.py @@ -19,6 +19,7 @@ from rsMap3D.transforms.polemaptransform3d import PoleMapTransform3D # Input forms Looking for a way to set these up. from rsMap3D.gui.input.s33specscanfileform import S33SpecScanFileForm +from rsMap3D.gui.input.s28specscanfileform import S28SpecScanFileForm from rsMap3D.gui.input.s12specscanfileform import S12SpecScanFileForm from rsMap3D.gui.input.s34hdfescanfileform import S34HDFEScanFileForm from rsMap3D.gui.input.s1highenergydiffractionform import S1HighEnergyDiffractionForm @@ -59,6 +60,7 @@ def __init__(self, parent=None, appConfig=None): #Build a list of fileForms self.fileForms = [] self.fileForms.append(S33SpecScanFileForm) + self.fileForms.append(S28SpecScanFileForm) self.fileForms.append(S12SpecScanFileForm) self.fileForms.append(S34HDFEScanFileForm) self.fileForms.append(S1HighEnergyDiffractionForm) diff --git a/rsMap3D/gui/input/s12specscanfileform.py b/rsMap3D/gui/input/s12specscanfileform.py index 3b252e4..0aaa7f7 100644 --- a/rsMap3D/gui/input/s12specscanfileform.py +++ b/rsMap3D/gui/input/s12specscanfileform.py @@ -1,400 +1,400 @@ -''' - Copyright (c) 2014, UChicago Argonne, LLC - See LICENSE file. -''' -import os - -import logging -from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR -from rsMap3D.gui.output.processpowderscanform import ProcessPowderScanForm -logger = logging.getLogger(__name__) -import PyQt5.QtGui as qtGui -import PyQt5.QtCore as qtCore -import PyQt5.QtWidgets as qtWidgets - -from rsMap3D.gui.rsm3dcommonstrings import WARNING_STR, BROWSE_STR,\ - COMMA_STR, QLINEEDIT_COLOR_STYLE, BLACK, RED, EMPTY_STR,\ - BAD_PIXEL_FILE_FILTER, SELECT_BAD_PIXEL_TITLE, SELECT_FLAT_FIELD_TITLE,\ - TIFF_FILE_FILTER -from rsMap3D.datasource.Sector12SpecDataSource import Sector12SpecDataSource -from rsMap3D.transforms.unitytransform3d import UnityTransform3D -from rsMap3D.transforms.polemaptransform3d import PoleMapTransform3D -from rsMap3D.gui.input.specxmldrivenfileform import SpecXMLDrivenFileForm -from rsMap3D.gui.output.processvtioutputform import ProcessVTIOutputForm -from rsMap3D.gui.output.processimagestackform import ProcessImageStackForm -from PyQt5.QtWidgets import QAbstractButton - -class S12SpecScanFileForm(SpecXMLDrivenFileForm): - ''' - This class presents information for selecting input files - ''' - - FORM_TITLE = "Sector 12 Spec/XML Setup" - - #UPDATE_PROGRESS_SIGNAL = "updateProgress" - # Regular expressions for string validation - PIX_AVG_REGEXP_1 = "^(\d*,*)+$" - PIX_AVG_REGEXP_2 = "^((\d)+,*){2}$" - #Strings for Text Widgets - - NONE_RADIO_NAME = "None" - BAD_PIXEL_RADIO_NAME = "Bad Pixel File" - FLAT_FIELD_RADIO_NAME = "Flat Field Correction" - - @staticmethod - def createInstance(parent=None, appConfig=None): - return S12SpecScanFileForm(parent = parent, appConfig=appConfig) - - def __init__(self, **kwargs): - ''' - Constructor - Layout Widgets on the page and link actions - ''' - logger.debug(METHOD_ENTER_STR) - super(S12SpecScanFileForm, self).__init__(**kwargs) - - #Initialize parameters - self.projectionDirection = [0,0,1] - - #Initialize a couple of widgets to do setup. - self.noFieldRadio.setChecked(True) - self._fieldCorrectionTypeChanged(*(self.noFieldRadio,)) - logger.debug(METHOD_EXIT_STR) - - @qtCore.pyqtSlot() - def _badPixelFileChanged(self): - ''' - Do some verification when the bad pixel file changes - ''' - logger.debug(METHOD_ENTER_STR) - if os.path.isfile(self.badPixelFileTxt.text()) or \ - self.badPixelFileTxt.text() == EMPTY_STR: - self.checkOkToLoad() - else: - message = qtWidgets.QMessageBox() - message.warning(self, \ - WARNING_STR, \ - "The filename entered for the bad pixel " + \ - "file is invalid") - logger.debug(METHOD_EXIT_STR) - - - @qtCore.pyqtSlot() - def _browseBadPixelFileName(self): - ''' - Launch file browser for bad pixel file - ''' - logger.debug(METHOD_ENTER_STR) - if self.badPixelFileTxt.text() == EMPTY_STR: - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_BAD_PIXEL_TITLE, \ - filter=BAD_PIXEL_FILE_FILTER)[0] - else: - fileDirectory = os.path.dirname(str(self.badPixelFileTxt.text())) - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_BAD_PIXEL_TITLE, \ - filter=BAD_PIXEL_FILE_FILTER, \ - directory = fileDirectory)[0] - if fileName != EMPTY_STR: - self.badPixelFileTxt.setText(fileName) - self.badPixelFileTxt.editingFinished.emit() - logger.debug(METHOD_EXIT_STR) - - @qtCore.pyqtSlot() - def _browseFlatFieldFileName(self): - ''' - Launch file browser for Flat field file - ''' - logger.debug(METHOD_ENTER_STR) - if self.flatFieldFileTxt.text() == EMPTY_STR: - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_FLAT_FIELD_TITLE, \ - filter=TIFF_FILE_FILTER)[0] - else: - fileDirectory = os.path.dirname(str(self.flatFieldFileTxt.text())) - fileName = qtWidgets.QFileDialog.getOpenFileName(None, - SELECT_FLAT_FIELD_TITLE, - filter=TIFF_FILE_FILTER, \ - directory = fileDirectory)[0] - if fileName != EMPTY_STR: - self.flatFieldFileTxt.setText(fileName) - self.flatFieldFileTxt.editingFinished.emit() - logger.debug(METHOD_EXIT_STR) - - def checkOkToLoad(self): - ''' - Make sure we have valid file names for project, instrument config, - and the detector config. If we do enable load button. If not disable - the load button - ''' - logger.debug(METHOD_ENTER_STR) - retVal = False - if os.path.isfile(self.projNameTxt.text()) and \ - os.path.isfile(self.instConfigTxt.text()) and \ - os.path.isfile(self.detConfigTxt.text()) and \ - (self.noFieldRadio.isChecked() or \ - (self.badPixelRadio.isChecked() and \ - not (str(self.badPixelFileTxt.text()) == "")) or \ - (self.flatFieldRadio.isChecked() and \ - not (str(self.flatFieldFileTxt.text()) == ""))) and \ - self.pixAvgValid(self.pixAvgTxt.text()) and \ - self.detROIValid(self.detROITxt.text()): - retVal = True - self.loadButton.setEnabled(retVal) - else: - retVal = False - self.loadButton.setDisabled(not retVal) - self.okToLoad.emit(retVal) - logger.debug(METHOD_EXIT_STR) - return retVal - - def _createControlBox(self): - ''' - Create Layout holding controls widgets - ''' - logger.debug(METHOD_ENTER_STR) - controlBox = super(S12SpecScanFileForm, self)._createControlBox() - logger.debug(METHOD_EXIT_STR) - return controlBox - - def _createDataBox(self): - ''' - Create widgets for collecting data - ''' - logger.debug(METHOD_ENTER_STR) - dataBox = super(S12SpecScanFileForm, self)._createDataBox() - dataLayout = dataBox.layout() - row = dataLayout.rowCount() - self._createInstConfig(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createDetConfig(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self.fieldCorrectionGroup = qtWidgets.QButtonGroup(self) - self.noFieldRadio = qtWidgets.QRadioButton(self.NONE_RADIO_NAME) - self.badPixelRadio = qtWidgets.QRadioButton(self.BAD_PIXEL_RADIO_NAME) - self.flatFieldRadio = qtWidgets.QRadioButton(self.FLAT_FIELD_RADIO_NAME) - self.fieldCorrectionGroup.addButton(self.noFieldRadio, 1) - self.fieldCorrectionGroup.addButton(self.badPixelRadio, 2) - self.fieldCorrectionGroup.addButton(self.flatFieldRadio, 3) - self.badPixelFileTxt = qtWidgets.QLineEdit() - self.flatFieldFileTxt = qtWidgets.QLineEdit() - self.badPixelFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) - self.flatFieldFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) - - - dataLayout.addWidget(self.noFieldRadio, row, 0) - row += 1 - dataLayout.addWidget(self.badPixelRadio, row, 0) - dataLayout.addWidget(self.badPixelFileTxt, row, 1) - dataLayout.addWidget(self.badPixelFileBrowseButton, row, 2) - row += 1 - dataLayout.addWidget(self.flatFieldRadio, row, 0) - dataLayout.addWidget(self.flatFieldFileTxt, row, 1) - dataLayout.addWidget(self.flatFieldFileBrowseButton, row, 2) - - row += 1 - label = qtWidgets.QLabel("Number of Pixels To Average:"); - self.pixAvgTxt = qtWidgets.QLineEdit("1,1") - rxAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_1) - self.pixAvgTxt.setValidator(qtGui.QRegExpValidator(rxAvg,self.pixAvgTxt)) - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.pixAvgTxt, row, 1) - - row = dataLayout.rowCount() + 1 - self._createDetectorROIInput(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createScanNumberInput(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createOutputType(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createHKLOutput(dataLayout, row) - - # Add Signals between widgets - self.fieldCorrectionGroup.buttonClicked\ - .connect(self._fieldCorrectionTypeChanged) - - self.badPixelFileTxt.editingFinished.connect(self._badPixelFileChanged) - self.badPixelFileBrowseButton.clicked\ - .connect(self._browseBadPixelFileName) - - self.flatFieldFileTxt.editingFinished\ - .connect(self._flatFieldFileChanged) - self.flatFieldFileBrowseButton.clicked.\ - connect(self._browseFlatFieldFileName) - - self.pixAvgTxt.textChanged.connect(self._pixAvgTxtChanged) - - dataBox.setLayout(dataLayout) - logger.debug(METHOD_EXIT_STR) - return dataBox - - @qtCore.pyqtSlot(QAbstractButton) - def _fieldCorrectionTypeChanged(self, *fieldCorrType): - ''' - React when the field type radio buttons change. Disable/Enable other - widgets as appropriate - ''' - logger.debug(METHOD_ENTER_STR) - if fieldCorrType[0].text() == self.NONE_RADIO_NAME: - self.badPixelFileTxt.setDisabled(True) - self.badPixelFileBrowseButton.setDisabled(True) - self.flatFieldFileTxt.setDisabled(True) - self.flatFieldFileBrowseButton.setDisabled(True) - elif fieldCorrType[0].text() == self.BAD_PIXEL_RADIO_NAME: - self.badPixelFileTxt.setDisabled(False) - self.badPixelFileBrowseButton.setDisabled(False) - self.flatFieldFileTxt.setDisabled(True) - self.flatFieldFileBrowseButton.setDisabled(True) - elif fieldCorrType[0].text() == self.FLAT_FIELD_RADIO_NAME: - self.badPixelFileTxt.setDisabled(True) - self.badPixelFileBrowseButton.setDisabled(True) - self.flatFieldFileTxt.setDisabled(False) - self.flatFieldFileBrowseButton.setDisabled(False) - self.checkOkToLoad() - logger.debug(METHOD_EXIT_STR) - - @qtCore.pyqtSlot() - def _flatFieldFileChanged(self): - ''' - Do some verification when the flat field file changes - ''' - logger.debug(METHOD_ENTER_STR) - if os.path.isfile(self.flatFieldFileTxt.text()) or \ - self.flatFieldFileTxt.text() == "": - self.checkOkToLoad() - else: - message = qtWidgets.QMessageBox() - message.warning(self, \ - WARNING_STR, \ - "The filename entered for the flat field " + \ - "file is invalid") - logger.debug(METHOD_EXIT_STR) - - def getBadPixelFileName(self): - ''' - Return the badPixel file name. If empty or if the bad pixel radio - button is not checked return None - ''' - logger.debug(METHOD_ENTER_STR) - retVal = None - if (str(self.badPixelFileTxt.text()) == EMPTY_STR) or \ - (not self.badPixelRadio.isChecked()): - retVal = None - else: - retVal = str(self.badPixelFileTxt.text()) - logger.debug("Exit " + str(retVal)) - return retVal - - def getDataSource(self): - logger.debug(METHOD_ENTER_STR) - if self.getOutputType() == self.SIMPLE_GRID_MAP_STR: - self.transform = UnityTransform3D() - elif self.getOutputType() == self.POLE_MAP_STR: - self.transform = \ - PoleMapTransform3D(projectionDirection=\ - self.getProjectionDirection()) - else: - self.transform = None - - self.dataSource = \ - Sector12SpecDataSource(str(self.getProjectDir()), \ - str(self.getProjectName()), \ - str(self.getProjectExtension()), \ - str(self.getInstConfigName()), \ - str(self.getDetConfigName()), \ - transform = self.transform, \ - scanList = self.getScanList(), \ - roi = self.getDetectorROI(), \ - pixelsToAverage = \ - self.getPixelsToAverage(), \ - badPixelFile = \ - self.getBadPixelFileName(), \ - flatFieldFile = \ - self.getFlatFieldFileName(), \ - appConfig = self.appConfig - ) - self.dataSource.setProgressUpdater(self.updateProgress) - self.dataSource.setCurrentDetector(self.currentDetector) - self.dataSource.loadSource(mapHKL = self.getMapAsHKL()) - logger.debug(METHOD_EXIT_STR) - return self.dataSource - - def getFlatFieldFileName(self): - ''' - Return the flat field file name. If empty or if the bad pixel radio - button is not checked return None - ''' - logger.debug(METHOD_ENTER_STR) - retVal = None - if (str(self.flatFieldFileTxt.text()) == EMPTY_STR) or \ - (not self.flatFieldRadio.isChecked()): - retVal = None - else: - retVal = str(self.flatFieldFileTxt.text()) - logger.debug(METHOD_EXIT_STR) - return retVal - - def getOutputForms(self): - logger.debug(METHOD_ENTER_STR) - outputForms = [] - outputForms.append(ProcessVTIOutputForm) - outputForms.append(ProcessImageStackForm) - outputForms.append(ProcessPowderScanForm) - logger.debug(METHOD_EXIT_STR) - return outputForms - - def getPixelsToAverage(self): - ''' - :return: the pixels to average as a list - ''' - logger.debug(METHOD_ENTER_STR) - pixelStrings = str(self.pixAvgTxt.text()).split(COMMA_STR) - pixels = [] - for value in pixelStrings: - pixels.append(int(value)) - logger.debug(METHOD_EXIT_STR) - return pixels - - def getProjectionDirection(self): - ''' - Return projection direction for stereographic projections - ''' - logger.debug(METHOD_ENTER_STR) - logger.debug(METHOD_EXIT_STR) - return self.projectionDirection - - - def _pixAvgTxtChanged(self, text): - ''' - Check to make sure the text for pix to average is valid and indicate - by a color change - :param text: new values as a text list - ''' - logger.debug(METHOD_ENTER_STR) - if self.pixAvgValid(text): - self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % BLACK) - else: - self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % RED) - self.checkOkToLoad() - logger.debug(METHOD_EXIT_STR) - - def pixAvgValid(self, text): - ''' - Check to make sure that the pixAvgText is valid - :param text: new values as a text list - ''' - logger.debug(METHOD_ENTER_STR) - retVal = False - rxPixAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_2) - validator = qtGui.QRegExpValidator(rxPixAvg, None) - pos = 0 - if validator.validate(text, pos)[0] == qtGui.QValidator.Acceptable: - retVal = True - else: - retVal = False - logger.debug("Exit " + str(retVal)) - return retVal +''' + Copyright (c) 2014, UChicago Argonne, LLC + See LICENSE file. +''' +import os + +import logging +from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR +from rsMap3D.gui.output.processpowderscanform import ProcessPowderScanForm +logger = logging.getLogger(__name__) +import PyQt5.QtGui as qtGui +import PyQt5.QtCore as qtCore +import PyQt5.QtWidgets as qtWidgets + +from rsMap3D.gui.rsm3dcommonstrings import WARNING_STR, BROWSE_STR,\ + COMMA_STR, QLINEEDIT_COLOR_STYLE, BLACK, RED, EMPTY_STR,\ + BAD_PIXEL_FILE_FILTER, SELECT_BAD_PIXEL_TITLE, SELECT_FLAT_FIELD_TITLE,\ + TIFF_FILE_FILTER +from rsMap3D.datasource.Sector12SpecDataSource import Sector12SpecDataSource +from rsMap3D.transforms.unitytransform3d import UnityTransform3D +from rsMap3D.transforms.polemaptransform3d import PoleMapTransform3D +from rsMap3D.gui.input.specxmldrivenfileform import SpecXMLDrivenFileForm +from rsMap3D.gui.output.processvtioutputform import ProcessVTIOutputForm +from rsMap3D.gui.output.processimagestackform import ProcessImageStackForm +from PyQt5.QtWidgets import QAbstractButton + +class S12SpecScanFileForm(SpecXMLDrivenFileForm): + ''' + This class presents information for selecting input files + ''' + + FORM_TITLE = "Sector 12 Spec/XML Setup" + + #UPDATE_PROGRESS_SIGNAL = "updateProgress" + # Regular expressions for string validation + PIX_AVG_REGEXP_1 = r"^(\d*,*)+$" + PIX_AVG_REGEXP_2 = r"^((\d)+,*){2}$" + #Strings for Text Widgets + + NONE_RADIO_NAME = "None" + BAD_PIXEL_RADIO_NAME = "Bad Pixel File" + FLAT_FIELD_RADIO_NAME = "Flat Field Correction" + + @staticmethod + def createInstance(parent=None, appConfig=None): + return S12SpecScanFileForm(parent = parent, appConfig=appConfig) + + def __init__(self, **kwargs): + ''' + Constructor - Layout Widgets on the page and link actions + ''' + logger.debug(METHOD_ENTER_STR) + super(S12SpecScanFileForm, self).__init__(**kwargs) + + #Initialize parameters + self.projectionDirection = [0,0,1] + + #Initialize a couple of widgets to do setup. + self.noFieldRadio.setChecked(True) + self._fieldCorrectionTypeChanged(*(self.noFieldRadio,)) + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _badPixelFileChanged(self): + ''' + Do some verification when the bad pixel file changes + ''' + logger.debug(METHOD_ENTER_STR) + if os.path.isfile(self.badPixelFileTxt.text()) or \ + self.badPixelFileTxt.text() == EMPTY_STR: + self.checkOkToLoad() + else: + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR, \ + "The filename entered for the bad pixel " + \ + "file is invalid") + logger.debug(METHOD_EXIT_STR) + + + @qtCore.pyqtSlot() + def _browseBadPixelFileName(self): + ''' + Launch file browser for bad pixel file + ''' + logger.debug(METHOD_ENTER_STR) + if self.badPixelFileTxt.text() == EMPTY_STR: + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_BAD_PIXEL_TITLE, \ + filter=BAD_PIXEL_FILE_FILTER)[0] + else: + fileDirectory = os.path.dirname(str(self.badPixelFileTxt.text())) + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_BAD_PIXEL_TITLE, \ + filter=BAD_PIXEL_FILE_FILTER, \ + directory = fileDirectory)[0] + if fileName != EMPTY_STR: + self.badPixelFileTxt.setText(fileName) + self.badPixelFileTxt.editingFinished.emit() + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _browseFlatFieldFileName(self): + ''' + Launch file browser for Flat field file + ''' + logger.debug(METHOD_ENTER_STR) + if self.flatFieldFileTxt.text() == EMPTY_STR: + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_FLAT_FIELD_TITLE, \ + filter=TIFF_FILE_FILTER)[0] + else: + fileDirectory = os.path.dirname(str(self.flatFieldFileTxt.text())) + fileName = qtWidgets.QFileDialog.getOpenFileName(None, + SELECT_FLAT_FIELD_TITLE, + filter=TIFF_FILE_FILTER, \ + directory = fileDirectory)[0] + if fileName != EMPTY_STR: + self.flatFieldFileTxt.setText(fileName) + self.flatFieldFileTxt.editingFinished.emit() + logger.debug(METHOD_EXIT_STR) + + def checkOkToLoad(self): + ''' + Make sure we have valid file names for project, instrument config, + and the detector config. If we do enable load button. If not disable + the load button + ''' + logger.debug(METHOD_ENTER_STR) + retVal = False + if os.path.isfile(self.projNameTxt.text()) and \ + os.path.isfile(self.instConfigTxt.text()) and \ + os.path.isfile(self.detConfigTxt.text()) and \ + (self.noFieldRadio.isChecked() or \ + (self.badPixelRadio.isChecked() and \ + not (str(self.badPixelFileTxt.text()) == "")) or \ + (self.flatFieldRadio.isChecked() and \ + not (str(self.flatFieldFileTxt.text()) == ""))) and \ + self.pixAvgValid(self.pixAvgTxt.text()) and \ + self.detROIValid(self.detROITxt.text()): + retVal = True + self.loadButton.setEnabled(retVal) + else: + retVal = False + self.loadButton.setDisabled(not retVal) + self.okToLoad.emit(retVal) + logger.debug(METHOD_EXIT_STR) + return retVal + + def _createControlBox(self): + ''' + Create Layout holding controls widgets + ''' + logger.debug(METHOD_ENTER_STR) + controlBox = super(S12SpecScanFileForm, self)._createControlBox() + logger.debug(METHOD_EXIT_STR) + return controlBox + + def _createDataBox(self): + ''' + Create widgets for collecting data + ''' + logger.debug(METHOD_ENTER_STR) + dataBox = super(S12SpecScanFileForm, self)._createDataBox() + dataLayout = dataBox.layout() + row = dataLayout.rowCount() + self._createInstConfig(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createDetConfig(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self.fieldCorrectionGroup = qtWidgets.QButtonGroup(self) + self.noFieldRadio = qtWidgets.QRadioButton(self.NONE_RADIO_NAME) + self.badPixelRadio = qtWidgets.QRadioButton(self.BAD_PIXEL_RADIO_NAME) + self.flatFieldRadio = qtWidgets.QRadioButton(self.FLAT_FIELD_RADIO_NAME) + self.fieldCorrectionGroup.addButton(self.noFieldRadio, 1) + self.fieldCorrectionGroup.addButton(self.badPixelRadio, 2) + self.fieldCorrectionGroup.addButton(self.flatFieldRadio, 3) + self.badPixelFileTxt = qtWidgets.QLineEdit() + self.flatFieldFileTxt = qtWidgets.QLineEdit() + self.badPixelFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) + self.flatFieldFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) + + + dataLayout.addWidget(self.noFieldRadio, row, 0) + row += 1 + dataLayout.addWidget(self.badPixelRadio, row, 0) + dataLayout.addWidget(self.badPixelFileTxt, row, 1) + dataLayout.addWidget(self.badPixelFileBrowseButton, row, 2) + row += 1 + dataLayout.addWidget(self.flatFieldRadio, row, 0) + dataLayout.addWidget(self.flatFieldFileTxt, row, 1) + dataLayout.addWidget(self.flatFieldFileBrowseButton, row, 2) + + row += 1 + label = qtWidgets.QLabel("Number of Pixels To Average:"); + self.pixAvgTxt = qtWidgets.QLineEdit("1,1") + rxAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_1) + self.pixAvgTxt.setValidator(qtGui.QRegExpValidator(rxAvg,self.pixAvgTxt)) + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.pixAvgTxt, row, 1) + + row = dataLayout.rowCount() + 1 + self._createDetectorROIInput(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createScanNumberInput(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createOutputType(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createHKLOutput(dataLayout, row) + + # Add Signals between widgets + self.fieldCorrectionGroup.buttonClicked\ + .connect(self._fieldCorrectionTypeChanged) + + self.badPixelFileTxt.editingFinished.connect(self._badPixelFileChanged) + self.badPixelFileBrowseButton.clicked\ + .connect(self._browseBadPixelFileName) + + self.flatFieldFileTxt.editingFinished\ + .connect(self._flatFieldFileChanged) + self.flatFieldFileBrowseButton.clicked.\ + connect(self._browseFlatFieldFileName) + + self.pixAvgTxt.textChanged.connect(self._pixAvgTxtChanged) + + dataBox.setLayout(dataLayout) + logger.debug(METHOD_EXIT_STR) + return dataBox + + @qtCore.pyqtSlot(QAbstractButton) + def _fieldCorrectionTypeChanged(self, *fieldCorrType): + ''' + React when the field type radio buttons change. Disable/Enable other + widgets as appropriate + ''' + logger.debug(METHOD_ENTER_STR) + if fieldCorrType[0].text() == self.NONE_RADIO_NAME: + self.badPixelFileTxt.setDisabled(True) + self.badPixelFileBrowseButton.setDisabled(True) + self.flatFieldFileTxt.setDisabled(True) + self.flatFieldFileBrowseButton.setDisabled(True) + elif fieldCorrType[0].text() == self.BAD_PIXEL_RADIO_NAME: + self.badPixelFileTxt.setDisabled(False) + self.badPixelFileBrowseButton.setDisabled(False) + self.flatFieldFileTxt.setDisabled(True) + self.flatFieldFileBrowseButton.setDisabled(True) + elif fieldCorrType[0].text() == self.FLAT_FIELD_RADIO_NAME: + self.badPixelFileTxt.setDisabled(True) + self.badPixelFileBrowseButton.setDisabled(True) + self.flatFieldFileTxt.setDisabled(False) + self.flatFieldFileBrowseButton.setDisabled(False) + self.checkOkToLoad() + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _flatFieldFileChanged(self): + ''' + Do some verification when the flat field file changes + ''' + logger.debug(METHOD_ENTER_STR) + if os.path.isfile(self.flatFieldFileTxt.text()) or \ + self.flatFieldFileTxt.text() == "": + self.checkOkToLoad() + else: + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR, \ + "The filename entered for the flat field " + \ + "file is invalid") + logger.debug(METHOD_EXIT_STR) + + def getBadPixelFileName(self): + ''' + Return the badPixel file name. If empty or if the bad pixel radio + button is not checked return None + ''' + logger.debug(METHOD_ENTER_STR) + retVal = None + if (str(self.badPixelFileTxt.text()) == EMPTY_STR) or \ + (not self.badPixelRadio.isChecked()): + retVal = None + else: + retVal = str(self.badPixelFileTxt.text()) + logger.debug("Exit " + str(retVal)) + return retVal + + def getDataSource(self): + logger.debug(METHOD_ENTER_STR) + if self.getOutputType() == self.SIMPLE_GRID_MAP_STR: + self.transform = UnityTransform3D() + elif self.getOutputType() == self.POLE_MAP_STR: + self.transform = \ + PoleMapTransform3D(projectionDirection=\ + self.getProjectionDirection()) + else: + self.transform = None + + self.dataSource = \ + Sector12SpecDataSource(str(self.getProjectDir()), \ + str(self.getProjectName()), \ + str(self.getProjectExtension()), \ + str(self.getInstConfigName()), \ + str(self.getDetConfigName()), \ + transform = self.transform, \ + scanList = self.getScanList(), \ + roi = self.getDetectorROI(), \ + pixelsToAverage = \ + self.getPixelsToAverage(), \ + badPixelFile = \ + self.getBadPixelFileName(), \ + flatFieldFile = \ + self.getFlatFieldFileName(), \ + appConfig = self.appConfig + ) + self.dataSource.setProgressUpdater(self.updateProgress) + self.dataSource.setCurrentDetector(self.currentDetector) + self.dataSource.loadSource(mapHKL = self.getMapAsHKL()) + logger.debug(METHOD_EXIT_STR) + return self.dataSource + + def getFlatFieldFileName(self): + ''' + Return the flat field file name. If empty or if the bad pixel radio + button is not checked return None + ''' + logger.debug(METHOD_ENTER_STR) + retVal = None + if (str(self.flatFieldFileTxt.text()) == EMPTY_STR) or \ + (not self.flatFieldRadio.isChecked()): + retVal = None + else: + retVal = str(self.flatFieldFileTxt.text()) + logger.debug(METHOD_EXIT_STR) + return retVal + + def getOutputForms(self): + logger.debug(METHOD_ENTER_STR) + outputForms = [] + outputForms.append(ProcessVTIOutputForm) + outputForms.append(ProcessImageStackForm) + outputForms.append(ProcessPowderScanForm) + logger.debug(METHOD_EXIT_STR) + return outputForms + + def getPixelsToAverage(self): + ''' + :return: the pixels to average as a list + ''' + logger.debug(METHOD_ENTER_STR) + pixelStrings = str(self.pixAvgTxt.text()).split(COMMA_STR) + pixels = [] + for value in pixelStrings: + pixels.append(int(value)) + logger.debug(METHOD_EXIT_STR) + return pixels + + def getProjectionDirection(self): + ''' + Return projection direction for stereographic projections + ''' + logger.debug(METHOD_ENTER_STR) + logger.debug(METHOD_EXIT_STR) + return self.projectionDirection + + + def _pixAvgTxtChanged(self, text): + ''' + Check to make sure the text for pix to average is valid and indicate + by a color change + :param text: new values as a text list + ''' + logger.debug(METHOD_ENTER_STR) + if self.pixAvgValid(text): + self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % BLACK) + else: + self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % RED) + self.checkOkToLoad() + logger.debug(METHOD_EXIT_STR) + + def pixAvgValid(self, text): + ''' + Check to make sure that the pixAvgText is valid + :param text: new values as a text list + ''' + logger.debug(METHOD_ENTER_STR) + retVal = False + rxPixAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_2) + validator = qtGui.QRegExpValidator(rxPixAvg, None) + pos = 0 + if validator.validate(text, pos)[0] == qtGui.QValidator.Acceptable: + retVal = True + else: + retVal = False + logger.debug("Exit " + str(retVal)) + return retVal diff --git a/rsMap3D/gui/input/s28specscanfileform.py b/rsMap3D/gui/input/s28specscanfileform.py new file mode 100644 index 0000000..88195b2 --- /dev/null +++ b/rsMap3D/gui/input/s28specscanfileform.py @@ -0,0 +1,400 @@ +''' + Copyright (c) 2014, UChicago Argonne, LLC + See LICENSE file. +''' +import os + +import logging +from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR +from rsMap3D.gui.output.processpowderscanform import ProcessPowderScanForm +logger = logging.getLogger(__name__) +import PyQt5.QtGui as qtGui +import PyQt5.QtCore as qtCore +import PyQt5.QtWidgets as qtWidgets + +from rsMap3D.gui.rsm3dcommonstrings import WARNING_STR, BROWSE_STR,\ + COMMA_STR, QLINEEDIT_COLOR_STYLE, BLACK, RED, EMPTY_STR,\ + BAD_PIXEL_FILE_FILTER, SELECT_BAD_PIXEL_TITLE, SELECT_FLAT_FIELD_TITLE,\ + TIFF_FILE_FILTER +from rsMap3D.datasource.Sector28SpecDataSource import Sector28SpecDataSource +from rsMap3D.transforms.unitytransform3d import UnityTransform3D +from rsMap3D.transforms.polemaptransform3d import PoleMapTransform3D +from rsMap3D.gui.input.specxmldrivenfileform import SpecXMLDrivenFileForm +from rsMap3D.gui.output.processvtioutputform import ProcessVTIOutputForm +from rsMap3D.gui.output.processimagestackform import ProcessImageStackForm +from PyQt5.QtWidgets import QAbstractButton + +class S28SpecScanFileForm(SpecXMLDrivenFileForm): + ''' + This class presents information for selecting input files + ''' + + FORM_TITLE = "Sector 28 Spec/XML Setup" + + #UPDATE_PROGRESS_SIGNAL = "updateProgress" + # Regular expressions for string validation + PIX_AVG_REGEXP_1 = r"^(\d*,*)+$" + PIX_AVG_REGEXP_2 = r"^((\d)+,*){2}$" + #Strings for Text Widgets + + NONE_RADIO_NAME = "None" + BAD_PIXEL_RADIO_NAME = "Bad Pixel File" + FLAT_FIELD_RADIO_NAME = "Flat Field Correction" + + @staticmethod + def createInstance(parent=None, appConfig=None): + return S28SpecScanFileForm(parent = parent, appConfig=appConfig) + + def __init__(self, **kwargs): + ''' + Constructor - Layout Widgets on the page and link actions + ''' + logger.debug(METHOD_ENTER_STR) + super(S28SpecScanFileForm, self).__init__(**kwargs) + + #Initialize parameters + self.projectionDirection = [0,0,1] + + #Initialize a couple of widgets to do setup. + self.noFieldRadio.setChecked(True) + self._fieldCorrectionTypeChanged(*(self.noFieldRadio,)) + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _badPixelFileChanged(self): + ''' + Do some verification when the bad pixel file changes + ''' + logger.debug(METHOD_ENTER_STR) + if os.path.isfile(self.badPixelFileTxt.text()) or \ + self.badPixelFileTxt.text() == EMPTY_STR: + self.checkOkToLoad() + else: + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR, \ + "The filename entered for the bad pixel " + \ + "file is invalid") + logger.debug(METHOD_EXIT_STR) + + + @qtCore.pyqtSlot() + def _browseBadPixelFileName(self): + ''' + Launch file browser for bad pixel file + ''' + logger.debug(METHOD_ENTER_STR) + if self.badPixelFileTxt.text() == EMPTY_STR: + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_BAD_PIXEL_TITLE, \ + filter=BAD_PIXEL_FILE_FILTER)[0] + else: + fileDirectory = os.path.dirname(str(self.badPixelFileTxt.text())) + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_BAD_PIXEL_TITLE, \ + filter=BAD_PIXEL_FILE_FILTER, \ + directory = fileDirectory)[0] + if fileName != EMPTY_STR: + self.badPixelFileTxt.setText(fileName) + self.badPixelFileTxt.editingFinished.emit() + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _browseFlatFieldFileName(self): + ''' + Launch file browser for Flat field file + ''' + logger.debug(METHOD_ENTER_STR) + if self.flatFieldFileTxt.text() == EMPTY_STR: + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_FLAT_FIELD_TITLE, \ + filter=TIFF_FILE_FILTER)[0] + else: + fileDirectory = os.path.dirname(str(self.flatFieldFileTxt.text())) + fileName = qtWidgets.QFileDialog.getOpenFileName(None, + SELECT_FLAT_FIELD_TITLE, + filter=TIFF_FILE_FILTER, \ + directory = fileDirectory)[0] + if fileName != EMPTY_STR: + self.flatFieldFileTxt.setText(fileName) + self.flatFieldFileTxt.editingFinished.emit() + logger.debug(METHOD_EXIT_STR) + + def checkOkToLoad(self): + ''' + Make sure we have valid file names for project, instrument config, + and the detector config. If we do enable load button. If not disable + the load button + ''' + logger.debug(METHOD_ENTER_STR) + retVal = False + if os.path.isfile(self.projNameTxt.text()) and \ + os.path.isfile(self.instConfigTxt.text()) and \ + os.path.isfile(self.detConfigTxt.text()) and \ + (self.noFieldRadio.isChecked() or \ + (self.badPixelRadio.isChecked() and \ + not (str(self.badPixelFileTxt.text()) == "")) or \ + (self.flatFieldRadio.isChecked() and \ + not (str(self.flatFieldFileTxt.text()) == ""))) and \ + self.pixAvgValid(self.pixAvgTxt.text()) and \ + self.detROIValid(self.detROITxt.text()): + retVal = True + self.loadButton.setEnabled(retVal) + else: + retVal = False + self.loadButton.setDisabled(not retVal) + self.okToLoad.emit(retVal) + logger.debug(METHOD_EXIT_STR) + return retVal + + def _createControlBox(self): + ''' + Create Layout holding controls widgets + ''' + logger.debug(METHOD_ENTER_STR) + controlBox = super(S28SpecScanFileForm, self)._createControlBox() + logger.debug(METHOD_EXIT_STR) + return controlBox + + def _createDataBox(self): + ''' + Create widgets for collecting data + ''' + logger.debug(METHOD_ENTER_STR) + dataBox = super(S28SpecScanFileForm, self)._createDataBox() + dataLayout = dataBox.layout() + row = dataLayout.rowCount() + self._createInstConfig(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createDetConfig(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self.fieldCorrectionGroup = qtWidgets.QButtonGroup(self) + self.noFieldRadio = qtWidgets.QRadioButton(self.NONE_RADIO_NAME) + self.badPixelRadio = qtWidgets.QRadioButton(self.BAD_PIXEL_RADIO_NAME) + self.flatFieldRadio = qtWidgets.QRadioButton(self.FLAT_FIELD_RADIO_NAME) + self.fieldCorrectionGroup.addButton(self.noFieldRadio, 1) + self.fieldCorrectionGroup.addButton(self.badPixelRadio, 2) + self.fieldCorrectionGroup.addButton(self.flatFieldRadio, 3) + self.badPixelFileTxt = qtWidgets.QLineEdit() + self.flatFieldFileTxt = qtWidgets.QLineEdit() + self.badPixelFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) + self.flatFieldFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) + + + dataLayout.addWidget(self.noFieldRadio, row, 0) + row += 1 + dataLayout.addWidget(self.badPixelRadio, row, 0) + dataLayout.addWidget(self.badPixelFileTxt, row, 1) + dataLayout.addWidget(self.badPixelFileBrowseButton, row, 2) + row += 1 + dataLayout.addWidget(self.flatFieldRadio, row, 0) + dataLayout.addWidget(self.flatFieldFileTxt, row, 1) + dataLayout.addWidget(self.flatFieldFileBrowseButton, row, 2) + + row += 1 + label = qtWidgets.QLabel("Number of Pixels To Average:"); + self.pixAvgTxt = qtWidgets.QLineEdit("1,1") + rxAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_1) + self.pixAvgTxt.setValidator(qtGui.QRegExpValidator(rxAvg,self.pixAvgTxt)) + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.pixAvgTxt, row, 1) + + row = dataLayout.rowCount() + 1 + self._createDetectorROIInput(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createScanNumberInput(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createOutputType(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createHKLOutput(dataLayout, row) + + # Add Signals between widgets + self.fieldCorrectionGroup.buttonClicked\ + .connect(self._fieldCorrectionTypeChanged) + + self.badPixelFileTxt.editingFinished.connect(self._badPixelFileChanged) + self.badPixelFileBrowseButton.clicked\ + .connect(self._browseBadPixelFileName) + + self.flatFieldFileTxt.editingFinished\ + .connect(self._flatFieldFileChanged) + self.flatFieldFileBrowseButton.clicked.\ + connect(self._browseFlatFieldFileName) + + self.pixAvgTxt.textChanged.connect(self._pixAvgTxtChanged) + + dataBox.setLayout(dataLayout) + logger.debug(METHOD_EXIT_STR) + return dataBox + + @qtCore.pyqtSlot(QAbstractButton) + def _fieldCorrectionTypeChanged(self, *fieldCorrType): + ''' + React when the field type radio buttons change. Disable/Enable other + widgets as appropriate + ''' + logger.debug(METHOD_ENTER_STR) + if fieldCorrType[0].text() == self.NONE_RADIO_NAME: + self.badPixelFileTxt.setDisabled(True) + self.badPixelFileBrowseButton.setDisabled(True) + self.flatFieldFileTxt.setDisabled(True) + self.flatFieldFileBrowseButton.setDisabled(True) + elif fieldCorrType[0].text() == self.BAD_PIXEL_RADIO_NAME: + self.badPixelFileTxt.setDisabled(False) + self.badPixelFileBrowseButton.setDisabled(False) + self.flatFieldFileTxt.setDisabled(True) + self.flatFieldFileBrowseButton.setDisabled(True) + elif fieldCorrType[0].text() == self.FLAT_FIELD_RADIO_NAME: + self.badPixelFileTxt.setDisabled(True) + self.badPixelFileBrowseButton.setDisabled(True) + self.flatFieldFileTxt.setDisabled(False) + self.flatFieldFileBrowseButton.setDisabled(False) + self.checkOkToLoad() + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _flatFieldFileChanged(self): + ''' + Do some verification when the flat field file changes + ''' + logger.debug(METHOD_ENTER_STR) + if os.path.isfile(self.flatFieldFileTxt.text()) or \ + self.flatFieldFileTxt.text() == "": + self.checkOkToLoad() + else: + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR, \ + "The filename entered for the flat field " + \ + "file is invalid") + logger.debug(METHOD_EXIT_STR) + + def getBadPixelFileName(self): + ''' + Return the badPixel file name. If empty or if the bad pixel radio + button is not checked return None + ''' + logger.debug(METHOD_ENTER_STR) + retVal = None + if (str(self.badPixelFileTxt.text()) == EMPTY_STR) or \ + (not self.badPixelRadio.isChecked()): + retVal = None + else: + retVal = str(self.badPixelFileTxt.text()) + logger.debug("Exit " + str(retVal)) + return retVal + + def getDataSource(self): + logger.debug(METHOD_ENTER_STR) + if self.getOutputType() == self.SIMPLE_GRID_MAP_STR: + self.transform = UnityTransform3D() + elif self.getOutputType() == self.POLE_MAP_STR: + self.transform = \ + PoleMapTransform3D(projectionDirection=\ + self.getProjectionDirection()) + else: + self.transform = None + + self.dataSource = \ + Sector28SpecDataSource(str(self.getProjectDir()), \ + str(self.getProjectName()), \ + str(self.getProjectExtension()), \ + str(self.getInstConfigName()), \ + str(self.getDetConfigName()), \ + transform = self.transform, \ + scanList = self.getScanList(), \ + roi = self.getDetectorROI(), \ + pixelsToAverage = \ + self.getPixelsToAverage(), \ + badPixelFile = \ + self.getBadPixelFileName(), \ + flatFieldFile = \ + self.getFlatFieldFileName(), \ + appConfig = self.appConfig + ) + self.dataSource.setProgressUpdater(self.updateProgress) + self.dataSource.setCurrentDetector(self.currentDetector) + self.dataSource.loadSource(mapHKL = self.getMapAsHKL()) + logger.debug(METHOD_EXIT_STR) + return self.dataSource + + def getFlatFieldFileName(self): + ''' + Return the flat field file name. If empty or if the bad pixel radio + button is not checked return None + ''' + logger.debug(METHOD_ENTER_STR) + retVal = None + if (str(self.flatFieldFileTxt.text()) == EMPTY_STR) or \ + (not self.flatFieldRadio.isChecked()): + retVal = None + else: + retVal = str(self.flatFieldFileTxt.text()) + logger.debug(METHOD_EXIT_STR) + return retVal + + def getOutputForms(self): + logger.debug(METHOD_ENTER_STR) + outputForms = [] + outputForms.append(ProcessVTIOutputForm) + outputForms.append(ProcessImageStackForm) + outputForms.append(ProcessPowderScanForm) + logger.debug(METHOD_EXIT_STR) + return outputForms + + def getPixelsToAverage(self): + ''' + :return: the pixels to average as a list + ''' + logger.debug(METHOD_ENTER_STR) + pixelStrings = str(self.pixAvgTxt.text()).split(COMMA_STR) + pixels = [] + for value in pixelStrings: + pixels.append(int(value)) + logger.debug(METHOD_EXIT_STR) + return pixels + + def getProjectionDirection(self): + ''' + Return projection direction for stereographic projections + ''' + logger.debug(METHOD_ENTER_STR) + logger.debug(METHOD_EXIT_STR) + return self.projectionDirection + + + def _pixAvgTxtChanged(self, text): + ''' + Check to make sure the text for pix to average is valid and indicate + by a color change + :param text: new values as a text list + ''' + logger.debug(METHOD_ENTER_STR) + if self.pixAvgValid(text): + self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % BLACK) + else: + self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % RED) + self.checkOkToLoad() + logger.debug(METHOD_EXIT_STR) + + def pixAvgValid(self, text): + ''' + Check to make sure that the pixAvgText is valid + :param text: new values as a text list + ''' + logger.debug(METHOD_ENTER_STR) + retVal = False + rxPixAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_2) + validator = qtGui.QRegExpValidator(rxPixAvg, None) + pos = 0 + if validator.validate(text, pos)[0] == qtGui.QValidator.Acceptable: + retVal = True + else: + retVal = False + logger.debug("Exit " + str(retVal)) + return retVal diff --git a/rsMap3D/gui/input/s33specscanfileform.py b/rsMap3D/gui/input/s33specscanfileform.py index 8166b2d..4ce6d19 100644 --- a/rsMap3D/gui/input/s33specscanfileform.py +++ b/rsMap3D/gui/input/s33specscanfileform.py @@ -1,400 +1,400 @@ -''' - Copyright (c) 2014, UChicago Argonne, LLC - See LICENSE file. -''' -import os - -import logging -from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR -from rsMap3D.gui.output.processpowderscanform import ProcessPowderScanForm -logger = logging.getLogger(__name__) -import PyQt5.QtGui as qtGui -import PyQt5.QtCore as qtCore -import PyQt5.QtWidgets as qtWidgets - -from rsMap3D.gui.rsm3dcommonstrings import WARNING_STR, BROWSE_STR,\ - COMMA_STR, QLINEEDIT_COLOR_STYLE, BLACK, RED, EMPTY_STR,\ - BAD_PIXEL_FILE_FILTER, SELECT_BAD_PIXEL_TITLE, SELECT_FLAT_FIELD_TITLE,\ - TIFF_FILE_FILTER -from rsMap3D.datasource.Sector33SpecDataSource import Sector33SpecDataSource -from rsMap3D.transforms.unitytransform3d import UnityTransform3D -from rsMap3D.transforms.polemaptransform3d import PoleMapTransform3D -from rsMap3D.gui.input.specxmldrivenfileform import SpecXMLDrivenFileForm -from rsMap3D.gui.output.processvtioutputform import ProcessVTIOutputForm -from rsMap3D.gui.output.processimagestackform import ProcessImageStackForm -from PyQt5.QtWidgets import QAbstractButton - -class S33SpecScanFileForm(SpecXMLDrivenFileForm): - ''' - This class presents information for selecting input files - ''' - - FORM_TITLE = "Sector 33 Spec/XML Setup" - - #UPDATE_PROGRESS_SIGNAL = "updateProgress" - # Regular expressions for string validation - PIX_AVG_REGEXP_1 = "^(\d*,*)+$" - PIX_AVG_REGEXP_2 = "^((\d)+,*){2}$" - #Strings for Text Widgets - - NONE_RADIO_NAME = "None" - BAD_PIXEL_RADIO_NAME = "Bad Pixel File" - FLAT_FIELD_RADIO_NAME = "Flat Field Correction" - - @staticmethod - def createInstance(parent=None, appConfig=None): - return S33SpecScanFileForm(parent = parent, appConfig=appConfig) - - def __init__(self, **kwargs): - ''' - Constructor - Layout Widgets on the page and link actions - ''' - logger.debug(METHOD_ENTER_STR) - super(S33SpecScanFileForm, self).__init__(**kwargs) - - #Initialize parameters - self.projectionDirection = [0,0,1] - - #Initialize a couple of widgets to do setup. - self.noFieldRadio.setChecked(True) - self._fieldCorrectionTypeChanged(*(self.noFieldRadio,)) - logger.debug(METHOD_EXIT_STR) - - @qtCore.pyqtSlot() - def _badPixelFileChanged(self): - ''' - Do some verification when the bad pixel file changes - ''' - logger.debug(METHOD_ENTER_STR) - if os.path.isfile(self.badPixelFileTxt.text()) or \ - self.badPixelFileTxt.text() == EMPTY_STR: - self.checkOkToLoad() - else: - message = qtWidgets.QMessageBox() - message.warning(self, \ - WARNING_STR, \ - "The filename entered for the bad pixel " + \ - "file is invalid") - logger.debug(METHOD_EXIT_STR) - - - @qtCore.pyqtSlot() - def _browseBadPixelFileName(self): - ''' - Launch file browser for bad pixel file - ''' - logger.debug(METHOD_ENTER_STR) - if self.badPixelFileTxt.text() == EMPTY_STR: - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_BAD_PIXEL_TITLE, \ - filter=BAD_PIXEL_FILE_FILTER)[0] - else: - fileDirectory = os.path.dirname(str(self.badPixelFileTxt.text())) - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_BAD_PIXEL_TITLE, \ - filter=BAD_PIXEL_FILE_FILTER, \ - directory = fileDirectory)[0] - if fileName != EMPTY_STR: - self.badPixelFileTxt.setText(fileName) - self.badPixelFileTxt.editingFinished.emit() - logger.debug(METHOD_EXIT_STR) - - @qtCore.pyqtSlot() - def _browseFlatFieldFileName(self): - ''' - Launch file browser for Flat field file - ''' - logger.debug(METHOD_ENTER_STR) - if self.flatFieldFileTxt.text() == EMPTY_STR: - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_FLAT_FIELD_TITLE, \ - filter=TIFF_FILE_FILTER)[0] - else: - fileDirectory = os.path.dirname(str(self.flatFieldFileTxt.text())) - fileName = qtWidgets.QFileDialog.getOpenFileName(None, - SELECT_FLAT_FIELD_TITLE, - filter=TIFF_FILE_FILTER, \ - directory = fileDirectory)[0] - if fileName != EMPTY_STR: - self.flatFieldFileTxt.setText(fileName) - self.flatFieldFileTxt.editingFinished.emit() - logger.debug(METHOD_EXIT_STR) - - def checkOkToLoad(self): - ''' - Make sure we have valid file names for project, instrument config, - and the detector config. If we do enable load button. If not disable - the load button - ''' - logger.debug(METHOD_ENTER_STR) - retVal = False - if os.path.isfile(self.projNameTxt.text()) and \ - os.path.isfile(self.instConfigTxt.text()) and \ - os.path.isfile(self.detConfigTxt.text()) and \ - (self.noFieldRadio.isChecked() or \ - (self.badPixelRadio.isChecked() and \ - not (str(self.badPixelFileTxt.text()) == "")) or \ - (self.flatFieldRadio.isChecked() and \ - not (str(self.flatFieldFileTxt.text()) == ""))) and \ - self.pixAvgValid(self.pixAvgTxt.text()) and \ - self.detROIValid(self.detROITxt.text()): - retVal = True - self.loadButton.setEnabled(retVal) - else: - retVal = False - self.loadButton.setDisabled(not retVal) - self.okToLoad.emit(retVal) - logger.debug(METHOD_EXIT_STR) - return retVal - - def _createControlBox(self): - ''' - Create Layout holding controls widgets - ''' - logger.debug(METHOD_ENTER_STR) - controlBox = super(S33SpecScanFileForm, self)._createControlBox() - logger.debug(METHOD_EXIT_STR) - return controlBox - - def _createDataBox(self): - ''' - Create widgets for collecting data - ''' - logger.debug(METHOD_ENTER_STR) - dataBox = super(S33SpecScanFileForm, self)._createDataBox() - dataLayout = dataBox.layout() - row = dataLayout.rowCount() - self._createInstConfig(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createDetConfig(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self.fieldCorrectionGroup = qtWidgets.QButtonGroup(self) - self.noFieldRadio = qtWidgets.QRadioButton(self.NONE_RADIO_NAME) - self.badPixelRadio = qtWidgets.QRadioButton(self.BAD_PIXEL_RADIO_NAME) - self.flatFieldRadio = qtWidgets.QRadioButton(self.FLAT_FIELD_RADIO_NAME) - self.fieldCorrectionGroup.addButton(self.noFieldRadio, 1) - self.fieldCorrectionGroup.addButton(self.badPixelRadio, 2) - self.fieldCorrectionGroup.addButton(self.flatFieldRadio, 3) - self.badPixelFileTxt = qtWidgets.QLineEdit() - self.flatFieldFileTxt = qtWidgets.QLineEdit() - self.badPixelFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) - self.flatFieldFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) - - - dataLayout.addWidget(self.noFieldRadio, row, 0) - row += 1 - dataLayout.addWidget(self.badPixelRadio, row, 0) - dataLayout.addWidget(self.badPixelFileTxt, row, 1) - dataLayout.addWidget(self.badPixelFileBrowseButton, row, 2) - row += 1 - dataLayout.addWidget(self.flatFieldRadio, row, 0) - dataLayout.addWidget(self.flatFieldFileTxt, row, 1) - dataLayout.addWidget(self.flatFieldFileBrowseButton, row, 2) - - row += 1 - label = qtWidgets.QLabel("Number of Pixels To Average:"); - self.pixAvgTxt = qtWidgets.QLineEdit("1,1") - rxAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_1) - self.pixAvgTxt.setValidator(qtGui.QRegExpValidator(rxAvg,self.pixAvgTxt)) - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.pixAvgTxt, row, 1) - - row = dataLayout.rowCount() + 1 - self._createDetectorROIInput(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createScanNumberInput(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createOutputType(dataLayout, row) - - row = dataLayout.rowCount() + 1 - self._createHKLOutput(dataLayout, row) - - # Add Signals between widgets - self.fieldCorrectionGroup.buttonClicked\ - .connect(self._fieldCorrectionTypeChanged) - - self.badPixelFileTxt.editingFinished.connect(self._badPixelFileChanged) - self.badPixelFileBrowseButton.clicked\ - .connect(self._browseBadPixelFileName) - - self.flatFieldFileTxt.editingFinished\ - .connect(self._flatFieldFileChanged) - self.flatFieldFileBrowseButton.clicked.\ - connect(self._browseFlatFieldFileName) - - self.pixAvgTxt.textChanged.connect(self._pixAvgTxtChanged) - - dataBox.setLayout(dataLayout) - logger.debug(METHOD_EXIT_STR) - return dataBox - - @qtCore.pyqtSlot(QAbstractButton) - def _fieldCorrectionTypeChanged(self, *fieldCorrType): - ''' - React when the field type radio buttons change. Disable/Enable other - widgets as appropriate - ''' - logger.debug(METHOD_ENTER_STR) - if fieldCorrType[0].text() == self.NONE_RADIO_NAME: - self.badPixelFileTxt.setDisabled(True) - self.badPixelFileBrowseButton.setDisabled(True) - self.flatFieldFileTxt.setDisabled(True) - self.flatFieldFileBrowseButton.setDisabled(True) - elif fieldCorrType[0].text() == self.BAD_PIXEL_RADIO_NAME: - self.badPixelFileTxt.setDisabled(False) - self.badPixelFileBrowseButton.setDisabled(False) - self.flatFieldFileTxt.setDisabled(True) - self.flatFieldFileBrowseButton.setDisabled(True) - elif fieldCorrType[0].text() == self.FLAT_FIELD_RADIO_NAME: - self.badPixelFileTxt.setDisabled(True) - self.badPixelFileBrowseButton.setDisabled(True) - self.flatFieldFileTxt.setDisabled(False) - self.flatFieldFileBrowseButton.setDisabled(False) - self.checkOkToLoad() - logger.debug(METHOD_EXIT_STR) - - @qtCore.pyqtSlot() - def _flatFieldFileChanged(self): - ''' - Do some verification when the flat field file changes - ''' - logger.debug(METHOD_ENTER_STR) - if os.path.isfile(self.flatFieldFileTxt.text()) or \ - self.flatFieldFileTxt.text() == "": - self.checkOkToLoad() - else: - message = qtWidgets.QMessageBox() - message.warning(self, \ - WARNING_STR, \ - "The filename entered for the flat field " + \ - "file is invalid") - logger.debug(METHOD_EXIT_STR) - - def getBadPixelFileName(self): - ''' - Return the badPixel file name. If empty or if the bad pixel radio - button is not checked return None - ''' - logger.debug(METHOD_ENTER_STR) - retVal = None - if (str(self.badPixelFileTxt.text()) == EMPTY_STR) or \ - (not self.badPixelRadio.isChecked()): - retVal = None - else: - retVal = str(self.badPixelFileTxt.text()) - logger.debug("Exit " + str(retVal)) - return retVal - - def getDataSource(self): - logger.debug(METHOD_ENTER_STR) - if self.getOutputType() == self.SIMPLE_GRID_MAP_STR: - self.transform = UnityTransform3D() - elif self.getOutputType() == self.POLE_MAP_STR: - self.transform = \ - PoleMapTransform3D(projectionDirection=\ - self.getProjectionDirection()) - else: - self.transform = None - - self.dataSource = \ - Sector33SpecDataSource(str(self.getProjectDir()), \ - str(self.getProjectName()), \ - str(self.getProjectExtension()), \ - str(self.getInstConfigName()), \ - str(self.getDetConfigName()), \ - transform = self.transform, \ - scanList = self.getScanList(), \ - roi = self.getDetectorROI(), \ - pixelsToAverage = \ - self.getPixelsToAverage(), \ - badPixelFile = \ - self.getBadPixelFileName(), \ - flatFieldFile = \ - self.getFlatFieldFileName(), \ - appConfig = self.appConfig - ) - self.dataSource.setProgressUpdater(self.updateProgress) - self.dataSource.setCurrentDetector(self.currentDetector) - self.dataSource.loadSource(mapHKL = self.getMapAsHKL()) - logger.debug(METHOD_EXIT_STR) - return self.dataSource - - def getFlatFieldFileName(self): - ''' - Return the flat field file name. If empty or if the bad pixel radio - button is not checked return None - ''' - logger.debug(METHOD_ENTER_STR) - retVal = None - if (str(self.flatFieldFileTxt.text()) == EMPTY_STR) or \ - (not self.flatFieldRadio.isChecked()): - retVal = None - else: - retVal = str(self.flatFieldFileTxt.text()) - logger.debug(METHOD_EXIT_STR) - return retVal - - def getOutputForms(self): - logger.debug(METHOD_ENTER_STR) - outputForms = [] - outputForms.append(ProcessVTIOutputForm) - outputForms.append(ProcessImageStackForm) - outputForms.append(ProcessPowderScanForm) - logger.debug(METHOD_EXIT_STR) - return outputForms - - def getPixelsToAverage(self): - ''' - :return: the pixels to average as a list - ''' - logger.debug(METHOD_ENTER_STR) - pixelStrings = str(self.pixAvgTxt.text()).split(COMMA_STR) - pixels = [] - for value in pixelStrings: - pixels.append(int(value)) - logger.debug(METHOD_EXIT_STR) - return pixels - - def getProjectionDirection(self): - ''' - Return projection direction for stereographic projections - ''' - logger.debug(METHOD_ENTER_STR) - logger.debug(METHOD_EXIT_STR) - return self.projectionDirection - - - def _pixAvgTxtChanged(self, text): - ''' - Check to make sure the text for pix to average is valid and indicate - by a color change - :param text: new values as a text list - ''' - logger.debug(METHOD_ENTER_STR) - if self.pixAvgValid(text): - self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % BLACK) - else: - self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % RED) - self.checkOkToLoad() - logger.debug(METHOD_EXIT_STR) - - def pixAvgValid(self, text): - ''' - Check to make sure that the pixAvgText is valid - :param text: new values as a text list - ''' - logger.debug(METHOD_ENTER_STR) - retVal = False - rxPixAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_2) - validator = qtGui.QRegExpValidator(rxPixAvg, None) - pos = 0 - if validator.validate(text, pos)[0] == qtGui.QValidator.Acceptable: - retVal = True - else: - retVal = False - logger.debug("Exit " + str(retVal)) - return retVal +''' + Copyright (c) 2014, UChicago Argonne, LLC + See LICENSE file. +''' +import os + +import logging +from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR +from rsMap3D.gui.output.processpowderscanform import ProcessPowderScanForm +logger = logging.getLogger(__name__) +import PyQt5.QtGui as qtGui +import PyQt5.QtCore as qtCore +import PyQt5.QtWidgets as qtWidgets + +from rsMap3D.gui.rsm3dcommonstrings import WARNING_STR, BROWSE_STR,\ + COMMA_STR, QLINEEDIT_COLOR_STYLE, BLACK, RED, EMPTY_STR,\ + BAD_PIXEL_FILE_FILTER, SELECT_BAD_PIXEL_TITLE, SELECT_FLAT_FIELD_TITLE,\ + TIFF_FILE_FILTER +from rsMap3D.datasource.Sector33SpecDataSource import Sector33SpecDataSource +from rsMap3D.transforms.unitytransform3d import UnityTransform3D +from rsMap3D.transforms.polemaptransform3d import PoleMapTransform3D +from rsMap3D.gui.input.specxmldrivenfileform import SpecXMLDrivenFileForm +from rsMap3D.gui.output.processvtioutputform import ProcessVTIOutputForm +from rsMap3D.gui.output.processimagestackform import ProcessImageStackForm +from PyQt5.QtWidgets import QAbstractButton + +class S33SpecScanFileForm(SpecXMLDrivenFileForm): + ''' + This class presents information for selecting input files + ''' + + FORM_TITLE = "Sector 33 Spec/XML Setup" + + #UPDATE_PROGRESS_SIGNAL = "updateProgress" + # Regular expressions for string validation + PIX_AVG_REGEXP_1 = r"^(\d*,*)+$" + PIX_AVG_REGEXP_2 = r"^((\d)+,*){2}$" + #Strings for Text Widgets + + NONE_RADIO_NAME = "None" + BAD_PIXEL_RADIO_NAME = "Bad Pixel File" + FLAT_FIELD_RADIO_NAME = "Flat Field Correction" + + @staticmethod + def createInstance(parent=None, appConfig=None): + return S33SpecScanFileForm(parent = parent, appConfig=appConfig) + + def __init__(self, **kwargs): + ''' + Constructor - Layout Widgets on the page and link actions + ''' + logger.debug(METHOD_ENTER_STR) + super(S33SpecScanFileForm, self).__init__(**kwargs) + + #Initialize parameters + self.projectionDirection = [0,0,1] + + #Initialize a couple of widgets to do setup. + self.noFieldRadio.setChecked(True) + self._fieldCorrectionTypeChanged(*(self.noFieldRadio,)) + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _badPixelFileChanged(self): + ''' + Do some verification when the bad pixel file changes + ''' + logger.debug(METHOD_ENTER_STR) + if os.path.isfile(self.badPixelFileTxt.text()) or \ + self.badPixelFileTxt.text() == EMPTY_STR: + self.checkOkToLoad() + else: + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR, \ + "The filename entered for the bad pixel " + \ + "file is invalid") + logger.debug(METHOD_EXIT_STR) + + + @qtCore.pyqtSlot() + def _browseBadPixelFileName(self): + ''' + Launch file browser for bad pixel file + ''' + logger.debug(METHOD_ENTER_STR) + if self.badPixelFileTxt.text() == EMPTY_STR: + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_BAD_PIXEL_TITLE, \ + filter=BAD_PIXEL_FILE_FILTER)[0] + else: + fileDirectory = os.path.dirname(str(self.badPixelFileTxt.text())) + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_BAD_PIXEL_TITLE, \ + filter=BAD_PIXEL_FILE_FILTER, \ + directory = fileDirectory)[0] + if fileName != EMPTY_STR: + self.badPixelFileTxt.setText(fileName) + self.badPixelFileTxt.editingFinished.emit() + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _browseFlatFieldFileName(self): + ''' + Launch file browser for Flat field file + ''' + logger.debug(METHOD_ENTER_STR) + if self.flatFieldFileTxt.text() == EMPTY_STR: + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_FLAT_FIELD_TITLE, \ + filter=TIFF_FILE_FILTER)[0] + else: + fileDirectory = os.path.dirname(str(self.flatFieldFileTxt.text())) + fileName = qtWidgets.QFileDialog.getOpenFileName(None, + SELECT_FLAT_FIELD_TITLE, + filter=TIFF_FILE_FILTER, \ + directory = fileDirectory)[0] + if fileName != EMPTY_STR: + self.flatFieldFileTxt.setText(fileName) + self.flatFieldFileTxt.editingFinished.emit() + logger.debug(METHOD_EXIT_STR) + + def checkOkToLoad(self): + ''' + Make sure we have valid file names for project, instrument config, + and the detector config. If we do enable load button. If not disable + the load button + ''' + logger.debug(METHOD_ENTER_STR) + retVal = False + if os.path.isfile(self.projNameTxt.text()) and \ + os.path.isfile(self.instConfigTxt.text()) and \ + os.path.isfile(self.detConfigTxt.text()) and \ + (self.noFieldRadio.isChecked() or \ + (self.badPixelRadio.isChecked() and \ + not (str(self.badPixelFileTxt.text()) == "")) or \ + (self.flatFieldRadio.isChecked() and \ + not (str(self.flatFieldFileTxt.text()) == ""))) and \ + self.pixAvgValid(self.pixAvgTxt.text()) and \ + self.detROIValid(self.detROITxt.text()): + retVal = True + self.loadButton.setEnabled(retVal) + else: + retVal = False + self.loadButton.setDisabled(not retVal) + self.okToLoad.emit(retVal) + logger.debug(METHOD_EXIT_STR) + return retVal + + def _createControlBox(self): + ''' + Create Layout holding controls widgets + ''' + logger.debug(METHOD_ENTER_STR) + controlBox = super(S33SpecScanFileForm, self)._createControlBox() + logger.debug(METHOD_EXIT_STR) + return controlBox + + def _createDataBox(self): + ''' + Create widgets for collecting data + ''' + logger.debug(METHOD_ENTER_STR) + dataBox = super(S33SpecScanFileForm, self)._createDataBox() + dataLayout = dataBox.layout() + row = dataLayout.rowCount() + self._createInstConfig(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createDetConfig(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self.fieldCorrectionGroup = qtWidgets.QButtonGroup(self) + self.noFieldRadio = qtWidgets.QRadioButton(self.NONE_RADIO_NAME) + self.badPixelRadio = qtWidgets.QRadioButton(self.BAD_PIXEL_RADIO_NAME) + self.flatFieldRadio = qtWidgets.QRadioButton(self.FLAT_FIELD_RADIO_NAME) + self.fieldCorrectionGroup.addButton(self.noFieldRadio, 1) + self.fieldCorrectionGroup.addButton(self.badPixelRadio, 2) + self.fieldCorrectionGroup.addButton(self.flatFieldRadio, 3) + self.badPixelFileTxt = qtWidgets.QLineEdit() + self.flatFieldFileTxt = qtWidgets.QLineEdit() + self.badPixelFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) + self.flatFieldFileBrowseButton = qtWidgets.QPushButton(BROWSE_STR) + + + dataLayout.addWidget(self.noFieldRadio, row, 0) + row += 1 + dataLayout.addWidget(self.badPixelRadio, row, 0) + dataLayout.addWidget(self.badPixelFileTxt, row, 1) + dataLayout.addWidget(self.badPixelFileBrowseButton, row, 2) + row += 1 + dataLayout.addWidget(self.flatFieldRadio, row, 0) + dataLayout.addWidget(self.flatFieldFileTxt, row, 1) + dataLayout.addWidget(self.flatFieldFileBrowseButton, row, 2) + + row += 1 + label = qtWidgets.QLabel("Number of Pixels To Average:"); + self.pixAvgTxt = qtWidgets.QLineEdit("1,1") + rxAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_1) + self.pixAvgTxt.setValidator(qtGui.QRegExpValidator(rxAvg,self.pixAvgTxt)) + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.pixAvgTxt, row, 1) + + row = dataLayout.rowCount() + 1 + self._createDetectorROIInput(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createScanNumberInput(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createOutputType(dataLayout, row) + + row = dataLayout.rowCount() + 1 + self._createHKLOutput(dataLayout, row) + + # Add Signals between widgets + self.fieldCorrectionGroup.buttonClicked\ + .connect(self._fieldCorrectionTypeChanged) + + self.badPixelFileTxt.editingFinished.connect(self._badPixelFileChanged) + self.badPixelFileBrowseButton.clicked\ + .connect(self._browseBadPixelFileName) + + self.flatFieldFileTxt.editingFinished\ + .connect(self._flatFieldFileChanged) + self.flatFieldFileBrowseButton.clicked.\ + connect(self._browseFlatFieldFileName) + + self.pixAvgTxt.textChanged.connect(self._pixAvgTxtChanged) + + dataBox.setLayout(dataLayout) + logger.debug(METHOD_EXIT_STR) + return dataBox + + @qtCore.pyqtSlot(QAbstractButton) + def _fieldCorrectionTypeChanged(self, *fieldCorrType): + ''' + React when the field type radio buttons change. Disable/Enable other + widgets as appropriate + ''' + logger.debug(METHOD_ENTER_STR) + if fieldCorrType[0].text() == self.NONE_RADIO_NAME: + self.badPixelFileTxt.setDisabled(True) + self.badPixelFileBrowseButton.setDisabled(True) + self.flatFieldFileTxt.setDisabled(True) + self.flatFieldFileBrowseButton.setDisabled(True) + elif fieldCorrType[0].text() == self.BAD_PIXEL_RADIO_NAME: + self.badPixelFileTxt.setDisabled(False) + self.badPixelFileBrowseButton.setDisabled(False) + self.flatFieldFileTxt.setDisabled(True) + self.flatFieldFileBrowseButton.setDisabled(True) + elif fieldCorrType[0].text() == self.FLAT_FIELD_RADIO_NAME: + self.badPixelFileTxt.setDisabled(True) + self.badPixelFileBrowseButton.setDisabled(True) + self.flatFieldFileTxt.setDisabled(False) + self.flatFieldFileBrowseButton.setDisabled(False) + self.checkOkToLoad() + logger.debug(METHOD_EXIT_STR) + + @qtCore.pyqtSlot() + def _flatFieldFileChanged(self): + ''' + Do some verification when the flat field file changes + ''' + logger.debug(METHOD_ENTER_STR) + if os.path.isfile(self.flatFieldFileTxt.text()) or \ + self.flatFieldFileTxt.text() == "": + self.checkOkToLoad() + else: + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR, \ + "The filename entered for the flat field " + \ + "file is invalid") + logger.debug(METHOD_EXIT_STR) + + def getBadPixelFileName(self): + ''' + Return the badPixel file name. If empty or if the bad pixel radio + button is not checked return None + ''' + logger.debug(METHOD_ENTER_STR) + retVal = None + if (str(self.badPixelFileTxt.text()) == EMPTY_STR) or \ + (not self.badPixelRadio.isChecked()): + retVal = None + else: + retVal = str(self.badPixelFileTxt.text()) + logger.debug("Exit " + str(retVal)) + return retVal + + def getDataSource(self): + logger.debug(METHOD_ENTER_STR) + if self.getOutputType() == self.SIMPLE_GRID_MAP_STR: + self.transform = UnityTransform3D() + elif self.getOutputType() == self.POLE_MAP_STR: + self.transform = \ + PoleMapTransform3D(projectionDirection=\ + self.getProjectionDirection()) + else: + self.transform = None + + self.dataSource = \ + Sector33SpecDataSource(str(self.getProjectDir()), \ + str(self.getProjectName()), \ + str(self.getProjectExtension()), \ + str(self.getInstConfigName()), \ + str(self.getDetConfigName()), \ + transform = self.transform, \ + scanList = self.getScanList(), \ + roi = self.getDetectorROI(), \ + pixelsToAverage = \ + self.getPixelsToAverage(), \ + badPixelFile = \ + self.getBadPixelFileName(), \ + flatFieldFile = \ + self.getFlatFieldFileName(), \ + appConfig = self.appConfig + ) + self.dataSource.setProgressUpdater(self.updateProgress) + self.dataSource.setCurrentDetector(self.currentDetector) + self.dataSource.loadSource(mapHKL = self.getMapAsHKL()) + logger.debug(METHOD_EXIT_STR) + return self.dataSource + + def getFlatFieldFileName(self): + ''' + Return the flat field file name. If empty or if the bad pixel radio + button is not checked return None + ''' + logger.debug(METHOD_ENTER_STR) + retVal = None + if (str(self.flatFieldFileTxt.text()) == EMPTY_STR) or \ + (not self.flatFieldRadio.isChecked()): + retVal = None + else: + retVal = str(self.flatFieldFileTxt.text()) + logger.debug(METHOD_EXIT_STR) + return retVal + + def getOutputForms(self): + logger.debug(METHOD_ENTER_STR) + outputForms = [] + outputForms.append(ProcessVTIOutputForm) + outputForms.append(ProcessImageStackForm) + outputForms.append(ProcessPowderScanForm) + logger.debug(METHOD_EXIT_STR) + return outputForms + + def getPixelsToAverage(self): + ''' + :return: the pixels to average as a list + ''' + logger.debug(METHOD_ENTER_STR) + pixelStrings = str(self.pixAvgTxt.text()).split(COMMA_STR) + pixels = [] + for value in pixelStrings: + pixels.append(int(value)) + logger.debug(METHOD_EXIT_STR) + return pixels + + def getProjectionDirection(self): + ''' + Return projection direction for stereographic projections + ''' + logger.debug(METHOD_ENTER_STR) + logger.debug(METHOD_EXIT_STR) + return self.projectionDirection + + + def _pixAvgTxtChanged(self, text): + ''' + Check to make sure the text for pix to average is valid and indicate + by a color change + :param text: new values as a text list + ''' + logger.debug(METHOD_ENTER_STR) + if self.pixAvgValid(text): + self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % BLACK) + else: + self.pixAvgTxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % RED) + self.checkOkToLoad() + logger.debug(METHOD_EXIT_STR) + + def pixAvgValid(self, text): + ''' + Check to make sure that the pixAvgText is valid + :param text: new values as a text list + ''' + logger.debug(METHOD_ENTER_STR) + retVal = False + rxPixAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_2) + validator = qtGui.QRegExpValidator(rxPixAvg, None) + pos = 0 + if validator.validate(text, pos)[0] == qtGui.QValidator.Acceptable: + retVal = True + else: + retVal = False + logger.debug("Exit " + str(retVal)) + return retVal diff --git a/rsMap3D/gui/input/specxmldrivenfileform.py b/rsMap3D/gui/input/specxmldrivenfileform.py index 89109b9..4b20ba7 100644 --- a/rsMap3D/gui/input/specxmldrivenfileform.py +++ b/rsMap3D/gui/input/specxmldrivenfileform.py @@ -21,7 +21,7 @@ class SpecXMLDrivenFileForm(AbstractImagePerFileView, UsesXMLInstConfig, UsesXMLDetectorConfig): - SCAN_LIST_REGEXP = "((\d)+(-(\d)+)?\,( )?)+" + SCAN_LIST_REGEXP = r"((\d)+(-(\d)+)?\,( )?)+" def __init__(self, **kwargs): super(SpecXMLDrivenFileForm, self).__init__(**kwargs) diff --git a/rsMap3D/gui/input/usesxmldetectorconfig.py b/rsMap3D/gui/input/usesxmldetectorconfig.py index ed14864..864c202 100644 --- a/rsMap3D/gui/input/usesxmldetectorconfig.py +++ b/rsMap3D/gui/input/usesxmldetectorconfig.py @@ -1,316 +1,318 @@ -# coding=utf-8 -''' - Copyright (c) 2017, UChicago Argonne, LLC - See LICENSE file. -''' -import os -import logging -from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR -logger = logging.getLogger(__name__) - -import PyQt5.QtGui as qtGui -import PyQt5.QtCore as qtCore -import PyQt5.QtWidgets as qtWidgets - -from rsMap3D.gui.input.abstractfileview import AbstractFileView -from rsMap3D.datasource.DetectorGeometryForXrayutilitiesReader \ - import DetectorGeometryForXrayutilitiesReader -from rsMap3D.exception.rsmap3dexception import RSMap3DException,\ - DetectorConfigException -from rsMap3D.gui.rsm3dcommonstrings import COMMA_STR, EMPTY_STR,\ - QLINEEDIT_COLOR_STYLE, WARNING_STR, BROWSE_STR, \ - DETECTOR_CONFIG_FILE_FILTER,\ - SELECT_DETECTOR_CONFIG_TITLE, BLACK, RED - -class UsesXMLDetectorConfig(AbstractFileView): - ''' - class to provide functionality provided by the XML detector configuration - file. Designed for use along with other view classes. Use multiple inheritance - such as "class myView(specXmlDrivenFileForm, usesXMLDetectorConfig): - then add the gui blocks in _createDataBox to add fike selection stuff. - This provides gui that allows file selection, then detector selection since - multiple detectors can be defined in a file and then ROI selection. - ''' - DET_ROI_REGEXP_1 = "^(\d*,*)+$" - DET_ROI_REGEXP_2 = "^(\d)+,(\d)+,(\d)+,(\d)+$" - - #UPDATE_PROGRESS_SIGNAL = "updateProgress" - # Regular expressions for string validation - PIX_AVG_REGEXP_1 = "^(\d*,*)+$" - PIX_AVG_REGEXP_2 = "^((\d)+,*){2}$" - - def __init__(self, parent=None, **kwargs): - ''' - constructor - ''' - super(UsesXMLDetectorConfig, self).__init__(parent, **kwargs) - logger.debug(METHOD_ENTER_STR) - self.roixmin = 1 - self.roixmax = 680 - self.roiymin = 1 - self.roiymax = 480 - self.currentDetector = "" - self.detConfig = None - self.detFileOk = False - logger.debug(METHOD_EXIT_STR) - -# @qtCore.pyqtSlot() - def _browseForDetFile(self): - ''' - Launch file selection dialog for Detector file. - ''' - logger.debug(METHOD_ENTER_STR) - if self.detConfigTxt.text() == EMPTY_STR: - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_DETECTOR_CONFIG_TITLE, \ - filter=DETECTOR_CONFIG_FILE_FILTER)[0] - else: - fileDirectory = os.path.dirname(str(self.detConfigTxt.text())) - fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ - SELECT_DETECTOR_CONFIG_TITLE, \ - filter=DETECTOR_CONFIG_FILE_FILTER, \ - directory = fileDirectory)[0] - if fileName != EMPTY_STR: - self.detConfigTxt.setText(fileName) - self.detConfigTxt.editingFinished.emit() - logger.debug(METHOD_EXIT_STR) - - def _createDetConfig(self, layout, row): - ''' - Add in gui elements for selecting a detector config file and then - selecting from the list of detectors provided. - ''' - logger.debug(METHOD_ENTER_STR) - label = qtWidgets.QLabel("Detector Config File:"); - self.detConfigTxt = qtWidgets.QLineEdit() - self.detConfigFileButton = qtWidgets.QPushButton(BROWSE_STR) - layout.addWidget(label, row, 0) - layout.addWidget(self.detConfigTxt, row, 1) - layout.addWidget(self.detConfigFileButton, row, 2) - - row += 1 - label = qtWidgets.QLabel("Select Detector") - self.detSelect = qtWidgets.QComboBox() - layout.addWidget(label, row, 0) - layout.addWidget(self.detSelect, row, 1) - - # use new style to emit edit finished signal - self.detConfigFileButton.clicked.connect(self._browseForDetFile) - self.detConfigTxt.editingFinished.connect(self._detConfigChanged) - self.detSelect.currentIndexChanged[str].connect(self._currentDetectorChanged) - logger.debug(METHOD_EXIT_STR) - - - def _createDetectorROIInput(self, layout, row, silent=False): - ''' - Adds gui elements for entering the ROI - ''' - logger.debug(METHOD_ENTER_STR) - label = qtWidgets.QLabel("Detector ROI:"); - self.detROITxt = qtWidgets.QLineEdit() - self.updateROITxt() - rxROI = qtCore.QRegExp(self.DET_ROI_REGEXP_1) - self.detROITxt.setValidator(qtGui.QRegExpValidator(rxROI,self.detROITxt)) - - if (silent==False): - layout.addWidget(label, row, 0) - layout.addWidget(self.detROITxt, row, 1) - - # use new style to emit edit finished signal - self.detROITxt.textChanged.connect(self._detROITxtEntered) - logger.debug(METHOD_EXIT_STR) - - def _createNumberOfPixelsToAverage(self, layout, row, silent=False): - logger.debug(METHOD_ENTER_STR) - label = qtWidgets.QLabel("Number of Pixels To Average:"); - self.pixAvgTxt = qtWidgets.QLineEdit("1,1") - rxAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_1) - self.pixAvgTxt.setValidator(qtGui.QRegExpValidator(rxAvg,self.pixAvgTxt)) - if (silent == False): - layout.addWidget(label, row, 0) - layout.addWidget(self.pixAvgTxt, row, 1) - logger.debug(METHOD_EXIT_STR) - -# @qtCore.pyqtSlot(str) - def _currentDetectorChanged(self, currentDetector): - logger.debug(METHOD_ENTER_STR % str(currentDetector)) - self.currentDetector = str(currentDetector) - self.updateROIandNumAvg() - logger.debug(METHOD_EXIT_STR % self.currentDetector) - -# @qtCore.pyqtSlot() - def _detConfigChanged(self): - ''' - ''' - logger.debug(METHOD_ENTER_STR) - if self.detFileExists() or \ - self.detConfigTxt.text() == "": - if self.detConfigTxt.text() != "": - try: - self.detFileOk = self.isDetFileOk() - #self.updateDetectorList() - #self.updateROIandNumAvg() - except DetectorConfigException as ex: - logger.error( ex) - message = qtWidgets.QMessageBox() - message.warning(self, \ - WARNING_STR,\ - "Trouble getting ROI or Num average " + \ - "from the detector config file") - self.checkOkToLoad() - else: - message = qtWidgets.QMessageBox() - message.warning(self, \ - WARNING_STR,\ - "The filename entered for the detector " + \ - "configuration is invalid") - logger.debug(METHOD_EXIT_STR) - - - def detFileExists(self): - logger.debug(METHOD_ENTER_STR) - fileExists = os.path.isfile(self.detConfigTxt.text()) - logger.debug(METHOD_EXIT_STR + str(fileExists)) - return fileExists - - def isDetFileOk(self): - logger.debug(METHOD_ENTER_STR) - detFileExists = self.detFileExists() - if detFileExists: - try: - self.updateDetectorList() - #self.updateROIandNumAvg() - except DetectorConfigException: - # not a well formed deteector config - logger.debug("Exiting by Exception") - return False - logger.debug(METHOD_EXIT_STR + str(detFileExists)) - return detFileExists - -# @qtCore.pyqtSlot(str) - def _detROITxtChanged(self, text): - ''' - Check to make sure the text for detector roi is valid and indicate - by a color change - ''' - logger.debug(METHOD_ENTER_STR) - if self.detROIValid(text): - self.detROITxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % BLACK) - else: - self.detROITxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % RED) - logger.debug(METHOD_ENTER_STR) - - def _detROITxtEntered(self, text): - logger.debug(METHOD_ENTER_STR) - self._detROITxtChanged(text) - self.checkOkToLoad() - logger.debug(METHOD_EXIT_STR) - - def detROIValid(self, text): - ''' - Check to make sure the text for is a vaid detector roi - ''' - logger.debug(METHOD_ENTER_STR) - rxROI = qtCore.QRegExp(self.DET_ROI_REGEXP_2) - validator = qtGui.QRegExpValidator(rxROI, None) - pos = 0 - if validator.validate(text, pos)[0] == qtGui.QValidator.Acceptable: - roiVals = self.getDetectorROI(rois=str(text)) - if (roiVals[0] <= roiVals[1]) and \ - (roiVals[2] <= roiVals[3]): - logger.debug(METHOD_EXIT_STR + str(True)) - return True - else: - logger.debug(METHOD_EXIT_STR + str(False)) - return False - else: - logger.debug(METHOD_EXIT_STR + str(False)) - return False - - def getDetConfigName(self): - ''' - Return the selected Detector Configuration file - ''' - logger.debug(METHOD_ENTER_STR) - nameText = self.detConfigTxt.text() - logger.debug(METHOD_EXIT_STR % nameText) - return nameText - - def getDetectorROI(self, rois=EMPTY_STR): - ''' - :param rois: a string list with the roi values - :return: The detector ROI as a list - :raises RSMap3DException: if the string is not a 4 element list - ''' - logger.debug(METHOD_ENTER_STR) - if rois == EMPTY_STR: - roiStrings = str(self.detROITxt.text()).split(COMMA_STR) - else: - roiStrings = rois.split(COMMA_STR) - - roi = [] - if len(roiStrings) != 4: - logger.debug("Exiting via exception" + - "Detector ROI needs 4 values. " + \ - str(len(roiStrings)) + \ - " were given.") - raise RSMap3DException("Detector ROI needs 4 values. " + \ - str(len(roiStrings)) + \ - " were given.") - for value in roiStrings: - roi.append(int(value)) - logger.debug(METHOD_EXIT_STR) - return roi - - def updateDetectorList(self): - logger.debug(METHOD_ENTER_STR) - oldNumDet = self.detSelect.count() - for index in reversed(range(oldNumDet)): - self.detSelect.removeItem(index) - detConfigFileName = str(self.detConfigTxt.text()) - logger.debug("detectorConfigFile - " + detConfigFileName) - self.detConfig = \ - DetectorGeometryForXrayutilitiesReader(detConfigFileName) - detectors = self.detConfig.getDetectors() - for detector in detectors: - detID = self.detConfig.getDetectorID(detector) - self.detSelect.addItem(detID) - logger.debug("updateDetectorList - " + str(detID)) - self.detSelect.itemText(0) - - self.detSelect.currentIndexChanged[str].emit(self.detSelect.itemText(0)) - logger.debug(METHOD_EXIT_STR) - - def updateROIandNumAvg(self): - ''' - Set default values into the ROI and number of pixel to average text - boxes - ''' - logger.debug(METHOD_ENTER_STR) -# detConfig = \ -# DetectorGeometryForXrayutilitiesReader(self.detConfigTxt.text()) - logger.debug("self.currentDetector " + str(self.currentDetector)) - detector = self.detConfig.getDetectorById(str(self.currentDetector)) - detSize = self.detConfig.getNpixels(detector) - xmax = detSize[0] - ymax = detSize[1] - self.roixmax = xmax - self.roiymax = ymax - self.updateROITxt() - logger.debug(METHOD_EXIT_STR) - - def updateROITxt(self): - ''' - Update the ROI string with the current value of the ROI - ''' - logger.debug(METHOD_ENTER_STR) - roiStr = str(self.roixmin) +\ - COMMA_STR +\ - str(self.roixmax) +\ - COMMA_STR +\ - str(self.roiymin) +\ - COMMA_STR +\ - str(self.roiymax) - self.detROITxt.setText(roiStr) - logger.debug(METHOD_EXIT_STR) - +# coding=utf-8 +''' + Copyright (c) 2017, UChicago Argonne, LLC + See LICENSE file. +''' +import os +import logging +from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR +logger = logging.getLogger(__name__) + +import PyQt5.QtGui as qtGui +import PyQt5.QtCore as qtCore +import PyQt5.QtWidgets as qtWidgets + +from rsMap3D.gui.input.abstractfileview import AbstractFileView +from rsMap3D.datasource.DetectorGeometryForXrayutilitiesReader \ + import DetectorGeometryForXrayutilitiesReader +from rsMap3D.exception.rsmap3dexception import RSMap3DException,\ + DetectorConfigException +from rsMap3D.gui.rsm3dcommonstrings import COMMA_STR, EMPTY_STR,\ + QLINEEDIT_COLOR_STYLE, WARNING_STR, BROWSE_STR, \ + DETECTOR_CONFIG_FILE_FILTER,\ + SELECT_DETECTOR_CONFIG_TITLE, BLACK, RED + +class UsesXMLDetectorConfig(AbstractFileView): + ''' + class to provide functionality provided by the XML detector configuration + file. Designed for use along with other view classes. Use multiple inheritance + such as "class myView(specXmlDrivenFileForm, usesXMLDetectorConfig): + then add the gui blocks in _createDataBox to add fike selection stuff. + This provides gui that allows file selection, then detector selection since + multiple detectors can be defined in a file and then ROI selection. + ''' + DET_ROI_REGEXP_1 = r"^(\d*,*)+$" + DET_ROI_REGEXP_2 = r"^(\d)+,(\d)+,(\d)+,(\d)+$" + + #UPDATE_PROGRESS_SIGNAL = "updateProgress" + # Regular expressions for string validation + PIX_AVG_REGEXP_1 = r"^(\d*,*)+$" + PIX_AVG_REGEXP_2 = r"^((\d)+,*){2}$" + + def __init__(self, parent=None, **kwargs): + ''' + constructor + ''' + super(UsesXMLDetectorConfig, self).__init__(parent, **kwargs) + logger.debug(METHOD_ENTER_STR) + self.roixmin = 1 + self.roixmax = 680 + self.roiymin = 1 + self.roiymax = 480 + self.currentDetector = "" + self.detConfig = None + self.detFileOk = False + logger.debug(METHOD_EXIT_STR) + +# @qtCore.pyqtSlot() + def _browseForDetFile(self): + ''' + Launch file selection dialog for Detector file. + ''' + logger.debug(METHOD_ENTER_STR) + if self.detConfigTxt.text() == EMPTY_STR: + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_DETECTOR_CONFIG_TITLE, \ + filter=DETECTOR_CONFIG_FILE_FILTER)[0] + else: + fileDirectory = os.path.dirname(str(self.detConfigTxt.text())) + fileName = qtWidgets.QFileDialog.getOpenFileName(None, \ + SELECT_DETECTOR_CONFIG_TITLE, \ + filter=DETECTOR_CONFIG_FILE_FILTER, \ + directory = fileDirectory)[0] + if fileName != EMPTY_STR: + self.detConfigTxt.setText(fileName) + self.detConfigTxt.editingFinished.emit() + logger.debug(METHOD_EXIT_STR) + + def _createDetConfig(self, layout, row): + ''' + Add in gui elements for selecting a detector config file and then + selecting from the list of detectors provided. + ''' + logger.debug(METHOD_ENTER_STR) + label = qtWidgets.QLabel("Detector Config File:"); + self.detConfigTxt = qtWidgets.QLineEdit() + self.detConfigFileButton = qtWidgets.QPushButton(BROWSE_STR) + layout.addWidget(label, row, 0) + layout.addWidget(self.detConfigTxt, row, 1) + layout.addWidget(self.detConfigFileButton, row, 2) + + row += 1 + label = qtWidgets.QLabel("Select Detector") + self.detSelect = qtWidgets.QComboBox() + layout.addWidget(label, row, 0) + layout.addWidget(self.detSelect, row, 1) + + # use new style to emit edit finished signal + self.detConfigFileButton.clicked.connect(self._browseForDetFile) + self.detConfigTxt.editingFinished.connect(self._detConfigChanged) + self.detSelect.currentIndexChanged[str].connect(self._currentDetectorChanged) + logger.debug(METHOD_EXIT_STR) + + + def _createDetectorROIInput(self, layout, row, silent=False): + ''' + Adds gui elements for entering the ROI + ''' + logger.debug(METHOD_ENTER_STR) + label = qtWidgets.QLabel("Detector ROI:"); + self.detROITxt = qtWidgets.QLineEdit() + self.updateROITxt() + rxROI = qtCore.QRegExp(self.DET_ROI_REGEXP_1) + self.detROITxt.setValidator(qtGui.QRegExpValidator(rxROI,self.detROITxt)) + + if (silent==False): + layout.addWidget(label, row, 0) + layout.addWidget(self.detROITxt, row, 1) + + # use new style to emit edit finished signal + self.detROITxt.textChanged.connect(self._detROITxtEntered) + logger.debug(METHOD_EXIT_STR) + + def _createNumberOfPixelsToAverage(self, layout, row, silent=False): + logger.debug(METHOD_ENTER_STR) + label = qtWidgets.QLabel("Number of Pixels To Average:"); + self.pixAvgTxt = qtWidgets.QLineEdit("1,1") + rxAvg = qtCore.QRegExp(self.PIX_AVG_REGEXP_1) + self.pixAvgTxt.setValidator(qtGui.QRegExpValidator(rxAvg,self.pixAvgTxt)) + if (silent == False): + layout.addWidget(label, row, 0) + layout.addWidget(self.pixAvgTxt, row, 1) + logger.debug(METHOD_EXIT_STR) + +# @qtCore.pyqtSlot(str) + def _currentDetectorChanged(self, currentDetector): + logger.debug(METHOD_ENTER_STR % str(currentDetector)) + self.currentDetector = str(currentDetector) + # if the detector list is empty, let's not update ROI etc. + if currentDetector != "": + self.updateROIandNumAvg() + logger.debug(METHOD_EXIT_STR % self.currentDetector) + +# @qtCore.pyqtSlot() + def _detConfigChanged(self): + ''' + ''' + logger.debug(METHOD_ENTER_STR) + if self.detFileExists() or \ + self.detConfigTxt.text() == "": + if self.detConfigTxt.text() != "": + try: + self.detFileOk = self.isDetFileOk() + #self.updateDetectorList() + #self.updateROIandNumAvg() + except DetectorConfigException as ex: + logger.error( ex) + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR,\ + "Trouble getting ROI or Num average " + \ + "from the detector config file") + self.checkOkToLoad() + else: + message = qtWidgets.QMessageBox() + message.warning(self, \ + WARNING_STR,\ + "The filename entered for the detector " + \ + "configuration is invalid") + logger.debug(METHOD_EXIT_STR) + + + def detFileExists(self): + logger.debug(METHOD_ENTER_STR) + fileExists = os.path.isfile(self.detConfigTxt.text()) + logger.debug(METHOD_EXIT_STR + str(fileExists)) + return fileExists + + def isDetFileOk(self): + logger.debug(METHOD_ENTER_STR) + detFileExists = self.detFileExists() + if detFileExists: + try: + self.updateDetectorList() + #self.updateROIandNumAvg() + except DetectorConfigException: + # not a well formed deteector config + logger.debug("Exiting by Exception") + return False + logger.debug(METHOD_EXIT_STR + str(detFileExists)) + return detFileExists + +# @qtCore.pyqtSlot(str) + def _detROITxtChanged(self, text): + ''' + Check to make sure the text for detector roi is valid and indicate + by a color change + ''' + logger.debug(METHOD_ENTER_STR) + if self.detROIValid(text): + self.detROITxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % BLACK) + else: + self.detROITxt.setStyleSheet(QLINEEDIT_COLOR_STYLE % RED) + logger.debug(METHOD_ENTER_STR) + + def _detROITxtEntered(self, text): + logger.debug(METHOD_ENTER_STR) + self._detROITxtChanged(text) + self.checkOkToLoad() + logger.debug(METHOD_EXIT_STR) + + def detROIValid(self, text): + ''' + Check to make sure the text for is a vaid detector roi + ''' + logger.debug(METHOD_ENTER_STR) + rxROI = qtCore.QRegExp(self.DET_ROI_REGEXP_2) + validator = qtGui.QRegExpValidator(rxROI, None) + pos = 0 + if validator.validate(text, pos)[0] == qtGui.QValidator.Acceptable: + roiVals = self.getDetectorROI(rois=str(text)) + if (roiVals[0] <= roiVals[1]) and \ + (roiVals[2] <= roiVals[3]): + logger.debug(METHOD_EXIT_STR + str(True)) + return True + else: + logger.debug(METHOD_EXIT_STR + str(False)) + return False + else: + logger.debug(METHOD_EXIT_STR + str(False)) + return False + + def getDetConfigName(self): + ''' + Return the selected Detector Configuration file + ''' + logger.debug(METHOD_ENTER_STR) + nameText = self.detConfigTxt.text() + logger.debug(METHOD_EXIT_STR % nameText) + return nameText + + def getDetectorROI(self, rois=EMPTY_STR): + ''' + :param rois: a string list with the roi values + :return: The detector ROI as a list + :raises RSMap3DException: if the string is not a 4 element list + ''' + logger.debug(METHOD_ENTER_STR) + if rois == EMPTY_STR: + roiStrings = str(self.detROITxt.text()).split(COMMA_STR) + else: + roiStrings = rois.split(COMMA_STR) + + roi = [] + if len(roiStrings) != 4: + logger.debug("Exiting via exception" + + "Detector ROI needs 4 values. " + \ + str(len(roiStrings)) + \ + " were given.") + raise RSMap3DException("Detector ROI needs 4 values. " + \ + str(len(roiStrings)) + \ + " were given.") + for value in roiStrings: + roi.append(int(value)) + logger.debug(METHOD_EXIT_STR) + return roi + + def updateDetectorList(self): + logger.debug(METHOD_ENTER_STR) + oldNumDet = self.detSelect.count() + for index in reversed(range(oldNumDet)): + self.detSelect.removeItem(index) + detConfigFileName = str(self.detConfigTxt.text()) + logger.debug("detectorConfigFile - " + detConfigFileName) + self.detConfig = \ + DetectorGeometryForXrayutilitiesReader(detConfigFileName) + detectors = self.detConfig.getDetectors() + for detector in detectors: + detID = self.detConfig.getDetectorID(detector) + self.detSelect.addItem(detID) + logger.debug("updateDetectorList - " + str(detID)) + self.detSelect.itemText(0) + + self.detSelect.currentIndexChanged[str].emit(self.detSelect.itemText(0)) + logger.debug(METHOD_EXIT_STR) + + def updateROIandNumAvg(self): + ''' + Set default values into the ROI and number of pixel to average text + boxes + ''' + logger.debug(METHOD_ENTER_STR) +# detConfig = \ +# DetectorGeometryForXrayutilitiesReader(self.detConfigTxt.text()) + logger.debug("self.currentDetector " + str(self.currentDetector)) + detector = self.detConfig.getDetectorById(str(self.currentDetector)) + detSize = self.detConfig.getNpixels(detector) + xmax = detSize[0] + ymax = detSize[1] + self.roixmax = xmax + self.roiymax = ymax + self.updateROITxt() + logger.debug(METHOD_EXIT_STR) + + def updateROITxt(self): + ''' + Update the ROI string with the current value of the ROI + ''' + logger.debug(METHOD_ENTER_STR) + roiStr = str(self.roixmin) +\ + COMMA_STR +\ + str(self.roixmax) +\ + COMMA_STR +\ + str(self.roiymin) +\ + COMMA_STR +\ + str(self.roiymax) + self.detROITxt.setText(roiStr) + logger.debug(METHOD_EXIT_STR) + diff --git a/rsMap3D/gui/input/xpcsspecscanfileform.py b/rsMap3D/gui/input/xpcsspecscanfileform.py index 8370fbf..4cca99b 100644 --- a/rsMap3D/gui/input/xpcsspecscanfileform.py +++ b/rsMap3D/gui/input/xpcsspecscanfileform.py @@ -33,9 +33,9 @@ class XPCSSpecScanFileForm(SpecXMLDrivenFileForm): ''' FORM_TITLE = "XPCS SPEC/XML Setup" - DET_ROI_REGEXP_1 = "^(\d*,*)+$" - DET_ROI_REGEXP_2 = "^(\d)+,(\d)+,(\d)+,(\d)+$" - SCAN_LIST_REGEXP = "((\d)+(-(\d)+)?\,( )?)+" + DET_ROI_REGEXP_1 = r"^(\d*,*)+$" + DET_ROI_REGEXP_2 = r"^(\d)+,(\d)+,(\d)+,(\d)+$" + SCAN_LIST_REGEXP = r"((\d)+(-(\d)+)?\,( )?)+" @staticmethod def createInstance(parent=None, appConfig=None): diff --git a/rsMap3D/gui/output/processimagestackform.py b/rsMap3D/gui/output/processimagestackform.py index ffeec75..530773d 100644 --- a/rsMap3D/gui/output/processimagestackform.py +++ b/rsMap3D/gui/output/processimagestackform.py @@ -1,174 +1,174 @@ -''' - Copyright (c) 2016, UChicago Argonne, LLC - See LICENSE file. -''' -import PyQt5.QtGui as qtGui -import PyQt5.QtCore as qtCore -import PyQt5.QtWidgets as qtWidgets - -from PyQt5.QtCore import pyqtSlot as Slot - -from rsMap3D.gui.output.abstractgridoutputform import AbstractGridOutputForm -from rsMap3D.gui.rsm3dcommonstrings import BROWSE_STR, SAVE_DIR_STR, WARNING_STR,\ - BINARY_OUTPUT -import os -from rsMap3D.mappers.output.imagestackwriter import ImageStackWriter - -class ProcessImageStackForm(AbstractGridOutputForm): - ''' - Process grid data and output a stack of TIFF images. - ''' - FORM_TITLE = "Image Stack Output" - - @staticmethod - def createInstance(parent=None, appConfig=None): - ''' - A static method to create an instance of this class. The UI selects which processor method to use - from a menu so this method allows creating an instance without knowing what to create ahead of time. - ''' - return ProcessImageStackForm(parent=parent, appConfig=appConfig) - - def __init__(self, **kwargs): - ''' - Constructor. Typically instances should be created by createInstance method. - ''' - super(ProcessImageStackForm, self).__init__(**kwargs) - self.gridWriter = ImageStackWriter() - layout = qtWidgets.QVBoxLayout() - self.dataBox = self._createDataBox() - controlBox = self._createControlBox() - - layout.addWidget(self.dataBox) - layout.addWidget(controlBox) - self.setLayout(layout) - self.outputType = BINARY_OUTPUT - - @Slot() - def _browseForOutputDirectory(self): - ''' - Launch file browser to find location to write image files. - ''' - if self.outputDirTxt.text() == "": - dirName = str(qtWidgets.QFileDialog.getExistingDirectory(None, - SAVE_DIR_STR)[0]) - else: - curName = str(self.outputDirTxt.text()) - dirName = str(qtWidgets.QFileDialog.getExistingDirectory(None, - SAVE_DIR_STR, - directory = curName)[0]) - - if dirName != "": - if os.path.exists(str(dirName)): - self.outputDirTxt.setText(dirName) - self.outputDirName = dirName - self.outputDirTxt.editingFinished.emit() - else: - message = qtWidgets.QMessageBox() - message.warning(self, - WARNING_STR, - "The specified directory does not exist") - self.outputDirTxt.setText(dirName) - self.outputDirName = dirName - self.outputDirTxt.editingFinished.emit() - if not os.access(dirName, os.W_OK): - message = qtWidgets.QMessageBox() - message.warning(self, - WARNING_STR, - "The specified directory is not writable") - - def _createDataBox(self): - dataBox = super(ProcessImageStackForm, self)._createDataBox() - layout = dataBox.layout() - - row = layout.rowCount() - row += 1 - - label = qtWidgets.QLabel("Output Directory") - layout.addWidget(label, row,0) - self.outputDirName = "" - self.outputDirTxt = qtWidgets.QLineEdit() - self.outputDirTxt.setText(self.outputDirName) - layout.addWidget(self.outputDirTxt, row,1) - self.outputDirButton = qtWidgets.QPushButton(BROWSE_STR) - layout.addWidget(self.outputDirButton, row, 2) - - row += 1 - label = qtWidgets.QLabel("Image File Prefix") - layout.addWidget(label, row, 0) - self.imageFilePrefix = "" - self.imageFilePrefixTxt = qtWidgets.QLineEdit() - self.imageFilePrefixTxt.setText(self.imageFilePrefix) - layout.addWidget(self.imageFilePrefixTxt, row,1) - - row += 1 - label = qtWidgets.QLabel("Slice Axis") - layout.addWidget(label, row, 0) - self.axisChoices = ["0", "1", "2"] - self.axisSelector = qtWidgets.QComboBox() - self.axisSelector.addItems(self.axisChoices) - self.axisSelector.setCurrentIndex(self.gridWriter.getSliceIndex()) - layout.addWidget(self.axisSelector) - - #Connect signals for this class - self.outputDirButton.clicked.connect(self._browseForOutputDirectory) -# self.connect(self.outputDirButton, \ -# qtCore.SIGNAL(EDIT_FINISHED_SIGNAL), -# self._editFinishedOutputDir) - self.outputDirTxt.editingFinished.connect(self._editFinishedOutputDir) - self.setFileName[str].connect(self.outputDirTxt.setText) - self.imageFilePrefixTxt.editingFinished.connect( - self._editFinishedOutputDir) - self.axisSelector.currentIndexChanged[int].connect(self.updateSliceAxis) - - - return dataBox - - @Slot() - def _editFinishedOutputDir(self): - ''' - Process output directory name as inputs are completed. - ''' - dirName = str(self.outputDirTxt.text()) - imageFilePrefix = str(self.imageFilePrefixTxt.text()) - - "process directory" - if dirName != "": - if os.path.exists(dirName): #use specified dir if it exists - self.outputDirName = dirName - else: #use current path - if dirName == "": - dirName = os.path.realpath(os.path.curdir) - else: - message = qtWidgets.QMessageBox() - message.warning(self, - WARNING_STR, - "The specified directory \n" + - str(dirName) + - "\ndoes not exist") - - "process file prefix" - if imageFilePrefix != "": - for badChar in ['\\', '/', ':', '*', '?', '<', '>', '|']: - if badChar in imageFilePrefix: - message = qtWidgets.QMessageBox() - message.warning(self, - WARNING_STR, - "The specified imagePrefix conatins one " + - "of the following invalid characters \/:*?<>|") - else: - self.imageFilePrefix = imageFilePrefix - - def getOutputFileName(self): - ''' - Override from base class. In this case return a join of two of the inputs. This is used to - provide info during runMapper method. - ''' - return os.path.join(self.outputDirName, self.imageFilePrefix) - - - @Slot(int) - def updateSliceAxis(self, index): - ''' - record changes as the axis for slicing is changed - ''' +''' + Copyright (c) 2016, UChicago Argonne, LLC + See LICENSE file. +''' +import PyQt5.QtGui as qtGui +import PyQt5.QtCore as qtCore +import PyQt5.QtWidgets as qtWidgets + +from PyQt5.QtCore import pyqtSlot as Slot + +from rsMap3D.gui.output.abstractgridoutputform import AbstractGridOutputForm +from rsMap3D.gui.rsm3dcommonstrings import BROWSE_STR, SAVE_DIR_STR, WARNING_STR,\ + BINARY_OUTPUT +import os +from rsMap3D.mappers.output.imagestackwriter import ImageStackWriter + +class ProcessImageStackForm(AbstractGridOutputForm): + ''' + Process grid data and output a stack of TIFF images. + ''' + FORM_TITLE = "Image Stack Output" + + @staticmethod + def createInstance(parent=None, appConfig=None): + ''' + A static method to create an instance of this class. The UI selects which processor method to use + from a menu so this method allows creating an instance without knowing what to create ahead of time. + ''' + return ProcessImageStackForm(parent=parent, appConfig=appConfig) + + def __init__(self, **kwargs): + ''' + Constructor. Typically instances should be created by createInstance method. + ''' + super(ProcessImageStackForm, self).__init__(**kwargs) + self.gridWriter = ImageStackWriter() + layout = qtWidgets.QVBoxLayout() + self.dataBox = self._createDataBox() + controlBox = self._createControlBox() + + layout.addWidget(self.dataBox) + layout.addWidget(controlBox) + self.setLayout(layout) + self.outputType = BINARY_OUTPUT + + @Slot() + def _browseForOutputDirectory(self): + ''' + Launch file browser to find location to write image files. + ''' + if self.outputDirTxt.text() == "": + dirName = str(qtWidgets.QFileDialog.getExistingDirectory(None, + SAVE_DIR_STR)[0]) + else: + curName = str(self.outputDirTxt.text()) + dirName = str(qtWidgets.QFileDialog.getExistingDirectory(None, + SAVE_DIR_STR, + directory = curName)[0]) + + if dirName != "": + if os.path.exists(str(dirName)): + self.outputDirTxt.setText(dirName) + self.outputDirName = dirName + self.outputDirTxt.editingFinished.emit() + else: + message = qtWidgets.QMessageBox() + message.warning(self, + WARNING_STR, + "The specified directory does not exist") + self.outputDirTxt.setText(dirName) + self.outputDirName = dirName + self.outputDirTxt.editingFinished.emit() + if not os.access(dirName, os.W_OK): + message = qtWidgets.QMessageBox() + message.warning(self, + WARNING_STR, + "The specified directory is not writable") + + def _createDataBox(self): + dataBox = super(ProcessImageStackForm, self)._createDataBox() + layout = dataBox.layout() + + row = layout.rowCount() + row += 1 + + label = qtWidgets.QLabel("Output Directory") + layout.addWidget(label, row,0) + self.outputDirName = "" + self.outputDirTxt = qtWidgets.QLineEdit() + self.outputDirTxt.setText(self.outputDirName) + layout.addWidget(self.outputDirTxt, row,1) + self.outputDirButton = qtWidgets.QPushButton(BROWSE_STR) + layout.addWidget(self.outputDirButton, row, 2) + + row += 1 + label = qtWidgets.QLabel("Image File Prefix") + layout.addWidget(label, row, 0) + self.imageFilePrefix = "" + self.imageFilePrefixTxt = qtWidgets.QLineEdit() + self.imageFilePrefixTxt.setText(self.imageFilePrefix) + layout.addWidget(self.imageFilePrefixTxt, row,1) + + row += 1 + label = qtWidgets.QLabel("Slice Axis") + layout.addWidget(label, row, 0) + self.axisChoices = ["0", "1", "2"] + self.axisSelector = qtWidgets.QComboBox() + self.axisSelector.addItems(self.axisChoices) + self.axisSelector.setCurrentIndex(self.gridWriter.getSliceIndex()) + layout.addWidget(self.axisSelector) + + #Connect signals for this class + self.outputDirButton.clicked.connect(self._browseForOutputDirectory) +# self.connect(self.outputDirButton, \ +# qtCore.SIGNAL(EDIT_FINISHED_SIGNAL), +# self._editFinishedOutputDir) + self.outputDirTxt.editingFinished.connect(self._editFinishedOutputDir) + self.setFileName[str].connect(self.outputDirTxt.setText) + self.imageFilePrefixTxt.editingFinished.connect( + self._editFinishedOutputDir) + self.axisSelector.currentIndexChanged[int].connect(self.updateSliceAxis) + + + return dataBox + + @Slot() + def _editFinishedOutputDir(self): + ''' + Process output directory name as inputs are completed. + ''' + dirName = str(self.outputDirTxt.text()) + imageFilePrefix = str(self.imageFilePrefixTxt.text()) + + "process directory" + if dirName != "": + if os.path.exists(dirName): #use specified dir if it exists + self.outputDirName = dirName + else: #use current path + if dirName == "": + dirName = os.path.realpath(os.path.curdir) + else: + message = qtWidgets.QMessageBox() + message.warning(self, + WARNING_STR, + "The specified directory \n" + + str(dirName) + + "\ndoes not exist") + + "process file prefix" + if imageFilePrefix != "": + for badChar in ['\\', '/', ':', '*', '?', '<', '>', '|']: + if badChar in imageFilePrefix: + message = qtWidgets.QMessageBox() + message.warning(self, + WARNING_STR, + "The specified imagePrefix conatins one " + + "of the following invalid characters \\/:*?<>|") + else: + self.imageFilePrefix = imageFilePrefix + + def getOutputFileName(self): + ''' + Override from base class. In this case return a join of two of the inputs. This is used to + provide info during runMapper method. + ''' + return os.path.join(self.outputDirName, self.imageFilePrefix) + + + @Slot(int) + def updateSliceAxis(self, index): + ''' + record changes as the axis for slicing is changed + ''' self.gridWriter.setSliceIndex(index) \ No newline at end of file diff --git a/rsMap3D/gui/output/processpowderscanform.py b/rsMap3D/gui/output/processpowderscanform.py index a4578b7..684cd9d 100644 --- a/rsMap3D/gui/output/processpowderscanform.py +++ b/rsMap3D/gui/output/processpowderscanform.py @@ -1,231 +1,231 @@ -''' - Copyright (c) 2017 UChicago Argonne, LLC - See LICENSE file. -''' -import logging -from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR -from rsMap3D.gui.rsm3dcommonstrings import EMPTY_STR, SAVE_FILE_STR, WARNING_STR -import os -from rsMap3D.mappers.powderscanmapper import PowderScanMapper, X_COORD_OPTIONS,\ - Y_SCALING_OPTIONS -from rsMap3D.mappers.output.powderscanwriter import PowderScanWriter -logger = logging.getLogger(__name__) - -import PyQt5.QtGui as qtGui -import PyQt5.QtWidgets as qtWidgets - -from rsMap3D.gui.output.abstractoutputview import AbstractOutputView - -XYE_FILTER_STR = "*.xye" - -class ProcessPowderScanForm(AbstractOutputView): - ''' - This class gives a front end to an external script that has been used at - sector 33 for some time. The original powderscan script was written by - Christian Schleputz, (originally at the APS and now at PSI). This script - allows processing many scans but places the results of each into a separate - file. - ''' - FORM_TITLE = "Powder Scan Output" - - @staticmethod - def createInstance(parent=None, appConfig=None): - return ProcessPowderScanForm(parent=parent, appConfig=appConfig) - - def __init__(self, **kwargs): - super(ProcessPowderScanForm, self).__init__(**kwargs) - logger.debug(METHOD_ENTER_STR) - self.mapper = None - self.outputFileName = None - layout = qtWidgets.QVBoxLayout() - self.dataBox = self._createDataBox() - controlBox = self._createControlBox() - - layout.addWidget(self.dataBox) - layout.addWidget(controlBox) - self.setLayout(layout) - #self.outputType = BINARY_OUTPUT - logger.debug(METHOD_EXIT_STR) - - def _browseForOutputFile(self): - ''' - Launch File Browser to select output file name and directory. Checks - are done to make sure the selected directory exists and that the - selected file is writable - ''' - logger.debug(METHOD_ENTER_STR) - if self.outFileNameTxt == EMPTY_STR: - fileName = str(qtWidgets.QFileDialog.getSaveFileName(None, - SAVE_FILE_STR, - filter=XYE_FILTER_STR)[0]) - else: - inFileName = str(self.outFileNameTxt.text()) - fileName = str(qtWidgets.QFileDialog.getSaveFileName(None, - SAVE_FILE_STR, - filter=XYE_FILTER_STR, - directory=inFileName)[0]) - if fileName != EMPTY_STR: - if os.path.exists(os.path.dirname(str(fileName))): - self.outFileNameTxt.setText(fileName) - self.outputFileName = fileName - self.outFileNameTxt.editingFinished.emit() - else: - message = qtWidgets.QMessageBox() - message.warning(self, WARNING_STR, - "The specified directory does not exist") - - self.outFileNameTxt.setText(fileName) - self.outputFileName = fileName - self.outFileNameTxt.editingFinished.emit() - if not os.access(os.path.dirname(fileName), os.W_OK): - message = qtWidgets.QMessageBox() - message.warning(self, WARNING_STR, - "The specified file is not writable") - else: - self.outputFileName = EMPTY_STR - self.setFileName.emit(EMPTY_STR) - - def _createDataBox(self): - ''' - create the GUI for this processing. This should be run in __init__ - ''' - logger.debug(METHOD_ENTER_STR) - dataBox = super(ProcessPowderScanForm, self)._createDataBox() - dataLayout = dataBox.layout() - row = dataLayout.rowCount() - - label = qtWidgets.QLabel("Output File Name:") - self.outFileNameTxt = qtWidgets.QLineEdit() - self.outFileNameButton = qtWidgets.QPushButton("Browse") - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.outFileNameTxt, row, 1) - dataLayout.addWidget(self.outFileNameButton, row, 2) - - row = dataLayout.rowCount() - label = qtWidgets.QLabel("Data Coordinate:") - self.dataCoordinate = qtWidgets.QComboBox() - self.dataCoordinate.addItems(X_COORD_OPTIONS) - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.dataCoordinate, row, 1) - - row = dataLayout.rowCount() + 1 - label = qtWidgets.QLabel("Output Data Range - leave max/min blank for automatic range") - dataLayout.addWidget(label, row, 0, 2, -1) - - row = dataLayout.rowCount() - label = qtWidgets.QLabel("Min:") - self.outMinTxt = qtWidgets.QLineEdit() - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.outMinTxt, row, 1) - - row = dataLayout.rowCount() - label = qtWidgets.QLabel("Max:") - self.outMaxTxt = qtWidgets.QLineEdit() - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.outMaxTxt, row, 1) - - row = dataLayout.rowCount() - label = qtWidgets.QLabel("Step:") - self.outStepTxt = qtWidgets.QLineEdit("0.01") - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.outStepTxt, row, 1) - - row = dataLayout.rowCount() +1 - label = qtWidgets.QLabel("Plot Results:") - self.plotResults = qtWidgets.QCheckBox() - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.plotResults, row, 1) - - row = dataLayout.rowCount() - label = qtWidgets.QLabel("Y Scaling:") - self.yScaling = qtWidgets.QComboBox() - self.yScaling.addItems(Y_SCALING_OPTIONS) - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.yScaling, row, 1) - - row = dataLayout.rowCount() +1 - label = qtWidgets.QLabel("Write xye file:") - self.writeFile = qtWidgets.QCheckBox() - self.writeFile.setChecked(True) - dataLayout.addWidget(label, row, 0) - dataLayout.addWidget(self.writeFile, row, 1) - - self.outFileNameTxt.editingFinished.connect(self._editFinishedOutFileName) - self.outFileNameButton.clicked.connect(self._browseForOutputFile) - self.setFileName[str].connect(self.setOutFileNameText) - - - logger.debug(METHOD_EXIT_STR) - return dataBox - - def _editFinishedOutFileName(self): - ''' - process the output file name when user has finished entering this into - the text box. This may also be entered by browsing. - ''' - logger.debug(METHOD_ENTER_STR) - fileName = str(self.outFileNameTxt.text()) - if fileName != EMPTY_STR: - if os.path.exists(os.path.dirname(fileName)): - self.outputFileName = fileName - else: - if os.path.dirname(fileName) == EMPTY_STR: - curDir = os.path.realpath(os.path.curdir) - fileName = str(os.path.join(curDir, fileName)) - else: - message = qtWidgets.QMessageBox() - message.warning(self, - WARNING_STR, - "The specified directory \n" + \ - str(os.path.dirname(fileName)) + \ - "\does not exist") - self.setFileName.emit(fileName) - if not os.access(os.path.dirname(fileName), os.W_OK): - self.outputFileName = EMPTY_STR - #self.setFileName.emit(EMPTY_STR) - - logger.debug(METHOD_EXIT_STR) - - def runMapper(self, dataSource, transform, gridWriter=None): - ''' - This method is run by the runMapper method of the ProcessScanController, - This will launch proceesing by a mapper appropriate for the data that we - have & requested output. This ultimately runs the doMap of the selected - Mapper - ''' - logger.debug(METHOD_ENTER_STR) - self.dataSource = dataSource - -# if self.outputFileName == "" or self.outputFileName is None: -# self.outputFileName = os.path.join(dataSource.projectDir, -# "%s.xye" % dataSource.projectName) -# self.setFileName[str].emit(self.outputFileName) -# if os.access(os.path.dirname(self.outputFileName), os.W_OK): - logger.debug("Loading PowderScanMapper with xmin %s, xmax %s, xstep %s" % \ - (str(self.outMinTxt.text()), - str(self.outMaxTxt.text()), - str(self.outStepTxt.text()))) - self.mapper = PowderScanMapper(self.dataSource, - self.outputFileName, - transform=transform, - appConfig = self.appConfig, - dataCoord = str(self.dataCoordinate.currentText()), - xCoordMin = str(self.outMinTxt.text()), - xCoordMax = str(self.outMaxTxt.text()), - xCoordStep = str(self.outStepTxt.text()), - plotResults = self.plotResults.isChecked(), - yScaling = str(self.yScaling.currentText()), - writeXyeFile = self.writeFile.isChecked()) - self.mapper.setGridWriter(PowderScanWriter()) - self.mapper.setProgressUpdater(self._updateProgress) - self.mapper.doMap() - - - def setOutFileNameText(self, outFile): - ''' - Method to help with coordination between browsing for the fike and typing - it into the text box. - ''' - logger.debug(METHOD_ENTER_STR) - self.outFileNameTxt.setText(outFile) +''' + Copyright (c) 2017 UChicago Argonne, LLC + See LICENSE file. +''' +import logging +from rsMap3D.config.rsmap3dlogging import METHOD_ENTER_STR, METHOD_EXIT_STR +from rsMap3D.gui.rsm3dcommonstrings import EMPTY_STR, SAVE_FILE_STR, WARNING_STR +import os +from rsMap3D.mappers.powderscanmapper import PowderScanMapper, X_COORD_OPTIONS,\ + Y_SCALING_OPTIONS +from rsMap3D.mappers.output.powderscanwriter import PowderScanWriter +logger = logging.getLogger(__name__) + +import PyQt5.QtGui as qtGui +import PyQt5.QtWidgets as qtWidgets + +from rsMap3D.gui.output.abstractoutputview import AbstractOutputView + +XYE_FILTER_STR = "*.xye" + +class ProcessPowderScanForm(AbstractOutputView): + ''' + This class gives a front end to an external script that has been used at + sector 33 for some time. The original powderscan script was written by + Christian Schleputz, (originally at the APS and now at PSI). This script + allows processing many scans but places the results of each into a separate + file. + ''' + FORM_TITLE = "Powder Scan Output" + + @staticmethod + def createInstance(parent=None, appConfig=None): + return ProcessPowderScanForm(parent=parent, appConfig=appConfig) + + def __init__(self, **kwargs): + super(ProcessPowderScanForm, self).__init__(**kwargs) + logger.debug(METHOD_ENTER_STR) + self.mapper = None + self.outputFileName = None + layout = qtWidgets.QVBoxLayout() + self.dataBox = self._createDataBox() + controlBox = self._createControlBox() + + layout.addWidget(self.dataBox) + layout.addWidget(controlBox) + self.setLayout(layout) + #self.outputType = BINARY_OUTPUT + logger.debug(METHOD_EXIT_STR) + + def _browseForOutputFile(self): + ''' + Launch File Browser to select output file name and directory. Checks + are done to make sure the selected directory exists and that the + selected file is writable + ''' + logger.debug(METHOD_ENTER_STR) + if self.outFileNameTxt == EMPTY_STR: + fileName = str(qtWidgets.QFileDialog.getSaveFileName(None, + SAVE_FILE_STR, + filter=XYE_FILTER_STR)[0]) + else: + inFileName = str(self.outFileNameTxt.text()) + fileName = str(qtWidgets.QFileDialog.getSaveFileName(None, + SAVE_FILE_STR, + filter=XYE_FILTER_STR, + directory=inFileName)[0]) + if fileName != EMPTY_STR: + if os.path.exists(os.path.dirname(str(fileName))): + self.outFileNameTxt.setText(fileName) + self.outputFileName = fileName + self.outFileNameTxt.editingFinished.emit() + else: + message = qtWidgets.QMessageBox() + message.warning(self, WARNING_STR, + "The specified directory does not exist") + + self.outFileNameTxt.setText(fileName) + self.outputFileName = fileName + self.outFileNameTxt.editingFinished.emit() + if not os.access(os.path.dirname(fileName), os.W_OK): + message = qtWidgets.QMessageBox() + message.warning(self, WARNING_STR, + "The specified file is not writable") + else: + self.outputFileName = EMPTY_STR + self.setFileName.emit(EMPTY_STR) + + def _createDataBox(self): + ''' + create the GUI for this processing. This should be run in __init__ + ''' + logger.debug(METHOD_ENTER_STR) + dataBox = super(ProcessPowderScanForm, self)._createDataBox() + dataLayout = dataBox.layout() + row = dataLayout.rowCount() + + label = qtWidgets.QLabel("Output File Name:") + self.outFileNameTxt = qtWidgets.QLineEdit() + self.outFileNameButton = qtWidgets.QPushButton("Browse") + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.outFileNameTxt, row, 1) + dataLayout.addWidget(self.outFileNameButton, row, 2) + + row = dataLayout.rowCount() + label = qtWidgets.QLabel("Data Coordinate:") + self.dataCoordinate = qtWidgets.QComboBox() + self.dataCoordinate.addItems(X_COORD_OPTIONS) + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.dataCoordinate, row, 1) + + row = dataLayout.rowCount() + 1 + label = qtWidgets.QLabel("Output Data Range - leave max/min blank for automatic range") + dataLayout.addWidget(label, row, 0, 2, -1) + + row = dataLayout.rowCount() + label = qtWidgets.QLabel("Min:") + self.outMinTxt = qtWidgets.QLineEdit() + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.outMinTxt, row, 1) + + row = dataLayout.rowCount() + label = qtWidgets.QLabel("Max:") + self.outMaxTxt = qtWidgets.QLineEdit() + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.outMaxTxt, row, 1) + + row = dataLayout.rowCount() + label = qtWidgets.QLabel("Step:") + self.outStepTxt = qtWidgets.QLineEdit("0.01") + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.outStepTxt, row, 1) + + row = dataLayout.rowCount() +1 + label = qtWidgets.QLabel("Plot Results:") + self.plotResults = qtWidgets.QCheckBox() + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.plotResults, row, 1) + + row = dataLayout.rowCount() + label = qtWidgets.QLabel("Y Scaling:") + self.yScaling = qtWidgets.QComboBox() + self.yScaling.addItems(Y_SCALING_OPTIONS) + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.yScaling, row, 1) + + row = dataLayout.rowCount() +1 + label = qtWidgets.QLabel("Write xye file:") + self.writeFile = qtWidgets.QCheckBox() + self.writeFile.setChecked(True) + dataLayout.addWidget(label, row, 0) + dataLayout.addWidget(self.writeFile, row, 1) + + self.outFileNameTxt.editingFinished.connect(self._editFinishedOutFileName) + self.outFileNameButton.clicked.connect(self._browseForOutputFile) + self.setFileName[str].connect(self.setOutFileNameText) + + + logger.debug(METHOD_EXIT_STR) + return dataBox + + def _editFinishedOutFileName(self): + ''' + process the output file name when user has finished entering this into + the text box. This may also be entered by browsing. + ''' + logger.debug(METHOD_ENTER_STR) + fileName = str(self.outFileNameTxt.text()) + if fileName != EMPTY_STR: + if os.path.exists(os.path.dirname(fileName)): + self.outputFileName = fileName + else: + if os.path.dirname(fileName) == EMPTY_STR: + curDir = os.path.realpath(os.path.curdir) + fileName = str(os.path.join(curDir, fileName)) + else: + message = qtWidgets.QMessageBox() + message.warning(self, + WARNING_STR, + "The specified directory \n" + \ + str(os.path.dirname(fileName)) + \ + "does not exist") + self.setFileName.emit(fileName) + if not os.access(os.path.dirname(fileName), os.W_OK): + self.outputFileName = EMPTY_STR + #self.setFileName.emit(EMPTY_STR) + + logger.debug(METHOD_EXIT_STR) + + def runMapper(self, dataSource, transform, gridWriter=None): + ''' + This method is run by the runMapper method of the ProcessScanController, + This will launch proceesing by a mapper appropriate for the data that we + have & requested output. This ultimately runs the doMap of the selected + Mapper + ''' + logger.debug(METHOD_ENTER_STR) + self.dataSource = dataSource + +# if self.outputFileName == "" or self.outputFileName is None: +# self.outputFileName = os.path.join(dataSource.projectDir, +# "%s.xye" % dataSource.projectName) +# self.setFileName[str].emit(self.outputFileName) +# if os.access(os.path.dirname(self.outputFileName), os.W_OK): + logger.debug("Loading PowderScanMapper with xmin %s, xmax %s, xstep %s" % \ + (str(self.outMinTxt.text()), + str(self.outMaxTxt.text()), + str(self.outStepTxt.text()))) + self.mapper = PowderScanMapper(self.dataSource, + self.outputFileName, + transform=transform, + appConfig = self.appConfig, + dataCoord = str(self.dataCoordinate.currentText()), + xCoordMin = str(self.outMinTxt.text()), + xCoordMax = str(self.outMaxTxt.text()), + xCoordStep = str(self.outStepTxt.text()), + plotResults = self.plotResults.isChecked(), + yScaling = str(self.yScaling.currentText()), + writeXyeFile = self.writeFile.isChecked()) + self.mapper.setGridWriter(PowderScanWriter()) + self.mapper.setProgressUpdater(self._updateProgress) + self.mapper.doMap() + + + def setOutFileNameText(self, outFile): + ''' + Method to help with coordination between browsing for the fike and typing + it into the text box. + ''' + logger.debug(METHOD_ENTER_STR) + self.outFileNameTxt.setText(outFile) self.outFileNameTxt.editingFinished.emit() \ No newline at end of file diff --git a/rsMap3D/mappers/abstractmapper.py b/rsMap3D/mappers/abstractmapper.py index 9618d90..a1d3207 100644 --- a/rsMap3D/mappers/abstractmapper.py +++ b/rsMap3D/mappers/abstractmapper.py @@ -1,140 +1,140 @@ -''' - Copyright (c) 2014, UChicago Argonne, LLC - See LICENSE file. -''' -import abc -import time -from rsMap3D.transforms.unitytransform3d import UnityTransform3D -from rsMap3D.exception.rsmap3dexception import RSMap3DException - - - -class AbstractGridMapper(object): - __metaclass__ = abc.ABCMeta - ''' - This class is an abstract class around which to build a reciprocal space - mapping class using the xrayutilities module. It requires an input of the - type AbstractXrayUtilitiesDataSource provided in the rsMap3D.datasource - package. - ''' - - - def __init__(self, dataSource, \ - outputFileName, \ - nx=200, ny=201, nz=202, \ - transform = None, \ - gridWriter = None, - appConfig = None): - ''' - Constructor - :param dataSource: source of scan data - :param outputFileName: filename for output - :param nx: number of points in x direction for griidder - :param ny: number of points in y direction for griidder - :param nz: number of points in z direction for griidder - :param transform: Transform to be applied to the axes before gridding - ''' - self.appConfig = None - if not (appConfig is None): - self.appConfig = appConfig - else: - raise RSMap3DException("no AppConfig object received.") - - self.dataSource = dataSource - self.outputFileName = outputFileName - self.nx = nx - self.ny = ny - self.nz = nz - self.haltMap = False - self.progressUpdater = None - self.gridWriter = gridWriter - if transform is None: - self.transform = UnityTransform3D() - else: - self.transform = transform - - def doMap(self): - ''' - Produce a q map of the data. This is the method typically called to - run the mapper. This method calls the processMap method which is an - abstract method which needs to be defined in subclasses. - ''' - - # read and grid data with helper function - _start_time = time.time() - #rangeBounds = self.dataSource.getRangeBounds() - qx, qy, qz, gint, gridder = \ - self.processMap() - print ('Elapsed time for gridding: %.3f seconds' % \ - (time.time() - _start_time)) - - # print some information - print ('qx: ', qx.min(), ' .... ', qx.max()) - print ('qy: ', qy.min(), ' .... ', qy.max()) - print ('qz: ', qz.min(), ' .... ', qz.max()) - self.gridWriter.setData(qx, qy, qz, gint) - self.gridWriter.setFileInfo(self.getFileInfo()) - self.gridWriter.write() - - def getFileInfo(self): - return (self.dataSource.projectName, - self.dataSource.availableScans[0], - self.nx, self.ny, self.nz, - self.outputFileName) - - @abc.abstractmethod - def processMap(self, **kwargs): - """ - Abstract method for processing data. This method is called by the - method doMap. Typical access to this method is through the doMap - method. - """ - raise NotImplementedError("Subclasses should define this method") - - def setGridWriter(self, writer): - self.gridWriter = writer - - def setGridSize(self, nx, ny, nz): - """ - Set the grid size to be used for outputting data. - :param nx: number of points in x direction for griidder - :param ny: number of points in y direction for griidder - :param nz: number of points in z direction for griidder - """ - self.nx = nx - self.ny = ny - self.nz = nz - - def setProgressUpdater(self, updater): - ''' - Set the updater that will be used to maintain the progress bar value - ''' - self.progressUpdater = updater - - def setTransform(self, transform): - ''' - Set a transform which will define a mapping of q space to some other - system. The transform set here should be a subclass of - AbstractTransform3D which is defined in - rsMap3D.transform.abstracttransform3D. - :param tranfoem: - ''' - self.transform = transform - - def stopMap(self): - ''' - Set a flag that will be used to halt processing the scan using - ''' - self.haltMap = True - self.dataSource.stopMap() - -class ProcessCanceledException(RSMap3DException): - ''' - Exception Thrown when loading data is canceled. - ''' - def __init__(self, message): - ''' - Constructor - :param message: Message to be carried conveyed with exception - ''' - super(ProcessCanceledException, self).__init__(message) +''' + Copyright (c) 2014, UChicago Argonne, LLC + See LICENSE file. +''' +import abc +import time +from rsMap3D.transforms.unitytransform3d import UnityTransform3D +from rsMap3D.exception.rsmap3dexception import RSMap3DException + + + +class AbstractGridMapper(object): + __metaclass__ = abc.ABCMeta + ''' + This class is an abstract class around which to build a reciprocal space + mapping class using the xrayutilities module. It requires an input of the + type AbstractXrayUtilitiesDataSource provided in the rsMap3D.datasource + package. + ''' + + + def __init__(self, dataSource, \ + outputFileName, \ + nx=200, ny=201, nz=202, \ + transform = None, \ + gridWriter = None, + appConfig = None): + ''' + Constructor + :param dataSource: source of scan data + :param outputFileName: filename for output + :param nx: number of points in x direction for griidder + :param ny: number of points in y direction for griidder + :param nz: number of points in z direction for griidder + :param transform: Transform to be applied to the axes before gridding + ''' + self.appConfig = None + if not (appConfig is None): + self.appConfig = appConfig + else: + raise RSMap3DException("no AppConfig object received.") + + self.dataSource = dataSource + self.outputFileName = outputFileName + self.nx = nx + self.ny = ny + self.nz = nz + self.haltMap = False + self.progressUpdater = None + self.gridWriter = gridWriter + if transform is None: + self.transform = UnityTransform3D() + else: + self.transform = transform + + def doMap(self): + ''' + Produce a q map of the data. This is the method typically called to + run the mapper. This method calls the processMap method which is an + abstract method which needs to be defined in subclasses. + ''' + + # read and grid data with helper function + _start_time = time.time() + #rangeBounds = self.dataSource.getRangeBounds() + qx, qy, qz, gint, gridder = \ + self.processMap() + print ('Elapsed time for gridding: %.3f seconds' % \ + (time.time() - _start_time)) + + # print some information + print ('Qx/H: ', qx.min(), ' .... ', qx.max()) + print ('Qy/K: ', qy.min(), ' .... ', qy.max()) + print ('Qz/L: ', qz.min(), ' .... ', qz.max()) + self.gridWriter.setData(qx, qy, qz, gint) + self.gridWriter.setFileInfo(self.getFileInfo()) + self.gridWriter.write() + + def getFileInfo(self): + return (self.dataSource.projectName, + self.dataSource.availableScans[0], + self.nx, self.ny, self.nz, + self.outputFileName) + + @abc.abstractmethod + def processMap(self, **kwargs): + """ + Abstract method for processing data. This method is called by the + method doMap. Typical access to this method is through the doMap + method. + """ + raise NotImplementedError("Subclasses should define this method") + + def setGridWriter(self, writer): + self.gridWriter = writer + + def setGridSize(self, nx, ny, nz): + """ + Set the grid size to be used for outputting data. + :param nx: number of points in x direction for griidder + :param ny: number of points in y direction for griidder + :param nz: number of points in z direction for griidder + """ + self.nx = nx + self.ny = ny + self.nz = nz + + def setProgressUpdater(self, updater): + ''' + Set the updater that will be used to maintain the progress bar value + ''' + self.progressUpdater = updater + + def setTransform(self, transform): + ''' + Set a transform which will define a mapping of q space to some other + system. The transform set here should be a subclass of + AbstractTransform3D which is defined in + rsMap3D.transform.abstracttransform3D. + :param tranfoem: + ''' + self.transform = transform + + def stopMap(self): + ''' + Set a flag that will be used to halt processing the scan using + ''' + self.haltMap = True + self.dataSource.stopMap() + +class ProcessCanceledException(RSMap3DException): + ''' + Exception Thrown when loading data is canceled. + ''' + def __init__(self, message): + ''' + Constructor + :param message: Message to be carried conveyed with exception + ''' + super(ProcessCanceledException, self).__init__(message) diff --git a/rsMap3D/mappers/powderscanmapper.py b/rsMap3D/mappers/powderscanmapper.py index e1a8034..d6e1ca7 100644 --- a/rsMap3D/mappers/powderscanmapper.py +++ b/rsMap3D/mappers/powderscanmapper.py @@ -215,7 +215,8 @@ def processMap(self): self.progressUpdater(int(progress)) #===== else: - nPasses = np.int(np.floor(imageSize*4*numImages/maxImageMem)+1) + # np.int() deprecated; use int() instead. ZZ + nPasses = int(np.floor(imageSize*4*numImages/maxImageMem)+1) for thisPass in range(nPasses): logger.info("Pass Number %d of %d" % \ (thisPass+1, nPasses))