From 5e2f5041812c4baae056988fdf659861fee27787 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 31 Aug 2015 15:46:04 -0400 Subject: [PATCH 001/253] adding imageseries files and script --- hexrd/xrd/image_io.py | 384 +++++++++++++++++++++ hexrd/xrd/imageseries/__init__.py | 43 +++ hexrd/xrd/imageseries/adapters/__init__.py | 28 ++ hexrd/xrd/imageseries/adapters/hdf5.py | 81 +++++ hexrd/xrd/imageseries/adapters/registry.py | 13 + hexrd/xrd/imageseries/adapters/trivial.py | 5 + hexrd/xrd/imageseries/imageseriesabc.py | 6 + hexrd/xrd/imageseries/imageseriesiter.py | 23 ++ scripts/make_imageseries.py | 300 ++++++++++++++++ 9 files changed, 883 insertions(+) create mode 100644 hexrd/xrd/image_io.py create mode 100644 hexrd/xrd/imageseries/__init__.py create mode 100644 hexrd/xrd/imageseries/adapters/__init__.py create mode 100644 hexrd/xrd/imageseries/adapters/hdf5.py create mode 100644 hexrd/xrd/imageseries/adapters/registry.py create mode 100644 hexrd/xrd/imageseries/adapters/trivial.py create mode 100644 hexrd/xrd/imageseries/imageseriesabc.py create mode 100644 hexrd/xrd/imageseries/imageseriesiter.py create mode 100755 scripts/make_imageseries.py diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py new file mode 100644 index 00000000..5a5f16fe --- /dev/null +++ b/hexrd/xrd/image_io.py @@ -0,0 +1,384 @@ +"""Image reading (mostly) and writing + +Classes +------- +Framer2DRC: base class for reader/writers +ReadGeneric: +ReadGE: + +ThreadReadFrame: class for using threads to read frames + +Functions +--------- +newGenericReader - returns a reader instance + +""" +import copy +import os +import time +import warnings + +import numpy as num + +import imageseries + +warnings.filterwarnings('always', '', DeprecationWarning) + +class ReaderDeprecationWarning(DeprecationWarning): + """Warnings on use of old reader features""" + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class OmegaImageSeries(object): + """Facade for frame_series class, replacing other readers, primarily ReadGE""" + OMEGA_TAG = 'omega' + + def __init__(self, fname, fmt='hdf5', **kwargs): + """Initialize frame readerOmegaFrameReader + + *fileinfo* is a string + *fmt* is the format to be passed to imageseries.open() + *kwargs* is the option list to be passed to imageseries.open() + + NOTES: + * The shape returned from imageseries is cast to int from numpy.uint64 + to allow for addition of indices with regular ints + """ + self._imseries = imageseries.open(fname, fmt, **kwargs) + self._shape = self._imseries.shape + self._meta = self._imseries.metadata + + if self.OMEGA_TAG not in self._meta: + raise RuntimeError('No omega data found in data file') + return + + def __getitem__(self, k): + print 'new image: ', k + return self._imseries[k] + + @property + def nframes(self): + """(get-only) number of frames""" + return self._shape[0] + + @property + def nrows(self): + """(get-only) number of rows""" + return self._shape[1] + + @property + def ncols(self): + """(get-only) number of columns""" + return self._shape[2] + + @property + def omega(self): + """ (get-only) array of omega min/max per frame""" + return self._meta[self.OMEGA_TAG] + + pass + +class Framer2DRC(object): + """Base class for readers. + """ + def __init__(self, ncols, nrows, + dtypeDefault='int16', dtypeRead='uint16', dtypeFloat='float64'): + self.__nrows = nrows + self.__ncols = ncols + self.__frame_dtype_dflt = dtypeDefault + self.__frame_dtype_read = dtypeRead + self.__frame_dtype_float = dtypeFloat + + self.__nbytes_frame = num.nbytes[dtypeRead]*nrows*ncols + + return + + def get_nrows(self): + return self.__nrows + nrows = property(get_nrows, None, None) + + def get_ncols(self): + return self.__ncols + ncols = property(get_ncols, None, None) + + def get_nbytesFrame(self): + return self.__nbytes_frame + nbytesFrame = property(get_nbytesFrame, None, None) + + def get_dtypeDefault(self): + return self.__frame_dtype_dflt + dtypeDefault = property(get_dtypeDefault, None, None) + + def get_dtypeRead(self): + return self.__frame_dtype_read + dtypeRead = property(get_dtypeRead, None, None) + + def get_dtypeFloat(self): + return self.__frame_dtype_float + dtypeFloat = property(get_dtypeFloat, None, None) + + def getEmptyMask(self): + """convenience method for getting an emtpy mask""" + # this used to be a class method + return num.zeros([self.nrows, self.ncols], dtype=bool) + +class OmegaFramer(object): + """Omega information associated with frame numbers""" + def __init__(self, omegas): + """Initialize omega ranges + + *omegas* is nframes x 2 + + Could check for monotonicity. + """ + self._omegas = omegas + self._omin = omegas.min() + self._omax = omegas.max() + self._omean = omegas.mean(axis=1) + self._odels = omegas[:, 1] - omegas[:, 0] + self._delta = self._odels[0] + self._orange = num.hstack((omegas[:, 0], omegas[-1, 1])) + + return + + # property: + + def _omin(self): + return self._omin + + def _omax(self): + return self._omax + + def getDeltaOmega(self, nframes=1): + return self._omax - self._omin + + def getOmegaMinMax(self): + return self._omin, self._omax + + def frameToOmega(self, frame): + """can frame be nonintegral? round to int ... """ + return self._omean[frame] + + def omegaToFrame(self, omega): + return num.searchsorted(self._orange) - 1 + + + def omegaToFrameRange(self, omega): + # note: old code assumed single delta omega + return omeToFrameRange(omega, self._omean, self._delta) + + +class ReadGeneric(Framer2DRC, OmegaFramer): + """Generic reader with omega information +""" + def __init__(self, filename, ncols, nrows, *args, **kwargs): + + Framer2DRC.__init__(self, ncols, nrows, **kwargs) + return + + def read(self, nskip=0, nframes=1, sumImg=False): + """ + sumImg can be set to True or to something like numpy.maximum + """ + raise RuntimeError("Generic reader not available for reading") + + def getNFrames(self): + return 0 + + + def getWriter(self, filename): + return None + +class ReadGE(Framer2DRC,OmegaFramer): + """General reader for omega scans + + Originally, this was for reading GE format images, but this is now + a general reader accessing the OmegaFrameReader facade class. The main + functionality to read a sequence of images with associated omega ranges. + + ORIGINAL DOCS + ============= + + *) In multiframe images where background subtraction is requested but no + dark is specified, attempts to use the + empty frame(s). An error is returned if there are not any specified. + If there are multiple empty frames, the average is used. + + """ + def __init__(self, file_info, *args, **kwargs): + """Initialize the reader + + *file_info* is now just the filename + *kwargs* is a dictionary + keys include: "path" path in hdf5 file + + Of original kwargs, only using "mask" + """ + self._fname = file_info + self._kwargs = kwargs + self._omis = OmegaImageSeries(file_info, **kwargs) + self.mask = None + + Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) + OmegaFramer.__init__(self, self._omis.omega) + + # counter for last global frame that was read + self.iFrame = -1 + + return + + + def __call__(self, *args, **kwargs): + return self.read(*args, **kwargs) + + @classmethod + def makeNew(cls): + """return another copy of this reader""" + return cls(self._fname, **self._kwargs) + + def getWriter(self, filename): + return None + + def getNFrames(self): + """number of total frames with real data, not number remaining""" + return self._omis.nframes + + def getFrameOmega(self, iFrame=None): + """if iFrame is none, use internal counter""" + if iFrame is None: + iFrame = self.iFrame + if hasattr(iFrame, '__len__'): + # in case last read was multiframe + oms = [self.frameToOmega(frm) for frm in iFrame] + retval = num.mean(num.asarray(oms)) + else: + retval = self.frameToOmega(iFrame) + return retval + + + def readBBox(self, bbox, raw=True, doFlip=None): + """ + with raw=True, read more or less raw data, with bbox = [(iLo,iHi),(jLo,jHi),(fLo,fHi)] + + """ + # implement in OmegaFrameReader + nskip = bbox[2][0] + bBox = num.array(bbox) + sl_i = slice(*bBox[0]) + sl_j = slice(*bBox[1]) + 'plenty of performance optimization might be possible here' + if raw: + retval = num.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_read ) + else: + retval = num.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_dflt ) + for iFrame in range(retval.shape[2]): + thisframe = reader.read(nskip=nskip) + nskip = 0 + retval[:,:,iFrame] = copy.deepcopy(thisframe[sl_i, sl_j]) + return retval + + def getDark(self): + return 0 + + def indicesToMask(self, indices): + """Create mask from list of indices + + Indices can be a list of indices, as from makeIndicesTThRanges + """ + mask = self.getEmptyMask() + if hasattr(indices,'__len__'): + for indThese in indices: + mask[indThese] = True + else: + mask[indices] = True + return mask + + def read(self, nskip=0, nframes=1, sumImg=False): + """Read one or more frames, possibly operating on them + + This returns a single frame is nframes is 1, multiple + frames if nframes > 1 with sumImg off, or a single frame + resulting from some operation on the multiple frames if + sumImg is true or a function. + + *sumImg* can be set to True or to a function of two frames like numpy.maximum + *nskip* applies only to the first frame + """ + self.iFrame = num.atleast_1d(self.iFrame)[-1] + nskip + + multiframe = nframes > 1 + sumimg_callable = hasattr(sumImg, '__call__') + + if not multiframe: + self.iFrame += 1 + img = self._omis[self.iFrame] + if self.mask is not None: + img[self.mask] = 0 + return img + + # multiframe case + self.iFrame = self.iFrame + 1 + range(nframes) + + if not sumImg: + # return multiple frames + imgs = self._omis[self.iFrame] + for i in range(nframes): + if self.mask is not None: + imgs[i, self.mask] = 0 + return imgs + + # Now, operate on frames consecutively + op = sumImg if sumimg_callable else num.add + + ifrm = self.iFrame + 1 + + img = self._omis[ifrm] + for i in range(1, nframes): + ifrm += 1 + img = op(img, self._omis[ifrm]) + if not sumimg_callable: + img = img * (1.0/nframes) + + if self.mask is not None: + img[self.mask] = 0 + + return img + + def close(self): + return + + @classmethod + def display(cls, + thisframe, + roi = None, + pw = None, + **kwargs + ): + warnings.warn('display method on readers no longer implemented', + ReaderDeprecationWarning) + +# +# Module functions +# +def omeToFrameRange(omega, omegas, omegaDelta): + """ + check omega range for the frames in + stead of omega center; + result can be a pair of frames if the specified omega is + exactly on the border + """ + retval = num.where(num.abs(omegas - omega) <= omegaDelta*0.5)[0] + return retval + +def newGenericReader(ncols, nrows, *args, **kwargs): + """ Currently just returns a Framer2DRC + """ + + # retval = Framer2DRC(ncols, nrows, **kwargs) + filename = kwargs.pop('filename', None) + retval = ReadGeneric(filename, ncols, nrows, *args, **kwargs) + + return retval + diff --git a/hexrd/xrd/imageseries/__init__.py b/hexrd/xrd/imageseries/__init__.py new file mode 100644 index 00000000..06ab65f1 --- /dev/null +++ b/hexrd/xrd/imageseries/__init__.py @@ -0,0 +1,43 @@ +"""Handles series of images + +This file contains the generic ImageSeries class +and a function for loading. Adapters for particular +data formats are managed in a subpackage. +""" +from imageseriesabc import ImageSeriesABC +import adapters + +class ImageSeries(ImageSeriesABC): + """collection of images + + Basic sequence class with additional properties for image shape and + metadata (possibly None). + """ + + def __init__(self, adapter): + """Build FrameSeries from adapter instance + + *adapter* - object instance based on abstract Sequence class with + properties for image shape and, optionally, metadata. + """ + self.__adapter = adapter + + return + + def __getitem__(self, key): + return self.__adapter[key] + + def __len__(self): + return len(self.__adapter) + + def __getattr__(self, attrname): + return getattr(self.__adapter, attrname) + + pass # end class + +def open(filename, format=None, **kwargs): + # find the appropriate adapter based on format specified + reg = adapters.registry.Registry.adapter_registry + adapter = reg[format](filename, **kwargs) + print adapter + return ImageSeries(adapter) diff --git a/hexrd/xrd/imageseries/adapters/__init__.py b/hexrd/xrd/imageseries/adapters/__init__.py new file mode 100644 index 00000000..bcbae650 --- /dev/null +++ b/hexrd/xrd/imageseries/adapters/__init__.py @@ -0,0 +1,28 @@ +import abc +import pkgutil + +from ..imageseriesabc import ImageSeriesABC +from .registry import Registry + +# Metaclass for adapter registry + +class _RegisterAdapterClass(abc.ABCMeta): + + def __init__(cls, name, bases, attrs): + abc.ABCMeta.__init__(cls, name, bases, attrs) + Registry.register(cls) + +class ImageSeriesAdapter(ImageSeriesABC): + + __metaclass__ = _RegisterAdapterClass + + format = None + +# import all adapter modules + +for loader, name, ispkg in pkgutil.iter_modules(__path__): + if name is not 'registry': + __import__(name, globals=globals()) + # + # couldn't get the following line to work due to relative import issue: + # loader.find_module(name).load_module(name) diff --git a/hexrd/xrd/imageseries/adapters/hdf5.py b/hexrd/xrd/imageseries/adapters/hdf5.py new file mode 100644 index 00000000..5b8900b9 --- /dev/null +++ b/hexrd/xrd/imageseries/adapters/hdf5.py @@ -0,0 +1,81 @@ +"""HDF5 adapter class +""" +import h5py +from . import ImageSeriesAdapter +from ..imageseriesiter import ImageSeriesIterator + +class HDF5ImageSeriesAdapter(ImageSeriesAdapter): + """collection of images in HDF5 format""" + + format = 'hdf5' + #The code below failed with: "Error when calling the metaclass bases" + # "'property' object is not callable" + #@property + #def format(self): + # return 'hdf5' + + @property + def _dset(self): + # return a context manager to ensure proper file handling + # always use like: "with self._dset as dset:" + return H5ContextManager(self.__h5name, self.__path) + + @property + #@memoize + def metadata(self): + """(read-only) Image sequence metadata + + Currently returns any dimension scales in a dictionary + """ + mdict = {} + with self._dset as dset: + for k in dset.dims[0].keys(): + mdict[k] = dset.dims[0][k][...] + + return mdict + + @property + #@memoize so you only need to do this once + def shape(self): + with self._dset as dset: + return dset.shape + + def __init__(self, fname, **kwargs): + """Constructor for H5FrameSeries + + *fname* - filename of the HDF5 file + *kwargs* - keyword arguments, choices are: + path - (required) path of dataset in HDF5 file + """ + self.__h5name = fname + self.__path = kwargs['path'] + + def __getitem__(self, key): + with self._dset as dset: + return dset.__getitem__(key) + + def __iter__(self): + return ImageSeriesIterator(self) + + #@memoize + def __len__(self): + with self._dset as dset: + return len(dset) + + pass # end class + + +class H5ContextManager: + + def __init__(self, fname, path): + self._fname = fname + self._path = path + self._f = None + + def __enter__(self): + self._f = h5py.File(self._fname, 'r') + return self._f[self._path] + + def __exit__(self, *args): + self._f.close() + diff --git a/hexrd/xrd/imageseries/adapters/registry.py b/hexrd/xrd/imageseries/adapters/registry.py new file mode 100644 index 00000000..529487c7 --- /dev/null +++ b/hexrd/xrd/imageseries/adapters/registry.py @@ -0,0 +1,13 @@ +"""Adapter registry +""" +class Registry(object): + """Registry for imageseries adapters""" + adapter_registry = dict() + + @classmethod + def register(cls, acls): + """Register adapter class""" + if acls.__name__ is not 'ImageSeriesAdapter': + cls.adapter_registry[acls.format] = acls + + pass # end class diff --git a/hexrd/xrd/imageseries/adapters/trivial.py b/hexrd/xrd/imageseries/adapters/trivial.py new file mode 100644 index 00000000..c3871138 --- /dev/null +++ b/hexrd/xrd/imageseries/adapters/trivial.py @@ -0,0 +1,5 @@ +"""Trivial adapter: just testing auto-import""" +from . import ImageSeriesAdapter + +class TrivialAdapter(ImageSeriesAdapter): + pass diff --git a/hexrd/xrd/imageseries/imageseriesabc.py b/hexrd/xrd/imageseries/imageseriesabc.py new file mode 100644 index 00000000..9d203648 --- /dev/null +++ b/hexrd/xrd/imageseries/imageseriesabc.py @@ -0,0 +1,6 @@ +"""Abstract Base Class""" +import collections + +class ImageSeriesABC(collections.Sequence): + pass + # define interface here diff --git a/hexrd/xrd/imageseries/imageseriesiter.py b/hexrd/xrd/imageseries/imageseriesiter.py new file mode 100644 index 00000000..a4e7dea3 --- /dev/null +++ b/hexrd/xrd/imageseries/imageseriesiter.py @@ -0,0 +1,23 @@ +"""imageseries iterator + +For use by adapter classes. +""" +import collections + +class ImageSeriesIterator(collections.Iterator): + + def __init__(self, iterable): + self._iterable = iterable + self._remaining = range(len(iterable)) + + def __iter__(self): + return self + + def __next__(self): + try: + return self._iterable[self._remaining.pop(0)] + except IndexError: + raise StopIteration + + def next(self): + return self.__next__() diff --git a/scripts/make_imageseries.py b/scripts/make_imageseries.py new file mode 100755 index 00000000..18e4a458 --- /dev/null +++ b/scripts/make_imageseries.py @@ -0,0 +1,300 @@ +#! /usr/bin/env python +# +"""Make an imageseries from a list of image files +""" +import sys +import argparse +import logging + +# Put this before fabio import and reset level if you +# want to control its import warnings. +logging.basicConfig(level=logging.DEBUG) + +import numpy +import h5py +import fabio + +# Error messages + +ERR_NO_FILE = 'Append specified, but could not open file' +ERR_NO_DATA = 'Append specified, but dataset not found in file' +ERR_OVERWRITE = 'Failed to create new dataset. Does it already exist?' +ERR_SHAPE = 'Image shape not consistent with previous images' +ERR_NOEMPTY = 'dark-from-empty specified, but number of empty frames not given' +ERR_OMEGASPEC = 'Must specify both omega-min and omega-max if either is given' + +DSetPath = lambda f, p: "%s['%s']" % (f, p) + +class MakeImageSeriesError(Exception): + """Class for MakeImageSeriesError errors""" + def __init__(self, message): + self.message = message + return + + def __str__(self): + return self.message + + pass # end class + + +def write_file(a, **kwargs): + # + # Get shape and dtype information from files + # + shp, dtp = image_info(a) + # + # Open file and dataset + # + f, ds = open_dset(a, shp, dtp) + # + # Image options + # + popts = process_img_opts(a, **kwargs) + # + # Now add the images + # . empty frames only apply to multiframe images + # + nframes = ds.shape[0] + nfiles = len(a.imagefiles) + for i in range(nfiles): + if a.max_frames and nframes >= a.max_frames: + break + logging.debug('processing file %d of %d' % (i, nfiles)) + popts['filenumber'] = i + img_i = fabio.open(a.imagefiles[i]) + nfi = img_i.nframes + for j in range(nfi): + if a.max_frames and nframes >= a.max_frames: + break + logging.debug('... processing image %d of %d' % (j, img_i.nframes)) + if nfi > 1 and j < a.empty: + logging.debug('...empty frame ... skipping') + continue + nframes += 1 + ds.resize(nframes, 0) + ds[nframes - 1, :, :] = process_img(a, img_i.data, popts) + if (j + 1) < nfi: + img_i = img_i.next() + pass + + add_metadata(ds, a, **kwargs) + + f.close() + return + +def open_dset(a, shp, dtp): + """open HDF5 file and dataset""" + # + # If append option is true, file and target group must exist; + # otherwise, file may exist but may not already contain the + # target dataset. + # + if a.append: + try: + f = h5py.File(a.outfile, "r+") + except: + errmsg = '%s: %s' % (ERR_NO_FILE, a.outfile) + raise MakeImageSeriesError(errmsg) + + ds = f.get(a.dset) + if ds is None: + errmsg = '%s: %s' % (ERR_NO_DATA, DSetPath(a.outfile, a.dset)) + raise MakeImageSeriesError(errmsg) + else: + f = h5py.File(a.outfile, "a") + chsize = (1, int(numpy.floor(1e6/shp[1])), shp[1]) if shp[1] < 1.e6 else True + try: + ds = f.create_dataset(a.dset, (0, shp[0], shp[1]), dtp, + maxshape=(None, shp[0], shp[1]), chunks=chsize, + compression="gzip") + except Exception as e: + errmsg = '%s: %s\n... exception: ' % (ERR_OVERWRITE, DSetPath(a.outfile, a.dset)) + raise MakeImageSeriesError(errmsg + str(e)) + + return f, ds + +def process_img_opts(a, **kwargs): + """make dictionary to pass to process_img""" + pdict = {} + # dark file (possibly need to transpose [not done yet]) + if a.dark_file: + dark = fabio.open(a.dark_file) + pdict['dark'] = dark.data + + # dark from empty + if a.dark_from_empty: + if a.empty == 0: + raise MakeImageSeriesError(ERR_NOEMPTY) + darks = [] + for i in range(len(a.imagefiles)): + img_i = fabio.open(a.imagefiles[i]) + drk_i = img_i.data + for j in range(1, a.empty): + img_i = img_i.next() + drk_i += img_i.data + darks += [drk_i*(1/a.empty)] + pdict['darks'] = darks + + return pdict + +def process_img(a, img, pdict): + """process image data according to options + + * need to check on image shape in case not square +""" + # flip: added some other option specifiers + if a.flip in ('y','v'): # about y-axis (vertical) + pimg = img[:, ::-1] + elif a.flip in ('x', 'h'): # about x-axis (horizontal) + pimg = img[::-1, :] + elif a.flip in ('vh', 'hv', 'r180'): # 180 degree rotation + pimg = img[::-1, ::-1] + elif a.flip in ('t', 'T'): # transpose (possible shape change) + pimg = img.T + elif a.flip in ('ccw90', 'r90'): # rotate 90 (possible shape change) + pimg = img.T[:, ::-1] + elif a.flip in ('cw90', 'r270'): # rotate 270 (possible shape change) + pimg = img.T[::-1, :] + else: + pimg = img + + # dark image(s) + if 'dark' in pdict: + pimg = pimg - pdict['dark'] + if 'darks' in pdict: + fnum = pdict['filenumber'] + pimg = pimg - pdict['darks'][fnum] + + return pimg + +def add_metadata(ds, a, **kwargs): + """Add metadata. Right now, that just includes omega information.""" + omkey = 'omega' + ominatt = 'omega_min' + omaxatt = 'omega_max' + hasval = lambda a, att: hasattr(a, att) and getattr(a, att) is not None + hasone = lambda a, att1, att2: hasval(a, att1) and not hasval(a, att2) + + if (hasone(a, ominatt, omaxatt) or hasone(a, omaxatt, ominatt)): + raise MakeImageSeriesError(ERR_OMEGASPEC) + + if not (hasval(a, ominatt) and hasval(a, omaxatt)): + return + + omin = getattr(a, ominatt) + omax = getattr(a, omaxatt) + + if a.append: + om = ds.dims[0][omkey] + n0 = om.shape[1] + n1 = ds.shape[0] + else: + om = ds.parent.file.create_dataset(a.dset + '_omega', (0, 2), + numpy.dtype(float), + maxshape=(None, 2)) + n0 = 0 + n1 = ds.shape[0] + ds.dims.create_scale(om, omkey) + ds.dims[0].attach_scale(om) + + dn = n1 - n0 + ominmax = numpy.linspace(omin, omax, num=(dn + 1)) + + om.resize(n1, 0) + om[n0:n1, :] = numpy.array([ominmax[0:dn], ominmax[1:(dn+1)]]).T + + return + +def image_info(a): + """Return shape and dtype of first image + + * See process_img for options that transpose shape +""" + img_0 = fabio.open(a.imagefiles[0]) + imgshape = img_0.data.shape + if a.flip in ('t', 'T') + ('ccw90', 'r90') + ('cw90', 'r270'): + imgshape = imgshape[::-1] + + return imgshape, img_0.data.dtype + +def describe_imgs(a): + print 'image files are: ', a.imagefiles + im0 = fabio.open(a.imagefiles[0]) + print 'Total number of files: %d' % len(a.imagefiles) + print 'First file: %s' % a.imagefiles[0] + print '... fabio class: %s' % im0.__class__ + print '... number of frames: %d' % im0.nframes + print '... image dimensions: %d X %d' % (im0.dim1, im0.dim2) + print '... image data type: %s' % im0.data.dtype + + pass + +def set_options(): + """Set options for command line""" + parser = argparse.ArgumentParser(description="imageseries builder") + + parser.add_argument("-i", "--info", help="describe the input files and quit", + action="store_true") + + # file options + parser.add_argument("-o", "--outfile", help="name of HDF5 output file", + default="imageseries.h5") + parser.add_argument("-a", "--append", + help="append to the dataset instead of making a new one", + action="store_true") + + help_d = "path to HDF5 data set" + parser.add_argument("-d", "--dset", help=help_d, default="/imageseries") + + parser.add_argument("imagefiles", nargs="+", help="image files") + + # image processing options + parser.add_argument("--flip", + help="reorient the image according to specification", + metavar="FLIPARG", action="store", default=None) + + parser.add_argument("--empty", "--blank", + help="number of blank frames in beginning of file", + metavar="N", type=int, action="store", default=0) + + parser.add_argument("--dark-file", help="name of file containing dark image") + + parser.add_argument("--dark-from-empty", + help="use empty frames to build dark image", + action="store_true") + # metadata + parser.add_argument("--omega-min", + help="minimum omega for this series of images", + type=float, action="store") + parser.add_argument("--omega-max", + help="minimum omega for this series of images", + type=float, action="store") + parser.add_argument("--max-frames", + help="maximum number of frames in file (for testing)", + metavar="N", type=int, action="store", default=0) + + return parser + +def execute(args, **kwargs): + """Main execution + + * kwargs added to allow passing further options when not called from command line + """ + p = set_options() + a = p.parse_args(args) + logging.info(str(a)) + + if a.info: + describe_imgs(a) + return + + write_file(a, **kwargs) + + return + +if __name__ == '__main__': + # + # run + # + execute(sys.argv[1:]) + From 241c2194c68bbbc8f22e080ee09432edef952a07 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 31 Aug 2015 16:45:48 -0400 Subject: [PATCH 002/253] renamed imageseries builder and fixed white space; works for GE files, but fabio not loading dark image (need to solve) --- scripts/{make_imageseries.py => make_imageseriesh5.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename scripts/{make_imageseries.py => make_imageseriesh5.py} (99%) diff --git a/scripts/make_imageseries.py b/scripts/make_imageseriesh5.py similarity index 99% rename from scripts/make_imageseries.py rename to scripts/make_imageseriesh5.py index 18e4a458..c61e6534 100755 --- a/scripts/make_imageseries.py +++ b/scripts/make_imageseriesh5.py @@ -28,11 +28,11 @@ class MakeImageSeriesError(Exception): """Class for MakeImageSeriesError errors""" def __init__(self, message): - self.message = message + self.message = message return def __str__(self): - return self.message + return self.message pass # end class From 51504f30b360ba8a15fdbd3ea5cbfb547ada1474 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Fri, 11 Sep 2015 11:44:48 -0400 Subject: [PATCH 003/253] fixed typo in error message --- hexrd/xrd/experiment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/xrd/experiment.py b/hexrd/xrd/experiment.py index 0c588cfd..bce4ceda 100644 --- a/hexrd/xrd/experiment.py +++ b/hexrd/xrd/experiment.py @@ -581,7 +581,7 @@ def loadDetector(self, fname): det_class_str = lines[i] f.seek(0) if det_class_str is None: - raise RuntimeError, "detector class label not recongined in file!" + raise RuntimeError, "detector class label not recongized in file!" else: plist_rflags = numpy.loadtxt(f) plist = plist_rflags[:, 0] From 8432ee4a17a6153d49b45c627c0aafe0f6eaedd8 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Fri, 11 Sep 2015 20:03:01 -0400 Subject: [PATCH 004/253] handles case of empty imageseries instantiation --- hexrd/xrd/image_io.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index 5a5f16fe..b11e3bac 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -55,7 +55,6 @@ def __init__(self, fname, fmt='hdf5', **kwargs): return def __getitem__(self, k): - print 'new image: ', k return self._imseries[k] @property @@ -218,11 +217,14 @@ def __init__(self, file_info, *args, **kwargs): """ self._fname = file_info self._kwargs = kwargs - self._omis = OmegaImageSeries(file_info, **kwargs) + try: + self._omis = OmegaImageSeries(file_info, **kwargs) + Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) + OmegaFramer.__init__(self, self._omis.omega) + except: + self._omis = None self.mask = None - Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) - OmegaFramer.__init__(self, self._omis.omega) # counter for last global frame that was read self.iFrame = -1 From 7090d01ded92eea08e0cb4e5eebcde820954c2f6 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 12 Sep 2015 20:42:17 -0400 Subject: [PATCH 005/253] relocated imageseries and added structure for imageseries_ops --- hexrd/{xrd => }/imageseries/__init__.py | 0 hexrd/{xrd => }/imageseries/adapters/__init__.py | 0 hexrd/{xrd => }/imageseries/adapters/hdf5.py | 0 hexrd/{xrd => }/imageseries/adapters/registry.py | 0 hexrd/{xrd => }/imageseries/adapters/trivial.py | 0 hexrd/{xrd => }/imageseries/imageseriesabc.py | 0 hexrd/{xrd => }/imageseries/imageseriesiter.py | 0 hexrd/imageseries_ops/__init__.py | 0 hexrd/imageseries_ops/operators.py | 0 hexrd/imageseries_ops/writers.py | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename hexrd/{xrd => }/imageseries/__init__.py (100%) rename hexrd/{xrd => }/imageseries/adapters/__init__.py (100%) rename hexrd/{xrd => }/imageseries/adapters/hdf5.py (100%) rename hexrd/{xrd => }/imageseries/adapters/registry.py (100%) rename hexrd/{xrd => }/imageseries/adapters/trivial.py (100%) rename hexrd/{xrd => }/imageseries/imageseriesabc.py (100%) rename hexrd/{xrd => }/imageseries/imageseriesiter.py (100%) create mode 100644 hexrd/imageseries_ops/__init__.py create mode 100644 hexrd/imageseries_ops/operators.py create mode 100644 hexrd/imageseries_ops/writers.py diff --git a/hexrd/xrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py similarity index 100% rename from hexrd/xrd/imageseries/__init__.py rename to hexrd/imageseries/__init__.py diff --git a/hexrd/xrd/imageseries/adapters/__init__.py b/hexrd/imageseries/adapters/__init__.py similarity index 100% rename from hexrd/xrd/imageseries/adapters/__init__.py rename to hexrd/imageseries/adapters/__init__.py diff --git a/hexrd/xrd/imageseries/adapters/hdf5.py b/hexrd/imageseries/adapters/hdf5.py similarity index 100% rename from hexrd/xrd/imageseries/adapters/hdf5.py rename to hexrd/imageseries/adapters/hdf5.py diff --git a/hexrd/xrd/imageseries/adapters/registry.py b/hexrd/imageseries/adapters/registry.py similarity index 100% rename from hexrd/xrd/imageseries/adapters/registry.py rename to hexrd/imageseries/adapters/registry.py diff --git a/hexrd/xrd/imageseries/adapters/trivial.py b/hexrd/imageseries/adapters/trivial.py similarity index 100% rename from hexrd/xrd/imageseries/adapters/trivial.py rename to hexrd/imageseries/adapters/trivial.py diff --git a/hexrd/xrd/imageseries/imageseriesabc.py b/hexrd/imageseries/imageseriesabc.py similarity index 100% rename from hexrd/xrd/imageseries/imageseriesabc.py rename to hexrd/imageseries/imageseriesabc.py diff --git a/hexrd/xrd/imageseries/imageseriesiter.py b/hexrd/imageseries/imageseriesiter.py similarity index 100% rename from hexrd/xrd/imageseries/imageseriesiter.py rename to hexrd/imageseries/imageseriesiter.py diff --git a/hexrd/imageseries_ops/__init__.py b/hexrd/imageseries_ops/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hexrd/imageseries_ops/operators.py b/hexrd/imageseries_ops/operators.py new file mode 100644 index 00000000..e69de29b diff --git a/hexrd/imageseries_ops/writers.py b/hexrd/imageseries_ops/writers.py new file mode 100644 index 00000000..e69de29b From 675ef541db77bec2dcd0d58c4b709a30d937f96b Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 13 Sep 2015 14:44:04 -0400 Subject: [PATCH 006/253] fixed location of registry in imageseries/adapters --- hexrd/imageseries/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py index 06ab65f1..67f3e57a 100644 --- a/hexrd/imageseries/__init__.py +++ b/hexrd/imageseries/__init__.py @@ -37,7 +37,7 @@ def __getattr__(self, attrname): def open(filename, format=None, **kwargs): # find the appropriate adapter based on format specified - reg = adapters.registry.Registry.adapter_registry + reg = adapters.Registry.adapter_registry adapter = reg[format](filename, **kwargs) print adapter return ImageSeries(adapter) From d0bea6ec0e6b9c69351c2d51afb13ce7d56701ed Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 13 Sep 2015 16:03:04 -0400 Subject: [PATCH 007/253] added array adapter for imageseries --- hexrd/imageseries/adapters/array.py | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 hexrd/imageseries/adapters/array.py diff --git a/hexrd/imageseries/adapters/array.py b/hexrd/imageseries/adapters/array.py new file mode 100644 index 00000000..5b69be1f --- /dev/null +++ b/hexrd/imageseries/adapters/array.py @@ -0,0 +1,47 @@ +"""Adapter class for numpy array (3D) +""" +from . import ImageSeriesAdapter +from ..imageseriesiter import ImageSeriesIterator + +class ArrayImageSeriesAdapter(ImageSeriesAdapter): + """collection of images in numpy array""" + + format = 'array' + + def __init__(self, fname, **kwargs): + """Constructor for frame cache image series + + *fname* - should be None + *kwargs* - keyword arguments + . 'data' = a 3D array (double/float) + . 'metadata' = a dictionary + """ + self._data = kwargs['data'] + self._meta = kwargs.pop('metadata', dict()) + self._shape = self._data.shape + self._nframes = self._shape[0] + self._nxny = self._shape[1:3] + + @property + def metadata(self): + """(read-only) Image sequence metadata + + Currently returns none + """ + return self._meta + + @property + def shape(self): + return self._nxny + + def __getitem__(self, key): + return self._data[key] + + def __iter__(self): + return ImageSeriesIterator(self) + + #@memoize + def __len__(self): + return self._nframes + + pass # end class From 0a3b135cd505a4d998ad748ef3c0e4cb5b7737ae Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 14 Sep 2015 09:56:38 -0400 Subject: [PATCH 008/253] updated writer registry and started hdf5 adapter --- hexrd/imageseries_ops/operators.py | 0 hexrd/imageseries_ops/process.py | 12 ++++++ hexrd/imageseries_ops/write.py | 59 ++++++++++++++++++++++++++++++ hexrd/imageseries_ops/writers.py | 0 4 files changed, 71 insertions(+) delete mode 100644 hexrd/imageseries_ops/operators.py create mode 100644 hexrd/imageseries_ops/process.py create mode 100644 hexrd/imageseries_ops/write.py delete mode 100644 hexrd/imageseries_ops/writers.py diff --git a/hexrd/imageseries_ops/operators.py b/hexrd/imageseries_ops/operators.py deleted file mode 100644 index e69de29b..00000000 diff --git a/hexrd/imageseries_ops/process.py b/hexrd/imageseries_ops/process.py new file mode 100644 index 00000000..b51fb9d0 --- /dev/null +++ b/hexrd/imageseries_ops/process.py @@ -0,0 +1,12 @@ +"""Class for processing frames or frame groups""" + +class ModifiedImageSeries(Imageseries): + """Images series with mapping applied to frames""" + def __init__(self, imser, cfg): + """Instantiate imsageseries based on existing one with mapping options + + *imser* - an existing imageseries + *cfg* - configuration for processing options + """ + self._imser = imser + pass # end class diff --git a/hexrd/imageseries_ops/write.py b/hexrd/imageseries_ops/write.py new file mode 100644 index 00000000..a6b474a5 --- /dev/null +++ b/hexrd/imageseries_ops/write.py @@ -0,0 +1,59 @@ +"""Write imageseries to various formats""" + +import abc + +import h5py + +def write(ims, fname, fmt, opts): + """write imageseries to file with options + + *ims* - an imageseries + *fname* - name of file + *fmt* - a format string + *opts* - namespace of options + """ + print('writing format ', fmt, ' to file: ', fname) + w = _Registry.getwriter(fmt) + w.write(ims, fname, opts) + + +# Registry + +class _RegisterWriter(abc.ABCMeta): + + def __init__(cls, name, bases, attrs): + abc.ABCMeta.__init__(cls, name, bases, attrs) + _Registry.register(cls) + +class _Registry(object): + """Registry for symmetry type instances""" + writer_registry = dict() + + @classmethod + def register(cls, wcls): + """Register writer class""" + if wcls.__name__ is not 'Writer': + cls.writer_registry[wcls.fmt] = wcls + + @classmethod + def getwriter(cls, name): + """return instance associated with name""" + return cls.writer_registry[name] + # + pass # end class + +class Writer(object): + """Base class for writers""" + __metaclass__ = _RegisterWriter + fmt = None + +class WriteH5(Writer): + fmt = 'hdf5' + + @classmethod + def write(cls, ims, fname, opts): + """Write imageseries to HDF5 file""" + print('writing hdf5') + pass + + pass # end class diff --git a/hexrd/imageseries_ops/writers.py b/hexrd/imageseries_ops/writers.py deleted file mode 100644 index e69de29b..00000000 From 9581bad03c6122d4a0247786899198d110327ddc Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 14 Sep 2015 21:35:03 -0400 Subject: [PATCH 009/253] simple write for hdf5 works --- hexrd/imageseries/adapters/array.py | 4 +++ hexrd/imageseries_ops/write.py | 39 +++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/hexrd/imageseries/adapters/array.py b/hexrd/imageseries/adapters/array.py index 5b69be1f..99f3eac9 100644 --- a/hexrd/imageseries/adapters/array.py +++ b/hexrd/imageseries/adapters/array.py @@ -34,6 +34,10 @@ def metadata(self): def shape(self): return self._nxny + @property + def dtype(self): + return self._data.dtype + def __getitem__(self, key): return self._data[key] diff --git a/hexrd/imageseries_ops/write.py b/hexrd/imageseries_ops/write.py index a6b474a5..dd60939b 100644 --- a/hexrd/imageseries_ops/write.py +++ b/hexrd/imageseries_ops/write.py @@ -2,6 +2,7 @@ import abc +import numpy import h5py def write(ims, fname, fmt, opts): @@ -13,9 +14,10 @@ def write(ims, fname, fmt, opts): *opts* - namespace of options """ print('writing format ', fmt, ' to file: ', fname) - w = _Registry.getwriter(fmt) - w.write(ims, fname, opts) - + wcls = _Registry.getwriter(fmt) + print(wcls) + w = wcls(ims, fname, opts) + w.write() # Registry @@ -26,7 +28,7 @@ def __init__(cls, name, bases, attrs): _Registry.register(cls) class _Registry(object): - """Registry for symmetry type instances""" + """Registry for imageseries writers""" writer_registry = dict() @classmethod @@ -50,10 +52,33 @@ class Writer(object): class WriteH5(Writer): fmt = 'hdf5' - @classmethod - def write(cls, ims, fname, opts): + def __init__(self, ims, fname, opts): + self._ims = ims + self._shape = ims.shape + self._dtype = ims.dtype + self._nframes = len(ims) + + self._fname = fname + self._opts = opts + self._path = self._opts['path'] + + def _open_dset(self): + """open HDF5 file and dataset""" + f = h5py.File(self._fname, "a") + s0, s1 = self._shape + + return f.create_dataset(self._path, (self._nframes, s0, s1), self._dtype, + compression="gzip") + # + # ======================================== API + # + def write(self): """Write imageseries to HDF5 file""" print('writing hdf5') - pass + ds = self._open_dset() + for i in range(self._nframes): + ds[i, :, :] = self._ims[i] + + # next: add metadata pass # end class From f19b95740c1706656db8895c8969acaae2cf3c9b Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 16 Sep 2015 10:03:20 -0400 Subject: [PATCH 010/253] added writer for simple frame-cache imageseries --- hexrd/imageseries_ops/write.py | 41 ++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/hexrd/imageseries_ops/write.py b/hexrd/imageseries_ops/write.py index dd60939b..a284d536 100644 --- a/hexrd/imageseries_ops/write.py +++ b/hexrd/imageseries_ops/write.py @@ -48,20 +48,23 @@ class Writer(object): """Base class for writers""" __metaclass__ = _RegisterWriter fmt = None - -class WriteH5(Writer): - fmt = 'hdf5' - def __init__(self, ims, fname, opts): self._ims = ims self._shape = ims.shape self._dtype = ims.dtype self._nframes = len(ims) - self._fname = fname self._opts = opts - self._path = self._opts['path'] + pass # end class + +class WriteH5(Writer): + fmt = 'hdf5' + + def __init__(self, ims, fname, opts): + Writer.__init__(self, ims, fname, opts) + self._path = self._opts['path'] + def _open_dset(self): """open HDF5 file and dataset""" f = h5py.File(self._fname, "a") @@ -74,7 +77,7 @@ def _open_dset(self): # def write(self): """Write imageseries to HDF5 file""" - print('writing hdf5') + print('writing ', self.fmt) ds = self._open_dset() for i in range(self._nframes): ds[i, :, :] = self._ims[i] @@ -82,3 +85,27 @@ def write(self): # next: add metadata pass # end class + +class WriteFrameCache(Writer): + + fmt = 'frame-cache' + def __init__(self, ims, fname, opts): + Writer.__init__(self, ims, fname, opts) + self._thresh = self._opts['threshold'] + + def write(self): + """writes frame cache for imageseries + + presumes sparse forms are small enough to contain all frames + """ + print('writing ', self.fmt) + arrd = dict() + for i in range(self._nframes): + frame = self._ims[i] + mask = frame > self._thresh + row, col = mask.nonzero() + arrd['%d_data' % i] = frame[mask] + arrd['%d_row' % i] = row + arrd['%d_col' % i] = col + + numpy.savez_compressed(self._fname, **arrd) From 48a889b728c6a70a0911d016a6257ec9250652cf Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 17 Sep 2015 09:50:39 -0400 Subject: [PATCH 011/253] added framecache, added dtype to hdf5, now basic functionality works for array, framecache and hdf5 --- hexrd/imageseries/adapters/framecache.py | 64 ++++++++++++++++++++++++ hexrd/imageseries/adapters/hdf5.py | 5 ++ hexrd/imageseries_ops/write.py | 27 +++++----- 3 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 hexrd/imageseries/adapters/framecache.py diff --git a/hexrd/imageseries/adapters/framecache.py b/hexrd/imageseries/adapters/framecache.py new file mode 100644 index 00000000..f10ed51e --- /dev/null +++ b/hexrd/imageseries/adapters/framecache.py @@ -0,0 +1,64 @@ +"""Adapter class for frame caches +""" +from . import ImageSeriesAdapter +from ..imageseriesiter import ImageSeriesIterator + +import numpy as np +from scipy.sparse import csr_matrix + +class FrameCacheImageSeriesAdapter(ImageSeriesAdapter): + """collection of images in HDF5 format""" + + format = 'frame-cache' + + def __init__(self, fname, **kwargs): + """Constructor for frame cache image series + + *fname* - filename of the yml file + *kwargs* - keyword arguments (none required + """ + self._fname = fname + self._load() + + def _load(self): + """load into list of csr sparse matrices""" + arrs = np.load(self._fname) + self._shape = tuple(arrs['shape'].tolist()) + self._dtype = None + + arrsh = tuple(arrs['shape']) + nk = len(arrs.files) - 1 + self._nframes = nk/3 + self._framelist = [] + for i in range(self._nframes): + row = arrs["%d_row" % i] + col = arrs["%d_col" % i] + data = arrs["%d_data" % i] + frame = csr_matrix((data, (row, col)), shape=arrsh) + self._framelist.append(frame) + if self._dtype is None: + self._dtype = data.dtype + + def metadata(self): + """(read-only) Image sequence metadata + + Currently returns none + """ + return None + + def shape(self): + return self._dtype + + + def __getitem__(self, key): + with self._dset as dset: + return dset.__getitem__(key) + + def __iter__(self): + return ImageSeriesIterator(self) + + #@memoize + def __len__(self): + return self._nframes + + pass # end class diff --git a/hexrd/imageseries/adapters/hdf5.py b/hexrd/imageseries/adapters/hdf5.py index 5b8900b9..927887e2 100644 --- a/hexrd/imageseries/adapters/hdf5.py +++ b/hexrd/imageseries/adapters/hdf5.py @@ -34,6 +34,11 @@ def metadata(self): return mdict + @property + def dtype(self): + with self._dset as dset: + return dset.dtype + @property #@memoize so you only need to do this once def shape(self): diff --git a/hexrd/imageseries_ops/write.py b/hexrd/imageseries_ops/write.py index a284d536..db2fbb97 100644 --- a/hexrd/imageseries_ops/write.py +++ b/hexrd/imageseries_ops/write.py @@ -1,22 +1,22 @@ """Write imageseries to various formats""" - +from __future__ import print_function import abc -import numpy +import numpy as np import h5py -def write(ims, fname, fmt, opts): +def write(ims, fname, fmt, **kwargs): """write imageseries to file with options *ims* - an imageseries *fname* - name of file *fmt* - a format string - *opts* - namespace of options + *kwargs* - options specific to format """ print('writing format ', fmt, ' to file: ', fname) wcls = _Registry.getwriter(fmt) print(wcls) - w = wcls(ims, fname, opts) + w = wcls(ims, fname, **kwargs) w.write() # Registry @@ -48,21 +48,21 @@ class Writer(object): """Base class for writers""" __metaclass__ = _RegisterWriter fmt = None - def __init__(self, ims, fname, opts): + def __init__(self, ims, fname, **kwargs): self._ims = ims self._shape = ims.shape self._dtype = ims.dtype self._nframes = len(ims) self._fname = fname - self._opts = opts + self._opts = kwargs pass # end class class WriteH5(Writer): fmt = 'hdf5' - def __init__(self, ims, fname, opts): - Writer.__init__(self, ims, fname, opts) + def __init__(self, ims, fname, **kwargs): + Writer.__init__(self, ims, fname, **kwargs) self._path = self._opts['path'] def _open_dset(self): @@ -89,8 +89,8 @@ def write(self): class WriteFrameCache(Writer): fmt = 'frame-cache' - def __init__(self, ims, fname, opts): - Writer.__init__(self, ims, fname, opts) + def __init__(self, ims, fname, **kwargs): + Writer.__init__(self, ims, fname, **kwargs) self._thresh = self._opts['threshold'] def write(self): @@ -100,6 +100,7 @@ def write(self): """ print('writing ', self.fmt) arrd = dict() + sh = None for i in range(self._nframes): frame = self._ims[i] mask = frame > self._thresh @@ -107,5 +108,7 @@ def write(self): arrd['%d_data' % i] = frame[mask] arrd['%d_row' % i] = row arrd['%d_col' % i] = col + if sh is None: + arrd['shape'] = np.array(frame.shape) - numpy.savez_compressed(self._fname, **arrd) + np.savez_compressed(self._fname, **arrd) From 5aed01390daee78f7ceded1d3673fe787401c22a Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 17 Sep 2015 10:22:06 -0400 Subject: [PATCH 012/253] fixes to get test problems to work (last message wrong); made properties of dtype & shape, corrected __get_item__ --- hexrd/imageseries/__init__.py | 1 - hexrd/imageseries/adapters/framecache.py | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/hexrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py index 67f3e57a..ad9c45ab 100644 --- a/hexrd/imageseries/__init__.py +++ b/hexrd/imageseries/__init__.py @@ -39,5 +39,4 @@ def open(filename, format=None, **kwargs): # find the appropriate adapter based on format specified reg = adapters.Registry.adapter_registry adapter = reg[format](filename, **kwargs) - print adapter return ImageSeries(adapter) diff --git a/hexrd/imageseries/adapters/framecache.py b/hexrd/imageseries/adapters/framecache.py index f10ed51e..9d8e7a87 100644 --- a/hexrd/imageseries/adapters/framecache.py +++ b/hexrd/imageseries/adapters/framecache.py @@ -45,14 +45,17 @@ def metadata(self): Currently returns none """ return None - - def shape(self): + + @property + def dtype(self): return self._dtype + @property + def shape(self): + return self._shape def __getitem__(self, key): - with self._dset as dset: - return dset.__getitem__(key) + return self._framelist[key] def __iter__(self): return ImageSeriesIterator(self) From 18615ef164d4d9c619c86d514628fb0a580a2fbf Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 26 Sep 2015 17:58:50 -0400 Subject: [PATCH 013/253] nominally filled in methods --- hexrd/imageseries_ops/process.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/hexrd/imageseries_ops/process.py b/hexrd/imageseries_ops/process.py index b51fb9d0..0eab3275 100644 --- a/hexrd/imageseries_ops/process.py +++ b/hexrd/imageseries_ops/process.py @@ -9,4 +9,23 @@ def __init__(self, imser, cfg): *cfg* - configuration for processing options """ self._imser = imser + + def __getitem__(self, key): + return self._imser.__getitem__(key) + + def __len__(self): + return len(self._imser) + + @property + def dtype(self): + return self._imser.dtype + + @property + def shape(self): + return self._imser.shape + + def process_frame(self, key): + img = self._imser[key] + return self._process_frame(img) + pass # end class From 56bde11386e653377d2fb0dd7751ed1faf064f2f Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 26 Sep 2015 17:59:47 -0400 Subject: [PATCH 014/253] moving process/write to imageseries --- hexrd/{imageseries_ops => imageseries}/process.py | 0 hexrd/{imageseries_ops => imageseries}/write.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename hexrd/{imageseries_ops => imageseries}/process.py (100%) rename hexrd/{imageseries_ops => imageseries}/write.py (100%) diff --git a/hexrd/imageseries_ops/process.py b/hexrd/imageseries/process.py similarity index 100% rename from hexrd/imageseries_ops/process.py rename to hexrd/imageseries/process.py diff --git a/hexrd/imageseries_ops/write.py b/hexrd/imageseries/write.py similarity index 100% rename from hexrd/imageseries_ops/write.py rename to hexrd/imageseries/write.py From 54b61524b205acfe4af63f937b80bdb90e608d53 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 26 Sep 2015 18:01:20 -0400 Subject: [PATCH 015/253] clearing old imageseries_ops --- hexrd/imageseries_ops/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 hexrd/imageseries_ops/__init__.py diff --git a/hexrd/imageseries_ops/__init__.py b/hexrd/imageseries_ops/__init__.py deleted file mode 100644 index e69de29b..00000000 From ffd0121c8b17508e233c638d3c1b1fdf27a124e0 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 27 Sep 2015 14:58:04 -0400 Subject: [PATCH 016/253] beginning process module; basic structure tested; removed prints from write module --- hexrd/imageseries/process.py | 12 +++++++----- hexrd/imageseries/write.py | 2 -- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 0eab3275..726d8e86 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -1,6 +1,7 @@ """Class for processing frames or frame groups""" +from hexrd.imageseries import ImageSeries -class ModifiedImageSeries(Imageseries): +class ProcessedImageSeries(ImageSeries): """Images series with mapping applied to frames""" def __init__(self, imser, cfg): """Instantiate imsageseries based on existing one with mapping options @@ -11,11 +12,15 @@ def __init__(self, imser, cfg): self._imser = imser def __getitem__(self, key): - return self._imser.__getitem__(key) + return self._process_frame(key) def __len__(self): return len(self._imser) + def _process_frame(self, key): + img = self._imser[key] + return img + @property def dtype(self): return self._imser.dtype @@ -24,8 +29,5 @@ def dtype(self): def shape(self): return self._imser.shape - def process_frame(self, key): - img = self._imser[key] - return self._process_frame(img) pass # end class diff --git a/hexrd/imageseries/write.py b/hexrd/imageseries/write.py index db2fbb97..91e90d00 100644 --- a/hexrd/imageseries/write.py +++ b/hexrd/imageseries/write.py @@ -13,9 +13,7 @@ def write(ims, fname, fmt, **kwargs): *fmt* - a format string *kwargs* - options specific to format """ - print('writing format ', fmt, ' to file: ', fname) wcls = _Registry.getwriter(fmt) - print(wcls) w = wcls(ims, fname, **kwargs) w.write() From a6de83f508927d6cd956a93c780e846ca54a8dc9 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 27 Sep 2015 17:10:16 -0400 Subject: [PATCH 017/253] added flip & test for process module --- .gitignore | 1 + conda.recipe/meta.yaml | 2 +- hexrd/coreutil.py | 19 +- hexrd/imageseries/process.py | 31 +- hexrd/xrd/detector.py | 1475 +++------------------------------- 5 files changed, 170 insertions(+), 1358 deletions(-) diff --git a/.gitignore b/.gitignore index f5331f50..58309e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ hexrd.egg-info *.so *.*~ *# +.#* diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index e5711dff..adf87298 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -9,7 +9,7 @@ source: build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} - detect_binary_files_with_prefix: true + # detect_binary_files_with_prefix: true osx_is_app: yes entry_points: - hexrd = hexrd.cli:main diff --git a/hexrd/coreutil.py b/hexrd/coreutil.py index 188fbec8..a0afd1a4 100644 --- a/hexrd/coreutil.py +++ b/hexrd/coreutil.py @@ -175,15 +175,16 @@ def initialize_experiment(cfg): # detector data try: - reader = ReadGE( - [(f, image_start) for f in cfg.image_series.files], - np.radians(cfg.image_series.omega.start), - np.radians(cfg.image_series.omega.step), - subtractDark=dark is not None, # TODO: get rid of this - dark=dark, - doFlip=flip is not None, - flipArg=flip, # TODO: flip=flip - ) + reader = ReadGE('imageseries.h5', path='imageseries') + #reader = ReadGE( + # [(f, image_start) for f in cfg.image_series.files], + # np.radians(cfg.image_series.omega.start), + # np.radians(cfg.image_series.omega.step), + # subtractDark=dark is not None, # TODO: get rid of this + # dark=dark, + # doFlip=flip is not None, + # flipArg=flip, # TODO: flip=flip + # ) except IOError: logger.info("raw data not found, skipping reader init") reader = None diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 726d8e86..8664d765 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -3,13 +3,15 @@ class ProcessedImageSeries(ImageSeries): """Images series with mapping applied to frames""" - def __init__(self, imser, cfg): + FLIP = 'flip' + def __init__(self, imser, **kwargs): """Instantiate imsageseries based on existing one with mapping options *imser* - an existing imageseries - *cfg* - configuration for processing options + *kwargs* - dictionary for processing options """ self._imser = imser + self._opts = kwargs def __getitem__(self, key): return self._process_frame(key) @@ -19,8 +21,33 @@ def __len__(self): def _process_frame(self, key): img = self._imser[key] + img = self._flip(img) return img + def _flip(self, img): + if self.FLIP in self._opts: + flip = self._opts['flip'] + else: + return img + + if flip in ('y','v'): # about y-axis (vertical) + pimg = img[:, ::-1] + elif flip in ('x', 'h'): # about x-axis (horizontal) + pimg = img[::-1, :] + elif flip in ('vh', 'hv', 'r180'): # 180 degree rotation + pimg = img[::-1, ::-1] + elif flip in ('t', 'T'): # transpose (possible shape change) + pimg = img.T + elif flip in ('ccw90', 'r90'): # rotate 90 (possible shape change) + pimg = img.T[:, ::-1] + elif flip in ('cw90', 'r270'): # rotate 270 (possible shape change) + pimg = img.T[::-1, :] + else: + pimg = img + + return pimg + + @property def dtype(self): return self._imser.dtype diff --git a/hexrd/xrd/detector.py b/hexrd/xrd/detector.py index 0e79f654..3fad9274 100644 --- a/hexrd/xrd/detector.py +++ b/hexrd/xrd/detector.py @@ -47,22 +47,20 @@ from matplotlib import mlab from matplotlib.widgets import Slider, Button, RadioButtons -from hexrd import xrd -from hexrd.xrd.xrdbase import getGaussNDParams, dataToFrame -from hexrd.xrd.crystallography import processWavelength -from hexrd.xrd import distortion -from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.xrd.rotations import mapAngle -from hexrd.xrd.rotations import rotMatOfExpMap -from hexrd.xrd.rotations import rotMatOfExpMap, arccosSafe -from hexrd.quadrature import q1db -from hexrd.quadrature import q2db -from hexrd.matrixutil import unitVector -from hexrd import valunits +from .image_io import ReadGeneric, ReadGE, ReaderDeprecationWarning, Framer2DRC +from .xrdbase import getGaussNDParams, dataToFrame +from .crystallography import processWavelength +from .rotations import mapAngle +from .rotations import rotMatOfExpMap +from .rotations import rotMatOfExpMap, arccosSafe +from ..quadrature import q1db +from ..quadrature import q2db +from ..matrixutil import unitVector +from .. import valunits havePlotWrap = True try: - from hexrd import plotwrap + from .. import plotwrap except: havePlotWrap = False @@ -74,38 +72,6 @@ DFLT_XTOL = 1e-6 -####### -# GE, Perkin -NROWS = 2048 -NCOLS = 2048 -PIXEL = 0.2 - -# CHESS retiga -#NROWS = 2048 -#NCOLS = 2048 -#PIXEL = 0.00148 - -# vert -#NROWS = 2048 -#NCOLS = 2048 -#PIXEL = 0.0045 - -# pscam -#NROWS = 2671 -#NCOLS = 4008 -#PIXEL = 0.03 - -# LCLS CSpad0 -#NROWS = 825 -#NCOLS = 840 -#PIXEL = 0.10992 - -## LCLS CSpad1,2 -#NROWS = 400 -#NCOLS = 400 -#PIXEL = 0.10992 -####### - def angToXYIdeal(tTh, eta, workDist): rho = num.tan(tTh) * workDist x = rho * num.cos(eta) @@ -192,1182 +158,6 @@ def getCMap(spec): raise RuntimeError, 'unknown: '+str(spec) return cmap - -class Framer2DRC(object): - """ - Base class for readers. - - You can make an instance of this class and use it for most of the - things a reader would do, other than actually reading frames - """ - def __init__(self, - ncols, nrows, - dtypeDefault='int16', dtypeRead='uint16', dtypeFloat='float64'): - self.__ncols = ncols - self.__nrows = nrows - self.__frame_dtype_dflt = dtypeDefault - self.__frame_dtype_read = dtypeRead - self.__frame_dtype_float = dtypeFloat - - self.__nbytes_frame = num.nbytes[dtypeRead]*nrows*ncols - - return - - def get_ncols(self): - return self.__ncols - ncols = property(get_ncols, None, None) - - def get_nbytesFrame(self): - return self.__nbytes_frame - nbytesFrame = property(get_nbytesFrame, None, None) - - def get_nrows(self): - return self.__nrows - nrows = property(get_nrows, None, None) - - def get_dtypeDefault(self): - return self.__frame_dtype_dflt - dtypeDefault = property(get_dtypeDefault, None, None) - def get_dtypeRead(self): - return self.__frame_dtype_read - dtypeRead = property(get_dtypeRead, None, None) - def get_dtypeFloat(self): - return self.__frame_dtype_float - dtypeFloat = property(get_dtypeFloat, None, None) - - def getOmegaMinMax(self): - raise NotImplementedError - def getDeltaOmega(self): - 'needed in findSpotsOmegaStack' - raise NotImplementedError - def getNFrames(self): - """ - number of total frames with real data, not number remaining - needed in findSpotsOmegaStack - """ - raise NotImplementedError - def read(self, nskip=0, nframes=1, sumImg=False): - 'needed in findSpotsOmegaStack' - raise NotImplementedError - def getDark(self): - 'needed in findSpotsOmegaStack' - raise NotImplementedError - def getFrameOmega(self, iFrame=None): - 'needed in findSpotsOmegaStack' - raise NotImplementedError - - - @classmethod - def maxVal(cls, dtypeRead): - """ - maximum value that can be stored in the image pixel data type; - redefine as desired - """ - maxInt = num.iinfo(dtypeRead).max - return maxInt - - def getEmptyMask(self): - """ - convenience method for getting an emtpy mask or bin frame - """ - # this used to be a class method - mask = num.zeros([self.nrows, self.ncols], dtype=bool) - return mask - - def getSize(self): - retval = (self.nrows, self.ncols) - return retval - - def frame(self, nframes=None, dtype=None, buffer=None, mask=None): - if buffer is not None and dtype is None: - if hasattr(buffer,'dtype'): - dtype = buffer.dtype - if dtype is None: - dtype = self.__frame_dtype_dflt - if nframes is None: - shape = (self.nrows, self.ncols) - else: - assert mask is None,\ - 'not coded: multiframe with mask' - shape = (nframes, self.nrows, self.ncols) - if buffer is None: - retval = num.zeros(shape, dtype=dtype) - else: - retval = num.array(buffer, dtype=dtype).reshape(shape) - if mask is not None: - retval = num.ma.masked_array(retval, mask, hard_mask=True, copy=False) - return retval - - @classmethod - def display(cls, - thisframe, - roi = None, - pw = None, - **kwargs - ): - # ... interpolation method that looks like max() so that do not miss peak pixels? - - if roi is not None: - dROI = thisframe[ roi[0][0]:roi[0][1], roi[1][0]:roi[1][1] ] - else: - dROI = thisframe - vmin, vmax, cmap = cls.getDisplayArgs(dROI, kwargs) - - if havePlotWrap: - if pw is None: - p = plotwrap.PlotWrap(**kwargs) - kwargs = {} - else: - p = pw - p(dROI, vmin=vmin, vmax=vmax, cmap=cmap, **kwargs) - # 'turn off format_coord because have not made this one report correctly' - # p.a.format_coord = lambda x,y: '' - # elif havePylab: - # assert pw is None, 'do not specify pw without plotwrap' - # retval = pylab.imshow(dROI, vmin=vmin, vmax=vmax, cmap=cm.bone) - else: - raise RuntimeError, 'no plotting pacakge available' - - retval = p - return retval - - @classmethod - def getDisplayArgs(cls, dROI, kwargs): - range = kwargs.pop('range',None) - cmap = kwargs.pop('cmap',None) - dtypeRead = kwargs.pop('dtypeRead','uint16') - - roiMin = dROI.min() - roiMax = dROI.max() - # - centered = getCentered(roiMin, roiMax) - if dROI.dtype == 'bool' and range is None: - centered = False - vmin = 0 - vmax = 1 - elif dROI.dtype == 'float64' and \ - centered and \ - range is None: - range = 2.0*num.max(num.abs(dROI)) - thr = 0.0 - vmin = thr-range/2 - vmax = thr+range/2 - else: - centered = False - vmin, vmax = cls.getVMM(dROI, range=range, dtypeRead=dtypeRead) - # - if cmap is None: - cmap = getCMap(centered) - - return vmin, vmax, cmap - - @classmethod - def getVMM(cls, dROI, range=None, dtypeRead='uint16'): - if range is None: - range = 200. - if hasattr(range,'__len__'): - assert len(range) == 2, 'wrong length for value range' - vmin = range[0] - vmax = range[1] - else: - thr = dROI.mean() - vmin = max(0, thr-range/2) # max(dROI.min(), thr-range/2) - vmax = min(cls.maxVal(dtypeRead), thr+range/2) - return vmin, vmax - -def omeRangeToFrameRange(omeA, omeB, omegaStart, omegaDelta, nFrames, checkWrap=True, slicePad=1): - """ - assumes omegas are evenly spaced - omegaDelta may be negative - """ - retval = None - - wrapsAround = abs( ( nFrames * abs(omegaDelta) ) - ( 2.0 * num.pi ) ) < 1.0e-6 - iFA = int((omeA - omegaStart) / omegaDelta) - iFB = int((omeB - omegaStart) / omegaDelta) - - if checkWrap and wrapsAround: - iFAW = iFA % nFrames - shift = iFAW - iFA - iFBW = iFB + shift - if iFBW < 0: - retval = [ (iFBW+nFrames, nFrames-1 + slicePad), (0, iFAW + slicePad) ] - # print '...*** making split range ...*** %g %g %g %g ' % (iFA, iFB, iFAW, iFBW) +str(retval) - elif iFBW >= nFrames: - retval = [ (iFA, nFrames-1 + slicePad), (0, iFBW-nFrames + slicePad) ] - # print '...*** making split range ...*** %g %g %g %g ' % (iFA, iFB, iFAW, iFBW) +str(retval) - else: - iFA = iFAW - iFB = iFBW - retval = None - - if retval is None: - rawFrameRange = num.sort(num.hstack( (iFA, iFB) )) - retval = ( - num.hstack( (rawFrameRange, 0) )[0], - num.hstack( (nFrames-1, rawFrameRange ) )[-1] + slicePad, - ) - return retval -# -def frameInRange(iFrame, frameRange): - """ - for use with output from omeRangeToFrameRange; - trust that slicePad=1 was used in omeRangeToFrameRange - """ - retval = False - if hasattr(frameRange[0],'index'): - for frameRangeThis in frameRange: - if iFrame >= frameRangeThis[0] and iFrame < frameRangeThis[1]: - retval = True - # print '...*** found in range for split range ...***' - break - else: - if iFrame >= frameRange[0] and iFrame < frameRange[1]: - retval = True - return retval - -def getNFramesFromBytes(fileBytes, nbytesHeader, nbytesFrame): - assert (fileBytes - nbytesHeader) % nbytesFrame == 0,\ - 'file size not correct' - nFrames = int((fileBytes - nbytesHeader) / nbytesFrame) - if nFrames*nbytesFrame + nbytesHeader != fileBytes: - raise RuntimeError, 'file size not correctly calculated' - return nFrames - -class FrameWriter(Framer2DRC): - def __init__(self, *args, **kwargs): - self.filename = kwargs.pop('filename') - self.__nbytes_header = kwargs.pop('nbytesHeader', 0) - self.__nempty = kwargs.pop('nempty', 0) - - Framer2DRC.__init__(self, *args, **kwargs) - - self.nFrame = 0 - self.img = open(self.filename, mode='wb') - - # skip header for now - self.img.seek(self.__nbytes_header, 0) - if self.__nempty > 0: - self.img.seek(self.nbytesFrame*self.__nempty, 1) - - return - def write(self, data, doAllChecks=True): - - # if nskip > 0: - # self.img.seek(self.__nbytes_frame*nskip, 1) - - assert len(data.shape) == 2, 'data is not 2D' - assert data.shape[0] == self.nrows, 'number of rows is wrong' - assert data.shape[1] == self.ncols, 'number of rows is wrong' - - intType = False - - if num.result_type(self.dtypeRead).kind == 'u': - intType = True - if data.dtype.kind == 'u': - 'all set' - else: - if num.any(data < 0): - raise RuntimeError, 'trying to write negative data to unsigned type' - data = data.astype(self.dtypeRead) - elif num.result_type(self.dtypeRead).kind == 'i': - intType = True - data = data.astype(self.dtypeRead) - else: - data = data.astype(self.dtypeRead) - - if doAllChecks and intType: - dataMax = data.max() - readMax = num.iinfo(self.dtypeRead).max - if dataMax > readMax : - raise RuntimeError, 'max of %g greater than supported value of %g' % (dataMax, readMax) - - data.tofile(self.img) - - return - def __call__(self, *args, **kwargs): - return self.write(*args, **kwargs) - def close(self): - self.img.close() - return - -class ReadGeneric(Framer2DRC): - ''' - may eventually want ReadGE to inherit from this, or pull common things - off to a base class - ''' - def __init__(self, filename, ncols, nrows, *args, **kwargs): - self.filename = filename - self.__nbytes_header = kwargs.pop('nbytes_header', 0) - self.__nempty = kwargs.pop('nempty', 0) - doFlip = kwargs.pop('doFlip', False) - self.subtractDark = kwargs.pop('subtractDark', False) - - 'keep things for makeNew convenience' - self.__args = args - self.__kwargs = kwargs - - if doFlip is not False: - raise NotImplementedError, 'doFlip not False' - if self.subtractDark is not False: - raise NotImplementedError, 'subtractDark not False' - - Framer2DRC.__init__(self, ncols, nrows, **kwargs) - - self.dark = None - self.dead = None - self.mask = None - - self.__wrapsAround = False # default - - self.omegaStart = None - self.omegaDelta = None - self.omegas = None - # - if len(args) == 0: - pass - elif len(args) == 2: - self.omegaStart = omegaStart = args[0] - self.omegaDelta = omegaDelta = args[1] - else: - raise RuntimeError, 'do not know what to do with args: '+str(args) - self.omegas = None - if self.omegaStart is not None: - if hasattr(omegaStart, 'getVal'): - omegaStart = omegaStart.getVal('radians') - if hasattr(omegaDelta, 'getVal'): - omegaDelta = omegaDelta.getVal('radians') - nFramesTot = self.getNFrames() - self.omegas = \ - num.arange(omegaStart, omegaStart+omegaDelta*(nFramesTot-0.5), omegaDelta) + \ - 0.5 * omegaDelta # put omegas at mid-points of omega range for frame - omegaEnd = omegaStart+omegaDelta*(nFramesTot) - self.omegaMin = min(omegaStart, omegaEnd) - self.omegaMax = max(omegaStart, omegaEnd) - self.omegaDelta = omegaDelta - self.omegaStart = omegaStart - self.__wrapsAround = abs( ( nFramesTot * abs(omegaDelta) ) / ( 2.0 * num.pi ) - 1.0 ) < 1.0e-6 - - if len(kwargs) > 0: - raise RuntimeError, 'unparsed kwargs : %s' + str(kwargs.keys()) - - self.iFrame = -1 # counter for last global frame that was read - - self.img = None - if self.filename is not None: - if self.filename.split('.') == 'bz2': - self.img = bz2.BZ2File(self.filename, mode='rb') - else: - self.img = open(self.filename, mode='rb') - # skip header for now - self.img.seek(self.__nbytes_header, 0) - if self.__nempty > 0: - self.img.seek(self.nbytesFrame*self.__nempty, 1) - - return - - def makeNew(self): - """return a clean instance for the same data files - useful if want to start reading from the beginning""" - newSelf = self.__class__(self.filename, self.ncols, self.nrows, *self.__args, **self.__kwargs) - return newSelf - def get_wrapsAround(self): - return self.__wrapsAround - wrapsAround = property(get_wrapsAround, None, None) - - def getFrameUseMask(self): - return False - def __flip(self, thisframe): - return thisframe - - ''' - def read(self, nskip=0, nframes=1, sumImg=False): - - if not nframes == 1: - raise NotImplementedError, 'nframes != 1' - if not sumImg == False: - raise NotImplementedError, 'sumImg != False' - - data = self.__readNext(nskip=nskip) - - self.iFrame += nskip + 1 - - return data - ''' - def __call__(self, *args, **kwargs): - return self.read(*args, **kwargs) - def read(self, nskip=0, nframes=1, sumImg=False): - """ - sumImg can be set to True or to something like numpy.maximum - """ - - if self.img is None: - raise RuntimeError, 'no image file open' - - 'get iFrame ready for how it is used here' - self.iFrame = num.atleast_1d(self.iFrame)[-1] - iFrameList = [] - multiframe = nframes > 1 - - nFramesInv = 1.0 / nframes - doDarkSub = self.subtractDark # and self.dark is not None - - if doDarkSub: - assert self.dark is not None, 'self.dark is None' - - # assign storage array - if sumImg: - sumImgCallable = hasattr(sumImg,'__call__') - imgOut = self.frame(dtype=self.dtypeFloat, mask=self.dead) - elif multiframe: - imgOut = self.frame(nframes=nframes, dtype=self.dtypeDflt, mask=self.dead) - - - # now read data frames - for i in range(nframes): - - #data = self.__readNext(nskip=nskip) - #thisframe = data.reshape(self.__nrows, self.__ncols) - data = self.__readNext(nskip=nskip) # .reshape(self.__nrows, self.__ncols) - self.iFrame += nskip + 1 - nskip=0 # all done skipping once have the first frame! - iFrameList.append(self.iFrame) - # dark subtraction - if doDarkSub: - 'used to have self.dtypeFloat here, but self.dtypeDflt does the trick' - thisframe = self.frame(buffer=data, - dtype=self.dtypeDflt, mask=self.dead) - self.dark - else: - thisframe = self.frame(buffer=data, - mask=self.dead) - - # flipping - thisframe = self.__flip(thisframe) - - # masking (True get zeroed) - if self.mask is not None: - if self.getFrameUseMask(): - thisframe[self.mask] = 0 - - # assign output - if sumImg: - if sumImgCallable: - imgOut = sumImg(imgOut, thisframe) - else: - imgOut = imgOut + thisframe * nFramesInv - elif multiframe: - imgOut[i, :, :] = thisframe[:, :] - 'end of loop over nframes' - - if sumImg: - # imgOut = imgOut / nframes # now taken care of above - pass - elif not multiframe: - imgOut = thisframe - - if multiframe: - 'make iFrame a list so that omega or whatever can be averaged appropriately' - self.iFrame = iFrameList - return imgOut - - def getNFrames(self, lessEmpty=True): - fileBytes = os.stat(self.filename).st_size - nFrames = getNFramesFromBytes(fileBytes, self.__nbytes_header, self.nbytesFrame) - if lessEmpty: - nFrames -= self.__nempty - return nFrames - - def getOmegaMinMax(self): - assert self.omegas is not None,\ - """instance does not have omega information""" - return self.omegaMin, self.omegaMax - def getDeltaOmega(self, nframes=1): - assert self.omegas is not None,\ - """instance does not have omega information""" - return self.omegaDelta * nframes - def getDark(self): - 'no dark yet supported' - return 0 - def frameToOmega(self, frame): - scalar = num.isscalar(frame) - frames = num.asarray(frame) - if frames.dtype == int: - retval = self.omegas[frames] - else: - retval = (frames + 0.5) * self.omegaDelta + self.omegaStart - if scalar: - retval = num.asscalar(retval) - return retval - def getFrameOmega(self, iFrame=None): - """if iFrame is none, use internal counter""" - assert self.omegas is not None,\ - """instance does not have omega information""" - if iFrame is None: - iFrame = self.iFrame - if hasattr(iFrame, '__len__'): - 'take care of case nframes>1 in last call to read' - retval = num.mean(self.omegas[iFrame]) - else: - retval = self.omegas[iFrame] - return retval - - def __readNext(self, nskip=0): - if self.img is None: - raise RuntimeError, 'no image file open' - - if nskip > 0: - self.img.seek(self.nbytesFrame*nskip, 1) - data = num.fromfile(self.img, - dtype=self.dtypeRead, - count=self.nrows*self.ncols) - return data - - def getNFrames(self, lessEmpty=True): - fileBytes = os.stat(self.filename).st_size - nFrames = getNFramesFromBytes(fileBytes, self.__nbytes_header, self.nbytesFrame) - if lessEmpty: - nFrames -= self.__nempty - return nFrames - - def getOmegaMinMax(self): - assert self.omegas is not None,\ - """instance does not have omega information""" - return self.omegaMin, self.omegaMax - def getDeltaOmega(self, nframes=1): - assert self.omegas is not None,\ - """instance does not have omega information""" - return self.omegaDelta * nframes - def getDark(self): - 'no dark yet supported' - return 0 - def getFrameOmega(self, iFrame=None): - """if iFrame is none, use internal counter""" - assert self.omegas is not None,\ - """instance does not have omega information""" - if iFrame is None: - iFrame = self.iFrame - if hasattr(iFrame, '__len__'): - 'take care of case nframes>1 in last call to read' - retval = num.mean(self.omegas[iFrame]) - else: - retval = self.omegas[iFrame] - return retval - - def getWriter(self, filename): - # if not self.doFlip is False: - # raise NotImplementedError, 'doFlip true not coded' - new = FrameWriter(self.ncols, self.nrows, - filename=filename, - dtypeDefault=self.dtypeDefault, - dtypeRead=self.dtypeRead, - dtypeFloat=self.dtypeFloat, - nbytesHeader=self.__nbytes_header) - return new - -class ReadGE(Framer2DRC): - """ - Read in raw GE files; this is the class version of the foregoing functions - - NOTES - - *) The flip axis ('v'ertical) was verified on 06 March 2009 by - JVB and UL. This should be rechecked if the configuration of the GE - changes or you are unsure. - - *) BE CAREFUL! nframes should be < 10 or so, or you will run out of - memory in the namespace on a typical machine. - - *) The header is currently ignored - - *) If a dark is specified, this overrides the use of empty frames as - background; dark can be a file name or frame - - *) In multiframe images where background subtraction is requested but no - dark is specified, attempts to use the - empty frame(s). An error is returned if there are not any specified. - If there are multiple empty frames, the average is used. - - - NOTES: - - It is likely that some of the methods here should be moved up to a base class - """ - __nbytes_header = 8192 - __idim = min(NROWS, NCOLS) - __nrows = NROWS - __ncols = NCOLS - __frame_dtype_dflt = 'int16' # good for doing subtractions - __frame_dtype_read = 'uint16' - __frame_dtype_float = 'float64' - __nbytes_frame = num.nbytes[num.uint16]*__nrows*__ncols # = 2*__nrows*__ncols - __debug = False - __location = ' ReadGE' - __readArgs = { - 'dtype' : __frame_dtype_read, - 'count' : __nrows*__ncols - } - __castArgs = { - 'dtype' : __frame_dtype_dflt - } - __inParmDict = { - 'omegaStart':None, - 'omegaDelta':None, - 'subtractDark':False, - 'mask':None, - 'useMask':None, - 'dark':None, - 'dead':None, - 'nDarkFrames':1, - 'doFlip':True, - 'flipArg':'v', - } - # 'readHeader':False - def __init__(self, - fileInfo, - *args, - **kwargs): - """ - meant for reading a series of frames from an omega sweep, with - fixed delta-omega for each frame - - omegaStart and omegaDelta can follow fileInfo or be specified - in whatever order by keyword - - fileInfo: string, (string, nempty), or list of (string, - nempty) for multiple files - - for multiple files and no dark, dark is formed only from empty - frames in the first file - """ - - # parse kwargs first - self.__kwPassed = {} - for parm, val in self.__inParmDict.iteritems(): - self.__kwPassed[parm] = kwargs.has_key(parm) - if kwargs.has_key(parm): - val = kwargs.pop(parm) - self.__setattr__(parm, val) - if len(kwargs) > 0: - raise RuntimeError, 'unparsed keyword arguments: '+str(kwargs.keys()) - - Framer2DRC.__init__(self, - self.__ncols, self.__nrows, - dtypeDefault = self.__frame_dtype_dflt, - dtypeRead = self.__frame_dtype_read, - dtypeFloat = self.__frame_dtype_float, - ) - - # omega information - if len(args) == 0: - pass - elif len(args) == 2: - self.omegaStart = args[0] - self.omegaDelta = args[1] - else: - raise RuntimeError, 'do not know what to do with args : '+str(args) - - # initialization - self.omegas = None - self.img = None - self.th = None - self.fileInfo = None - self.fileInfoR = None - self.nFramesRemain = None # remaining in current file - self.iFrame = -1 # counter for last global frame that was read - self.__wrapsAround = False # default - - if self.dark is not None: - if not self.__kwPassed['subtractDark']: - 'subtractDark was not explicitly passed, set it True' - self.subtractDark = True - if isinstance(self.dark, str): - darkFile = self.dark - self.dark = ReadGE.readDark(darkFile, nframes=self.nDarkFrames) - self.__log('got dark from %d frames in file %s' % (self.nDarkFrames, darkFile)) - elif isinstance(self.dark, num.ndarray): - assert self.dark.size == self.__nrows * self.__ncols, \ - 'self.dark wrong size' - self.dark.shape = (self.__nrows, self.__ncols) - if self.dark.dtype.name == self.__frame_dtype_read: - 'protect against unsigned-badness when subtracting' - self.dark = self.dark.astype(self.__frame_dtype_dflt) - self.__log('got dark from ndarray input') - else: - raise RuntimeError, 'do not know what to do with dark of type : '+str(type(self.dark)) - - if fileInfo is not None: - self.__setupRead(fileInfo, self.subtractDark, self.mask, self.omegaStart, self.omegaDelta) - - return - - @classmethod - def display(cls, - thisframe, - roi = None, - pw = None, - **kwargs - ): - 'this is a bit ugly in that it sidesteps the dtypeRead property' - retval = Framer2DRC.display(thisframe, roi=roi, pw=pw, dtypeRead=cls.__frame_dtype_read) - return retval - - @classmethod - def readRaw(cls, fname, mode='raw', headerlen=0): - ''' - read a raw binary file; - if specified, headerlen is in bytes; - does not do any flipping - ''' - print cls - if hasattr(cls, 'doFlip'): - print 'has doFlip' - img = open(fname, mode='rb') - if headerlen > 0: - img.seek(headerlen, 0) - if mode == 'raw' or mode == 'avg': - dtype = cls.__frame_dtype_read - elif mode == 'sum': - dtype = 'float32' - else: - raise RuntimeError, 'unknown mode : '+str(mode) - thisframe = num.fromfile(img, dtype=dtype, count=cls.__nrows*cls.__ncols).reshape(cls.__nrows, cls.__ncols) - return thisframe - def rawRead(self, *args, **kwargs): - ''' - wrapper around readRaw that does the same flipping as the reader instance from which it is called - ''' - thisframe = self.__flip(self.readRaw(*args, **kwargs)) - return thisframe - @classmethod - def readDark(cls, darkFile, nframes=1): - 'dark subtraction is done before flipping, so do not flip when reading either' - darkReader = ReadGE(darkFile, doFlip=False) - dark = darkReader.read(nframes=nframes, sumImg=True).astype(cls.__frame_dtype_dflt) - darkReader.close() - return dark - def makeNew(self): - """return a clean instance for the same data files - useful if want to start reading from the beginning""" - inParmDict = {} - inParmDict.update(self.__inParmDict) - for key in self.__inParmDict.keys(): - inParmDict[key] = eval("self."+key) - newSelf = self.__class__(self.fileInfo, **inParmDict) - return newSelf - def getRawReader(self, doFlip=False): - new = self.__class__(self.fileInfo, doFlip=doFlip) - return new - - def get_nbytes_header(self): - return self.__nbytes_header - nbytesHeader = property(get_nbytes_header, None, None) - - def getWriter(self, filename): - if not self.doFlip is False: - raise NotImplementedError, 'doFlip true not coded' - new = FrameWriter(self.ncols, self.nrows, - filename=filename, - dtypeDefault=self.dtypeDefault, - dtypeRead=self.dtypeRead, - dtypeFloat=self.dtypeFloat, - nbytesHeader=self.nbytesHeader) - return new - - def __setupRead(self, fileInfo, subtractDark, mask, omegaStart, omegaDelta): - - self.fileInfo = fileInfo - self.fileListR = self.__convertFileInfo(self.fileInfo) - self.fileListR.reverse() # so that pop reads in order - - self.subtractDark = subtractDark - self.mask = mask - - if self.dead is not None: - self.deadFlipped = self.__flip(self.dead) - - assert (omegaStart is None) == (omegaDelta is None),\ - 'must provide either both or neither of omega start and delta' - if omegaStart is not None: - if hasattr(omegaStart, 'getVal'): - omegaStart = omegaStart.getVal('radians') - if hasattr(omegaDelta, 'getVal'): - omegaDelta = omegaDelta.getVal('radians') - nFramesTot = self.getNFrames() - self.omegas = \ - num.arange(omegaStart, omegaStart+omegaDelta*(nFramesTot-0.5), omegaDelta) + \ - 0.5 * omegaDelta # put omegas at mid-points of omega range for frame - omegaEnd = omegaStart+omegaDelta*(nFramesTot) - self.omegaMin = min(omegaStart, omegaEnd) - self.omegaMax = max(omegaStart, omegaEnd) - self.omegaDelta = omegaDelta - self.omegaStart = omegaStart - self.__wrapsAround = abs( ( nFramesTot * abs(omegaDelta) ) / ( 2.0 * num.pi ) - 1.0 ) < 1.0e-6 - - self.__nextFile() - - return - - def get_wrapsAround(self): - return self.__wrapsAround - wrapsAround = property(get_wrapsAround, None, None) - - def getNFrames(self): - """number of total frames with real data, not number remaining""" - nFramesTot = self.getNFramesFromFileInfo(self.fileInfo) - return nFramesTot - def getDeltaOmega(self, nframes=1): - assert self.omegas is not None,\ - """instance does not have omega information""" - return self.omegaDelta * nframes - def getOmegaMinMax(self): - assert self.omegas is not None,\ - """instance does not have omega information""" - return self.omegaMin, self.omegaMax - def frameToOmega(self, frame): - scalar = num.isscalar(frame) - frames = num.asarray(frame) - if frames.dtype == int: - retval = self.omegas[frames] - else: - retval = (frames + 0.5) * self.omegaDelta + self.omegaStart - if scalar: - retval = num.asscalar(retval) - return retval - def getFrameOmega(self, iFrame=None): - """if iFrame is none, use internal counter""" - assert self.omegas is not None,\ - """instance does not have omega information""" - if iFrame is None: - iFrame = self.iFrame - if hasattr(iFrame, '__len__'): - 'take care of case nframes>1 in last call to read' - retval = num.mean(self.omegas[iFrame]) - else: - retval = self.omegas[iFrame] - return retval - def omegaToFrame(self, omega, float=False): - assert self.omegas is not None,\ - 'instance does not have omega information' - if self.__wrapsAround: - 'need to map omegas into range in case omega spans the branch cut' - omega = self.omegaMin + omega % (2.0*num.pi) - if float: - assert omega >= self.omegaMin and omega <= self.omegaMax,\ - 'omega %g is outside of the range [%g,%g] for the reader' % (omega, self.omegaMin, self.omegaMax) - retval = (omega - self.omegaStart)/self.omegaDelta - 0.5*self.omegaDelta - else: - # temp = num.where(self.omegas == omega)[0] - temp = num.where( num.abs(self.omegas - omega) < 0.1*abs(self.omegaDelta) )[0] - assert len(temp) == 1, 'omega not found, or found more than once' - retval = temp[0] - return retval - def getFrameUseMask(self): - """this is an optional toggle to turn the mask on/off""" - assert isinstance(self.iFrame, int), \ - 'self.iFrame needs to be an int for calls to getFrameUseMask' - if self.useMask is None: - retval = True - else: - assert len(self.useMask) == self.getNFrames(),\ - "len(useMask) must be %d; yours is %d" % (self.getNFrames(), len(self.useMask)) - retval = self.useMask[self.iFrame] - return retval - @classmethod - def __getNFrames(cls, fileBytes): - retval = getNFramesFromBytes(fileBytes, cls.__nbytes_header, cls.__nbytes_frame) - return retval - def __nextFile(self): - - # close in case already have a file going - self.close() - - fname, nempty = self.fileListR.pop() - - # open file - fileBytes = os.stat(fname).st_size - self.img = open(fname, mode='rb') - - # skip header for now - self.img.seek(self.__nbytes_header, 0) - - # figure out number of frames - self.nFramesRemain = self.__getNFrames(fileBytes) - - if nempty > 0: # 1 or more empty frames - if self.dark is None: - scale = 1.0 / nempty - self.dark = self.frame(dtype=self.__frame_dtype_float) - for i in range(nempty): - self.dark = self.dark + num.fromfile( - self.img, **self.__readArgs - ).reshape(self.__nrows, self.__ncols) * scale - self.dark.astype(self.__frame_dtype_dflt) - self.__log('got dark from %d empty frames in file %s' % (nempty, fname)) - else: - self.img.seek(self.nbytesFrame*nempty, 1) - self.nFramesRemain -= nempty - - if self.subtractDark and self.dark is None: - raise RuntimeError, "Requested dark field subtraction, but no file or empty frames specified!" - - return - @staticmethod - def __convertFileInfo(fileInfo): - if isinstance(fileInfo,str): - fileList = [(fileInfo, 0)] - elif hasattr(fileInfo,'__len__'): - assert len(fileInfo) > 0, 'length zero' - if hasattr(fileInfo[0],'__iter__'): # checking __len__ bad because has len attribute - fileList = copy.copy(fileInfo) - else: - assert len(fileInfo) == 2, 'bad file info' - fileList = [fileInfo] - else: - raise RuntimeError, 'do not know what to do with fileInfo '+str(fileInfo) - # fileList.reverse() - return fileList - def readBBox(self, bbox, raw=True, doFlip=None): - """ - with raw=True, read more or less raw data, with bbox = [(iLo,iHi),(jLo,jHi),(fLo,fHi)] - - careful: if raw is True, must set doFlip if want frames - potentially flipped; can set it to a reader instance to pull - the doFlip value from that instance - """ - - if raw: - if hasattr(doFlip,'doFlip'): - 'probably a ReadGe instance, pull doFlip from it' - doFlip = doFlip.doFlip - doFlip = doFlip or False # set to False if is None - reader = self.getRawReader(doFlip=doFlip) - else: - assert doFlip is None, 'do not specify doFlip if raw is True' - reader = self.makeNew() - - nskip = bbox[2][0] - bBox = num.array(bbox) - sl_i = slice(*bBox[0]) - sl_j = slice(*bBox[1]) - 'plenty of performance optimization might be possible here' - if raw: - retval = num.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_read ) - else: - retval = num.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_dflt ) - for iFrame in range(retval.shape[2]): - thisframe = reader.read(nskip=nskip) - nskip = 0 - retval[:,:,iFrame] = copy.deepcopy(thisframe[sl_i, sl_j]) - if not raw and self.dead is not None: - 'careful: have already flipped, so need deadFlipped instead of dead here' - mask = num.tile(self.deadFlipped[sl_i, sl_j].T, (retval.shape[2],1,1)).T - retval = num.ma.masked_array(retval, mask, hard_mask=True, copy=False) - return retval - def __flip(self, thisframe): - if self.doFlip: - if self.flipArg == 'v': - thisframe = thisframe[:, ::-1] - elif self.flipArg == 'h': - thisframe = thisframe[::-1, :] - elif self.flipArg == 'vh' or self.flipArg == 'hv': - thisframe = thisframe[::-1, ::-1] - elif self.flipArg == 'cw90': - thisframe = thisframe.T[:, ::-1] - elif self.flipArg == 'ccw90': - thisframe = thisframe.T[::-1, :] - else: - raise RuntimeError, "unrecognized flip token." - return thisframe - def getDark(self): - if self.dark is None: - retval = 0 - else: - retval = self.dark - return retval - def read(self, nskip=0, nframes=1, sumImg=False, mask=None): - """ - sumImg can be set to True or to something like numpy.maximum - """ - - 'get iFrame ready for how it is used here' - self.iFrame = num.atleast_1d(self.iFrame)[-1] - iFrameList = [] - multiframe = nframes > 1 - - nFramesInv = 1.0 / nframes - doDarkSub = self.subtractDark # and self.dark is not None - - if doDarkSub: - assert self.dark is not None, 'self.dark is None' - - # assign storage array - if sumImg: - sumImgCallable = hasattr(sumImg,'__call__') - imgOut = self.frame(dtype=self.__frame_dtype_float, mask=self.dead) - elif multiframe: - imgOut = self.frame(nframes=nframes, dtype=self.__frame_dtype_dflt, mask=self.dead) - - - # now read data frames - for i in range(nframes): - - #data = self.__readNext(nskip=nskip) - #thisframe = data.reshape(self.__nrows, self.__ncols) - data = self.__readNext(nskip=nskip) # .reshape(self.__nrows, self.__ncols) - self.iFrame += nskip + 1 - nskip=0 # all done skipping once have the first frame! - iFrameList.append(self.iFrame) - # dark subtraction - if doDarkSub: - 'used to have self.__frame_dtype_float here, but self.__frame_dtype_dflt does the trick' - thisframe = self.frame(buffer=data, - dtype=self.__frame_dtype_dflt, mask=self.dead) - self.dark - else: - thisframe = self.frame(buffer=data, - mask=self.dead) - - # flipping - thisframe = self.__flip(thisframe) - - # masking (True get zeroed) - if self.mask is not None: - if self.getFrameUseMask(): - thisframe[self.mask] = 0 - elif self.mask is None and mask is not None: - thisframe[mask] = 0 - - # assign output - if sumImg: - if sumImgCallable: - imgOut = sumImg(imgOut, thisframe) - else: - imgOut = imgOut + thisframe * nFramesInv - elif multiframe: - imgOut[i, :, :] = thisframe[:, :] - 'end of loop over nframes' - - if sumImg: - # imgOut = imgOut / nframes # now taken care of above - pass - elif not multiframe: - imgOut = thisframe - - if multiframe: - 'make iFrame a list so that omega or whatever can be averaged appropriately' - self.iFrame = iFrameList - return imgOut - def __log(self, message): - if self.__debug: - print self.__location+' : '+message - return - def __readNext(self, nskip=0): - - if self.img is None: - raise RuntimeError, 'no image file set' - - nHave = 0 - - nskipThis = nskip - while self.nFramesRemain+nHave - nskipThis < 1: - 'not enough frames left in this file' - nskipThis = nskipThis - self.nFramesRemain - self.nFramesRemain = 0 # = self.nFramesRemain - self.nFramesRemain - self.__nextFile() - if nskipThis > 0: - # advance counter past empty frames - self.img.seek(self.nbytesFrame*nskipThis, 1) - self.nFramesRemain -= nskipThis - - # grab current frame - data = num.fromfile(self.img, **self.__readArgs) - data = num.array(data, **self.__castArgs) - self.nFramesRemain -= 1 - - return data - def __call__(self, *args, **kwargs): - return self.read(*args, **kwargs) - def close(self): - # if already have a file going, close it out - if self.img is not None: - self.img.close() - return - """ - getReadDtype function replaced by dtypeRead property - """ - @classmethod - def maxVal(cls, dummy): - 'maximum value that can be stored in the image pixel data type' - # dtype = reader._ReadGE__frame_dtype - # maxInt = num.iinfo(cls.__frame_dtype_read).max # bigger than it really is - maxInt = 2 ** 14 - return maxInt - @classmethod - def getNFramesFromFileInfo(cls, fileInfo, lessEmpty=True): - fileList = cls.__convertFileInfo(fileInfo) - nFramesTot = 0 - for fname, nempty in fileList: - fileBytes = os.stat(fname).st_size - nFrames = cls.__getNFrames(fileBytes) - if lessEmpty: - nFrames -= nempty - nFramesTot += nFrames - return nFramesTot - - def indicesToMask(self, indices): - """ - Indices can be a list of indices, as from makeIndicesTThRanges - """ - mask = self.getEmptyMask() - if hasattr(indices,'__len__'): - for indThese in indices: - mask[indThese] = True - else: - mask[indices] = True - return mask - -class ReadMar165(Framer2DRC): - """ - placeholder; not yet really implemented - - """ - __frame_dtype_read = 'uint16' - __frame_dtype_dflt = 'int16' # good for doing subtractions - def __init__(self, mode): - if not isinstance(mode, int) or not [1,2,4,8].count(mode): - raise RuntimeError, 'unknown mode : '+str(mode) - - self.__mode = mode - self.__idim = mar165IDim(mode) - return - def __call__(self, filename): - if not haveImageModule: - msg = "PIL Image module is required for this operation, "\ - "but not loaded\n" - raise NameError(msg) - - i = Image.open(filename, mode='r') - a = num.array(i, dtype=self.__frame_dtype_read) - frame = num.array(a, dtype=self.__frame_dtype_dflt) - return frame - - -class ReadMar165NB1(ReadMar165): - def __init__(self, *args, **kwargs): - ReadMar165.__init__(self, 1, *args, **kwargs) - return -class ReadMar165NB2(ReadMar165): - def __init__(self, *args, **kwargs): - ReadMar165.__init__(self, 2, *args, **kwargs) - return -class ReadMar165NB3(ReadMar165): - def __init__(self, *args, **kwargs): - ReadMar165.__init__(self, 3, *args, **kwargs) - return -class ReadMar165NB4(ReadMar165): - def __init__(self, *args, **kwargs): - ReadMar165.__init__(self, 4, *args, **kwargs) - return - class LineStyles: """ do not want to just cycle through default plot line colors, as end up with black lines @@ -3501,7 +2291,7 @@ def cartesianCoordsOfPixelIndices(self, row, col, ROI=None): # properly offset in case if ROI is not None: - assert len(ROI) == 2, 'wrong length for ROI; should be 2 integers representing the UL corner' + assert len(ROI) is 2, 'wrong length for ROI; should be 2 integers representing the UL corner' row = row + ROI[0] col = col + ROI[1] @@ -3529,7 +2319,7 @@ def pixelIndicesOfCartesianCoords(self, x, y, ROI=None): # properly offset in case if ROI is not None: - assert len(ROI) == 2, 'wrong length for ROI; should be 2 integers representing the UL corner' + assert len(ROI) is 2, 'wrong length for ROI; should be 2 integers representing the UL corner' row = row - ROI[0] col = col - ROI[1] @@ -3578,7 +2368,7 @@ def angToXYO_V(self, tth, eta_l, *args, **kwargs): raise RuntimeError, 'Output units \'%s\'not understood!' % (str(kwargs[argkeys[i]])) elif argkeys[i] is 'rhoRange': tthRange = kwargs[argkeys[i]] - assert len(tthRange) == 2, 'Radial range should have length 2' + assert len(tthRange) is 2, 'Radial range should have length 2' elif argkeys[i] is 'rdist': if not isinstance(kwargs[argkeys[i]], bool): raise RuntimeError, 'Expecting boolean for rdist kewyord argument; got' \ @@ -3651,7 +2441,7 @@ def angToXYO_V(self, tth, eta_l, *args, **kwargs): # note that the Z comps should all be zeros anyhow P4_d = num.vstack( [X_d, Y_d, nzeros] ) - if len(rhoRange) == 2: + if len(rhoRange) is 2: rhoMin = min(rhoRange) rhoMax = max(rhoRange) # @@ -3889,7 +2679,7 @@ def xyoToAng_V(self, x0, y0, *args, **kwargs): raise RuntimeError, 'Input units \'%s\' not understood!' % (str(kwargs[argkeys[i]])) elif argkeys[i] is 'tthRange': tthRange = kwargs[argkeys[i]] - assert len(tthRange) == 2, 'Two-theta range should have length 2' + assert len(tthRange) is 2, 'Two-theta range should have length 2' elif argkeys[i] is 'rdist': if not isinstance(kwargs[argkeys[i]], bool): raise RuntimeError, 'Expecting boolean for rdist kewyord argument; got' \ @@ -3992,7 +2782,7 @@ def xyoToAng_V(self, x0, y0, *args, **kwargs): # transform data tmpData = num.vstack( [measTTH, eta_l] ) - if len(tthRange) == 2: + if len(tthRange) is 2: tthMin = min(tthRange) tthMax = max(tthRange) @@ -4024,9 +2814,8 @@ def xyoToAngAll(self): """ get angular positions of all pixels """ - jVals, iVals = num.meshgrid(num.arange(self.__ncols) + 0.5, - num.arange(self.__nrows) + 0.5) - + jVals = num.tile(num.arange(self.__ncols),(self.__nrows,1)) + iVals = jVals.T twoTheta, eta = self.xyoToAng(iVals, jVals) return twoTheta, eta def xyoToAngCorners(self): @@ -4070,9 +2859,6 @@ def makeTThRanges(self, planeData, cullDupl=False): def makeIndicesTThRanges(self, planeData, cullDupl=False): """ return a list of indices for sets of overlaping two-theta ranges; - to plot, can do something like: - mask = self.reader.getEmptyMask() - mask[indices] = True With cullDupl set true, eliminate HKLs with duplicate 2-thetas """ @@ -4208,18 +2994,22 @@ def angToXYOBBox(self, *args, **kwargs): given either angBBox or angCOM (angular center) and angPM (+-values), compute the bounding box on the image frame if forSlice=True, then returned bbox is appropriate for use in array slicing - - if reader or omegas is passed, then convert from omegas to frames; - and if doWrap=True, then frames may be a list for an omega range that spans the branch cut """ units = kwargs.setdefault('units', 'pixels') # - reader = kwargs.pop('reader', None) - omegas = kwargs.pop('omegas', None) - doWrap = kwargs.pop('doWrap', False) - forSlice = kwargs.pop('forSlice', True) + # reader = kwargs.get('reader', None) + reader = None + if kwargs.has_key('reader'): + reader = kwargs.pop('reader') + # + omegas = None + if kwargs.has_key('omegas'): + omegas = kwargs.pop('omegas') # + forSlice = True + if kwargs.has_key('forSlice'): + forSlice = kwargs.pop('forSlice') slicePad = 0 if forSlice: slicePad = 1 @@ -4256,21 +3046,19 @@ def angToXYOBBox(self, *args, **kwargs): xyoBBox[1] = ( max( int(math.floor(xyoBBox[1][0])), 0), min( int(math.floor(xyoBBox[1][1])), self.ncols-1)+slicePad, ) - if (reader is not None) or (omegas is not None): - if reader is not None: - omegaDelta = reader.omegaDelta - omegaStart = reader.omegaStart - nFrames = reader.getNFrames() - else: - 'omegas is not None' - omegaDelta = num.mean(omegas[1:]-omegas[:-1]) # assumes uniform omegas - omegaStart = omegas[0]-omegaDelta*0.5 - nFrames = len(omegas) - frameRange = omeRangeToFrameRange(xyoBBox[2][0], xyoBBox[2][1], - omegaStart, omegaDelta, nFrames, - checkWrap=doWrap, slicePad=slicePad) - xyoBBox[2] = frameRange - 'try using frameInRange(iFrame, xyoBBox[2])' + if reader is not None: + 'convert bounding box from omegas to frames' + xyoBBox[2] = ( num.hstack( (reader.omegaToFrameRange(xyoBBox[2][0]), 0) )[0], + num.hstack( (reader.getNFrames()-1, reader.omegaToFrameRange(xyoBBox[2][1]) ) )[-1] + slicePad, + ) + elif omegas is not None: + 'convert bounding box from omegas to frames' + omegaDelta = num.mean(omegas[1:]-omegas[:-1]) + nFrames = len(omegas) + xyoBBox[2] = ( + num.hstack( (omeToFrameRange(xyoBBox[2][0], omegas, omegaDelta), 0) )[0], + num.hstack( (nFrames-1, omeToFrameRange(xyoBBox[2][1], omegas, omegaDelta) ) )[-1] + slicePad, + ) return xyoBBox @@ -4648,7 +3436,7 @@ def clean(self): # self.fitRingsFunc = None self.xFitRings = None return - def fitRings(self, thisframe, planeData, xtol=DFLT_XTOL, xVec0=None, + def fitRings(self, thisframe, planeData, xVec0=None, funcType=funcTypeDflt, quadr=1, makePlots=False): # 'free up memory' @@ -4665,7 +3453,7 @@ def fitRings(self, thisframe, planeData, xtol=DFLT_XTOL, xVec0=None, xVec0 = func.guessXVec() self.xFitRings = None - x = func.doFit(xtol=xtol) + x = func.doFit(xtol=DFLT_XTOL) self.xFitRings = x # self.fitRingsFunc = func @@ -4794,25 +3582,21 @@ def polarRebin(self, thisFrame, rho, eta, x, y = self.pixelToPolar(rowInd, colInd, corrected=corrected, startEta=startEta) + # MAKE POLAR BIN CENTER ARRAY deltaEta = (stopEta - startEta) / numEta deltaRho = (stopRho - startRho) / numRho rowEta = startEta + deltaEta * ( num.arange(numEta) + 0.5 ) colRho = startRho + deltaRho * ( num.arange(numRho) + 0.5 ) - colTTh = num.arctan2(colRho, self.workDist) - if corrected: - colOut = colTTh - else: - colOut = colRho # initialize output dictionary polImg = {} - polImg['corrected'] = corrected - polImg['radius'] = colOut - polImg['azimuth'] = rowEta - polImg['deltaRho'] = deltaRho - polImg['intensity'] = num.empty( (numEta, numRho) ) + polImg['radius'] = colRho + polImg['azimuth'] = rowEta + polImg['intensity'] = num.empty( (numEta, numRho) ) + polImg['deltaRho'] = deltaRho + if verbose: msg = "INFO: Masking pixels\n" @@ -4902,49 +3686,6 @@ def polarRebin(self, thisFrame, return polImg - -def mar165IDim(mode): - if not isinstance(mode, int) or not [1,2,4,8].count(mode): - raise RuntimeError, 'unknown mode : '+str(mode) - idim = 4096 / mode - return idim - -class DetectorGeomMar165(Detector2DRC): - __vfu = 0.2 # made up - __vdk = 1800 # made up - def __init__(self, *args, **kwargs): - - mode = 1 - if kwargs.has_key('mode'): - mode = kwargs.pop('mode') - readerClass = eval('ReadMar165NB%d' % (mode)) - idim = mar165IDim(mode) - nrows = ncols = idim - pixelPitch = 165.0 / idim # mm - reader = readerClass() - - self.mode = mode - - Detector2DRC.__init__(self, - nrows, ncols, pixelPitch, - self.__vfu, self.__vdk, - reader, - *args, **kwargs) - return - - def getDParamDflt(self): - return [] - def setDParamZero(self): - return - def getDParamScalings(self): - return [] - def getDParamRefineDflt(self): - return [] - # - def radialDistortion(self, xin, yin, invert=False): - 'no distortion correction' - return xin, yin - class DetectorGeomGE(Detector2DRC): """ handle geometry of GE detector, such as geometric and radial distortion corrections; @@ -4955,10 +3696,10 @@ class DetectorGeomGE(Detector2DRC): __vfu = 0.2 # made up __vdk = 1800 # made up # 200 x 200 micron pixels - __pixelPitch = PIXEL # in mm - __idim = ReadGE._ReadGE__idim - __nrows = ReadGE._ReadGE__nrows - __ncols = ReadGE._ReadGE__ncols + __pixelPitch = 0.2 # in mm + __idim = 2048 + __nrows = 2048 + __ncols = 2048 __dParamDflt = [ 0.0, 0.0, 0.0, 2.0, 2.0, 2.0] __dParamZero = [ 0.0, 0.0, 0.0, 2.0, 2.0, 2.0] __dParamScalings = [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] @@ -4970,9 +3711,12 @@ def __init__(self, *args, **kwargs): if reader is None: readerKWArgs = kwargs.pop('readerKWArgs', {}) reader = ReadGE(None, **readerKWArgs) + else: + self.__nrows = self.__idim = reader.rnows + self.__ncols = reader.ncols Detector2DRC.__init__(self, - self.__ncols, self.__nrows, self.__pixelPitch, + self.__nrows, self.__ncols, self.__pixelPitch, self.__vfu, self.__vdk, reader, *args, **kwargs) @@ -4997,11 +3741,75 @@ def getDParamScalings(self): def getDParamRefineDflt(self): return self.__dParamRefineDflt def radialDistortion(self, xin, yin, invert=False): - xshape = xin.shape - yshape = yin.shape - xy_in = num.vstack([xin.flatten(), yin.flatten()]).T - xy_out = distortion.GE_41RT(xy_in, self.dparms, invert=invert) - return xy_out[:, 0].reshape(xshape), xy_out[:, 1].reshape(yshape) + """ + Apply radial distortion to polar coordinates on GE detector + + xin, yin are 1D arrays or scalars, assumed to be relative to self.xc, self.yc + Units are [mm, radians]. This is the power-law based function of Bernier. + + Available Keyword Arguments : + + invert = True or >False< :: apply inverse warping + """ + if self.dparms[0] == 0 and self.dparms[1] == 0 and self.dparms[2] == 0: + xout = xin + yout = yin + else: + # canonical max radius based on perfectly centered beam + # - 204.8 in mm or 1024 in pixel indices + rhoMax = self.__idim * self.__pixelPitch / 2 + + # make points relative to detector center + x0 = (xin + self.xc) - rhoMax + y0 = (yin + self.yc) - rhoMax + + # detector relative polar coordinates + # - this is the radius that gets rescaled + rho0 = num.sqrt( x0*x0 + y0*y0 ) + eta0 = num.arctan2( y0, x0 ) + + if invert: + # in here must do nonlinear solve for distortion + # must loop to call fsolve individually for each point + rho0 = num.atleast_1d(rho0) + rShape = rho0.shape + rho0 = num.atleast_1d(rho0).flatten() + rhoOut = num.zeros(len(rho0), dtype=float) + + eta0 = num.atleast_1d(eta0).flatten() + + rhoSclFuncInv = lambda ri, ni, ro, rx, p: \ + (p[0]*(ri/rx)**p[3] * num.cos(2.0 * ni) + \ + p[1]*(ri/rx)**p[4] * num.cos(4.0 * ni) + \ + p[2]*(ri/rx)**p[5] + 1)*ri - ro + + rhoSclFIprime = lambda ri, ni, ro, rx, p: \ + p[0]*(ri/rx)**p[3] * num.cos(2.0 * ni) * (p[3] + 1) + \ + p[1]*(ri/rx)**p[4] * num.cos(4.0 * ni) * (p[4] + 1) + \ + p[2]*(ri/rx)**p[5] * (p[5] + 1) + 1 + + for iRho in range(len(rho0)): + rhoOut[iRho] = fsolve(rhoSclFuncInv, rho0[iRho], + fprime=rhoSclFIprime, + args=(eta0[iRho], rho0[iRho], rhoMax, self.dparms) ) + pass + + rhoOut = rhoOut.reshape(rShape) + else: + # usual case: calculate scaling to take you from image to detector plane + # 1 + p[0]*(ri/rx)**p[2] * num.cos(p[4] * ni) + p[1]*(ri/rx)**p[3] + rhoSclFunc = lambda ri, rx=rhoMax, p=self.dparms, ni=eta0: \ + p[0]*(ri/rx)**p[3] * num.cos(2.0 * ni) + \ + p[1]*(ri/rx)**p[4] * num.cos(4.0 * ni) + \ + p[2]*(ri/rx)**p[5] + 1 + + rhoOut = num.squeeze( rho0 * rhoSclFunc(rho0) ) + pass + + xout = rhoOut * num.cos(eta0) + rhoMax - self.xc + yout = rhoOut * num.sin(eta0) + rhoMax - self.yc + + return xout, yout class DetectorGeomFrelon(Detector2DRC): """ @@ -5012,9 +3820,7 @@ class DetectorGeomFrelon(Detector2DRC): # 50 X 50 micron pixels __pixelPitch = 0.05 # in mm - __idim = ReadGE._ReadGE__idim - __nrows = ReadGE._ReadGE__nrows - __ncols = ReadGE._ReadGE__ncols + __ncols = __nrows = __idim = 2048 __dParamDflt = [ 0.0, 0.0, 0.0, 2.0, 2.0, 2.0] __dParamZero = [ 0.0, 0.0, 0.0, 2.0, 2.0, 2.0] __dParamScalings = [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] @@ -5411,18 +4217,6 @@ def fitProcedureA(self, planeData, framesQuad, iRefQuad=0, self.setQuadOffsets(iRefQuad) return -def getOmegaMMReaderList(readerList, overall=False): - """ - get omega min/max information from a list of readers - """ - retval = [] - for reader in num.atleast_1d(readerList): - omegaMin, omegaMax = reader.getOmegaMinMax() - retval.append((omegaMin,omegaMax)) - if overall: - retval = (min(zip(*retval)[0]), max(zip(*retval)[1])) - return retval - # ============================== Utility functions for instantiating detectors # def detectorList(): @@ -5466,17 +4260,6 @@ def newDetector(detectorType, *args, **kwargs): return d -def newGenericReader(ncols, nrows, *args, **kwargs): - ''' - currently just returns a Framer2DRC - ''' - - # retval = Framer2DRC(ncols, nrows, **kwargs) - filename = kwargs.pop('filename', None) - retval = ReadGeneric(filename, ncols, nrows, *args, **kwargs) - - return retval - def newGenericDetector(ncols, nrows, pixelPitch, *args, **kwargs): """ If reader is passed as None, then a generic reader is created From 2cebaa5572ffd8ad14120294f32100ff250d6b4c Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 27 Sep 2015 21:21:36 -0400 Subject: [PATCH 018/253] removed print statements --- hexrd/imageseries/write.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hexrd/imageseries/write.py b/hexrd/imageseries/write.py index 91e90d00..7788b247 100644 --- a/hexrd/imageseries/write.py +++ b/hexrd/imageseries/write.py @@ -75,7 +75,6 @@ def _open_dset(self): # def write(self): """Write imageseries to HDF5 file""" - print('writing ', self.fmt) ds = self._open_dset() for i in range(self._nframes): ds[i, :, :] = self._ims[i] @@ -96,7 +95,6 @@ def write(self): presumes sparse forms are small enough to contain all frames """ - print('writing ', self.fmt) arrd = dict() sh = None for i in range(self._nframes): From 4b0161b0fc9f946830d2bd950a3cde41d2761a67 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 28 Sep 2015 09:50:41 -0400 Subject: [PATCH 019/253] added dark file processing to imageseries --- hexrd/imageseries/process.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 8664d765..8cc3c238 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -4,6 +4,8 @@ class ProcessedImageSeries(ImageSeries): """Images series with mapping applied to frames""" FLIP = 'flip' + DARK = 'dark' + def __init__(self, imser, **kwargs): """Instantiate imsageseries based on existing one with mapping options @@ -21,9 +23,14 @@ def __len__(self): def _process_frame(self, key): img = self._imser[key] + img = self._subtract_dark(img) img = self._flip(img) return img + def _subtract_dark(self, img): + if self.DARK in self._opts: + return img - self._opts[self.DARK] + def _flip(self, img): if self.FLIP in self._opts: flip = self._opts['flip'] From 1c72a43de7e40ddda52f6e712e6f086c4ace58ab Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 28 Sep 2015 12:19:08 -0400 Subject: [PATCH 020/253] added median for processed imageseries --- hexrd/imageseries/process.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 8cc3c238..033c2605 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -1,4 +1,6 @@ """Class for processing frames or frame groups""" +import numpy as np + from hexrd.imageseries import ImageSeries class ProcessedImageSeries(ImageSeries): @@ -30,6 +32,8 @@ def _process_frame(self, key): def _subtract_dark(self, img): if self.DARK in self._opts: return img - self._opts[self.DARK] + else: + return img def _flip(self, img): if self.FLIP in self._opts: @@ -53,7 +57,16 @@ def _flip(self, img): pimg = img return pimg - + + def _toarray(self, nframes=0): + mynf = len(self) + nf = np.min(mynf, nframes) if nframes > 0 else mynf + ashp = (nf,) + self.shape + a = np.zeros(ashp, dtype=self.dtype) + for i in range(nf): + a[i] = self.__getitem__(i) + + return a @property def dtype(self): @@ -63,5 +76,7 @@ def dtype(self): def shape(self): return self._imser.shape + def median(self, nframes=0): + return np.median(self._toarray(nframes=nframes), axis=0) pass # end class From 2856fce4dc1a2577b3856ce9960108dd33bd9ba8 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 29 Sep 2015 10:02:31 -0400 Subject: [PATCH 021/253] updated frame cache format to use yml --- hexrd/imageseries/adapters/framecache.py | 35 +++++++++++++--------- hexrd/imageseries/write.py | 38 +++++++++++++++++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/hexrd/imageseries/adapters/framecache.py b/hexrd/imageseries/adapters/framecache.py index 9d8e7a87..6ce18e21 100644 --- a/hexrd/imageseries/adapters/framecache.py +++ b/hexrd/imageseries/adapters/framecache.py @@ -5,6 +5,7 @@ import numpy as np from scipy.sparse import csr_matrix +import yaml class FrameCacheImageSeriesAdapter(ImageSeriesAdapter): """collection of images in HDF5 format""" @@ -15,36 +16,42 @@ def __init__(self, fname, **kwargs): """Constructor for frame cache image series *fname* - filename of the yml file - *kwargs* - keyword arguments (none required + *kwargs* - keyword arguments (none required) """ self._fname = fname - self._load() + self._load_yml() + self._load_cache() - def _load(self): + def _load_yml(self): + with open(self._fname, "r") as f: + d = yaml.load(f) + datad = d['data'] + metad = d['meta'] + self._cache = datad['file'] + self._nframes = datad['nframes'] + self._shape = tuple(datad['shape']) + self._dtype = np.dtype(datad['dtype']) + self._meta = metad + + def _load_cache(self): """load into list of csr sparse matrices""" - arrs = np.load(self._fname) - self._shape = tuple(arrs['shape'].tolist()) - self._dtype = None + arrs = np.load(self._cache) - arrsh = tuple(arrs['shape']) - nk = len(arrs.files) - 1 - self._nframes = nk/3 self._framelist = [] for i in range(self._nframes): row = arrs["%d_row" % i] col = arrs["%d_col" % i] data = arrs["%d_data" % i] - frame = csr_matrix((data, (row, col)), shape=arrsh) + frame = csr_matrix((data, (row, col)), shape=self._shape) self._framelist.append(frame) - if self._dtype is None: - self._dtype = data.dtype - + + @property def metadata(self): """(read-only) Image sequence metadata Currently returns none """ - return None + return self._meta @property def dtype(self): diff --git a/hexrd/imageseries/write.py b/hexrd/imageseries/write.py index 7788b247..d1f3ffc2 100644 --- a/hexrd/imageseries/write.py +++ b/hexrd/imageseries/write.py @@ -4,6 +4,7 @@ import numpy as np import h5py +import yaml def write(ims, fname, fmt, **kwargs): """write imageseries to file with options @@ -84,17 +85,30 @@ def write(self): pass # end class class WriteFrameCache(Writer): - + """info from yml file""" fmt = 'frame-cache' def __init__(self, ims, fname, **kwargs): - Writer.__init__(self, ims, fname, **kwargs) - self._thresh = self._opts['threshold'] + """write yml file with frame cache info - def write(self): - """writes frame cache for imageseries + kwargs has keys: - presumes sparse forms are small enough to contain all frames + cache_file - name of array cache file + meta - metadata dictionary """ + Writer.__init__(self, ims, fname, **kwargs) + self._thresh = self._opts['threshold'] + self._cache = kwargs['cache_file'] + self._meta = kwargs['meta'] if 'meta' in kwargs else dict() + + def _write_yml(self): + datad = {'file': self._cache, 'dtype': str(self._ims.dtype), + 'nframes': len(self._ims), 'shape': list(self._ims.shape)} + info = {'data': datad, 'meta': self._meta} + with open(self._fname, "w") as f: + yaml.dump(info, f) + + def _write_frames(self): + """also save shape array as originally done (before yaml)""" arrd = dict() sh = None for i in range(self._nframes): @@ -107,4 +121,14 @@ def write(self): if sh is None: arrd['shape'] = np.array(frame.shape) - np.savez_compressed(self._fname, **arrd) + np.savez_compressed(self._cache, **arrd) + + def write(self): + """writes frame cache for imageseries + + presumes sparse forms are small enough to contain all frames + """ + self._write_frames() + self._write_yml() + + From f23d2ac2fbf092d3285bc583382e3935b6227d29 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 29 Sep 2015 14:42:38 -0400 Subject: [PATCH 022/253] a couple bugs: shape property in h5; median with subset of frames --- hexrd/imageseries/adapters/hdf5.py | 2 +- hexrd/imageseries/process.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/adapters/hdf5.py b/hexrd/imageseries/adapters/hdf5.py index 927887e2..de966a17 100644 --- a/hexrd/imageseries/adapters/hdf5.py +++ b/hexrd/imageseries/adapters/hdf5.py @@ -43,7 +43,7 @@ def dtype(self): #@memoize so you only need to do this once def shape(self): with self._dset as dset: - return dset.shape + return dset.shape[1:] def __init__(self, fname, **kwargs): """Constructor for H5FrameSeries diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 033c2605..10cb39e1 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -60,7 +60,7 @@ def _flip(self, img): def _toarray(self, nframes=0): mynf = len(self) - nf = np.min(mynf, nframes) if nframes > 0 else mynf + nf = np.min((mynf, nframes)) if nframes > 0 else mynf ashp = (nf,) + self.shape a = np.zeros(ashp, dtype=self.dtype) for i in range(nf): From 5c538fde8e3898fe5f902c5710fd7067ea6bbb4c Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 5 Oct 2015 14:24:16 -0400 Subject: [PATCH 023/253] added restriction to rectangle in processing --- hexrd/imageseries/process.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 10cb39e1..5d3a1b04 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -7,7 +7,8 @@ class ProcessedImageSeries(ImageSeries): """Images series with mapping applied to frames""" FLIP = 'flip' DARK = 'dark' - + RECT = 'rectangle' + def __init__(self, imser, **kwargs): """Instantiate imsageseries based on existing one with mapping options @@ -19,28 +20,39 @@ def __init__(self, imser, **kwargs): def __getitem__(self, key): return self._process_frame(key) - + def __len__(self): return len(self._imser) def _process_frame(self, key): + # apply flip at end img = self._imser[key] img = self._subtract_dark(img) + img = self._rectangle(img) img = self._flip(img) return img def _subtract_dark(self, img): + # need to check for values below zero if self.DARK in self._opts: return img - self._opts[self.DARK] else: return img + def _rectangle(self, img): + # restrict to rectangle + if self.RECT in self._opts: + r = self._opts[self.RECT] + return img[r[0,0]:r[0,1], r[1,0]:r[1,1]] + else: + return img + def _flip(self, img): if self.FLIP in self._opts: flip = self._opts['flip'] else: return img - + if flip in ('y','v'): # about y-axis (vertical) pimg = img[:, ::-1] elif flip in ('x', 'h'): # about x-axis (horizontal) @@ -68,7 +80,7 @@ def _toarray(self, nframes=0): return a - @property + @property def dtype(self): return self._imser.dtype @@ -78,5 +90,5 @@ def shape(self): def median(self, nframes=0): return np.median(self._toarray(nframes=nframes), axis=0) - + pass # end class From 6a0e15df77defb0927808592cebd2b5ead5a526e Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 6 Oct 2015 17:39:17 -0400 Subject: [PATCH 024/253] corrected dark subtraction to limit at zero --- hexrd/imageseries/process.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 5d3a1b04..6e699f08 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -34,11 +34,12 @@ def _process_frame(self, key): def _subtract_dark(self, img): # need to check for values below zero - if self.DARK in self._opts: - return img - self._opts[self.DARK] - else: + if self.DARK not in self._opts: return img + dark = self._opts[self.DARK] + return np.where(img > dark, img-dark, 0) + def _rectangle(self, img): # restrict to rectangle if self.RECT in self._opts: From 3aafe8440afe1274764b1bb3dbc03954de23e1ce Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 6 Oct 2015 20:43:53 -0400 Subject: [PATCH 025/253] reorganized imageseries, moving ImageSeries to baseclass module; loading process from __init__, and renaming adapters to load, write to save --- hexrd/imageseries/__init__.py | 38 +++---------------- hexrd/imageseries/baseclass.py | 36 ++++++++++++++++++ hexrd/imageseries/imageseriesabc.py | 1 - .../{adapters => load}/__init__.py | 0 hexrd/imageseries/{adapters => load}/array.py | 0 .../{adapters => load}/framecache.py | 0 hexrd/imageseries/{adapters => load}/hdf5.py | 4 +- .../{adapters => load}/registry.py | 0 .../imageseries/{adapters => load}/trivial.py | 0 hexrd/imageseries/process.py | 2 +- hexrd/imageseries/{write.py => save.py} | 0 11 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 hexrd/imageseries/baseclass.py rename hexrd/imageseries/{adapters => load}/__init__.py (100%) rename hexrd/imageseries/{adapters => load}/array.py (100%) rename hexrd/imageseries/{adapters => load}/framecache.py (100%) rename hexrd/imageseries/{adapters => load}/hdf5.py (99%) rename hexrd/imageseries/{adapters => load}/registry.py (100%) rename hexrd/imageseries/{adapters => load}/trivial.py (100%) rename hexrd/imageseries/{write.py => save.py} (100%) diff --git a/hexrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py index ad9c45ab..5099da6a 100644 --- a/hexrd/imageseries/__init__.py +++ b/hexrd/imageseries/__init__.py @@ -2,41 +2,15 @@ This file contains the generic ImageSeries class and a function for loading. Adapters for particular -data formats are managed in a subpackage. +data formats are managed in the "load" subpackage. """ -from imageseriesabc import ImageSeriesABC -import adapters - -class ImageSeries(ImageSeriesABC): - """collection of images - - Basic sequence class with additional properties for image shape and - metadata (possibly None). - """ - - def __init__(self, adapter): - """Build FrameSeries from adapter instance - - *adapter* - object instance based on abstract Sequence class with - properties for image shape and, optionally, metadata. - """ - self.__adapter = adapter - - return - - def __getitem__(self, key): - return self.__adapter[key] - - def __len__(self): - return len(self.__adapter) - - def __getattr__(self, attrname): - return getattr(self.__adapter, attrname) - - pass # end class +from .baseclass import ImageSeries +from . import load +from . import process +from . import save def open(filename, format=None, **kwargs): # find the appropriate adapter based on format specified - reg = adapters.Registry.adapter_registry + reg = load.Registry.adapter_registry adapter = reg[format](filename, **kwargs) return ImageSeries(adapter) diff --git a/hexrd/imageseries/baseclass.py b/hexrd/imageseries/baseclass.py new file mode 100644 index 00000000..989066be --- /dev/null +++ b/hexrd/imageseries/baseclass.py @@ -0,0 +1,36 @@ +"""Base class for imageseries +""" +from .imageseriesabc import ImageSeriesABC + +class ImageSeries(ImageSeriesABC): + """collection of images + + Basic sequence class with additional properties for image shape and + metadata (possibly None). + """ + + def __init__(self, adapter): + """Build FrameSeries from adapter instance + + *adapter* - object instance based on abstract Sequence class with + properties for image shape and, optionally, metadata. + """ + self._adapter = adapter + + return + + def __getitem__(self, key): + return self._adapter[key] + + def __len__(self): + return len(self._adapter) + + @property + def dtype(self): + return self._adapter.dtype + + @property + def shape(self): + return self._adapter.shape + + pass # end class diff --git a/hexrd/imageseries/imageseriesabc.py b/hexrd/imageseries/imageseriesabc.py index 9d203648..504ad45d 100644 --- a/hexrd/imageseries/imageseriesabc.py +++ b/hexrd/imageseries/imageseriesabc.py @@ -3,4 +3,3 @@ class ImageSeriesABC(collections.Sequence): pass - # define interface here diff --git a/hexrd/imageseries/adapters/__init__.py b/hexrd/imageseries/load/__init__.py similarity index 100% rename from hexrd/imageseries/adapters/__init__.py rename to hexrd/imageseries/load/__init__.py diff --git a/hexrd/imageseries/adapters/array.py b/hexrd/imageseries/load/array.py similarity index 100% rename from hexrd/imageseries/adapters/array.py rename to hexrd/imageseries/load/array.py diff --git a/hexrd/imageseries/adapters/framecache.py b/hexrd/imageseries/load/framecache.py similarity index 100% rename from hexrd/imageseries/adapters/framecache.py rename to hexrd/imageseries/load/framecache.py diff --git a/hexrd/imageseries/adapters/hdf5.py b/hexrd/imageseries/load/hdf5.py similarity index 99% rename from hexrd/imageseries/adapters/hdf5.py rename to hexrd/imageseries/load/hdf5.py index de966a17..4e684946 100644 --- a/hexrd/imageseries/adapters/hdf5.py +++ b/hexrd/imageseries/load/hdf5.py @@ -1,6 +1,7 @@ """HDF5 adapter class """ import h5py + from . import ImageSeriesAdapter from ..imageseriesiter import ImageSeriesIterator @@ -38,7 +39,7 @@ def metadata(self): def dtype(self): with self._dset as dset: return dset.dtype - + @property #@memoize so you only need to do this once def shape(self): @@ -83,4 +84,3 @@ def __enter__(self): def __exit__(self, *args): self._f.close() - diff --git a/hexrd/imageseries/adapters/registry.py b/hexrd/imageseries/load/registry.py similarity index 100% rename from hexrd/imageseries/adapters/registry.py rename to hexrd/imageseries/load/registry.py diff --git a/hexrd/imageseries/adapters/trivial.py b/hexrd/imageseries/load/trivial.py similarity index 100% rename from hexrd/imageseries/adapters/trivial.py rename to hexrd/imageseries/load/trivial.py diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 6e699f08..bb0c1953 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -1,7 +1,7 @@ """Class for processing frames or frame groups""" import numpy as np -from hexrd.imageseries import ImageSeries +from .baseclass import ImageSeries class ProcessedImageSeries(ImageSeries): """Images series with mapping applied to frames""" diff --git a/hexrd/imageseries/write.py b/hexrd/imageseries/save.py similarity index 100% rename from hexrd/imageseries/write.py rename to hexrd/imageseries/save.py From bb520b8ec95571bbbcefe77dc1753d4e364bad1a Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 6 Oct 2015 21:36:54 -0400 Subject: [PATCH 026/253] correction: added __iter__ method to base class to use adapter method --- hexrd/imageseries/baseclass.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hexrd/imageseries/baseclass.py b/hexrd/imageseries/baseclass.py index 989066be..779bf633 100644 --- a/hexrd/imageseries/baseclass.py +++ b/hexrd/imageseries/baseclass.py @@ -25,6 +25,9 @@ def __getitem__(self, key): def __len__(self): return len(self._adapter) + def __iter__(self): + return self._adapter.__iter__() + @property def dtype(self): return self._adapter.dtype From b1fff2411c122540c129028342eeeb51216af0b0 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 11 Oct 2015 10:19:42 -0400 Subject: [PATCH 027/253] corrections to framecache: specified dtype to csr_matrix and return frame as dense array --- hexrd/imageseries/load/framecache.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hexrd/imageseries/load/framecache.py b/hexrd/imageseries/load/framecache.py index 6ce18e21..409f396e 100644 --- a/hexrd/imageseries/load/framecache.py +++ b/hexrd/imageseries/load/framecache.py @@ -15,7 +15,7 @@ class FrameCacheImageSeriesAdapter(ImageSeriesAdapter): def __init__(self, fname, **kwargs): """Constructor for frame cache image series - *fname* - filename of the yml file + *fname* - filename of the yml file *kwargs* - keyword arguments (none required) """ self._fname = fname @@ -42,7 +42,8 @@ def _load_cache(self): row = arrs["%d_row" % i] col = arrs["%d_col" % i] data = arrs["%d_data" % i] - frame = csr_matrix((data, (row, col)), shape=self._shape) + frame = csr_matrix((data, (row, col)), + shape=self._shape, dtype=self._dtype) self._framelist.append(frame) @property @@ -62,7 +63,7 @@ def shape(self): return self._shape def __getitem__(self, key): - return self._framelist[key] + return self._framelist[key].toarray() def __iter__(self): return ImageSeriesIterator(self) From 9f716689f82606f2e4c6f796fca21e419e3d996f Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 11 Oct 2015 12:57:05 -0400 Subject: [PATCH 028/253] updated process module to use list of operations instead of dictionary --- hexrd/imageseries/process.py | 69 +++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index bb0c1953..b9d8406e 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -1,4 +1,4 @@ -"""Class for processing frames or frame groups""" +"""Class for processing individual frames""" import numpy as np from .baseclass import ImageSeries @@ -9,14 +9,23 @@ class ProcessedImageSeries(ImageSeries): DARK = 'dark' RECT = 'rectangle' - def __init__(self, imser, **kwargs): - """Instantiate imsageseries based on existing one with mapping options + _opdict = {} + + def __init__(self, imser, oplist): + """imsageseries based on existing one with image processing options *imser* - an existing imageseries - *kwargs* - dictionary for processing options + *oplist* - list of processing operations; + a list of pairs (key, data) pairs, with key specifying the + operation to perform using specified data + """ self._imser = imser - self._opts = kwargs + self._oplist = oplist + + self.addop(self.DARK, self._subtract_dark) + self.addop(self.FLIP, self._flip) + self.addop(self.RECT, self._rectangle) def __getitem__(self, key): return self._process_frame(key) @@ -25,35 +34,23 @@ def __len__(self): return len(self._imser) def _process_frame(self, key): - # apply flip at end - img = self._imser[key] - img = self._subtract_dark(img) - img = self._rectangle(img) - img = self._flip(img) + img = np.copy(self._imser[key]) + for op in self.oplist: + key, data = op + func = self._opdict[key] + img = func(img, data) + return img - def _subtract_dark(self, img): + def _subtract_dark(self, img, dark): # need to check for values below zero - if self.DARK not in self._opts: - return img - - dark = self._opts[self.DARK] return np.where(img > dark, img-dark, 0) - def _rectangle(self, img): + def _rectangle(self, img, r): # restrict to rectangle - if self.RECT in self._opts: - r = self._opts[self.RECT] - return img[r[0,0]:r[0,1], r[1,0]:r[1,1]] - else: - return img - - def _flip(self, img): - if self.FLIP in self._opts: - flip = self._opts['flip'] - else: - return img + return img[r[0,0]:r[0,1], r[1,0]:r[1,1]] + def _flip(self, img, flip): if flip in ('y','v'): # about y-axis (vertical) pimg = img[:, ::-1] elif flip in ('x', 'h'): # about x-axis (horizontal) @@ -80,7 +77,9 @@ def _toarray(self, nframes=0): a[i] = self.__getitem__(i) return a - + # + # ==================== API + # @property def dtype(self): return self._imser.dtype @@ -89,6 +88,20 @@ def dtype(self): def shape(self): return self._imser.shape + @classmethod + def addop(cls, key, func): + """Add operation to processing options + + *key* - string to use to specify this op + *func* - function to call for this op: f(data) + """ + cls._opdict[key] = func + + @property + def oplist(self): + """list of operations to apply""" + return self._oplist + def median(self, nframes=0): return np.median(self._toarray(nframes=nframes), axis=0) From 0877b21aeeab4490849e82d3ba05d7f81e03eac1 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 12 Oct 2015 11:38:13 -0400 Subject: [PATCH 029/253] added stats module, moving median() there and adding max() --- hexrd/imageseries/__init__.py | 1 + hexrd/imageseries/process.py | 3 --- hexrd/imageseries/stats.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 hexrd/imageseries/stats.py diff --git a/hexrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py index 5099da6a..f49eeffd 100644 --- a/hexrd/imageseries/__init__.py +++ b/hexrd/imageseries/__init__.py @@ -8,6 +8,7 @@ from . import load from . import process from . import save +from . import stats def open(filename, format=None, **kwargs): # find the appropriate adapter based on format specified diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index b9d8406e..de368d51 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -102,7 +102,4 @@ def oplist(self): """list of operations to apply""" return self._oplist - def median(self, nframes=0): - return np.median(self._toarray(nframes=nframes), axis=0) - pass # end class diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py new file mode 100644 index 00000000..1032d7b1 --- /dev/null +++ b/hexrd/imageseries/stats.py @@ -0,0 +1,32 @@ +"""Stats for imageseries""" +import numpy as np + +def max(ims, nframes=0): + nf = _nframes(ims, nframes) + imgmax = ims[0] + for i in range(1, nf): + imgmax = np.maximum(imgmax, ims[i]) + return imgmax + +def median(ims, nframes=0): + """return image with median values over all frames""" + # could be done by rectangle by rectangle if full series + # too big for memory + nf = _nframes(ims, nframes) + return np.median(_toarray(ims, nf), axis=0) + +# +# ==================== Utilities +# +def _nframes(ims, nframes): + """number of frames to use: len(ims) or specified number""" + mynf = len(ims) + return np.min((mynf, nframes)) if nframes > 0 else mynf + +def _toarray(ims, nframes): + ashp = (nframes,) + ims.shape + a = np.zeros(ashp, dtype=ims.dtype) + for i in range(nframes): + a[i] = ims[i] + + return a From 8b5b9f8e682b02e0341afd193c6eab55cd17d179 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 14 Oct 2015 18:37:18 -0400 Subject: [PATCH 030/253] changed hdf5 schema to use data group instead of data set; added general metadata mechanism --- hexrd/imageseries/load/hdf5.py | 64 ++++++++++++++++++---------------- hexrd/imageseries/save.py | 28 +++++++++------ 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/hexrd/imageseries/load/hdf5.py b/hexrd/imageseries/load/hdf5.py index 4e684946..9e706485 100644 --- a/hexrd/imageseries/load/hdf5.py +++ b/hexrd/imageseries/load/hdf5.py @@ -9,17 +9,41 @@ class HDF5ImageSeriesAdapter(ImageSeriesAdapter): """collection of images in HDF5 format""" format = 'hdf5' - #The code below failed with: "Error when calling the metaclass bases" - # "'property' object is not callable" - #@property - #def format(self): - # return 'hdf5' + + def __init__(self, fname, **kwargs): + """Constructor for H5FrameSeries + + *fname* - filename of the HDF5 file + *kwargs* - keyword arguments, choices are: + path - (required) path of dataset in HDF5 file + """ + self.__h5name = fname + self.__path = kwargs['path'] + self.__images = '/'.join([self.__path, 'images']) + + def __getitem__(self, key): + with self._dset as dset: + return dset.__getitem__(key) + + def __iter__(self): + return ImageSeriesIterator(self) + + #@memoize + def __len__(self): + with self._dset as dset: + return len(dset) + + @property + def _dgroup(self): + # return a context manager to ensure proper file handling + # always use like: "with self._dgroup as dgroup:" + return H5ContextManager(self.__h5name, self.__path) @property def _dset(self): # return a context manager to ensure proper file handling # always use like: "with self._dset as dset:" - return H5ContextManager(self.__h5name, self.__path) + return H5ContextManager(self.__h5name, self.__images) @property #@memoize @@ -29,9 +53,9 @@ def metadata(self): Currently returns any dimension scales in a dictionary """ mdict = {} - with self._dset as dset: - for k in dset.dims[0].keys(): - mdict[k] = dset.dims[0][k][...] + with self._dgroup as dgroup: + for k, v in dgroup.attrs.items(): + mdict[k] = v return mdict @@ -46,28 +70,6 @@ def shape(self): with self._dset as dset: return dset.shape[1:] - def __init__(self, fname, **kwargs): - """Constructor for H5FrameSeries - - *fname* - filename of the HDF5 file - *kwargs* - keyword arguments, choices are: - path - (required) path of dataset in HDF5 file - """ - self.__h5name = fname - self.__path = kwargs['path'] - - def __getitem__(self, key): - with self._dset as dset: - return dset.__getitem__(key) - - def __iter__(self): - return ImageSeriesIterator(self) - - #@memoize - def __len__(self): - with self._dset as dset: - return len(dset) - pass # end class diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index d1f3ffc2..a27fc004 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -56,20 +56,28 @@ def __init__(self, ims, fname, **kwargs): self._opts = kwargs pass # end class - + class WriteH5(Writer): fmt = 'hdf5' def __init__(self, ims, fname, **kwargs): + """Write imageseries in HDF5 file + + Required Args: + path - the path in HDF5 file + +""" Writer.__init__(self, ims, fname, **kwargs) self._path = self._opts['path'] - + self._meta = kwargs['meta'] if 'meta' in kwargs else dict() + def _open_dset(self): """open HDF5 file and dataset""" f = h5py.File(self._fname, "a") + g = f.create_group(self._path) s0, s1 = self._shape - - return f.create_dataset(self._path, (self._nframes, s0, s1), self._dtype, + + return g.create_dataset('images', (self._nframes, s0, s1), self._dtype, compression="gzip") # # ======================================== API @@ -80,8 +88,10 @@ def write(self): for i in range(self._nframes): ds[i, :, :] = self._ims[i] - # next: add metadata - + # add metadata + for k, v in self._meta.items(): + ds[k] = v + pass # end class class WriteFrameCache(Writer): @@ -105,7 +115,7 @@ def _write_yml(self): 'nframes': len(self._ims), 'shape': list(self._ims.shape)} info = {'data': datad, 'meta': self._meta} with open(self._fname, "w") as f: - yaml.dump(info, f) + yaml.dump(info, f) def _write_frames(self): """also save shape array as originally done (before yaml)""" @@ -120,7 +130,7 @@ def _write_frames(self): arrd['%d_col' % i] = col if sh is None: arrd['shape'] = np.array(frame.shape) - + np.savez_compressed(self._cache, **arrd) def write(self): @@ -130,5 +140,3 @@ def write(self): """ self._write_frames() self._write_yml() - - From 87022a31b0112eb721b3c97125ed6daedf587fdb Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 14 Oct 2015 22:12:48 -0400 Subject: [PATCH 031/253] added proper metadata read/write; may need some further work for frame-caches with np.array() metadata --- hexrd/imageseries/baseclass.py | 4 ++++ hexrd/imageseries/load/array.py | 6 +++--- hexrd/imageseries/save.py | 20 ++++++++------------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hexrd/imageseries/baseclass.py b/hexrd/imageseries/baseclass.py index 779bf633..8a457511 100644 --- a/hexrd/imageseries/baseclass.py +++ b/hexrd/imageseries/baseclass.py @@ -36,4 +36,8 @@ def dtype(self): def shape(self): return self._adapter.shape + @property + def metadata(self): + return self._adapter.metadata + pass # end class diff --git a/hexrd/imageseries/load/array.py b/hexrd/imageseries/load/array.py index 99f3eac9..4ce73a47 100644 --- a/hexrd/imageseries/load/array.py +++ b/hexrd/imageseries/load/array.py @@ -11,13 +11,13 @@ class ArrayImageSeriesAdapter(ImageSeriesAdapter): def __init__(self, fname, **kwargs): """Constructor for frame cache image series - *fname* - should be None + *fname* - should be None *kwargs* - keyword arguments . 'data' = a 3D array (double/float) . 'metadata' = a dictionary """ self._data = kwargs['data'] - self._meta = kwargs.pop('metadata', dict()) + self._meta = kwargs.pop('meta', dict()) self._shape = self._data.shape self._nframes = self._shape[0] self._nxny = self._shape[1:3] @@ -37,7 +37,7 @@ def shape(self): @property def dtype(self): return self._data.dtype - + def __getitem__(self, key): return self._data[key] diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index a27fc004..df7e5636 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -52,6 +52,7 @@ def __init__(self, ims, fname, **kwargs): self._shape = ims.shape self._dtype = ims.dtype self._nframes = len(ims) + self._meta = ims.metadata self._fname = fname self._opts = kwargs @@ -69,28 +70,24 @@ def __init__(self, ims, fname, **kwargs): """ Writer.__init__(self, ims, fname, **kwargs) self._path = self._opts['path'] - self._meta = kwargs['meta'] if 'meta' in kwargs else dict() - def _open_dset(self): - """open HDF5 file and dataset""" - f = h5py.File(self._fname, "a") - g = f.create_group(self._path) - s0, s1 = self._shape - - return g.create_dataset('images', (self._nframes, s0, s1), self._dtype, - compression="gzip") # # ======================================== API # def write(self): """Write imageseries to HDF5 file""" - ds = self._open_dset() + f = h5py.File(self._fname, "a") + g = f.create_group(self._path) + s0, s1 = self._shape + + ds = g.create_dataset('images', (self._nframes, s0, s1), self._dtype, + compression="gzip") for i in range(self._nframes): ds[i, :, :] = self._ims[i] # add metadata for k, v in self._meta.items(): - ds[k] = v + g.attrs[k] = v pass # end class @@ -108,7 +105,6 @@ def __init__(self, ims, fname, **kwargs): Writer.__init__(self, ims, fname, **kwargs) self._thresh = self._opts['threshold'] self._cache = kwargs['cache_file'] - self._meta = kwargs['meta'] if 'meta' in kwargs else dict() def _write_yml(self): datad = {'file': self._cache, 'dtype': str(self._ims.dtype), From 6d27e03d61f01dd4421c9f32d1f0f71659b9c373 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 15 Oct 2015 15:51:18 -0400 Subject: [PATCH 032/253] added numpy array metadata for frame-cache --- hexrd/imageseries/load/framecache.py | 10 +++++++++- hexrd/imageseries/save.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/load/framecache.py b/hexrd/imageseries/load/framecache.py index 409f396e..74f7132e 100644 --- a/hexrd/imageseries/load/framecache.py +++ b/hexrd/imageseries/load/framecache.py @@ -52,7 +52,15 @@ def metadata(self): Currently returns none """ - return self._meta + metad = {} + for k, v in self._meta.items(): + if v == '++np.array': + newk = k + '-array' + metad[k] = np.array(self._meta.pop(newk)) + metad.pop(newk, None) + else: + metad[k] = v + return metad @property def dtype(self): diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index df7e5636..bcd49397 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -106,10 +106,21 @@ def __init__(self, ims, fname, **kwargs): self._thresh = self._opts['threshold'] self._cache = kwargs['cache_file'] + def _process_meta(self): + d = {} + for k, v in self._meta.items(): + if isinstance(v, np.ndarray): + d[k] = '++np.array' + d[k + '-array'] = v.tolist() + else: + d[k] = v + + return d + def _write_yml(self): datad = {'file': self._cache, 'dtype': str(self._ims.dtype), 'nframes': len(self._ims), 'shape': list(self._ims.shape)} - info = {'data': datad, 'meta': self._meta} + info = {'data': datad, 'meta': self._process_meta()} with open(self._fname, "w") as f: yaml.dump(info, f) From 49d5013f04c60ae74a2d98ac229af4912f70ce7a Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 18 Oct 2015 15:40:58 -0400 Subject: [PATCH 033/253] comment in baseclass --- hexrd/imageseries/baseclass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/imageseries/baseclass.py b/hexrd/imageseries/baseclass.py index 8a457511..a99e0e49 100644 --- a/hexrd/imageseries/baseclass.py +++ b/hexrd/imageseries/baseclass.py @@ -13,7 +13,7 @@ def __init__(self, adapter): """Build FrameSeries from adapter instance *adapter* - object instance based on abstract Sequence class with - properties for image shape and, optionally, metadata. + properties for image shape, data type and metadata. """ self._adapter = adapter From 9b5c62eb3499d4aec9287eb8d0f365e28fb0f47a Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 18 Oct 2015 15:42:25 -0400 Subject: [PATCH 034/253] updated imageseries script, simplifying --- scripts/make_imageseriesh5.py | 338 ++++++++++++---------------------- 1 file changed, 119 insertions(+), 219 deletions(-) diff --git a/scripts/make_imageseriesh5.py b/scripts/make_imageseriesh5.py index c61e6534..18e11278 100755 --- a/scripts/make_imageseriesh5.py +++ b/scripts/make_imageseriesh5.py @@ -34,200 +34,120 @@ def __init__(self, message): def __str__(self): return self.message - pass # end class - - -def write_file(a, **kwargs): - # - # Get shape and dtype information from files - # - shp, dtp = image_info(a) - # - # Open file and dataset - # - f, ds = open_dset(a, shp, dtp) - # - # Image options - # - popts = process_img_opts(a, **kwargs) - # - # Now add the images - # . empty frames only apply to multiframe images - # - nframes = ds.shape[0] - nfiles = len(a.imagefiles) - for i in range(nfiles): - if a.max_frames and nframes >= a.max_frames: - break - logging.debug('processing file %d of %d' % (i, nfiles)) - popts['filenumber'] = i - img_i = fabio.open(a.imagefiles[i]) - nfi = img_i.nframes - for j in range(nfi): - if a.max_frames and nframes >= a.max_frames: - break - logging.debug('... processing image %d of %d' % (j, img_i.nframes)) - if nfi > 1 and j < a.empty: - logging.debug('...empty frame ... skipping') - continue - nframes += 1 - ds.resize(nframes, 0) - ds[nframes - 1, :, :] = process_img(a, img_i.data, popts) - if (j + 1) < nfi: - img_i = img_i.next() - pass - - add_metadata(ds, a, **kwargs) - - f.close() - return - -def open_dset(a, shp, dtp): - """open HDF5 file and dataset""" - # - # If append option is true, file and target group must exist; - # otherwise, file may exist but may not already contain the - # target dataset. - # - if a.append: +class ImageFiles(object): + """List of image files in sequence""" + def __init__(self, a): + """a is a namespace from parser""" + self.files = a.imagefiles + self.nfiles = len(self.files) + self.nempty = a.empty + self.maxframes = a.max_frames + + self._info() + + + self.ntowrite = numpy.min((self.maxframes, self.nframes))\ + if self.maxframes > 0 else self.nframes + + self.outfile = a.outfile + self.dgrppath = a.dset + self.dsetpath = '/'.join((a.dset, 'images')) + + @staticmethod + def _checkvalue(v, vtest, msg): + """helper: ensure value set conistently""" + if v is None: + val = vtest + else: + if vtest != v: + raise MakeImageSeriesError(msg) + else: + val = v + + return val + + def _info(self): + """basic info: dtype, shape, nframes, and verify consistency""" + cn = None + shp = None + dtp = None + + nf = 0 + for imgf in self.files: + img = fabio.open(imgf) + dat = img.data + shp = self._checkvalue(shp, dat.shape, "inconistent image shapes") + dtp = self._checkvalue(dtp, dat.dtype, "inconistent image dtypes") + cn = self._checkvalue(cn, img.classname, "inconistent image types") + if img.nframes >= self.nempty: + nf += img.nframes - self.nempty + else: + raise MakeImageSeriesError("more empty frames than images") + + self.nframes = nf + self.shape = shp + self.dtype = dtp + self.imagetype = cn + + def describe(self): + print('Number of Files: %d' % self.nfiles) + print('... image type: %s' % self.imagetype) + print('... image dimensions: %d X %d' % self.shape) + print('... image data type: %s' % self.dtype) + print('... empty frames per file: %d' % self.nempty) + print('... number of nonempty frames: %d' % self.nframes) + maxf = self.maxframes if self.maxframes > 0 else 'unlimited' + print('... max frames requested: %s' % maxf) + print('... will write: %d' % self.ntowrite) + + def opendset(self): + """open the HDF5 data set""" + # note: compression implies chunked storage + msg = 'writing to file/path: %s:%s' % (self.outfile, self.dgrppath) + logging.info(msg) + f = h5py.File(self.outfile, "a") try: - f = h5py.File(a.outfile, "r+") - except: - errmsg = '%s: %s' % (ERR_NO_FILE, a.outfile) - raise MakeImageSeriesError(errmsg) - - ds = f.get(a.dset) - if ds is None: - errmsg = '%s: %s' % (ERR_NO_DATA, DSetPath(a.outfile, a.dset)) - raise MakeImageSeriesError(errmsg) - else: - f = h5py.File(a.outfile, "a") - chsize = (1, int(numpy.floor(1e6/shp[1])), shp[1]) if shp[1] < 1.e6 else True - try: - ds = f.create_dataset(a.dset, (0, shp[0], shp[1]), dtp, - maxshape=(None, shp[0], shp[1]), chunks=chsize, - compression="gzip") + shp = (self.ntowrite,) + self.shape + ds = f.create_dataset(self.dsetpath, shp, self.dtype, + compression="gzip") except Exception as e: - errmsg = '%s: %s\n... exception: ' % (ERR_OVERWRITE, DSetPath(a.outfile, a.dset)) + errmsg = '%s: %s\n... exception: ' % \ + (ERR_OVERWRITE, DSetPath(self.outfile, self.dsetpath)) raise MakeImageSeriesError(errmsg + str(e)) - return f, ds - -def process_img_opts(a, **kwargs): - """make dictionary to pass to process_img""" - pdict = {} - # dark file (possibly need to transpose [not done yet]) - if a.dark_file: - dark = fabio.open(a.dark_file) - pdict['dark'] = dark.data - - # dark from empty - if a.dark_from_empty: - if a.empty == 0: - raise MakeImageSeriesError(ERR_NOEMPTY) - darks = [] - for i in range(len(a.imagefiles)): - img_i = fabio.open(a.imagefiles[i]) - drk_i = img_i.data - for j in range(1, a.empty): - img_i = img_i.next() - drk_i += img_i.data - darks += [drk_i*(1/a.empty)] - pdict['darks'] = darks - - return pdict - -def process_img(a, img, pdict): - """process image data according to options - - * need to check on image shape in case not square -""" - # flip: added some other option specifiers - if a.flip in ('y','v'): # about y-axis (vertical) - pimg = img[:, ::-1] - elif a.flip in ('x', 'h'): # about x-axis (horizontal) - pimg = img[::-1, :] - elif a.flip in ('vh', 'hv', 'r180'): # 180 degree rotation - pimg = img[::-1, ::-1] - elif a.flip in ('t', 'T'): # transpose (possible shape change) - pimg = img.T - elif a.flip in ('ccw90', 'r90'): # rotate 90 (possible shape change) - pimg = img.T[:, ::-1] - elif a.flip in ('cw90', 'r270'): # rotate 270 (possible shape change) - pimg = img.T[::-1, :] - else: - pimg = img - - # dark image(s) - if 'dark' in pdict: - pimg = pimg - pdict['dark'] - if 'darks' in pdict: - fnum = pdict['filenumber'] - pimg = pimg - pdict['darks'][fnum] - - return pimg - -def add_metadata(ds, a, **kwargs): - """Add metadata. Right now, that just includes omega information.""" - omkey = 'omega' - ominatt = 'omega_min' - omaxatt = 'omega_max' - hasval = lambda a, att: hasattr(a, att) and getattr(a, att) is not None - hasone = lambda a, att1, att2: hasval(a, att1) and not hasval(a, att2) - - if (hasone(a, ominatt, omaxatt) or hasone(a, omaxatt, ominatt)): - raise MakeImageSeriesError(ERR_OMEGASPEC) - - if not (hasval(a, ominatt) and hasval(a, omaxatt)): - return - - omin = getattr(a, ominatt) - omax = getattr(a, omaxatt) - - if a.append: - om = ds.dims[0][omkey] - n0 = om.shape[1] - n1 = ds.shape[0] - else: - om = ds.parent.file.create_dataset(a.dset + '_omega', (0, 2), - numpy.dtype(float), - maxshape=(None, 2)) - n0 = 0 - n1 = ds.shape[0] - ds.dims.create_scale(om, omkey) - ds.dims[0].attach_scale(om) - - dn = n1 - n0 - ominmax = numpy.linspace(omin, omax, num=(dn + 1)) - - om.resize(n1, 0) - om[n0:n1, :] = numpy.array([ominmax[0:dn], ominmax[1:(dn+1)]]).T - - return - -def image_info(a): - """Return shape and dtype of first image - - * See process_img for options that transpose shape -""" - img_0 = fabio.open(a.imagefiles[0]) - imgshape = img_0.data.shape - if a.flip in ('t', 'T') + ('ccw90', 'r90') + ('cw90', 'r270'): - imgshape = imgshape[::-1] - - return imgshape, img_0.data.dtype - -def describe_imgs(a): - print 'image files are: ', a.imagefiles - im0 = fabio.open(a.imagefiles[0]) - print 'Total number of files: %d' % len(a.imagefiles) - print 'First file: %s' % a.imagefiles[0] - print '... fabio class: %s' % im0.__class__ - print '... number of frames: %d' % im0.nframes - print '... image dimensions: %d X %d' % (im0.dim1, im0.dim2) - print '... image data type: %s' % im0.data.dtype - - pass + return f, ds + + def write(self): + """write to HDF5 file""" + f, ds = self.opendset() + # + # Now add the images + # + nframes = 0 # number completed + for i in range(self.nfiles): + if nframes >= self.ntowrite: break + + logging.debug('processing file %d of %d' % (i+1, self.nfiles)) + img_i = fabio.open(self.files[i]) + nfi = img_i.nframes + for j in range(nfi): + msg = '... file %d/image %d' % (i, j) + logging.debug(msg) + if j < self.nempty: + logging.debug('... empty frame ... skipping') + else: + ds[nframes, :, :] = img_i.data + nframes += 1 + logging.debug('... wrote image %s of %s' %\ + (nframes, self.ntowrite)) + if nframes >= self.ntowrite: + logging.debug('wrote last frame: stopping') + break + if j < nfi - 1: + # on last frame in file, fabio will look for next file + img_i = img_i.next() + + f.close() def set_options(): """Set options for command line""" @@ -239,36 +159,15 @@ def set_options(): # file options parser.add_argument("-o", "--outfile", help="name of HDF5 output file", default="imageseries.h5") - parser.add_argument("-a", "--append", - help="append to the dataset instead of making a new one", - action="store_true") - help_d = "path to HDF5 data set" parser.add_argument("-d", "--dset", help=help_d, default="/imageseries") + # image options parser.add_argument("imagefiles", nargs="+", help="image files") - # image processing options - parser.add_argument("--flip", - help="reorient the image according to specification", - metavar="FLIPARG", action="store", default=None) - parser.add_argument("--empty", "--blank", help="number of blank frames in beginning of file", metavar="N", type=int, action="store", default=0) - - parser.add_argument("--dark-file", help="name of file containing dark image") - - parser.add_argument("--dark-from-empty", - help="use empty frames to build dark image", - action="store_true") - # metadata - parser.add_argument("--omega-min", - help="minimum omega for this series of images", - type=float, action="store") - parser.add_argument("--omega-max", - help="minimum omega for this series of images", - type=float, action="store") parser.add_argument("--max-frames", help="maximum number of frames in file (for testing)", metavar="N", type=int, action="store", default=0) @@ -278,23 +177,24 @@ def set_options(): def execute(args, **kwargs): """Main execution - * kwargs added to allow passing further options when not called from command line + * kwargs added to allow passing further options when not called from + command line """ p = set_options() a = p.parse_args(args) logging.info(str(a)) - if a.info: - describe_imgs(a) - return + ifiles = ImageFiles(a) - write_file(a, **kwargs) + if a.info: + ifiles.describe() + else: + ifiles.write() - return + # write_file(a, **kwargs) if __name__ == '__main__': # # run # execute(sys.argv[1:]) - From 4c458e5322846e76074faa169ecb640121e65dfd Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 24 Oct 2015 19:58:25 -0400 Subject: [PATCH 035/253] fixed metadata property in process --- hexrd/imageseries/process.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index de368d51..d0f64e83 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -88,6 +88,10 @@ def dtype(self): def shape(self): return self._imser.shape + @property + def metadata(self): + return self._imser.metadata + @classmethod def addop(cls, key, func): """Add operation to processing options From 79cf8465da48206d1471ee436bb5904f192eeb5d Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 25 Oct 2015 21:27:33 -0400 Subject: [PATCH 036/253] updated config & unittests for new imageseries package --- hexrd/config/imageseries.py | 122 +------------- hexrd/config/tests/test_image_series.py | 210 +++--------------------- hexrd/xrd/image_io.py | 3 +- 3 files changed, 30 insertions(+), 305 deletions(-) diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index c4f8c293..a2ea643f 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -5,128 +5,20 @@ -class FileConfig(Config): +class ImageSeriesConfig(Config): @property - def stem(self): - temp = self._cfg.get('image_series:file:stem') + def filename(self): + temp = self._cfg.get('image_series:filename') if not os.path.isabs(temp): temp = os.path.join(self._cfg.working_dir, temp) return temp - - @property - def ids(self): - temp = self._cfg.get('image_series:file:ids') - return temp if isinstance(temp, list) else [temp] - - - -class ImagesConfig(Config): - - - @property - def start(self): - return self._cfg.get('image_series:images:start', default=0) - - - @property - def step(self): - return self._cfg.get('image_series:images:step', default=1) - - - @property - def stop(self): - return self._cfg.get('image_series:images:stop', default=None) - - - -class OmegaConfig(Config): - - - @property - def start(self): - return self._cfg.get('image_series:omega:start') - - - @property - def step(self): - return self._cfg.get('image_series:omega:step') - - - @property - def stop(self): - return self._cfg.get('image_series:omega:stop') - - - -class ImageSeriesConfig(Config): - - - @property - def dark(self): - temp = self._cfg.get( - 'image_series:dark', default=None - ) - if temp is None or os.path.exists(temp): - return temp - raise IOError( - '"image_series:dark": "%s" does not exist' % temp - ) - - - @property - def file(self): - return FileConfig(self._cfg) - - - @property - def files(self): - stem = self._cfg.image_series.file.stem - res = [] - missing = [] - for id in self._cfg.image_series.file.ids: - try: - id = stem % id - except TypeError: - # string interpolation failed, join stem and id: - id = stem + id - temp = glob.glob(id) - if temp: - res.extend(temp) - else: - missing.append(id) - if missing: - raise IOError( - 'Image files not found: %s' % (', '.join(missing)) - ) - return res - - - @property - def flip(self): - temp = self._cfg.get('image_series:flip', default=None) - if temp is None: - return - temp = temp.lower() - if temp not in ['h', 'v', 'hv', 'vh', 'cw', 'ccw']: - raise RuntimeError( - 'image_series:flip setting "%s" is not valid' % temp - ) - return temp - - - @property - def images(self): - return ImagesConfig(self._cfg) - - @property - def omega(self): - return OmegaConfig(self._cfg) - + def format(self): + return self._cfg.get('image_series:format') @property - def n_frames(self): - return (self.omega.stop - self.omega.start)/self.omega.step + def args(self): + return self._cfg.get('image_series:args') diff --git a/hexrd/config/tests/test_image_series.py b/hexrd/config/tests/test_image_series.py index 06885175..f4069ade 100644 --- a/hexrd/config/tests/test_image_series.py +++ b/hexrd/config/tests/test_image_series.py @@ -6,53 +6,19 @@ reference_data = \ """ +analysis_name: analysis working_dir: %(tempdir)s -image_series: -# dark: # not specified to test default is None ---- -image_series: - dark: %(existing_file)s - file: - flip: V - images: ---- -image_series: - dark: %(nonexistent_file)s - file: - stem: %(file_stem)s - ids: [1] - flip: triple_lindy - images: - start: 1 - step: 2 - stop: -1 - omega: - start: 0 - step: 0.25 - stop: 360 ---- -image_series: - file: - ids: 2 ---- -image_series: - file: - ids: [1,2] ---- -image_series: - file: - stem: %(tempdir)s%(pathsep)s%%s.dat - ids: ["*001*"] --- image_series: - file: - stem: %(tempdir)s - ids: %(nonexistent_file)s + filename: %(nonexistent_file)s + format: hdf5 + args: + path: %(nonexistent_path)s --- image_series: - file: - stem: %(tempdir)s%(pathsep)s - ids: ['foo.dat', 'bar.dat'] + filename: %(nonexistent_file)s + format: frame-cache + args: """ % test_data @@ -64,169 +30,37 @@ def get_reference_data(cls): return reference_data - def test_dark(self): - self.assertEqual(self.cfgs[0].image_series.dark, None) - self.assertEqual( - self.cfgs[1].image_series.dark, - test_data['existing_file'] - ) - self.assertRaises( - IOError, - getattr, self.cfgs[2].image_series, 'dark' - ) + def test_filename(self): - def test_files(self): - files = [] - for i in ['00011.dat', '00012.dat', '00021.dat']: - with tempfile.NamedTemporaryFile(delete=False, suffix=i) as f: - files.append(f.name) - f.file.write('foo') - try: - self.assertEqual( - sorted(self.cfgs[5].image_series.files), - sorted(files[:2]) - ) - finally: - for f in files: - os.remove(f) - self.assertRaises( - IOError, - getattr, self.cfgs[6].image_series, 'files' - ) - files = [] - for i in ['foo.dat', 'bar.dat']: - with open(os.path.join(tempfile.gettempdir(), i), 'w') as f: - files.append(f.name) - f.write('foo') - try: - self.assertEqual( - files, - self.cfgs[7].image_series.files - ) - finally: - for f in files: - os.remove(f) - - def test_flip(self): - self.assertEqual(self.cfgs[0].image_series.flip, None) - self.assertEqual(self.cfgs[1].image_series.flip, 'v') - self.assertRaises( - RuntimeError, - getattr, self.cfgs[2].image_series, 'flip' - ) - def test_n_frames(self): self.assertRaises( RuntimeError, - getattr, self.cfgs[0].image_series, 'n_frames' + getattr, self.cfgs[0].image_series, 'filename' ) - self.assertEqual(self.cfgs[2].image_series.n_frames, 1440) - - -class TestFileConfig(TestConfig): - - - @classmethod - def get_reference_data(cls): - return reference_data - - - def test_stem(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].image_series.file, 'stem' - ) self.assertRaises( RuntimeError, - getattr, self.cfgs[1].image_series.file, 'stem' + getattr, self.cfgs[0].image_series, 'format' ) - self.assertEqual( - self.cfgs[2].image_series.file.stem, - os.path.join(test_data['tempdir'], test_data['file_stem']) - ) - - def test_ids(self): self.assertRaises( RuntimeError, - getattr, self.cfgs[0].image_series.file, 'ids' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[1].image_series.file, 'ids' - ) - self.assertEqual( - self.cfgs[2].image_series.file.ids, - [1] - ) - self.assertEqual( - self.cfgs[3].image_series.file.ids, - [2] + getattr, self.cfgs[0].image_series, 'args' ) + self.assertEqual( - self.cfgs[4].image_series.file.ids, - [1, 2] + self.cfgs[1].image_series.filename, + os.path.join(test_data['tempdir'], test_data['nonexistent_file']) ) - - -class TestImagesConfig(TestConfig): - - - @classmethod - def get_reference_data(cls): - return reference_data - - - def test_start(self): - self.assertEqual(self.cfgs[0].image_series.images.start, 0) - self.assertEqual(self.cfgs[1].image_series.images.start, 0) - self.assertEqual(self.cfgs[2].image_series.images.start, 1) - - - def test_step(self): - self.assertEqual(self.cfgs[0].image_series.images.step, 1) - self.assertEqual(self.cfgs[1].image_series.images.step, 1) - self.assertEqual(self.cfgs[2].image_series.images.step, 2) - - - def test_stop(self): - self.assertEqual(self.cfgs[0].image_series.images.stop, None) - self.assertEqual(self.cfgs[1].image_series.images.stop, None) - self.assertEqual(self.cfgs[2].image_series.images.stop, -1) - - - -class TestOmegaConfig(TestConfig): - - - @classmethod - def get_reference_data(cls): - return reference_data - - - def test_start(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].image_series.omega, 'start' + self.assertEqual( + self.cfgs[1].image_series.format, 'hdf5' ) - self.assertEqual(self.cfgs[2].image_series.omega.start, 0) - - - def test_step(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].image_series.omega, 'step' + a = self.cfgs[1].image_series.args + self.assertEqual( + a['path'], test_data['nonexistent_path'] ) - self.assertEqual(self.cfgs[2].image_series.omega.step, 0.25) - - - def test_stop(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].image_series.omega, 'stop' - ) - self.assertEqual(self.cfgs[2].image_series.omega.stop, 360) + a = self.cfgs[2].image_series.args + self.assertEqual(a, None) diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index b11e3bac..db8eae3b 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -20,7 +20,7 @@ import numpy as num -import imageseries +from hexrd import imageseries warnings.filterwarnings('always', '', DeprecationWarning) @@ -383,4 +383,3 @@ def newGenericReader(ncols, nrows, *args, **kwargs): retval = ReadGeneric(filename, ncols, nrows, *args, **kwargs) return retval - From f19f77dd3014398fdea35f51c050d9912e403e27 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 27 Oct 2015 10:14:11 -0400 Subject: [PATCH 037/253] made HDF5 metadata modifyable; added frame-list keyword arg to process module --- hexrd/imageseries/load/hdf5.py | 18 +++++++++++------- hexrd/imageseries/process.py | 33 +++++++++++++++------------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/hexrd/imageseries/load/hdf5.py b/hexrd/imageseries/load/hdf5.py index 9e706485..935a470c 100644 --- a/hexrd/imageseries/load/hdf5.py +++ b/hexrd/imageseries/load/hdf5.py @@ -20,6 +20,7 @@ def __init__(self, fname, **kwargs): self.__h5name = fname self.__path = kwargs['path'] self.__images = '/'.join([self.__path, 'images']) + self._meta = self._getmeta() def __getitem__(self, key): with self._dset as dset: @@ -45,13 +46,7 @@ def _dset(self): # always use like: "with self._dset as dset:" return H5ContextManager(self.__h5name, self.__images) - @property - #@memoize - def metadata(self): - """(read-only) Image sequence metadata - - Currently returns any dimension scales in a dictionary - """ + def _getmeta(self): mdict = {} with self._dgroup as dgroup: for k, v in dgroup.attrs.items(): @@ -59,6 +54,15 @@ def metadata(self): return mdict + @property + #@memoize + def metadata(self): + """(read-only) Image sequence metadata + + note: metadata loaded on open and allowed to be modified + """ + return self._meta + @property def dtype(self): with self._dset as dset: diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index d0f64e83..a76f49ec 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -11,7 +11,7 @@ class ProcessedImageSeries(ImageSeries): _opdict = {} - def __init__(self, imser, oplist): + def __init__(self, imser, oplist, **kwargs): """imsageseries based on existing one with image processing options *imser* - an existing imageseries @@ -19,26 +19,33 @@ def __init__(self, imser, oplist): a list of pairs (key, data) pairs, with key specifying the operation to perform using specified data + *keyword args* + 'frame-list' - specify subset of frames by list + """ self._imser = imser self._oplist = oplist + self._frames = kwargs.pop('frame-list', None) + self._hasframelist = (self._frames is not None) self.addop(self.DARK, self._subtract_dark) self.addop(self.FLIP, self._flip) self.addop(self.RECT, self._rectangle) def __getitem__(self, key): - return self._process_frame(key) + return self._process_frame(self._get_index(key)) + + def _get_index(self, key): + return self._frames[key] if self._hasframelist else key def __len__(self): - return len(self._imser) + return len(self._frames) if self._hasframelist else len(self._imser) def _process_frame(self, key): - img = np.copy(self._imser[key]) - for op in self.oplist: - key, data = op - func = self._opdict[key] - img = func(img, data) + img = np.copy(self._imser[self._get_index(key)]) + for k, d in self.oplist: + func = self._opdict[k] + img = func(img, d) return img @@ -67,16 +74,6 @@ def _flip(self, img, flip): pimg = img return pimg - - def _toarray(self, nframes=0): - mynf = len(self) - nf = np.min((mynf, nframes)) if nframes > 0 else mynf - ashp = (nf,) + self.shape - a = np.zeros(ashp, dtype=self.dtype) - for i in range(nf): - a[i] = self.__getitem__(i) - - return a # # ==================== API # From 5f7ab2ff3ee342bc6559ca978fd0286f936bac3b Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 27 Oct 2015 10:16:55 -0400 Subject: [PATCH 038/253] added unit tests for imageseries --- hexrd/imageseries/tests/test_imageseries.py | 266 ++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 hexrd/imageseries/tests/test_imageseries.py diff --git a/hexrd/imageseries/tests/test_imageseries.py b/hexrd/imageseries/tests/test_imageseries.py new file mode 100644 index 00000000..ff099ca5 --- /dev/null +++ b/hexrd/imageseries/tests/test_imageseries.py @@ -0,0 +1,266 @@ +#! /usr/bin/env python +# +import sys +import os +import argparse +import unittest +import tempfile + +import numpy as np + +from hexrd import imageseries +from hexrd.imageseries import save, process, stats, ImageSeries + +# ========== Test Data + +_NFXY = (3, 7, 5) + +class TestImageSeriesProperties(unittest.TestCase): + def setUp(self): + self._a = make_array() + self._is_a = make_array_ims() + + def test_prop_nframes(self): + self.assertEqual(self._a.shape[0], len(self._is_a)) + + def test_prop_shape(self): + self.assertEqual(self._a.shape[1:], self._is_a.shape) + + def test_prop_dtype(self): + self.assertEqual(self._a.dtype, self._is_a.dtype) + +class TestImageSeriesFmts(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.tmpdir = tempfile.mkdtemp() + + @classmethod + def tearDownClass(cls): + os.rmdir(cls.tmpdir) + + # ==================== Tests + + def test_fmth5(self): + """save/load HDF5 format""" + h5file = os.path.join(self.tmpdir, 'test_ims.h5') + h5path = 'array-data' + fmt = 'hdf5' + + is_a = make_array_ims() + save.write(is_a, h5file, fmt, path=h5path) + is_h = imageseries.open(h5file, fmt, path=h5path) + diff = compare(is_a, is_h) + self.assertAlmostEqual(diff, 0., "h5 reconstruction failed") + self.assertTrue(compare_meta(is_a, is_h)) + del is_h + os.remove(h5file) + + def test_fmth5_nparray(self): + """HDF5 format with numpy array metadata""" + h5file = os.path.join(self.tmpdir, 'test_ims.h5') + h5path = 'imagedata' + fmt = 'hdf5' + key = 'np-array' + npa = np.array([0,2.0,1.3]) + + is_a = make_array_ims() + is_a.metadata[key] = npa + save.write(is_a, h5file, fmt, path=h5path) + is_h = imageseries.open(h5file, fmt, path=h5path) + meta = is_h.metadata + diff = np.linalg.norm(meta[key] - npa) + self.assertAlmostEqual(diff, 0., "h5 numpy array metadata failed") + + del is_h + os.remove(h5file) + + def test_fmtfc(self): + """save/load frame-cache format""" + fcfile = os.path.join(self.tmpdir, 'frame-cache.yml') + fmt = 'frame-cache' + thresh = 0.5 + + is_a = make_array_ims() + save.write(is_a, fcfile, fmt, + threshold=thresh, cache_file='frame-cache.npz') + is_fc = imageseries.open(fcfile, fmt) + diff = compare(is_a, is_fc) + self.assertAlmostEqual(diff, 0., "frame-cache reconstruction failed") + self.assertTrue(compare_meta(is_a, is_fc)) + + del is_fc + os.remove(fcfile) + + def test_fmtfc_nparray(self): + """frame-cache format with numpy array metadata""" + fcfile = os.path.join(self.tmpdir, 'frame-cache.yml') + fmt = 'frame-cache' + thresh = 0.5 + key = 'np-array' + npa = np.array([0,2.0,1.3]) + + is_a = make_array_ims() + is_a.metadata[key] = npa + save.write(is_a, fcfile, fmt, + threshold=thresh, cache_file='frame-cache.npz') + is_fc = imageseries.open(fcfile, fmt) + meta = is_fc.metadata + diff = np.linalg.norm(meta[key] - npa) + self.assertAlmostEqual(diff, 0., + "frame-cache numpy array metadata failed") + + + del is_fc + os.remove(fcfile) + +class TestImageSeriesProcess(unittest.TestCase): + + def _runfliptest(self, a, flip, aflip): + is_a = imageseries.open(None, 'array', data=a) + ops = [('flip', flip)] + is_p = process.ProcessedImageSeries(is_a, ops) + is_aflip = imageseries.open(None, 'array', data=aflip) + diff = compare(is_aflip, is_p) + msg = "flipped [%s] image series failed" % flip + self.assertAlmostEqual(diff, 0., msg=msg) + + def test_process(self): + """Processed image series""" + is_a = make_array_ims() + is_p = process.ProcessedImageSeries(is_a, []) + diff = compare(is_a, is_p) + msg = "processed image series failed to reproduce original" + self.assertAlmostEqual(diff, 0., msg) + + def test_process_flip_t(self): + """Processed image series: flip transpose""" + flip = 't' + a = make_array() + aflip = np.transpose(a, (0, 2, 1)) + self._runfliptest(a, flip, aflip) + + def test_process_flip_v(self): + """Processed image series: flip vertical""" + flip = 'v' + a = make_array() + aflip = a[:, :, ::-1] + self._runfliptest(a, flip, aflip) + + def test_process_flip_h(self): + """Processed image series: flip horizontal""" + flip = 'h' + a = make_array() + aflip = a[:, ::-1, :] + self._runfliptest(a, flip, aflip) + + def test_process_flip_vh(self): + """Processed image series: flip horizontal""" + flip = 'vh' + a = make_array() + aflip = a[:, ::-1, ::-1] + self._runfliptest(a, flip, aflip) + + def test_process_flip_r90(self): + """Processed image series: flip horizontal""" + flip = 'ccw90' + a = make_array() + aflip = np.transpose(a, (0, 2, 1))[:, :, ::-1] + self._runfliptest(a, flip, aflip) + + def test_process_flip_r270(self): + """Processed image series: flip horizontal""" + flip = 'cw90' + a = make_array() + aflip = np.transpose(a, (0, 2, 1))[:, ::-1, :] + self._runfliptest(a, flip, aflip) + + def test_process_dark(self): + """Processed image series: dark image""" + a = make_array() + dark = np.ones_like(a[0]) + is_a = imageseries.open(None, 'array', data=a) + apos = np.where(a >= 1, a-1, 0) + is_a1 = imageseries.open(None, 'array', data=apos) + ops = [('dark', dark)] + is_p = process.ProcessedImageSeries(is_a, ops) + diff = compare(is_a1, is_p) + self.assertAlmostEqual(diff, 0., msg="dark image failed") + +class TestImageSeriesStats(unittest.TestCase): + + def test_stats_median(self): + """Processed imageseries: median""" + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ismed = stats.median(is_a) + amed = np.median(a, axis=0) + err = np.linalg.norm(amed - ismed) + self.assertAlmostEqual(err, 0., msg="median image failed") + + def test_stats_max(self): + """Processed imageseries: median""" + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ismax = stats.max(is_a) + amax = np.max(a, axis=0) + err = np.linalg.norm(amax - ismax) + self.assertAlmostEqual(err, 0., msg="max image failed") + +# ==================== Utility functions + +def make_array(): + a = np.zeros(_NFXY) + ind = np.array([0,1,2]) + a[ind, 1,2] = 1 + ind + return a + +def make_meta(): + return {'testing': '1,2,3'} + +def make_array_ims(): + is_a = imageseries.open(None, 'array', data=make_array(), + meta=make_meta()) + return is_a + +def compare(ims1, ims2): + """compare two imageseries""" + if len(ims1) != len(ims2): + raise ValueError("lengths do not match") + + if ims1.dtype is not ims2.dtype: + raise ValueError("types do not match") + + maxdiff = 0.0 + for i in range(len(ims1)): + f1 = ims1[i] + f2 = ims2[i] + fdiff = np.linalg.norm(f1 - f2) + maxdiff = np.maximum(maxdiff, fdiff) + + return maxdiff + +def compare_meta(ims1, ims2): + # check metadata (simple immutable cases only for now) + + m1 = set(ims1.metadata.items()) + m2 = set(ims2.metadata.items()) + return m1.issubset(m2) and m2.issubset(m1) + +# ================================================== Execution +# +def set_options(): + """Set options for command line""" + parser = argparse.ArgumentParser(description='test hexrd.imageseries') + + return parser + +def execute(args): + """Main execution""" + p = set_options() + unittest.main() + + return + + +if __name__ == '__main__': + execute(sys.argv[1:]) From 683fa755bc43ad116a03597519317e43a558651d Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 27 Oct 2015 10:58:45 -0400 Subject: [PATCH 039/253] unit test (and fix) for process/frame-list --- hexrd/imageseries/process.py | 7 ++++--- hexrd/imageseries/tests/test_imageseries.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index a76f49ec..08a133cd 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -20,12 +20,12 @@ def __init__(self, imser, oplist, **kwargs): operation to perform using specified data *keyword args* - 'frame-list' - specify subset of frames by list + 'frame_list' - specify subset of frames by list """ self._imser = imser self._oplist = oplist - self._frames = kwargs.pop('frame-list', None) + self._frames = kwargs.pop('frame_list', None) self._hasframelist = (self._frames is not None) self.addop(self.DARK, self._subtract_dark) @@ -42,7 +42,8 @@ def __len__(self): return len(self._frames) if self._hasframelist else len(self._imser) def _process_frame(self, key): - img = np.copy(self._imser[self._get_index(key)]) + # note: key refers to original imageseries + img = np.copy(self._imser[key]) for k, d in self.oplist: func = self._opdict[k] img = func(img, d) diff --git a/hexrd/imageseries/tests/test_imageseries.py b/hexrd/imageseries/tests/test_imageseries.py index ff099ca5..93641266 100644 --- a/hexrd/imageseries/tests/test_imageseries.py +++ b/hexrd/imageseries/tests/test_imageseries.py @@ -186,6 +186,20 @@ def test_process_dark(self): diff = compare(is_a1, is_p) self.assertAlmostEqual(diff, 0., msg="dark image failed") + + def test_process_framelist(self): + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ops = [] + frames = [0, 2] + is_p = process.ProcessedImageSeries(is_a, ops, frame_list=frames) + print("1") + is_a2 = imageseries.open(None, 'array', data=a[(0,2), ...]) + print("lengths: ", len(is_p), len(is_a2), len(is_a)) + diff = compare(is_a2, is_p) + self.assertAlmostEqual(diff, 0., msg="frame list failed") + + class TestImageSeriesStats(unittest.TestCase): def test_stats_median(self): From fb48b58f88ba01d2103b477ac44255b3c3895900 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 29 Oct 2015 09:52:14 -0400 Subject: [PATCH 040/253] updated process module to allow metadata to be modifiable --- hexrd/imageseries/process.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index 08a133cd..f688731a 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -1,4 +1,6 @@ """Class for processing individual frames""" +import copy + import numpy as np from .baseclass import ImageSeries @@ -24,6 +26,7 @@ def __init__(self, imser, oplist, **kwargs): """ self._imser = imser + self._meta = copy.deepcopy(imser.metadata) self._oplist = oplist self._frames = kwargs.pop('frame_list', None) self._hasframelist = (self._frames is not None) @@ -88,7 +91,8 @@ def shape(self): @property def metadata(self): - return self._imser.metadata + # this is a modifiable copy of metadata of the original imageseries + return self._meta @classmethod def addop(cls, key, func): From bc17379079fd21375e9b828ba83e401c026a7b6e Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 29 Oct 2015 14:51:02 -0400 Subject: [PATCH 041/253] changes to get new ReadGE class to function without errors --- hexrd/config/imageseries.py | 23 +++++++++++++- hexrd/coreutil.py | 12 +++---- hexrd/xrd/image_io.py | 62 ++++++++++++++++++------------------- 3 files changed, 58 insertions(+), 39 deletions(-) diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index a2ea643f..fb2f5a23 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -2,11 +2,17 @@ import os from .config import Config - +from hexrd import imageseries class ImageSeriesConfig(Config): + def _open(self): + self._imser = imageseries.open(self.filename, self.format, **self.args) + + def _meta(self): + pass # to be done later + @property def filename(self): @@ -22,3 +28,18 @@ def format(self): @property def args(self): return self._cfg.get('image_series:args') + + @property + def omega(self): + return OmegaConfig(self._cfg) + + +class OmegaConfig(Config): + + @property + def step(self): + return self._cfg.get('image_series:omega:step') + + @property + def start(self): + return self._cfg.get('image_series:omega:start') diff --git a/hexrd/coreutil.py b/hexrd/coreutil.py index a0afd1a4..0693889a 100644 --- a/hexrd/coreutil.py +++ b/hexrd/coreutil.py @@ -1,5 +1,4 @@ import collections -from ConfigParser import SafeConfigParser import copy import logging import os @@ -169,13 +168,12 @@ def initialize_experiment(cfg): pd = ws.activeMaterial.planeData - image_start = cfg.image_series.images.start - dark = cfg.image_series.dark - flip = cfg.image_series.flip - + isfile = cfg.image_series.filename + isfmt = cfg.image_series.format + isargs = cfg.image_series.args # detector data try: - reader = ReadGE('imageseries.h5', path='imageseries') + reader = ReadGE(isfile, fmt=isfmt, **isargs) #reader = ReadGE( # [(f, image_start) for f in cfg.image_series.files], # np.radians(cfg.image_series.omega.start), @@ -193,7 +191,7 @@ def initialize_experiment(cfg): ws.loadDetector(os.path.join(cwd, detector_fname)) detector = ws.detector except IOError: - logger.info("old detector par file not found, skipping; \nalthough you may need this for find-orientations") + logger.info("old detector par file not found, skipping; \nalthough you may need this for find-orientations") detector = None return pd, reader, detector diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index db8eae3b..7f17d4d4 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -16,12 +16,14 @@ import copy import os import time +import logging import warnings -import numpy as num +import numpy as np from hexrd import imageseries +logging.basicConfig(level=logging.DEBUG) warnings.filterwarnings('always', '', DeprecationWarning) class ReaderDeprecationWarning(DeprecationWarning): @@ -31,6 +33,8 @@ def __init__(self, value): def __str__(self): return repr(self.value) + + class OmegaImageSeries(object): """Facade for frame_series class, replacing other readers, primarily ReadGE""" OMEGA_TAG = 'omega' @@ -47,12 +51,12 @@ def __init__(self, fname, fmt='hdf5', **kwargs): to allow for addition of indices with regular ints """ self._imseries = imageseries.open(fname, fmt, **kwargs) + self._nframes = len(self._imseries) self._shape = self._imseries.shape self._meta = self._imseries.metadata if self.OMEGA_TAG not in self._meta: raise RuntimeError('No omega data found in data file') - return def __getitem__(self, k): return self._imseries[k] @@ -60,24 +64,24 @@ def __getitem__(self, k): @property def nframes(self): """(get-only) number of frames""" - return self._shape[0] + return self._nframes @property def nrows(self): """(get-only) number of rows""" - return self._shape[1] + return self._shape[0] @property def ncols(self): """(get-only) number of columns""" - return self._shape[2] + return self._shape[1] @property def omega(self): """ (get-only) array of omega min/max per frame""" return self._meta[self.OMEGA_TAG] - pass + class Framer2DRC(object): """Base class for readers. @@ -90,7 +94,7 @@ def __init__(self, ncols, nrows, self.__frame_dtype_read = dtypeRead self.__frame_dtype_float = dtypeFloat - self.__nbytes_frame = num.nbytes[dtypeRead]*nrows*ncols + self.__nbytes_frame = np.nbytes[dtypeRead]*nrows*ncols return @@ -121,7 +125,7 @@ def get_dtypeFloat(self): def getEmptyMask(self): """convenience method for getting an emtpy mask""" # this used to be a class method - return num.zeros([self.nrows, self.ncols], dtype=bool) + return np.zeros([self.nrows, self.ncols], dtype=bool) class OmegaFramer(object): """Omega information associated with frame numbers""" @@ -133,35 +137,28 @@ def __init__(self, omegas): Could check for monotonicity. """ self._omegas = omegas - self._omin = omegas.min() - self._omax = omegas.max() - self._omean = omegas.mean(axis=1) + self._ombeg = omegas[0, :] + self._omend = omegas[1, :] + self._omean = omegas.mean(axis=0) self._odels = omegas[:, 1] - omegas[:, 0] self._delta = self._odels[0] - self._orange = num.hstack((omegas[:, 0], omegas[-1, 1])) + self._orange = np.hstack((omegas[:, 0], omegas[-1, 1])) return - # property: - - def _omin(self): - return self._omin - - def _omax(self): - return self._omax - def getDeltaOmega(self, nframes=1): - return self._omax - self._omin + """change in omega over n-frames, assuming constant delta""" + return nframes*(self._delta) def getOmegaMinMax(self): - return self._omin, self._omax + return self._ombeg, self._omend def frameToOmega(self, frame): """can frame be nonintegral? round to int ... """ return self._omean[frame] def omegaToFrame(self, omega): - return num.searchsorted(self._orange) - 1 + return np.searchsorted(self._orange) - 1 def omegaToFrameRange(self, omega): @@ -217,11 +214,14 @@ def __init__(self, file_info, *args, **kwargs): """ self._fname = file_info self._kwargs = kwargs + self._format = kwargs.pop('fmt', None) try: - self._omis = OmegaImageSeries(file_info, **kwargs) + self._omis = OmegaImageSeries(file_info, self._format, **kwargs) Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) OmegaFramer.__init__(self, self._omis.omega) except: + logging.info('READGE initializations failed') + if file_info is not None: raise self._omis = None self.mask = None @@ -254,7 +254,7 @@ def getFrameOmega(self, iFrame=None): if hasattr(iFrame, '__len__'): # in case last read was multiframe oms = [self.frameToOmega(frm) for frm in iFrame] - retval = num.mean(num.asarray(oms)) + retval = np.mean(np.asarray(oms)) else: retval = self.frameToOmega(iFrame) return retval @@ -267,14 +267,14 @@ def readBBox(self, bbox, raw=True, doFlip=None): """ # implement in OmegaFrameReader nskip = bbox[2][0] - bBox = num.array(bbox) + bBox = np.array(bbox) sl_i = slice(*bBox[0]) sl_j = slice(*bBox[1]) 'plenty of performance optimization might be possible here' if raw: - retval = num.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_read ) + retval = np.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_read ) else: - retval = num.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_dflt ) + retval = np.empty( tuple(bBox[:,1] - bBox[:,0]), dtype=self.__frame_dtype_dflt ) for iFrame in range(retval.shape[2]): thisframe = reader.read(nskip=nskip) nskip = 0 @@ -308,7 +308,7 @@ def read(self, nskip=0, nframes=1, sumImg=False): *sumImg* can be set to True or to a function of two frames like numpy.maximum *nskip* applies only to the first frame """ - self.iFrame = num.atleast_1d(self.iFrame)[-1] + nskip + self.iFrame = np.atleast_1d(self.iFrame)[-1] + nskip multiframe = nframes > 1 sumimg_callable = hasattr(sumImg, '__call__') @@ -332,7 +332,7 @@ def read(self, nskip=0, nframes=1, sumImg=False): return imgs # Now, operate on frames consecutively - op = sumImg if sumimg_callable else num.add + op = sumImg if sumimg_callable else np.add ifrm = self.iFrame + 1 @@ -371,7 +371,7 @@ def omeToFrameRange(omega, omegas, omegaDelta): result can be a pair of frames if the specified omega is exactly on the border """ - retval = num.where(num.abs(omegas - omega) <= omegaDelta*0.5)[0] + retval = np.where(np.abs(omegas - omega) <= omegaDelta*0.5)[0] return retval def newGenericReader(ncols, nrows, *args, **kwargs): From f7004bb5997fc734c4111c2987a7d50eb6bf91cd Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Fri, 6 Nov 2015 11:06:30 -0500 Subject: [PATCH 042/253] Final corrections before imageseries main merge * added stats.percentile * made write() method directly accessible in imageseries * added (temporarily) stop property to imageseries config * corrected test_gvecs unittest * corrected instantiation of OmegaImageSeries used by ReadGE --- hexrd/config/imageseries.py | 4 ++++ hexrd/config/tests/test_find_orientations.py | 2 +- hexrd/imageseries/__init__.py | 2 ++ hexrd/imageseries/stats.py | 9 +++++++++ hexrd/xrd/image_io.py | 7 ++++--- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index fb2f5a23..64d05050 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -43,3 +43,7 @@ def step(self): @property def start(self): return self._cfg.get('image_series:omega:start') + + @property + def stop(self): + return self._cfg.get('image_series:omega:stop') diff --git a/hexrd/config/tests/test_find_orientations.py b/hexrd/config/tests/test_find_orientations.py index eb1bd8b2..09b9e05e 100644 --- a/hexrd/config/tests/test_find_orientations.py +++ b/hexrd/config/tests/test_find_orientations.py @@ -70,7 +70,7 @@ def get_reference_data(cls): return reference_data - def test_threshold(self): + def test_gvecs(self): self.assertFalse( self.cfgs[0].find_orientations.extract_measured_g_vectors ) diff --git a/hexrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py index f49eeffd..9a48a8e5 100644 --- a/hexrd/imageseries/__init__.py +++ b/hexrd/imageseries/__init__.py @@ -15,3 +15,5 @@ def open(filename, format=None, **kwargs): reg = load.Registry.adapter_registry adapter = reg[format](filename, **kwargs) return ImageSeries(adapter) + +write = save.write diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index 1032d7b1..0efaac32 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -1,5 +1,6 @@ """Stats for imageseries""" import numpy as np +import logging def max(ims, nframes=0): nf = _nframes(ims, nframes) @@ -15,6 +16,13 @@ def median(ims, nframes=0): nf = _nframes(ims, nframes) return np.median(_toarray(ims, nf), axis=0) +def percentile(ims, pct, nframes=0): + """return image with given percentile values over all frames""" + # could be done by rectangle by rectangle if full series + # too big for memory + nf = _nframes(ims, nframes) + return np.percentile(_toarray(ims, nf), pct, axis=0) + # # ==================== Utilities # @@ -27,6 +35,7 @@ def _toarray(ims, nframes): ashp = (nframes,) + ims.shape a = np.zeros(ashp, dtype=ims.dtype) for i in range(nframes): + logging.info('frame: %s', i) a[i] = ims[i] return a diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index 7f17d4d4..35e2f18a 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -78,7 +78,7 @@ def ncols(self): @property def omega(self): - """ (get-only) array of omega min/max per frame""" + """ (get-only) array of omega begin/end per frame""" return self._meta[self.OMEGA_TAG] @@ -216,9 +216,10 @@ def __init__(self, file_info, *args, **kwargs): self._kwargs = kwargs self._format = kwargs.pop('fmt', None) try: - self._omis = OmegaImageSeries(file_info, self._format, **kwargs) + self._omis = OmegaImageSeries(file_info, fmt=self._format, **kwargs) Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) - OmegaFramer.__init__(self, self._omis.omega) + # note: Omegas are expected in radians, but input in degrees + OmegaFramer.__init__(self, (np.pi/180.)*self._omis.omega) except: logging.info('READGE initializations failed') if file_info is not None: raise From 2c43fbb7495a643c030a9393de044bbe0aec2f66 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 18 Nov 2015 18:55:19 -0500 Subject: [PATCH 043/253] added h5py to conda recipe --- conda.recipe/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 16f22086..e362ef7c 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -24,6 +24,7 @@ requirements: - python - setuptools run: + - h5py - dill - matplotlib - numba From a9ef1455925912c02ead210fc97daf66fe0c69c1 Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Wed, 18 Nov 2015 17:13:29 -0800 Subject: [PATCH 044/253] fixed clean option and chunk size --- hexrd/cli/findorientations.py | 6 +++--- hexrd/findorientations.py | 30 ++++++++++++++++-------------- hexrd/imageseries/save.py | 27 +++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/hexrd/cli/findorientations.py b/hexrd/cli/findorientations.py index 0d08d2d3..dc462300 100644 --- a/hexrd/cli/findorientations.py +++ b/hexrd/cli/findorientations.py @@ -81,9 +81,9 @@ def execute(args, parser): # prepare the analysis directory quats_f = os.path.join(cfg.working_dir, 'accepted_orientations.dat') - if os.path.exists(quats_f) and not args.force: + if os.path.exists(quats_f) and not ( args.force or args.clean ): logger.error( - '%s already exists. Change yml file or specify "force"', quats_f + '%s already exists. Change yml file or specify "force" or "clean"', quats_f ) sys.exit() if not os.path.exists(cfg.working_dir): @@ -111,7 +111,7 @@ def execute(args, parser): pr.enable() # process the data - find_orientations(cfg, hkls=args.hkls, profile=args.profile) + find_orientations(cfg, hkls=args.hkls, profile=args.profile, clean=args.clean) if args.profile: pr.disable() diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 743abaf9..1fc7079d 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -234,24 +234,26 @@ def quat_distance(x, y): return np.atleast_2d(qbar), cl -def load_eta_ome_maps(cfg, pd, reader, detector, hkls=None): +def load_eta_ome_maps(cfg, pd, reader, detector, hkls=None, clean=False): fn = os.path.join( cfg.working_dir, cfg.find_orientations.orientation_maps.file ) - try: - res = cPickle.load(open(fn, 'r')) - pd = res.planeData - available_hkls = pd.hkls.T - logger.info('loaded eta/ome orientation maps from %s', fn) - hkls = [str(i) for i in available_hkls[res.iHKLList]] - logger.info( - 'hkls used to generate orientation maps: %s', hkls) - return res - except (AttributeError, IOError): + if not clean: + try: + res = cPickle.load(open(fn, 'r')) + pd = res.planeData + available_hkls = pd.hkls.T + logger.info('loaded eta/ome orientation maps from %s', fn) + hkls = [str(i) for i in available_hkls[res.iHKLList]] + logger.info( + 'hkls used to generate orientation maps: %s', hkls) + return res + except (AttributeError, IOError): + return generate_eta_ome_maps(cfg, pd, reader, detector, hkls) + else: return generate_eta_ome_maps(cfg, pd, reader, detector, hkls) - def generate_eta_ome_maps(cfg, pd, reader, detector, hkls=None): available_hkls = pd.hkls.T @@ -295,7 +297,7 @@ def generate_eta_ome_maps(cfg, pd, reader, detector, hkls=None): return eta_ome -def find_orientations(cfg, hkls=None, profile=False): +def find_orientations(cfg, hkls=None, clean=False, profile=False): """ Takes a config dict as input, generally a yml document @@ -330,7 +332,7 @@ def find_orientations(cfg, hkls=None, profile=False): logger.info("beginning analysis '%s'", cfg.analysis_name) # load the eta_ome orientation maps - eta_ome = load_eta_ome_maps(cfg, pd, reader, detector, hkls) + eta_ome = load_eta_ome_maps(cfg, pd, reader, detector, hkls=hkls, clean=clean) ome_range = (np.min(eta_ome.omeEdges), np.max(eta_ome.omeEdges) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index bcd49397..49185a53 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -78,13 +78,32 @@ def write(self): """Write imageseries to HDF5 file""" f = h5py.File(self._fname, "a") g = f.create_group(self._path) - s0, s1 = self._shape - ds = g.create_dataset('images', (self._nframes, s0, s1), self._dtype, - compression="gzip") + s0, s1 = self._shape + shape = (self._nframes, s0, s1) + + # for chunking... results of experimentation + target_chunk_size = 50000 + bytes_per_pixel = np.dtype(self._dtype).itemsize + nbytes_per_row = s1*bytes_per_pixel + if nbytes_per_row < target_chunk_size: + nrows_to_read = target_chunk_size/nbytes_per_row + chunks = (1, min(nrows_to_read, s0), s1) + else: + ncols_to_read = int(target_chunk_size/float(nbytes_per_row) * s0) + chunks = (1, 1, ncols_to_read) + + # define dataset + ds = g.create_dataset('images', + shape, + dtype=self._dtype, + chunks=chunks, + compression="gzip") + + # write images to data_set for i in range(self._nframes): ds[i, :, :] = self._ims[i] - + # add metadata for k, v in self._meta.items(): g.attrs[k] = v From d69cb36e2513e3bcd64566cdf8b2f089e2c50f97 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 18 Nov 2015 21:17:06 -0500 Subject: [PATCH 045/253] unit tests: made existing_file into absolute path due to handling of working_dir; fixed warning message in root.multiprocessing --- hexrd/config/root.py | 2 +- hexrd/config/tests/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/config/root.py b/hexrd/config/root.py index f1acaf9f..bda741d8 100644 --- a/hexrd/config/root.py +++ b/hexrd/config/root.py @@ -82,7 +82,7 @@ def multiprocessing(self): if multiproc > ncpus: logger.warning( 'Resuested %s processes, %d available', - multiproc, ncpus, ncpus + multiproc, ncpus ) res = ncpus else: diff --git a/hexrd/config/tests/common.py b/hexrd/config/tests/common.py index 19603d8b..50cc955b 100644 --- a/hexrd/config/tests/common.py +++ b/hexrd/config/tests/common.py @@ -9,7 +9,7 @@ test_data = { 'existing_path': os.path.abspath('..'), 'nonexistent_path': 'an_unlikely_name_for_a_directory', - 'existing_file': __file__, + 'existing_file': os.path.abspath(__file__), 'nonexistent_file': 'an_unlikely_name_for_a_file.dat', 'file_stem': 'test_%%05d.dat', 'tempdir': tempfile.gettempdir(), From b2204065a9f55f2d4e3d62cd90a5665a06c5782c Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Fri, 20 Nov 2015 12:54:50 -0800 Subject: [PATCH 046/253] added new maps, hitched in to findorientations --- hexrd/findorientations.py | 71 ++++---- hexrd/xrd/xrdutil.py | 373 +++++++++++++++++++++++++++++++------- 2 files changed, 348 insertions(+), 96 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 1fc7079d..6e81ee51 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -23,11 +23,11 @@ from hexrd.xrd import rotations as rot from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.coreutil import initialize_experiment +from hexrd.xrd import image_io from hexrd.xrd import xrdutil from hexrd.xrd.detector import ReadGE -from hexrd.xrd.xrdutil import simulateGVecs +from hexrd.xrd.xrdutil import GenerateEtaOmeMaps, EtaOmeMaps, simulateGVecs from hexrd.xrd import distortion as dFuncs @@ -111,15 +111,12 @@ def generate_orientation_fibers(eta_ome, threshold, seed_hkl_ids, fiber_ndiv): eta_c = eta_ome.etaEdges[0] \ + (0.5 + coms[i][ispot][1])*del_eta - #gVec_s = xrdutil.makeMeasuredScatteringVectors( - # tTh[pd_hkl_ids[i]], eta_c, ome_c - # ) gVec_s = xfcapi.anglesToGVec( np.atleast_2d( [tTh[pd_hkl_ids[i]], eta_c, ome_c] ) ).T - + tmp = mutil.uniqueVectors( rot.discreteFiber( pd.hkls[:, pd_hkl_ids[i]].reshape(3, 1), @@ -152,7 +149,7 @@ def run_cluster(compl, qfib, qsym, cfg, min_samples=None, compl_thresh=None, rad if compl_thresh is not None: min_compl = compl_thresh - # check for override on radius + # check for override on radius if radius is not None: cl_radius = radius @@ -178,7 +175,7 @@ def quat_distance(x, y): if qfib_r.shape[1] > 10000: raise RuntimeError, \ "Requested clustering of %d orientations, which would be too slow!" %qfib_r.shape[1] - + logger.info( "Feeding %d orientations above %.1f%% to clustering", qfib_r.shape[1], 100*min_compl @@ -199,7 +196,9 @@ def quat_distance(x, y): min_samples=1, metric='precomputed' ) - cl = np.array(labels, dtype=int) + + cl = np.array(labels, dtype=int) + 1 + # dbscan indices start at 0; noise are -1 # ^^^CURRENTLY NOT SET UP TO HANDLE NOISE PTS! elif algorithm == 'fclusterdata': cl = cluster.hierarchy.fclusterdata( @@ -234,14 +233,16 @@ def quat_distance(x, y): return np.atleast_2d(qbar), cl -def load_eta_ome_maps(cfg, pd, reader, detector, hkls=None, clean=False): +def load_eta_ome_maps(cfg, pd, image_series, hkls=None, clean=False): fn = os.path.join( cfg.working_dir, cfg.find_orientations.orientation_maps.file ) + if fn.split('.')[-1] != 'npz': + fn = fn + '.npz' if not clean: try: - res = cPickle.load(open(fn, 'r')) + res = EtaOmeMaps(fn) pd = res.planeData available_hkls = pd.hkls.T logger.info('loaded eta/ome orientation maps from %s', fn) @@ -250,11 +251,11 @@ def load_eta_ome_maps(cfg, pd, reader, detector, hkls=None, clean=False): 'hkls used to generate orientation maps: %s', hkls) return res except (AttributeError, IOError): - return generate_eta_ome_maps(cfg, pd, reader, detector, hkls) + return generate_eta_ome_maps(cfg, pd, image_series, hkls) else: - return generate_eta_ome_maps(cfg, pd, reader, detector, hkls) + return generate_eta_ome_maps(cfg, pd, image_series, hkls) -def generate_eta_ome_maps(cfg, pd, reader, detector, hkls=None): +def generate_eta_ome_maps(cfg, pd, image_series, hkls=None): available_hkls = pd.hkls.T # default to all hkls defined for material @@ -270,20 +271,17 @@ def generate_eta_ome_maps(cfg, pd, reader, detector, hkls=None): ', '.join([str(i) for i in available_hkls[active_hkls]]) ) - # not ready # eta_ome = xrdutil.EtaOmeMaps(cfg, reader=reader, eta_step=None) bin_frames = cfg.find_orientations.orientation_maps.bin_frames - eta_bins = np.int(2*np.pi / abs(reader.getDeltaOmega())) / bin_frames - eta_ome = xrdutil.CollapseOmeEta( - reader, - pd, - pd.hkls[:, active_hkls], - detector, - nframesLump=bin_frames, - nEtaBins=eta_bins, - debug=False, - threshold=cfg.find_orientations.orientation_maps.threshold - ).getEtaOmeMaps() + ome_step = cfg.image_series.omega.step*bin_frames + instrument_params = yaml.load(open(cfg.instrument.parameters, 'r')) + # generate maps + eta_ome = GenerateEtaOmeMaps( + image_series, instrument_params, pd, active_hkls, + ome_step=ome_step, + threshold=cfg.find_orientations.orientation_maps.threshold + ) + fn = os.path.join( cfg.working_dir, cfg.find_orientations.orientation_maps.file @@ -291,8 +289,7 @@ def generate_eta_ome_maps(cfg, pd, reader, detector, hkls=None): fd = os.path.split(fn)[0] if not os.path.isdir(fd): os.makedirs(fd) - with open(fn, 'w') as f: - cPickle.dump(eta_ome, f) + eta_ome.save(fn) logger.info("saved eta/ome orientation maps to %s", fn) return eta_ome @@ -304,9 +301,17 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): NOTE: single cfg instance, not iterator! """ - # a goofy call, could be replaced with two more targeted calls - pd, reader, detector = initialize_experiment(cfg) - + # grab planeData object + matl = cPickle.load(open('materials.cpl', 'r')) + md = dict(zip([matl[i].name for i in range(len(matl))], matl)) + pd = md[cfg.material.active].planeData + + # make image_series, which must be an OmegaImageSeries + image_series = image_io.OmegaImageSeries( + cfg.image_series.filename, + fmt=cfg.image_series.format, + **cfg.image_series.args) + # need instrument cfg later on down... instr_cfg = get_instrument_parameters(cfg) detector_params = np.hstack([ @@ -332,7 +337,7 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): logger.info("beginning analysis '%s'", cfg.analysis_name) # load the eta_ome orientation maps - eta_ome = load_eta_ome_maps(cfg, pd, reader, detector, hkls=hkls, clean=clean) + eta_ome = load_eta_ome_maps(cfg, pd, image_series, hkls=hkls, clean=clean) ome_range = (np.min(eta_ome.omeEdges), np.max(eta_ome.omeEdges) @@ -398,7 +403,7 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): np.save(os.path.join(cfg.working_dir, 'scored_orientations.npy'), np.vstack([quats, compl]) ) - + ########################################################## ## Simulate N random grains to get neighborhood size ## ########################################################## diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 7ce9d3d4..d0d279e1 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -48,13 +48,15 @@ from hexrd import matrixutil as mutil from hexrd import pfigutil from hexrd import gridutil as gutil -from hexrd.valunits import toFloat +from hexrd.valunits import toFloat, valWUnit from hexrd import USE_NUMBA import hexrd.orientations as ors from hexrd.xrd import crystallography from hexrd.xrd.crystallography import latticeParameters, latticeVectors +from hexrd.constants import keVToAngstrom + from hexrd.xrd import detector from hexrd.xrd.detector import Framer2DRC, getCMap @@ -1701,91 +1703,330 @@ def getEtaOmeMaps(self): -class EtaOmeMaps(object): +class GenerateEtaOmeMaps(object): + """ + eta-ome map class derived from new image_series and YAML config + + ...for now... + + must provide: + + self.dataStore + self.planeData + self.iHKLList + self.etaEdges # IN RADIANS + self.omeEdges # IN RADIANS + self.etas # IN RADIANS + self.omegas # IN RADIANS + + """ + def __init__(self, image_series, instrument_params, planeData, active_hkls, + ome_step=0.25, eta_step=None, npdiv=2, threshold=None): + """ + image_series must be OmegaImageSeries class + instrument_params must be a dict (loaded from yaml spec) + active_hkls must be a list (required for now) + """ + # ome and eta steps are in DEGREES + self._ome_step = ome_step + + if eta_step is None: + self._eta_step = abs( + image_series.omega[0, 1] - image_series.omega[0, 0] + ) + else: + self._eta_step = abs(eta_step) # just in case negative... + + # ...TO DO: change name of iHKLList? + self._iHKLList = active_hkls + self._planeData = planeData + + """ + eta range is forced to be [-180, 180] for now, so step must be positive + + step is same as omega unless specified (in degrees) + + ...TO DO: FIX FOR GENERAL RANGE ONCE ETA_PERIOD IS SPEC'D + """ + num_eta = int(360./float(abs(self._eta_step))) + eta_step_r = num.radians(self._eta_step) + self._etas = eta_step_r*(num.arange(num_eta) + 0.5) - num.pi # RADIANS! + self._etaEdges = num.hstack([self._etas - 0.5*eta_step_r, + self._etas[-1] + 0.5*eta_step_r]) + + """ + omegas come from image series directly + """ + self._omegas = num.radians(num.average(image_series.omega, axis=0)) + self._omeEdges = num.radians(num.hstack([image_series.omega[0, :], + image_series.omega[1, -1] + ]) + ) + + """ + construct patches in place on init + + ...TO DO: rename to 'maps'? + """ + ij_patches = [] + + # grab relevant tolerances for patches + tth_tol = num.degrees(self._planeData.tThWidth) + eta_tol = num.degrees(abs(self._etas[1] - self._etas[0])) + + # grab distortion + if instrument_params['detector']['distortion']['function_name'] is None: + distortion = None + else: + # ...THIS IS STILL A KLUDGE!!!!! + distortion = (xf.dFunc_ref, + num.r_[instrument_params['detector']['distortion']['parameters']] + ) + + # stack parameters + detector_params = num.hstack([ + instrument_params['detector']['transform']['tilt_angles'], + instrument_params['detector']['transform']['t_vec_d'], + instrument_params['oscillation_stage']['chi'], + instrument_params['oscillation_stage']['t_vec_s'], + ]) + pixel_pitch = instrument_params['detector']['pixels']['size'] + + # 6 detector affine xform parameters + rMat_d = xfcapi.makeDetectorRotMat(detector_params[:3]) + tVec_d = detector_params[3:6] + + # 'dummy' sample frame rot mat + rMat_s = num.eye(3) + tVec_s = num.zeros(3) + + # since making maps for all eta, must hand trivial crystal params + rMat_c = num.eye(3) + tVec_c = num.zeros(3) + + # make full angs list (tth, eta, 0.) + angs = [num.vstack([tth*num.ones(num_eta), + self._etas, + num.zeros(num_eta)]) \ + for tth in self._planeData.getTTh()[active_hkls]] + + for i_ring in range(len(angs)): + # need xy coords and pixel sizes + gVec_ring_l = xfcapi.anglesToGVec(angs[i_ring].T) + xydet_ring = xfcapi.gvecToDetectorXY(gVec_ring_l, + rMat_d, rMat_s, rMat_c, + tVec_d, tVec_s, tVec_c) + + if distortion is not None: + det_xy = distortion[0](xydet_ring, + distortion[1], + invert=True) + ang_ps = angularPixelSize(det_xy, pixel_pitch, + rMat_d, rMat_s, + tVec_d, tVec_s, tVec_c, + distortion=distortion) + + patches = make_reflection_patches(instrument_params, + angs[i_ring].T[:, :2], ang_ps, + omega=None, + tth_tol=tth_tol, eta_tol=eta_tol, + distortion=distortion, + npdiv=npdiv, quiet=False, + compute_areas_func=gutil.compute_areas) + ij_patches.append(patches) + # DEBUGGING # mxf = num.amax([image_series[i] for i in range(image_series.nframes)], axis=0) + # DEBUGGING # xs = num.hstack([ij_patches[0][i][-1][1] for i in num.linspace(0, 1436, num=360, dtype=int)]) + # DEBUGGING # ys = num.hstack([ij_patches[0][i][-1][0] for i in num.linspace(0, 1436, num=360, dtype=int)]) + # DEBUGGING # import pdb; pdb.set_trace() + # initialize maps and loop + pbar = ProgressBar( + widgets=[Bar('>'), ' ', ETA(), ' ', ReverseBar('<')], + maxval=image_series.nframes + ).start() + maps = num.zeros((len(active_hkls), len(self._omegas), len(self._etas))) + for i_ome in range(image_series.nframes): + pbar.update(i_ome) + this_frame = image_series[i_ome] + if threshold is not None: + this_frame[this_frame < threshold] = 0 + for i_ring in range(len(active_hkls)): + for j_eta in range(num_eta): + ii = ij_patches[i_ring][j_eta][-1][0] + jj = ij_patches[i_ring][j_eta][-1][1] + areas = ij_patches[i_ring][j_eta][-2] + maps[i_ring, i_ome, j_eta] = num.sum(this_frame[ii, jj] * areas / float(num.sum(areas))) + pass # close eta loop + pass # close ring loop + pass # close ome loop + pbar.finish() + self._dataStore = maps + + @property + def dataStore(self): + return self._dataStore + + @property + def planeData(self): + return self._planeData + + @property + def iHKLList(self): + return num.atleast_1d(self._iHKLList).flatten() + + @property + def etaEdges(self): + return self._etaEdges + + @property + def omeEdges(self): + return self._omeEdges + + @property + def etas(self): + return self._etas + @property + def omegas(self): + return self._omegas + + def save(self, filename): + """ + self.dataStore + self.planeData + self.iHKLList + self.etaEdges + self.omeEdges + self.etas + self.omegas + """ + args = num.array(self.planeData.getParams())[:4] + args[2] = valWUnit('wavelength', 'length', args[2], 'angstrom') # force units... + hkls = self.planeData.hkls + + save_dict = {'dataStore':self.dataStore, + 'etas':self.etas, + 'etaEdges':self.etaEdges, + 'iHKLList':self.iHKLList, + 'omegas':self.omegas, + 'omeEdges':self.omeEdges, + 'planeData_args':args, + 'planeData_hkls':hkls, + } + num.savez(filename, **save_dict) + return + pass # end of class: GenerateEtaOmeMaps + + + +class EtaOmeMaps(object): """ find-orientations loads pickled eta-ome data, but CollapseOmeEta is not pickleable, because it holds a list of ReadGE, each of which holds a reference to an open file object, which is not pickleable. """ + + def __init__(self, ome_eta_archive): - def __init__(self, ome_eta): - self.dataStore = ome_eta.dataStore - self.planeData = ome_eta.planeData - self.iHKLList = ome_eta.iHKLList - self.etaEdges = ome_eta.etaEdges - self.omeEdges = ome_eta.omeEdges - self.etas = ome_eta.etas - self.omegas = ome_eta.omegas + ome_eta = num.load(ome_eta_archive) + + planeData_args = ome_eta['planeData_args'] + planeData_hkls = ome_eta['planeData_hkls'] + self.planeData = crystallography.PlaneData(planeData_hkls, *planeData_args) + + self.dataStore = ome_eta['dataStore'] + self.iHKLList = ome_eta['iHKLList'] + self.etaEdges = ome_eta['etaEdges'] + self.omeEdges = ome_eta['omeEdges'] + self.etas = ome_eta['etas'] + self.omegas = ome_eta['omegas'] + return - + pass # end of class: EtaOmeMaps + +# obselete # class EtaOmeMaps(object): +# obselete # +# obselete # """ +# obselete # find-orientations loads pickled eta-ome data, but CollapseOmeEta is not +# obselete # pickleable, because it holds a list of ReadGE, each of which holds a +# obselete # reference to an open file object, which is not pickleable. +# obselete # """ +# obselete # +# obselete # def __init__(self, ome_eta): +# obselete # self.dataStore = ome_eta.dataStore +# obselete # self.planeData = ome_eta.planeData +# obselete # self.iHKLList = ome_eta.iHKLList +# obselete # self.etaEdges = ome_eta.etaEdges +# obselete # self.omeEdges = ome_eta.omeEdges +# obselete # self.etas = ome_eta.etas +# obselete # self.omegas = ome_eta.omegas +# obselete # return # not ready # class BaseEtaOme(object): # not ready # """ -# not ready # eta-ome map base class derived from new YAML config -# not ready # +# not ready # eta-ome map base class derived from new YAML config +# not ready # # not ready # ...for now... -# not ready # +# not ready # # not ready # must provide: -# not ready # +# not ready # # not ready # self.dataStore # not ready # self.planeData -# not ready # self.iHKLList -# not ready # self.etaEdges # IN RADIANS +# not ready # self.iHKLList +# not ready # self.etaEdges # IN RADIANS # not ready # self.omeEdges # IN RADIANS # not ready # self.etas # IN RADIANS # not ready # self.omegas # IN RADIANS -# not ready # +# not ready # # not ready # This wrapper will provide all but dataStore. # not ready # """ # not ready # def __init__(self, cfg, reader=None, eta_step=None): # not ready # """ -# not ready # currently, reader has to be None *OLD* class type until fixed with new imageIO; +# not ready # currently, reader has to be None *OLD* class type until fixed with new imageIO; # not ready # if None, then the frame_cache.npz specified by the config must exist # not ready # """ # not ready # self.cfg = cfg # not ready # self.instr_cfg = get_instrument_parameters(cfg) -# not ready # +# not ready # # not ready # # currently hard-coded to do reader from npz frame cache # not ready # # kwarg *MUST* be 'new' style reader # not ready # if reader is None: # not ready # self.__reader = get_frames(reader, self.cfg) # not ready # else: # not ready # self.__reader = reader -# not ready # +# not ready # # not ready # # set eta_step IN DEGREES # not ready # if eta_step is None: # not ready # self._eta_step = self.cfg.image_series.omega.step # not ready # else: -# not ready # self._eta_step = abs(eta_step) # just in case negative... -# not ready # +# not ready # self._eta_step = abs(eta_step) # just in case negative... +# not ready # # not ready # material_list = cPickle.load(open(cfg.material.definitions, 'r')) # not ready # material_names = [material_list[i].name for i in range(len(material_list))] # not ready # material_dict = dict(zip(material_names, material_list)) # not ready # self.planeData = material_dict[cfg.material.active].planeData -# not ready # +# not ready # # not ready # self._iHKLList = None -# not ready # +# not ready # # not ready # self._etaEdges = None # not ready # self._omeEdges = None # not ready # self._etas = None # not ready # self._omegas = None -# not ready # +# not ready # # not ready # return -# not ready # +# not ready # # not ready # @property # not ready # def iHKLList(self): # not ready # return self._iHKLList # not ready # @iHKLList.getter # not ready # def iHKLList(self, ids=None): # not ready # """ -# not ready # ids must be a list +# not ready # ids must be a list # not ready # """ # not ready # if ids is not None: # not ready # assert hasattr(ids, '__len__'), "ids must be a list or list-like object" -# not ready # -# not ready # # start with all available +# not ready # +# not ready # # start with all available # not ready # active_hkls = range(pd.hkls.shape[1]) # not ready # # check cfg file # not ready # temp = cfg.find_orientations.orientation_maps.active_hkls @@ -1793,7 +2034,7 @@ def __init__(self, ome_eta): # not ready # active_hkls = active_hkls if temp == 'all' else temp # not ready # # override with hkls from command line, if specified # not ready # return ids if ids is not None else active_hkls -# not ready # +# not ready # # not ready # @property # not ready # def omegas(self): # not ready # return self._omegas @@ -1806,11 +2047,11 @@ def __init__(self, ome_eta): # not ready # ome_start = self.__reader[1][0] # not ready # ome_step = self.__reader[1][1] # not ready # return ome_step*(num.arange(num_ome) + 0.5) + ome_start -# not ready # +# not ready # # not ready # @property # not ready # def eta_step(self): # not ready # return self._eta_step -# not ready # +# not ready # # not ready # @property # not ready # def etas(self): # not ready # return self._etas @@ -1818,12 +2059,12 @@ def __init__(self, ome_eta): # not ready # def etas(self): # not ready # """ # not ready # range is forced to be [-180, 180] for now, so step must be positive -# not ready # +# not ready # # not ready # step is same as omega unless specified (in degrees) # not ready # """ # not ready # num_eta = int(360/float(abs(self.eta_step))) # not ready # return num.radians(self.eta_step)*(num.arange(num_eta) + 0.5) - num.pi -# not ready # +# not ready # # not ready # @property # not ready # def omeEdges(self): # not ready # return self._omeEdges @@ -1831,28 +2072,28 @@ def __init__(self, ome_eta): # not ready # def omeEdges(self): # not ready # ome_step = self.omegas[1] - self.omegas[0] # same as self.__reader[1][1] # not ready # return num.hstack([self.omegas - 0.5*ome_step, self.omegas[-1] + 0.5*ome_step]) -# not ready # +# not ready # # not ready # @property # not ready # def etaEdges(self): # not ready # return self._etaEdges # not ready # @etaEdges.getter # not ready # def etaEdges(self): # not ready # return num.hstack([self.etas - 0.5*eta_step, self.etas[-1] + 0.5*eta_step]) -# not ready # +# not ready # # not ready # class EtaOmeMaps(BaseEtaOme): # not ready # """ # not ready # """ # not ready # def __init__(self, cfg, reader=None, eta_step=None, -# not ready # omega=0., tVec_s=num.zeros(3), +# not ready # omega=0., tVec_s=num.zeros(3), # not ready # npdiv=2): -# not ready # +# not ready # # not ready # # first init the base class # not ready # super( EtaOmeMaps, self ).__init__(cfg, reader=reader, eta_step=eta_step) -# not ready # +# not ready # # not ready # # grac relevant tolerances for patches # not ready # tth_tol = num.degrees(self.planeData.tThWidth) # not ready # eta_tol = num.degrees(abs(self.etas[1]-self.etas[0])) -# not ready # +# not ready # # not ready # # grab distortion # not ready # if instr_cfg['detector']['distortion']['function_name'] is None: # not ready # distortion = None @@ -1863,35 +2104,35 @@ def __init__(self, ome_eta): # not ready # ) # not ready # # stack parameters # not ready # detector_params = num.hstack([ -# not ready # instr_cfg['detector']['transform']['tilt_angles'], +# not ready # instr_cfg['detector']['transform']['tilt_angles'], # not ready # instr_cfg['detector']['transform']['t_vec_d'], -# not ready # instr_cfg['oscillation_stage']['chi'], -# not ready # instr_cfg['oscillation_stage']['t_vec_s'], +# not ready # instr_cfg['oscillation_stage']['chi'], +# not ready # instr_cfg['oscillation_stage']['t_vec_s'], # not ready # ]) # not ready # pixel_pitch = instr_cfg['detector']['pixels']['size'] # not ready # chi = self.instr_cfg['oscillation_stage']['chi'] # in DEGREES -# not ready # +# not ready # # not ready # # 6 detector affine xform parameters # not ready # rMat_d = makeDetectorRotMat(detector_params[:3]) # not ready # tVec_d = detector_params[3:6] -# not ready # +# not ready # # not ready # # 'dummy' sample frame rot mat # not ready # rMats_s = makeOscillRotMat(num.radians([chi, omega])) -# not ready # +# not ready # # not ready # # since making maps for all eta, must hand trivial crystal params -# not ready # rMat_c = np.eye(3) -# not ready # tVec_c = np.zeros(3) -# not ready # +# not ready # rMat_c = num.eye(3) +# not ready # tVec_c = num.zeros(3) +# not ready # # not ready # # make angle arrays for patches # not ready # neta = len(self.etas) # not ready # nome = len(reader[0]) -# not ready # +# not ready # # not ready # # make full angs list # not ready # angs = [num.vstack([tth*num.ones(neta), # not ready # etas, # not ready # num.zeros(nome)]) # not ready # for tth in self.planeData.getTTh()] -# not ready # +# not ready # # not ready # """SET MAPS CONTAINER AS ATTRIBUTE""" # not ready # self.dataStore = num.zeros((len(angs), nome, neta)) # not ready # for i_ring in range(len(angs)): @@ -1900,7 +2141,7 @@ def __init__(self, ome_eta): # not ready # xydet_ring = xfcapi.gvecToDetectorXY(gVec_ring_l, # not ready # rMat_d, rMat_s, rMat_c, # not ready # tVec_d, tVec_s, tVec_c) -# not ready # +# not ready # # not ready # if distortion is not None: # not ready # det_xy = distortion[0](xydet_ring, # not ready # distortion[1], @@ -1909,7 +2150,7 @@ def __init__(self, ome_eta): # not ready # rMat_d, rMat_s, # not ready # tVec_d, tVec_s, tVec_c, # not ready # distortion=distortion) -# not ready # +# not ready # # not ready # patches = make_reflection_patches(self.instr_cfg, # not ready # angs[i_ring].T[:, :2], ang_ps, # not ready # omega=None, @@ -1917,7 +2158,7 @@ def __init__(self, ome_eta): # not ready # distortion=distortion, # not ready # npdiv=npdiv, quiet=False, # not ready # compute_areas_func=gutil.compute_areas) -# not ready # +# not ready # # not ready # for i in range(nome): # not ready # this_frame = num.array(reader[0][i].todense()) # not ready # for j in range(neta): @@ -3464,14 +3705,14 @@ def simulateGVecs(pd, detector_params, grain_params, # first find valid G-vectors angList = num.vstack(xfcapi.oscillAnglesOfHKLs(full_hkls[:, 1:], chi, rMat_c, bMat, wlen, vInv=vInv_s)) allAngs, allHKLs = _filter_hkls_eta_ome(full_hkls, angList, eta_range, ome_range) - + if len(allAngs) == 0: valid_ids = [] - valid_hkl = [] - valid_ang = [] - valid_xy = [] + valid_hkl = [] + valid_ang = [] + valid_xy = [] ang_ps = [] - else: + else: #...preallocate for speed...? det_xy, rMat_s = _project_on_detector_plane(allHKLs[:, 1:], allAngs, bMat, rMat_d, rMat_c, chi, @@ -3612,7 +3853,7 @@ def _coo_build_window(frame_i, min_row, max_row, min_col, max_col): min_row, max_row, min_col, max_col, window) - + @numba.jit def compute_areas_2(xy_eval_vtx, conn): areas = num.empty(len(conn)) @@ -3627,7 +3868,7 @@ def compute_areas_2(xy_eval_vtx, conn): v1x = vtx_x - vtx0x v1y = vtx_y - vtx0y acc += v0x*v1y - v1x*v0y - + areas[i] = 0.5 * acc return areas else: # not USE_NUMBA @@ -3733,6 +3974,8 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, # FOR ANGULAR MESH conn = gutil.cellConnectivity( sdims[0], sdims[1], origin='ll') + rMat_s = xfcapi.makeOscillRotMat([num.radians(chi), angs[2]]) + # make G-vectors gVec_c = xfcapi.anglesToGVec(gVec_angs_vtx, chi=chi, rMat_c=rMat_c) xy_eval_vtx = xfcapi.gvecToDetectorXY(gVec_c, @@ -3743,7 +3986,7 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, pass areas = compute_areas_func(xy_eval_vtx, conn) - + # EVALUATION POINTS # * for lack of a better option will use centroids tth_eta_cen = gutil.cellCentroids( num.atleast_2d(gVec_angs_vtx[:, :2]), conn ) @@ -3759,6 +4002,7 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, row_indices = gutil.cellIndices(row_edges, xy_eval[:, 1]) col_indices = gutil.cellIndices(col_edges, xy_eval[:, 0]) + # append patch data to list patches.append(((gVec_angs_vtx[:, 0].reshape(m_tth.shape), gVec_angs_vtx[:, 1].reshape(m_tth.shape)), (xy_eval_vtx[:, 0].reshape(m_tth.shape), @@ -4187,3 +4431,6 @@ def pullSpots(pd, detector_params, grain_params, reader, fid.close() return spot_list + +def validateQVecAngles(*args, **kwargs): + raise NotImplementedError From c3fa65208458972bb0d70a87ae171b4ffd2cdda3 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 21 Nov 2015 09:54:44 -0500 Subject: [PATCH 047/253] added options for compression/chunk size to hdf5 imageseries tools --- hexrd/imageseries/save.py | 28 ++++++++++- scripts/make_imageseriesh5.py | 95 +++++++++++++++++++++++++++++------ 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index bcd49397..59fc9a16 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -60,6 +60,8 @@ def __init__(self, ims, fname, **kwargs): class WriteH5(Writer): fmt = 'hdf5' + dflt_gzip = 4 + dflt_chrows = 0 def __init__(self, ims, fname, **kwargs): """Write imageseries in HDF5 file @@ -67,6 +69,9 @@ def __init__(self, ims, fname, **kwargs): Required Args: path - the path in HDF5 file + Options: + gzip - 0-9; 0 turns off compression; 4 is default + chunk_rows - number of rows per chunk; default is all """ Writer.__init__(self, ims, fname, **kwargs) self._path = self._opts['path'] @@ -79,9 +84,10 @@ def write(self): f = h5py.File(self._fname, "a") g = f.create_group(self._path) s0, s1 = self._shape + chnk = (1,) + self._shape ds = g.create_dataset('images', (self._nframes, s0, s1), self._dtype, - compression="gzip") + compression="gzip", chunks=chnk) for i in range(self._nframes): ds[i, :, :] = self._ims[i] @@ -89,6 +95,26 @@ def write(self): for k, v in self._meta.items(): g.attrs[k] = v + @property + def h5opts(self): + d = {} + # compression + compress = self._opts.pop('gzip', self.dflt_gzip) + if compress > 9: + raise ValueError('gzip compression cannot exceed 9: %s' % compress) + if compress > 0: + d['compression'] = 'gzip' + d['compression_opts'] = compress + + # chunk size + s0, s1 = self._shape + chrows = self._opts.pop('gzip', self.dflt_chrows) + if chrows < 1 or chrows > s0: + chrows = s0 + d['chunks'] = (1, chrows, s1) + + return d + pass # end class class WriteFrameCache(Writer): diff --git a/scripts/make_imageseriesh5.py b/scripts/make_imageseriesh5.py index 18e11278..1afddede 100755 --- a/scripts/make_imageseriesh5.py +++ b/scripts/make_imageseriesh5.py @@ -2,13 +2,16 @@ # """Make an imageseries from a list of image files """ +from __future__ import print_function + import sys import argparse import logging +import time # Put this before fabio import and reset level if you # want to control its import warnings. -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.INFO) import numpy import h5py @@ -42,8 +45,10 @@ def __init__(self, a): self.nfiles = len(self.files) self.nempty = a.empty self.maxframes = a.max_frames + #self._setloglevel(a) self._info() + self.h5opts = self._seth5opts(a) self.ntowrite = numpy.min((self.maxframes, self.nframes))\ @@ -53,6 +58,44 @@ def __init__(self, a): self.dgrppath = a.dset self.dsetpath = '/'.join((a.dset, 'images')) + def _seth5opts(self, a): + h5d = {} + + # compression type and level + clevel = min(a.compression_level, 9) + if clevel > 0: + h5d['compression'] = 'gzip' + h5d['compression_opts'] = clevel + logging.info('compression level: %s' % clevel) + else: + logging.info('compression off') + + # chunk size (chunking automatically on with compression) + ckb = 1024*a.chunk_KB + if ckb <= 0: + block = self.shape + else: + # take some number of rows: + # * at least one + # * no more than number of rows + sh0, sh1 = self.shape + bpp = self.dtype.itemsize + nrows = min(ckb / (bpp * sh1), sh0) + nrows = max(nrows, 1) # at least one row + block = (nrows, sh1) + h5d['chunks'] = (1,) + block + logging.info('chunk size: %s X %s' % block) + + return h5d + + @staticmethod + def _setloglevel(a): + # Not working: need to understand logging more + if a.log_level == 'debug': + logging.basicConfig(level=logging.DEBUG) + elif a.log_level == 'info': + logging.basicConfig(level=logging.INFO) + @staticmethod def _checkvalue(v, vtest, msg): """helper: ensure value set conistently""" @@ -76,9 +119,9 @@ def _info(self): for imgf in self.files: img = fabio.open(imgf) dat = img.data - shp = self._checkvalue(shp, dat.shape, "inconistent image shapes") - dtp = self._checkvalue(dtp, dat.dtype, "inconistent image dtypes") - cn = self._checkvalue(cn, img.classname, "inconistent image types") + shp = self._checkvalue(shp, dat.shape, "inconsistent image shapes") + dtp = self._checkvalue(dtp, dat.dtype, "inconsistent image dtypes") + cn = self._checkvalue(cn, img.classname, "inconsistent image types") if img.nframes >= self.nempty: nf += img.nframes - self.nempty else: @@ -105,11 +148,15 @@ def opendset(self): # note: compression implies chunked storage msg = 'writing to file/path: %s:%s' % (self.outfile, self.dgrppath) logging.info(msg) + + # grab file object f = h5py.File(self.outfile, "a") try: shp = (self.ntowrite,) + self.shape - ds = f.create_dataset(self.dsetpath, shp, self.dtype, - compression="gzip") + chunks = (1, self.shape[0], self.shape[1]) + ds = f.create_dataset(self.dsetpath, shp, dtype=self.dtype, + **self.h5opts + ) except Exception as e: errmsg = '%s: %s\n... exception: ' % \ (ERR_OVERWRITE, DSetPath(self.outfile, self.dsetpath)) @@ -123,7 +170,10 @@ def write(self): # # Now add the images # + start_time = time.clock() # time this nframes = 0 # number completed + print_every = 1; marker = " ."; + print('Frames written (of %s):' % self.ntowrite, end="") for i in range(self.nfiles): if nframes >= self.ntowrite: break @@ -138,6 +188,10 @@ def write(self): else: ds[nframes, :, :] = img_i.data nframes += 1 + if numpy.mod(nframes, print_every) == 0: + print(marker, nframes, end="") + print_every *= 2 + sys.stdout.flush() logging.debug('... wrote image %s of %s' %\ (nframes, self.ntowrite)) if nframes >= self.ntowrite: @@ -148,6 +202,7 @@ def write(self): img_i = img_i.next() f.close() + print("\nTime to write: %f seconds " %(time.clock()-start_time)) def set_options(): """Set options for command line""" @@ -157,10 +212,13 @@ def set_options(): action="store_true") # file options - parser.add_argument("-o", "--outfile", help="name of HDF5 output file", + parser.add_argument("-o", "--outfile", + help="name of HDF5 output file", default="imageseries.h5") - help_d = "path to HDF5 data set" - parser.add_argument("-d", "--dset", help=help_d, default="/imageseries") + help_d = "path to HDF5 data group" + parser.add_argument("-d", "--dset", + help=help_d, + metavar="PATH", default="/imageseries") # image options parser.add_argument("imagefiles", nargs="+", help="image files") @@ -169,8 +227,19 @@ def set_options(): help="number of blank frames in beginning of file", metavar="N", type=int, action="store", default=0) parser.add_argument("--max-frames", - help="maximum number of frames in file (for testing)", - metavar="N", type=int, action="store", default=0) + help="maximum number of frames to write", + metavar="M", type=int, action="store", default=0) + + # compression/chunking + help_d = "compression level for gzip (1-9); 0 or less for no compression; "\ + "above 9 sets level to 9" + parser.add_argument("-c", "--compression-level", + help=help_d, + metavar="LEVEL", type=int, action="store", default=4) + help_d = "target chunk size in KB (0 means single image size)" + parser.add_argument("--chunk-KB", + help=help_d, + metavar="K", type=int, action="store", default=0) return parser @@ -182,7 +251,7 @@ def execute(args, **kwargs): """ p = set_options() a = p.parse_args(args) - logging.info(str(a)) + # logging.info(str(a)) ifiles = ImageFiles(a) @@ -191,8 +260,6 @@ def execute(args, **kwargs): else: ifiles.write() - # write_file(a, **kwargs) - if __name__ == '__main__': # # run From c3555eb049f2365c4f677a26ede98ebfee041dad Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 21 Nov 2015 10:40:27 -0500 Subject: [PATCH 048/253] reorganized imageseries unittests; added tests for compression/chunk; fixes to save.py --- hexrd/imageseries/save.py | 4 +- hexrd/imageseries/tests/__init__.py | 0 hexrd/imageseries/tests/common.py | 47 ++++ hexrd/imageseries/tests/test_formats.py | 113 ++++++++ hexrd/imageseries/tests/test_imageseries.py | 280 -------------------- hexrd/imageseries/tests/test_process.py | 90 +++++++ hexrd/imageseries/tests/test_properties.py | 15 ++ hexrd/imageseries/tests/test_stats.py | 27 ++ 8 files changed, 294 insertions(+), 282 deletions(-) create mode 100644 hexrd/imageseries/tests/__init__.py create mode 100644 hexrd/imageseries/tests/common.py create mode 100644 hexrd/imageseries/tests/test_formats.py delete mode 100644 hexrd/imageseries/tests/test_imageseries.py create mode 100644 hexrd/imageseries/tests/test_process.py create mode 100644 hexrd/imageseries/tests/test_properties.py create mode 100644 hexrd/imageseries/tests/test_stats.py diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 59fc9a16..f708144c 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -87,7 +87,7 @@ def write(self): chnk = (1,) + self._shape ds = g.create_dataset('images', (self._nframes, s0, s1), self._dtype, - compression="gzip", chunks=chnk) + **self.h5opts) for i in range(self._nframes): ds[i, :, :] = self._ims[i] @@ -108,7 +108,7 @@ def h5opts(self): # chunk size s0, s1 = self._shape - chrows = self._opts.pop('gzip', self.dflt_chrows) + chrows = self._opts.pop('chunk_rows', self.dflt_chrows) if chrows < 1 or chrows > s0: chrows = s0 d['chunks'] = (1, chrows, s1) diff --git a/hexrd/imageseries/tests/__init__.py b/hexrd/imageseries/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hexrd/imageseries/tests/common.py b/hexrd/imageseries/tests/common.py new file mode 100644 index 00000000..a8f4b0f1 --- /dev/null +++ b/hexrd/imageseries/tests/common.py @@ -0,0 +1,47 @@ +import numpy as np +import unittest + +from hexrd import imageseries + +_NFXY = (3, 7, 5) + +class ImageSeriesTest(unittest.TestCase): + pass + +def make_array(): + a = np.zeros(_NFXY) + ind = np.array([0,1,2]) + a[ind, 1,2] = 1 + ind + return a + +def make_array_ims(): + is_a = imageseries.open(None, 'array', data=make_array(), + meta=make_meta()) + return is_a + +def compare(ims1, ims2): + """compare two imageseries""" + if len(ims1) != len(ims2): + raise ValueError("lengths do not match") + + if ims1.dtype is not ims2.dtype: + raise ValueError("types do not match") + + maxdiff = 0.0 + for i in range(len(ims1)): + f1 = ims1[i] + f2 = ims2[i] + fdiff = np.linalg.norm(f1 - f2) + maxdiff = np.maximum(maxdiff, fdiff) + + return maxdiff + +def make_meta(): + return {'testing': '1,2,3'} + +def compare_meta(ims1, ims2): + # check metadata (simple immutable cases only for now) + + m1 = set(ims1.metadata.items()) + m2 = set(ims2.metadata.items()) + return m1.issubset(m2) and m2.issubset(m1) diff --git a/hexrd/imageseries/tests/test_formats.py b/hexrd/imageseries/tests/test_formats.py new file mode 100644 index 00000000..39f4fe61 --- /dev/null +++ b/hexrd/imageseries/tests/test_formats.py @@ -0,0 +1,113 @@ +import os +import tempfile + +import numpy as np + +from .common import ImageSeriesTest +from .common import make_array_ims, compare, compare_meta + +from hexrd import imageseries + +class ImageSeriesFormatTest(ImageSeriesTest): + @classmethod + def setUpClass(cls): + cls.tmpdir = tempfile.mkdtemp() + + @classmethod + def tearDownClass(cls): + os.rmdir(cls.tmpdir) + +class TestFormatH5(ImageSeriesFormatTest): + + def setUp(self): + self.h5file = os.path.join(self.tmpdir, 'test_ims.h5') + self.h5path = 'array-data' + self.fmt = 'hdf5' + self.is_a = make_array_ims() + + def tearDown(self): + os.remove(self.h5file) + + + def test_fmth5(self): + """save/load HDF5 format""" + imageseries.write(self.is_a, self.h5file, self.fmt, path=self.h5path) + is_h = imageseries.open(self.h5file, self.fmt, path=self.h5path) + + diff = compare(self.is_a, is_h) + self.assertAlmostEqual(diff, 0., "h5 reconstruction failed") + self.assertTrue(compare_meta(self.is_a, is_h)) + + def test_fmth5_nparray(self): + """HDF5 format with numpy array metadata""" + key = 'np-array' + npa = np.array([0,2.0,1.3]) + self.is_a.metadata[key] = npa + imageseries.write(self.is_a, self.h5file, self.fmt, path=self.h5path) + is_h = imageseries.open(self.h5file, self.fmt, path=self.h5path) + meta = is_h.metadata + + diff = np.linalg.norm(meta[key] - npa) + self.assertAlmostEqual(diff, 0., "h5 numpy array metadata failed") + + def test_fmth5_nocompress(self): + """HDF5 options: no compression""" + imageseries.write(self.is_a, self.h5file, self.fmt, + path=self.h5path, gzip=0) + is_h = imageseries.open(self.h5file, self.fmt, path=self.h5path) + + diff = compare(self.is_a, is_h) + self.assertAlmostEqual(diff, 0., "h5 reconstruction failed") + self.assertTrue(compare_meta(self.is_a, is_h)) + + def test_fmth5_compress_err(self): + """HDF5 options: compression level out of range""" + with self.assertRaises(ValueError): + imageseries.write(self.is_a, self.h5file, self.fmt, + path=self.h5path, gzip=10) + + def test_fmth5_chunk(self): + """HDF5 options: chunk size""" + imageseries.write(self.is_a, self.h5file, self.fmt, + path=self.h5path, chunk_rows=0) + is_h = imageseries.open(self.h5file, self.fmt, path=self.h5path) + + diff = compare(self.is_a, is_h) + self.assertAlmostEqual(diff, 0., "h5 reconstruction failed") + self.assertTrue(compare_meta(self.is_a, is_h)) + +class TestFormatFrameCache(ImageSeriesFormatTest): + + def setUp(self): + self.fcfile = os.path.join(self.tmpdir, 'frame-cache.yml') + self.fmt = 'frame-cache' + self.thresh = 0.5 + self.cache_file='frame-cache.npz' + self.is_a = make_array_ims() + + def tearDown(self): + os.remove(self.fcfile) + + + def test_fmtfc(self): + """save/load frame-cache format""" + imageseries.write(self.is_a, self.fcfile, self.fmt, + threshold=self.thresh, cache_file=self.cache_file) + is_fc = imageseries.open(self.fcfile, self.fmt) + diff = compare(self.is_a, is_fc) + self.assertAlmostEqual(diff, 0., "frame-cache reconstruction failed") + self.assertTrue(compare_meta(self.is_a, is_fc)) + + def test_fmtfc_nparray(self): + """frame-cache format with numpy array metadata""" + key = 'np-array' + npa = np.array([0,2.0,1.3]) + self.is_a.metadata[key] = npa + + imageseries.write(self.is_a, self.fcfile, self.fmt, + threshold=self.thresh, cache_file=self.cache_file) + is_fc = imageseries.open(self.fcfile, self.fmt) + meta = is_fc.metadata + diff = np.linalg.norm(meta[key] - npa) + self.assertAlmostEqual(diff, 0., + "frame-cache numpy array metadata failed") diff --git a/hexrd/imageseries/tests/test_imageseries.py b/hexrd/imageseries/tests/test_imageseries.py deleted file mode 100644 index 93641266..00000000 --- a/hexrd/imageseries/tests/test_imageseries.py +++ /dev/null @@ -1,280 +0,0 @@ -#! /usr/bin/env python -# -import sys -import os -import argparse -import unittest -import tempfile - -import numpy as np - -from hexrd import imageseries -from hexrd.imageseries import save, process, stats, ImageSeries - -# ========== Test Data - -_NFXY = (3, 7, 5) - -class TestImageSeriesProperties(unittest.TestCase): - def setUp(self): - self._a = make_array() - self._is_a = make_array_ims() - - def test_prop_nframes(self): - self.assertEqual(self._a.shape[0], len(self._is_a)) - - def test_prop_shape(self): - self.assertEqual(self._a.shape[1:], self._is_a.shape) - - def test_prop_dtype(self): - self.assertEqual(self._a.dtype, self._is_a.dtype) - -class TestImageSeriesFmts(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.tmpdir = tempfile.mkdtemp() - - @classmethod - def tearDownClass(cls): - os.rmdir(cls.tmpdir) - - # ==================== Tests - - def test_fmth5(self): - """save/load HDF5 format""" - h5file = os.path.join(self.tmpdir, 'test_ims.h5') - h5path = 'array-data' - fmt = 'hdf5' - - is_a = make_array_ims() - save.write(is_a, h5file, fmt, path=h5path) - is_h = imageseries.open(h5file, fmt, path=h5path) - diff = compare(is_a, is_h) - self.assertAlmostEqual(diff, 0., "h5 reconstruction failed") - self.assertTrue(compare_meta(is_a, is_h)) - del is_h - os.remove(h5file) - - def test_fmth5_nparray(self): - """HDF5 format with numpy array metadata""" - h5file = os.path.join(self.tmpdir, 'test_ims.h5') - h5path = 'imagedata' - fmt = 'hdf5' - key = 'np-array' - npa = np.array([0,2.0,1.3]) - - is_a = make_array_ims() - is_a.metadata[key] = npa - save.write(is_a, h5file, fmt, path=h5path) - is_h = imageseries.open(h5file, fmt, path=h5path) - meta = is_h.metadata - diff = np.linalg.norm(meta[key] - npa) - self.assertAlmostEqual(diff, 0., "h5 numpy array metadata failed") - - del is_h - os.remove(h5file) - - def test_fmtfc(self): - """save/load frame-cache format""" - fcfile = os.path.join(self.tmpdir, 'frame-cache.yml') - fmt = 'frame-cache' - thresh = 0.5 - - is_a = make_array_ims() - save.write(is_a, fcfile, fmt, - threshold=thresh, cache_file='frame-cache.npz') - is_fc = imageseries.open(fcfile, fmt) - diff = compare(is_a, is_fc) - self.assertAlmostEqual(diff, 0., "frame-cache reconstruction failed") - self.assertTrue(compare_meta(is_a, is_fc)) - - del is_fc - os.remove(fcfile) - - def test_fmtfc_nparray(self): - """frame-cache format with numpy array metadata""" - fcfile = os.path.join(self.tmpdir, 'frame-cache.yml') - fmt = 'frame-cache' - thresh = 0.5 - key = 'np-array' - npa = np.array([0,2.0,1.3]) - - is_a = make_array_ims() - is_a.metadata[key] = npa - save.write(is_a, fcfile, fmt, - threshold=thresh, cache_file='frame-cache.npz') - is_fc = imageseries.open(fcfile, fmt) - meta = is_fc.metadata - diff = np.linalg.norm(meta[key] - npa) - self.assertAlmostEqual(diff, 0., - "frame-cache numpy array metadata failed") - - - del is_fc - os.remove(fcfile) - -class TestImageSeriesProcess(unittest.TestCase): - - def _runfliptest(self, a, flip, aflip): - is_a = imageseries.open(None, 'array', data=a) - ops = [('flip', flip)] - is_p = process.ProcessedImageSeries(is_a, ops) - is_aflip = imageseries.open(None, 'array', data=aflip) - diff = compare(is_aflip, is_p) - msg = "flipped [%s] image series failed" % flip - self.assertAlmostEqual(diff, 0., msg=msg) - - def test_process(self): - """Processed image series""" - is_a = make_array_ims() - is_p = process.ProcessedImageSeries(is_a, []) - diff = compare(is_a, is_p) - msg = "processed image series failed to reproduce original" - self.assertAlmostEqual(diff, 0., msg) - - def test_process_flip_t(self): - """Processed image series: flip transpose""" - flip = 't' - a = make_array() - aflip = np.transpose(a, (0, 2, 1)) - self._runfliptest(a, flip, aflip) - - def test_process_flip_v(self): - """Processed image series: flip vertical""" - flip = 'v' - a = make_array() - aflip = a[:, :, ::-1] - self._runfliptest(a, flip, aflip) - - def test_process_flip_h(self): - """Processed image series: flip horizontal""" - flip = 'h' - a = make_array() - aflip = a[:, ::-1, :] - self._runfliptest(a, flip, aflip) - - def test_process_flip_vh(self): - """Processed image series: flip horizontal""" - flip = 'vh' - a = make_array() - aflip = a[:, ::-1, ::-1] - self._runfliptest(a, flip, aflip) - - def test_process_flip_r90(self): - """Processed image series: flip horizontal""" - flip = 'ccw90' - a = make_array() - aflip = np.transpose(a, (0, 2, 1))[:, :, ::-1] - self._runfliptest(a, flip, aflip) - - def test_process_flip_r270(self): - """Processed image series: flip horizontal""" - flip = 'cw90' - a = make_array() - aflip = np.transpose(a, (0, 2, 1))[:, ::-1, :] - self._runfliptest(a, flip, aflip) - - def test_process_dark(self): - """Processed image series: dark image""" - a = make_array() - dark = np.ones_like(a[0]) - is_a = imageseries.open(None, 'array', data=a) - apos = np.where(a >= 1, a-1, 0) - is_a1 = imageseries.open(None, 'array', data=apos) - ops = [('dark', dark)] - is_p = process.ProcessedImageSeries(is_a, ops) - diff = compare(is_a1, is_p) - self.assertAlmostEqual(diff, 0., msg="dark image failed") - - - def test_process_framelist(self): - a = make_array() - is_a = imageseries.open(None, 'array', data=a) - ops = [] - frames = [0, 2] - is_p = process.ProcessedImageSeries(is_a, ops, frame_list=frames) - print("1") - is_a2 = imageseries.open(None, 'array', data=a[(0,2), ...]) - print("lengths: ", len(is_p), len(is_a2), len(is_a)) - diff = compare(is_a2, is_p) - self.assertAlmostEqual(diff, 0., msg="frame list failed") - - -class TestImageSeriesStats(unittest.TestCase): - - def test_stats_median(self): - """Processed imageseries: median""" - a = make_array() - is_a = imageseries.open(None, 'array', data=a) - ismed = stats.median(is_a) - amed = np.median(a, axis=0) - err = np.linalg.norm(amed - ismed) - self.assertAlmostEqual(err, 0., msg="median image failed") - - def test_stats_max(self): - """Processed imageseries: median""" - a = make_array() - is_a = imageseries.open(None, 'array', data=a) - ismax = stats.max(is_a) - amax = np.max(a, axis=0) - err = np.linalg.norm(amax - ismax) - self.assertAlmostEqual(err, 0., msg="max image failed") - -# ==================== Utility functions - -def make_array(): - a = np.zeros(_NFXY) - ind = np.array([0,1,2]) - a[ind, 1,2] = 1 + ind - return a - -def make_meta(): - return {'testing': '1,2,3'} - -def make_array_ims(): - is_a = imageseries.open(None, 'array', data=make_array(), - meta=make_meta()) - return is_a - -def compare(ims1, ims2): - """compare two imageseries""" - if len(ims1) != len(ims2): - raise ValueError("lengths do not match") - - if ims1.dtype is not ims2.dtype: - raise ValueError("types do not match") - - maxdiff = 0.0 - for i in range(len(ims1)): - f1 = ims1[i] - f2 = ims2[i] - fdiff = np.linalg.norm(f1 - f2) - maxdiff = np.maximum(maxdiff, fdiff) - - return maxdiff - -def compare_meta(ims1, ims2): - # check metadata (simple immutable cases only for now) - - m1 = set(ims1.metadata.items()) - m2 = set(ims2.metadata.items()) - return m1.issubset(m2) and m2.issubset(m1) - -# ================================================== Execution -# -def set_options(): - """Set options for command line""" - parser = argparse.ArgumentParser(description='test hexrd.imageseries') - - return parser - -def execute(args): - """Main execution""" - p = set_options() - unittest.main() - - return - - -if __name__ == '__main__': - execute(sys.argv[1:]) diff --git a/hexrd/imageseries/tests/test_process.py b/hexrd/imageseries/tests/test_process.py new file mode 100644 index 00000000..3acef9db --- /dev/null +++ b/hexrd/imageseries/tests/test_process.py @@ -0,0 +1,90 @@ +import numpy as np + +from .common import ImageSeriesTest, make_array, make_array_ims, compare + +from hexrd import imageseries +from hexrd.imageseries import process, ImageSeries + +class TestImageSeriesProcess(ImageSeriesTest): + + def _runfliptest(self, a, flip, aflip): + is_a = imageseries.open(None, 'array', data=a) + ops = [('flip', flip)] + is_p = process.ProcessedImageSeries(is_a, ops) + is_aflip = imageseries.open(None, 'array', data=aflip) + diff = compare(is_aflip, is_p) + msg = "flipped [%s] image series failed" % flip + self.assertAlmostEqual(diff, 0., msg=msg) + + def test_process(self): + """Processed image series""" + is_a = make_array_ims() + is_p = process.ProcessedImageSeries(is_a, []) + diff = compare(is_a, is_p) + msg = "processed image series failed to reproduce original" + self.assertAlmostEqual(diff, 0., msg) + + def test_process_flip_t(self): + """Processed image series: flip transpose""" + flip = 't' + a = make_array() + aflip = np.transpose(a, (0, 2, 1)) + self._runfliptest(a, flip, aflip) + + def test_process_flip_v(self): + """Processed image series: flip vertical""" + flip = 'v' + a = make_array() + aflip = a[:, :, ::-1] + self._runfliptest(a, flip, aflip) + + def test_process_flip_h(self): + """Processed image series: flip horizontal""" + flip = 'h' + a = make_array() + aflip = a[:, ::-1, :] + self._runfliptest(a, flip, aflip) + + def test_process_flip_vh(self): + """Processed image series: flip horizontal""" + flip = 'vh' + a = make_array() + aflip = a[:, ::-1, ::-1] + self._runfliptest(a, flip, aflip) + + def test_process_flip_r90(self): + """Processed image series: flip horizontal""" + flip = 'ccw90' + a = make_array() + aflip = np.transpose(a, (0, 2, 1))[:, :, ::-1] + self._runfliptest(a, flip, aflip) + + def test_process_flip_r270(self): + """Processed image series: flip horizontal""" + flip = 'cw90' + a = make_array() + aflip = np.transpose(a, (0, 2, 1))[:, ::-1, :] + self._runfliptest(a, flip, aflip) + + def test_process_dark(self): + """Processed image series: dark image""" + a = make_array() + dark = np.ones_like(a[0]) + is_a = imageseries.open(None, 'array', data=a) + apos = np.where(a >= 1, a-1, 0) + is_a1 = imageseries.open(None, 'array', data=apos) + ops = [('dark', dark)] + is_p = process.ProcessedImageSeries(is_a, ops) + diff = compare(is_a1, is_p) + self.assertAlmostEqual(diff, 0., msg="dark image failed") + + + def test_process_framelist(self): + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ops = [] + frames = [0, 2] + is_p = process.ProcessedImageSeries(is_a, ops, frame_list=frames) + is_a2 = imageseries.open(None, 'array', data=a[tuple(frames), ...]) + diff = compare(is_a2, is_p) + self.assertAlmostEqual(diff, 0., msg="frame list failed") diff --git a/hexrd/imageseries/tests/test_properties.py b/hexrd/imageseries/tests/test_properties.py new file mode 100644 index 00000000..9b0a08a3 --- /dev/null +++ b/hexrd/imageseries/tests/test_properties.py @@ -0,0 +1,15 @@ +from .common import ImageSeriesTest, make_array, make_array_ims + +class TestProperties(ImageSeriesTest): + def setUp(self): + self._a = make_array() + self._is_a = make_array_ims() + + def test_prop_nframes(self): + self.assertEqual(self._a.shape[0], len(self._is_a)) + + def test_prop_shape(self): + self.assertEqual(self._a.shape[1:], self._is_a.shape) + + def test_prop_dtype(self): + self.assertEqual(self._a.dtype, self._is_a.dtype) diff --git a/hexrd/imageseries/tests/test_stats.py b/hexrd/imageseries/tests/test_stats.py new file mode 100644 index 00000000..6d6b0491 --- /dev/null +++ b/hexrd/imageseries/tests/test_stats.py @@ -0,0 +1,27 @@ +import numpy as np + +from hexrd import imageseries +from hexrd.imageseries import stats + +from .common import ImageSeriesTest, make_array, make_array_ims + + +class TestImageSeriesStats(ImageSeriesTest): + + def test_stats_median(self): + """Processed imageseries: median""" + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ismed = stats.median(is_a) + amed = np.median(a, axis=0) + err = np.linalg.norm(amed - ismed) + self.assertAlmostEqual(err, 0., msg="median image failed") + + def test_stats_max(self): + """Processed imageseries: median""" + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ismax = stats.max(is_a) + amax = np.max(a, axis=0) + err = np.linalg.norm(amax - ismax) + self.assertAlmostEqual(err, 0., msg="max image failed") From 3b205c76a15c952b866deca2e2d719333a9ab4ed Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 22 Nov 2015 10:45:18 -0500 Subject: [PATCH 049/253] dropped dark file and flip options from gereader --- hexrd/wx/gereader.py | 114 +++---------------------------------------- 1 file changed, 7 insertions(+), 107 deletions(-) diff --git a/hexrd/wx/gereader.py b/hexrd/wx/gereader.py index a430d0d9..68cbbc9b 100644 --- a/hexrd/wx/gereader.py +++ b/hexrd/wx/gereader.py @@ -54,17 +54,6 @@ } IMAGE_MODE_DICT_SEL = dict(zip(IMG_MODES, range(len(MODE_CHOICES)))) # -# * Dark file choices -# -DARK_CHO_NONE = 'no dark image' -DARK_CHO_FILE = 'dark image file' -DARK_CHO_ARRAY = 'dark frame array' -DARK_CHO_EMPTY = 'empty frames' -DARK_CHOICES = [DARK_CHO_NONE, DARK_CHO_FILE, DARK_CHO_ARRAY, DARK_CHO_EMPTY] -DARK_MODES = [ReaderInput.DARK_MODE_NONE, ReaderInput.DARK_MODE_FILE, ReaderInput.DARK_MODE_ARRAY, ReaderInput.DARK_MODE_EMPTY] -DARK_MODE_DICT = dict(zip(DARK_CHOICES, DARK_MODES)) -DARK_MODE_DICT_INV = dict(zip(DARK_MODES, DARK_CHOICES)) -# # * Aggregation choices # AGG_CHO_NONE = 'SINGLE FRAMES' @@ -75,18 +64,6 @@ AGG_MODE_DICT = dict(zip(AGG_CHOICES, ReaderInput.AGG_MODES)) AGG_MODE_DICT_INV = dict(zip(ReaderInput.AGG_MODES, AGG_CHOICES)) # -# * FLIP choices -# -FLIP_CHO_NONE = 'no flip' -FLIP_CHO_V = 'vertical' -FLIP_CHO_H = 'horizontal' -FLIP_CHO_180 = '180 degrees' -FLIP_CHO_M90 = '-90 degrees' -FLIP_CHO_P90 = '+90 degrees' -FLIP_CHOICES = [FLIP_CHO_NONE, FLIP_CHO_V, FLIP_CHO_H, FLIP_CHO_180, FLIP_CHO_M90, FLIP_CHO_P90] -FLIP_MODE_DICT = dict(zip(FLIP_CHOICES, ReaderInput.FLIP_MODES)) -FLIP_MODE_DICT_INV = dict(zip(ReaderInput.FLIP_MODES, FLIP_CHOICES)) -# # Utility vFunctions # def getValStr(r, i): @@ -177,7 +154,6 @@ def __makeObjects(self): 'Current Reader', style=wx.ALIGN_CENTER) self.rdrs_cho = wx.Choice(self, wx.NewId(), choices=[r.name for r in exp.savedReaders]) - #self.save_but = wx.Button(self, wx.NewId(), 'Save Reader') self.new_but = wx.Button(self, wx.NewId(), 'New Reader') # # Reader Name @@ -193,22 +169,18 @@ def __makeObjects(self): style=wx.ALIGN_RIGHT) self.mode_cho = wx.Choice(self, wx.NewId(), choices=MODE_CHOICES) # - # - # Image and dark file names - # - self.img_but = wx.Button(self, wx.NewId(), 'Select Image Files') - self.dir_but = wx.Button(self, wx.NewId(), 'Change Image Folder') - - self.drk_lab = wx.StaticText(self, wx.NewId(), 'Dark Mode', - style=wx.ALIGN_RIGHT) - self.drk_cho = wx.Choice(self, wx.NewId(), choices=DARK_CHOICES) - self.drk_but = wx.Button(self, wx.NewId(), 'Select Dark File') - # # Aggregation # self.agg_lab = wx.StaticText(self, wx.NewId(), 'Frame Aggregation', style=wx.ALIGN_RIGHT) self.agg_cho = wx.Choice(self, wx.NewId(), choices=AGG_CHOICES) + # + # + # Image and dark file names + # + self.img_but = wx.Button(self, wx.NewId(), 'Select Imageseries File') + self.dir_but = wx.Button(self, wx.NewId(), 'Change Image Folder') + # # Action buttons # @@ -225,12 +197,6 @@ def __makeObjects(self): style=wx.RAISED_BORDER|wx.TE_READONLY) self.sizer = wx.BoxSizer(wx.VERTICAL) # - # Orientation - # - self.flip_lab = wx.StaticText(self, wx.NewId(), 'Image Orientation', - style=wx.ALIGN_RIGHT) - self.flip_cho = wx.Choice(self, wx.NewId(), choices=FLIP_CHOICES) - # # Subpanels # self.sp_single = SF_Subpanel(self, wx.NewId()) @@ -245,23 +211,17 @@ def __makeBindings(self): self.Bind(wx.EVT_TEXT_ENTER, self.OnNameChange, self.name_txt) - self.Bind(wx.EVT_BUTTON, self.OnDarkBut, self.drk_but) self.Bind(wx.EVT_BUTTON, self.OnImgBut, self.img_but) self.Bind(wx.EVT_BUTTON, self.OnImgDirBut, self.dir_but) self.Bind(wx.EVT_BUTTON, self.OnReadBut, self.read_but) - self.Bind(wx.EVT_CHOICE, self.OnDarkChoice, self.drk_cho) self.Bind(wx.EVT_CHOICE, self.OnAggChoice, self.agg_cho) - self.Bind(wx.EVT_CHOICE, self.OnFlipChoice, self.flip_cho) self.Bind(wx.EVT_SPINCTRL, self.OnBrowseSpin, self.browse_spn) self.Bind(wx.EVT_CHOICE, self.OnReaderChoice, self.rdrs_cho) - #self.Bind(wx.EVT_BUTTON, self.OnReaderSave, self.save_but) self.Bind(wx.EVT_BUTTON, self.OnReaderNew, self.new_but) - return - def __makeSizers(self): """Lay out the interactors""" @@ -291,16 +251,6 @@ def __makeSizers(self): self.fgsizer.Add(wx.Window(self, -1), 0, wx.EXPAND|wx.ALIGN_CENTER) self.fgsizer.Add(self.agg_cho, 0, wx.EXPAND|wx.ALIGN_CENTER) - self.fgsizer.Add(self.flip_lab, 0, wx.ALIGN_RIGHT) - self.fgsizer.Add(wx.Window(self, -1), 0, wx.EXPAND|wx.ALIGN_CENTER) - self.fgsizer.Add(wx.Window(self, -1), 0, wx.EXPAND|wx.ALIGN_CENTER) - self.fgsizer.Add(self.flip_cho, 0, wx.EXPAND|wx.ALIGN_CENTER) - - self.fgsizer.Add(self.drk_lab, 0, wx.ALIGN_RIGHT) - self.fgsizer.Add(wx.Window(self, -1), 0, wx.EXPAND|wx.ALIGN_CENTER) - self.fgsizer.Add(self.drk_cho, 0, wx.EXPAND|wx.ALIGN_CENTER) - self.fgsizer.Add(self.drk_but, 0, wx.ALIGN_RIGHT) - self.fgsizer.Add(self.files_lab, 0, wx.ALIGN_RIGHT) self.fgsizer.AddSpacer(1) self.fgsizer.Add(self.dir_but, 0, wx.ALIGN_RIGHT) @@ -348,10 +298,6 @@ def update(self): self.mode_cho.SetSelection(IMAGE_MODE_DICT_SEL[mode]) # Agg choice self.agg_cho.SetStringSelection(AGG_MODE_DICT_INV[rdr.aggMode]) - # Image Orientation - self.flip_cho.SetStringSelection(FLIP_MODE_DICT_INV[rdr.flipMode]) - # Dark mode - self.drk_cho.SetStringSelection(DARK_MODE_DICT_INV[rdr.darkMode]) # Mode Subpanel self.sizer.Show(self.sp_single, (mode == ImageModes.SINGLE_FRAME)) @@ -476,23 +422,6 @@ def OnBrowseSpin(self, e): return - def OnDarkChoice(self, e): - """Dark mode choice has been made""" - val = e.GetString() - mode = DARK_MODE_DICT[val] - exp = wx.GetApp().ws - exp.activeReader.darkMode = mode - # - # Enable/disable other interactors - # - self.drk_but.Enable(mode == ReaderInput.DARK_MODE_FILE or mode == ReaderInput.DARK_MODE_ARRAY) - - # Update info window - - self.sp_info.update() - - return - def OnAggChoice(self, e): """Aggregation function selection""" val = e.GetString() @@ -522,35 +451,6 @@ def OnModeChoice(self, e): return - def OnFlipChoice(self, e): - """Flip mode chosen""" - print 'flip mode: ', e.GetString() - wx.GetApp().ws.activeReader.flipMode = FLIP_MODE_DICT[e.GetString()] - return - - def OnDarkBut(self, e): - """Load dark file names with file dialogue""" - # - # !! Check that "subtract dark" is true - # - dlg = wx.FileDialog(self, 'Select Dark Image') - if dlg.ShowModal() == wx.ID_OK: - dir = str(dlg.GetDirectory()) - fil = str(dlg.GetFilename()) - if (fil): - # - # Set dark file and display name in info box. - # - exp = wx.GetApp().ws - exp.activeReader.darkDir = dir - exp.activeReader.darkName = fil - self.sp_info.update() - pass - pass - dlg.Destroy() - - return - def OnImgBut(self, e): """Load image file names with file dialogue From 614c5ca0d516f45a196eb0abb096449528d7788f Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 22 Nov 2015 21:07:39 -0500 Subject: [PATCH 050/253] updated wx gui to use new imageseries readers --- hexrd/wx/gereader.py | 43 ++------- hexrd/wx/readerinfo_dlg.py | 186 +++++++++++++++++++++++++++++++++++++ hexrd/xrd/experiment.py | 60 +----------- 3 files changed, 199 insertions(+), 90 deletions(-) create mode 100644 hexrd/wx/readerinfo_dlg.py diff --git a/hexrd/wx/gereader.py b/hexrd/wx/gereader.py index 68cbbc9b..12d8b58e 100644 --- a/hexrd/wx/gereader.py +++ b/hexrd/wx/gereader.py @@ -38,6 +38,7 @@ from hexrd.wx.guiconfig import WindowParameters as WP from hexrd.wx.guiutil import ResetChoice, makeTitleBar from hexrd.wx.canvaspanel import CanvasPanel +from hexrd.wx.readerinfo_dlg import ReaderInfoDialog # # DATA # @@ -456,38 +457,18 @@ def OnImgBut(self, e): NOTE: converts filenames to str from unicode """ - dlg = wx.FileDialog(self, 'Select Images', - style=wx.FD_MULTIPLE) + dlg = ReaderInfoDialog(self, -1) if dlg.ShowModal() == wx.ID_OK: - d = str(dlg.GetDirectory()) - fnames = [str(p) for p in dlg.GetFilenames()] - if (fnames): - # - # Set image file list and display name in box. - # - fnames.sort() - exp = wx.GetApp().ws - print d, fnames - exp.activeReader.imageDir = d - exp.activeReader.imageNames = fnames - - pass + d = dlg.GetInfo() + exp = wx.GetApp().ws + exp.activeReader.imageDir = d.pop('directory') + exp.activeReader.imageNames = [d.pop('file')] + exp.activeReader.imageFmt = d.pop('format') + exp.activeReader.imageOpts = d self.update() - pass dlg.Destroy() - return - - def OnDrkSubtract(self, e): - """Subtract dark checkbox - - * No other effects until read image button is pressed -""" - wx.GetApp().ws.drkSubtract = self.drk_box.GetValue() - - return - def OnReadBut(self, e): """Read the frames""" @@ -767,11 +748,6 @@ def __makeObjects(self): # File lists for display. # - self.drk_txt_lab = wx.StaticText(self, wx.NewId(), 'Dark File', - style=wx.ALIGN_CENTER) - self.drk_txt = wx.TextCtrl(self, wx.NewId(), value='', - style=wx.RAISED_BORDER) - self.img_txt_lab = wx.StaticText(self, wx.NewId(), 'Image Directory', style=wx.ALIGN_CENTER) self.img_txt = wx.TextCtrl(self, wx.NewId(), value='', @@ -793,8 +769,6 @@ def __makeSizers(self): self.fgsizer.Add(self.img_txt_lab, 0, wx.ALIGN_RIGHT|wx.RIGHT, 5) self.fgsizer.Add(self.img_txt, 1, wx.EXPAND|wx.ALIGN_CENTER) - self.fgsizer.Add(self.drk_txt_lab, 0, wx.ALIGN_RIGHT|wx.RIGHT, 5) - self.fgsizer.Add(self.drk_txt, 1, wx.EXPAND|wx.ALIGN_CENTER) # # Main sizer # @@ -812,7 +786,6 @@ def __makeSizers(self): def update(self): """Update information""" exp = wx.GetApp().ws - self.drk_txt.SetValue(exp.activeReader.darkFile) self.img_txt.SetValue(exp.activeReader.imageDir) return diff --git a/hexrd/wx/readerinfo_dlg.py b/hexrd/wx/readerinfo_dlg.py new file mode 100644 index 00000000..f16eba6c --- /dev/null +++ b/hexrd/wx/readerinfo_dlg.py @@ -0,0 +1,186 @@ +"""panel for reader input""" +import wx + +from guiconfig import WindowParameters as WP +from guiutil import makeTitleBar +# +# ---------------------------------------------------CLASS: ReaderInfoPanel +# +class ReaderInfoPanel(wx.Panel): + """ReaderInfoPanel """ + def __init__(self, parent, id, **kwargs): + + wx.Panel.__init__(self, parent, id, **kwargs) + # + # Data + # + self.image_dir = '' + self.image_fname = '' + # + # Window Objects. + # + self.__make_objects() + # + # Bindings. + # + self.__make_bindings() + # + # Sizing. + # + self.__make_sizers() + # + self.SetAutoLayout(True) + self.SetSizerAndFit(self.sizer) + # + return + # + # ============================== Internal Methods + # + def __make_objects(self): + """Add interactors""" + + self.tbarSizer = makeTitleBar(self, 'Reader Info') + self.file_but = wx.Button(self, wx.NewId(), + 'File' + ) + self.file_txt = wx.TextCtrl(self, wx.NewId(), + value="", + style=wx.RAISED_BORDER|wx.TE_READONLY + ) + self.format_lab = wx.StaticText(self, wx.NewId(), + 'Format', style=wx.ALIGN_RIGHT + ) + self.format_cho = wx.Choice(self, wx.NewId(), + choices=['hdf5', 'frame-cache'] + ) + self.option_lab = wx.StaticText(self, wx.NewId(), + 'Option', style=wx.ALIGN_RIGHT + ) + self.value_lab = wx.StaticText(self, wx.NewId(), + 'Value', style=wx.ALIGN_LEFT + ) + self.option_cho = wx.Choice(self, wx.NewId(), + choices=['path'] + ) + self.value_txt = wx.TextCtrl(self, wx.NewId(), + value="/imageseries", + style=wx.RAISED_BORDER + ) + + def __make_bindings(self): + """Bind interactors""" + self.Bind(wx.EVT_BUTTON, self.OnFileBut, self.file_but) + + def __make_sizers(self): + """Lay out the interactors""" + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.sizer.Add(self.tbarSizer, 0, wx.EXPAND|wx.ALIGN_CENTER) + + nrow = 4; ncol = 2; padx = 5; pady = 5 + self.info_sz = wx.FlexGridSizer(nrow, ncol, padx, pady) + self.info_sz.AddGrowableCol(0, 0) + self.info_sz.AddGrowableCol(1, 1) + self.info_sz.Add(self.file_but, 0, wx.ALIGN_RIGHT) + self.info_sz.Add(self.file_txt, 0, wx.ALIGN_LEFT|wx.EXPAND) + self.info_sz.Add(self.format_lab, 0, wx.ALIGN_RIGHT) + self.info_sz.Add(self.format_cho, 0, wx.ALIGN_LEFT|wx.EXPAND) + self.info_sz.Add(self.option_lab, 0, wx.ALIGN_RIGHT) + self.info_sz.Add(self.value_lab, 0, wx.ALIGN_LEFT) + self.info_sz.Add(self.option_cho, 0, wx.ALIGN_RIGHT) + self.info_sz.Add(self.value_txt, 0, wx.ALIGN_LEFT|wx.EXPAND) + + + self.sizer.Add(self.info_sz, 1, wx.EXPAND) + # + # ============================== API + # + # ========== *** Access Methods + # + + # + # ========== *** Event Callbacks + # + def OnFileBut(self, e): + """Load image file name with file dialogue + + NOTE: converts filenames to str from unicode +""" + dlg = wx.FileDialog(self, 'Select Imageseries File', + style=wx.FD_FILE_MUST_EXIST) + if dlg.ShowModal() == wx.ID_OK: + self.image_dir = str(dlg.GetDirectory()) + self.image_fname = dlg.GetFilename() + self.file_txt.SetValue(self.image_fname) + dlg.Destroy() + + pass # end class +# -----------------------------------------------END CLASS: ReaderInfoPanel +# ---------------------------------------------------CLASS: ReaderInfoDlg +# +class ReaderInfoDialog(wx.Dialog): + """Pop-Up for reader file, format and options """ + def __init__(self, parent, id, **kwargs): + """Constructor""" + # + myStyle = wx.RESIZE_BORDER|wx.DEFAULT_DIALOG_STYLE + wx.Dialog.__init__(self, parent, id, style=myStyle) + # + # Data Objects. + # + + # + # Windows. + # + self.tbarSizer = makeTitleBar(self, 'Reader Info', + color=WP.BG_COLOR_TITLEBAR_FRAME) + self.infopanel = ReaderInfoPanel(self, -1) + # + # Bindings. + # + self._makeBindings() + # + # Sizing. + # + self._makeSizers() + # + self.SetAutoLayout(True) + self.SetSizerAndFit(self.sizer) + # + # Events. + # + + # + return + # + # ============================== Internal Methods + # + def _makeBindings(self): + """Bind interactors to functions""" + return + + def _makeSizers(self): + """Lay out windows""" + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.sizer.Add(self.tbarSizer, 0, wx.EXPAND|wx.ALIGN_CENTER) + self.sizer.Add(self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL)) + self.sizer.Add(self.infopanel, 1, wx.EXPAND|wx.ALIGN_CENTER) + # + # ============================== API + # + # ========== *** Access Methods + # + def GetInfo(self): + p = self.infopanel + d = dict( + directory = p.image_dir, + file = p.image_fname, + format = p.format_cho.GetStringSelection(), + path = p.value_txt.GetValue() + ) + return d + + pass # end class +# +# -----------------------------------------------END CLASS: ReaderInfoDlg +# diff --git a/hexrd/xrd/experiment.py b/hexrd/xrd/experiment.py index bce4ceda..23a340f2 100644 --- a/hexrd/xrd/experiment.py +++ b/hexrd/xrd/experiment.py @@ -991,6 +991,8 @@ def __init__(self, name='reader', desc='no description'): self.imageDir = '' self.imageNames = [] self.imageNameD = dict() + self.imageFmt = None + self.imageOpts = {} # Dark file self.darkMode = ReaderInput.DARK_MODE_NONE self.darkDir = '' @@ -1005,7 +1007,6 @@ def __init__(self, name='reader', desc='no description'): def _check(self): """Check that input is ok for making a reader instance """ - # * Empty frames = 0 for single frame mode return # # ============================== API @@ -1091,63 +1092,12 @@ def getNumberOfFrames(self): return n def makeReader(self): - """Return a reader instance based on self -""" + """Return a reader instance based on self""" # check validity of input self._check() - # - # Set up image names in right format - # - fullPath = lambda fn: os.path.join(self.imageDir, fn) - numEmpty = lambda fn: self.imageNameD[fn][0] - imgInfo = [(fullPath(f), numEmpty(f)) for f in self.imageNames] - ref_reader = self.RC(imgInfo) - # - # Check for omega info - # - nfile = len(imgInfo) - dinfo = [self.imageNameD[f] for f in self.imageNames] - omin = dinfo[0][1] - if omin is not None: - odel = dinfo[nfile - 1][3] - print "omega min and delta: ", omin, odel - omargs = (valunits.valWUnit('omin', 'angle', float(omin), 'degrees'), - valunits.valWUnit('odel', 'angle', float(odel), 'degrees')) - else: - omargs = () - pass - print 'omargs: ', omargs - # - # Dark file - # - subDark = not (self.darkMode == ReaderInput.DARK_MODE_NONE) - if (self.darkMode == ReaderInput.DARK_MODE_FILE): - drkFile = os.path.join(self.darkDir, self.darkName) - elif (self.darkMode == ReaderInput.DARK_MODE_ARRAY): - drkFileName = os.path.join(self.darkDir, self.darkName) - drkFile = ref_reader.frame( - buffer=numpy.fromfile(drkFileName, - dtype=ref_reader.dtypeRead - ) - ) - else: - drkFile = None - pass - # - # Flip options - # - doFlip = not (self.flipMode == ReaderInput.FLIP_NONE) - flipArg = ReaderInput.FLIP_DICT[self.flipMode] - # - # Make the reader - # - print 'reader: \n', imgInfo, subDark, drkFile, doFlip, flipArg - r = self.RC(imgInfo, *omargs, - subtractDark = subDark, - dark = drkFile, - doFlip = doFlip, - flipArg = flipArg) + imagePath = os.path.join(self.imageDir, self.imageNames[0]) + r = self.RC(imagePath, fmt=self.imageFmt, **self.imageOpts) return r # From f503665c11effd28d0e4e48476acdbef9e47f03a Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Fri, 4 Dec 2015 16:07:20 -0500 Subject: [PATCH 051/253] added developers documentation for imageseries --- docs/source/dev/imageseries.rst | 17 +++++++++++++++++ docs/source/dev/index.rst | 1 + 2 files changed, 18 insertions(+) create mode 100644 docs/source/dev/imageseries.rst diff --git a/docs/source/dev/imageseries.rst b/docs/source/dev/imageseries.rst new file mode 100644 index 00000000..3c29db35 --- /dev/null +++ b/docs/source/dev/imageseries.rst @@ -0,0 +1,17 @@ +imageseries package +=============== +The *imageseries* package provides a standard API for accessing image-based data sets. The primary tool in the package is the ImageSeries class. It's interface is analagous to a list of images with associated image metadata. The number of images is given by the len() function. Properties are defined for image shape (shape), data type (dtype) and metadata (metadata). Individual images are accessed by standard subscripting (e.g. image[i]). + +The package contains interfaces for loading (load) and saving (save) imageseries. Images can be loaded in three formats: 'array', 'hdf5' and 'frame-cache'. The 'array' format takes the images from a 3D numpy array. With 'hdf5', images are stored in hdf5 file and accessed on demand. The 'frame-cache' is a list of sparse matrices, useful for thresholded images. An imageseries can be saved in 'hdf5' or 'frame-cache' format. + +The imageseries package also contains a module for modifying the images (process). The process module provides the ProcessedImageSeries class, which takes a given imageseries and produces a new one by modifying the images. It has certain built-in image operations including transposition, flipping, dark subtraction and restriction to a subset. + + +Metadata +---------------- + +The metadata property is generically a dictionary. The actual contents depends on the application. For common hexrd applications in which the specimen is rotated while being exposed to x-rays, the metadata has an 'omega' key with an associated value being an nx2 numpy array where n is the number of frames and the two associated values give the omega (rotation) range for that frame. + +Reader Refactor +------------- +While the imageseries package is in itself indpendent of hexrd, it was used as the basis of a refactoring of the reader classes originally found in the detector module. The main reader class was ReadGE. In the refactored code, the reader classes are now in their own module, image_io, but imported into detector to preserve the interface. The image_io module contains a generic OmegaImageSeries class for working with imageseries having omega metadata. The refactored ReadGE class simply uses the OmegaImageSeries class to provide the same methods as the old class. New code should use the OmegaImageSeries (or the standard ImageSeries) class directly. diff --git a/docs/source/dev/index.rst b/docs/source/dev/index.rst index e47a0606..cbc17adb 100644 --- a/docs/source/dev/index.rst +++ b/docs/source/dev/index.rst @@ -9,3 +9,4 @@ Contents: getting_started releases + imageseries From 795fb07043316c8ea2720813e44d370b34036b44 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 8 Sep 2016 15:42:25 -0700 Subject: [PATCH 052/253] Update meta.yaml require h5py to build --- conda.recipe/meta.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 67ff4ff1..270f37f9 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -21,12 +21,13 @@ app: requirements: build: # - nomkl # in case MKL is broken on Linux + - h5py - numpy - python - setuptools run: - - h5py - dill + - h5py - matplotlib # - nomkl # in case MKL is broken on Linux - numba From 040a92a897654b72821cdb61a58e0ac54944dfd3 Mon Sep 17 00:00:00 2001 From: darrencpagan Date: Tue, 4 Oct 2016 14:43:54 -0700 Subject: [PATCH 053/253] Adding new scripts and a few matrix utilities. --- hexrd/test.txt | 0 scripts/gen_schmid_tensors.py | 133 +++++++++++++++ scripts/post_process_stress.py | 154 ++++++++++++++++++ scripts/stitch_grains_files.py | 218 +++++++++++++++++++++++++ scripts/strength_extraction.py | 159 ++++++++++++++++++ scripts/virtual_diffractometer.py | 261 ++++++++++++++++++++++++++++++ 6 files changed, 925 insertions(+) create mode 100644 hexrd/test.txt create mode 100644 scripts/gen_schmid_tensors.py create mode 100644 scripts/post_process_stress.py create mode 100644 scripts/stitch_grains_files.py create mode 100644 scripts/strength_extraction.py create mode 100644 scripts/virtual_diffractometer.py diff --git a/hexrd/test.txt b/hexrd/test.txt new file mode 100644 index 00000000..e69de29b diff --git a/scripts/gen_schmid_tensors.py b/scripts/gen_schmid_tensors.py new file mode 100644 index 00000000..750c7433 --- /dev/null +++ b/scripts/gen_schmid_tensors.py @@ -0,0 +1,133 @@ +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on downloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +#%% +import sys + +import argparse + +import numpy as np +import cPickle as cpl + + +from hexrd import matrixutil as mutil +from hexrd.xrd import symmetry as sym + +#%% +def gen_schmid_tensors(pd,uvw,hkl): + + # slip plane directions + slipdir = mutil.unitVector( np.dot( pd.latVecOps['F'], uvw) ) # 2 -1 -1 0 + slipdir_sym = sym.applySym(slipdir, pd.getQSym(), csFlag=False, cullPM=True, tol=1e-08) + + # slip plane plane normals + n_plane = mutil.unitVector( np.dot( pd.latVecOps['B'], hkl ) ) + n_plane_sym = sym.applySym(n_plane, pd.getQSym(), csFlag=False, cullPM=True, tol=1e-08) + + + num_slip_plane= n_plane_sym.shape[1] + + num_slip_sys=0 + for i in range(num_slip_plane): + planeID = np.where(abs(np.dot(n_plane_sym[:, i],slipdir_sym)) < 1.e-8)[0] + num_slip_sys +=planeID.shape[0] + + T= np.zeros((num_slip_sys, 3, 3)) + counter=0 + # + for i in range(num_slip_plane): + planeID = np.where(abs(np.dot(n_plane_sym[:, i],slipdir_sym)) < 1.e-8)[0] + for j in np.arange(planeID.shape[0]): + T[counter, :, :] = np.dot(slipdir_sym[:, planeID[j]].reshape(3, 1), n_plane_sym[:, i].reshape(1, 3)) + counter+=1 + #Clean some round off errors + round_off_err=np.where(abs(T)<1e-8) + T[round_off_err[0],round_off_err[1],round_off_err[2]]=0. + + return T + + +#%% + +if __name__ == '__main__': + """ + USAGE : python genschmidtensors material_file material_name uvw hkl output_file_stem + """ + parser = argparse.ArgumentParser(description='Generate a set of schmid tensors for a given slip direction [uvw] and slip plane (hkl)') + + + parser.add_argument('mat_file_loc', type=str) + parser.add_argument('mat_name', type=str) + parser.add_argument('uvw', type=str) + parser.add_argument('hkl', type=str) + parser.add_argument('out_file', type=str) + + + args = vars(parser.parse_args(sys.argv[1:])) + + + mat_list = cpl.load(open(args['mat_file_loc'], 'r')) + + # need to find the index of the active material + # ***PROBABLY WILL CHANGE TO DICT INSTEAD OF LIST + mat_idx = np.where([mat_list[i].name == args['mat_name'] for i in range(len(mat_list))])[0] + + # grab plane data, and useful things hanging off of it + pd = mat_list[mat_idx[0]].planeData + + uvw=np.zeros([3,1]) + sign=1. + increment=0 + for ii in args['uvw']: + if ii =='-': + sign=-1. + else: + uvw[increment,0]=sign*float(ii) + sign=1. + increment+=1 + + hkl=np.zeros([3,1]) + sign=1. + increment=0 + for ii in args['hkl']: + if ii =='-': + sign=-1. + else: + hkl[increment,0]=sign*float(ii) + sign=1. + increment+=1 + + + T=gen_schmid_tensors(pd,uvw,hkl) + + + f=open(args['out_file']+'.txt','w') + + for i in np.arange(T.shape[0]): + f.write("%.12e %.12e %.12e %.12e %.12e %.12e %.12e %.12e %.12e \n" % \ + (T[i,0,0],T[i,0,1],T[i,0,2],T[i,1,0],T[i,1,1],T[i,1,2],T[i,2,0],T[i,2,1],T[i,2,2])) + f.close() + diff --git a/scripts/post_process_stress.py b/scripts/post_process_stress.py new file mode 100644 index 00000000..fd6d9223 --- /dev/null +++ b/scripts/post_process_stress.py @@ -0,0 +1,154 @@ +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on downloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +#%% +import sys +import cPickle as cpl + +import numpy as np + +import argparse + +from hexrd import matrixutil as mutil +from hexrd.xrd import rotations as rot + +#%% Extract stress data from grains.out data + + +def post_process_stress(grain_data,c_mat_C,schmid_T_list=None): + num_grains=grain_data.shape[0] + + stress_S=np.zeros([num_grains,6]) + stress_C=np.zeros([num_grains,6]) + hydrostatic=np.zeros([num_grains,1]) + pressure=np.zeros([num_grains,1]) + von_mises=np.zeros([num_grains,1]) + + if schmid_T_list is not None: + num_slip_systems=schmid_T_list.shape[0] + RSS=np.zeros([num_grains,num_slip_systems]) + + + for jj in np.arange(num_grains): + + expMap=np.atleast_2d(grain_data[jj,3:6]).T + strainTmp=np.atleast_2d(grain_data[jj,15:21]).T + + #Turn exponential map into an orientation matrix + Rsc=rot.rotMatOfExpMap(expMap) + + strainTenS = np.zeros((3, 3), dtype='float64') + strainTenS[0, 0] = strainTmp[0] + strainTenS[1, 1] = strainTmp[1] + strainTenS[2, 2] = strainTmp[2] + strainTenS[1, 2] = strainTmp[3] + strainTenS[0, 2] = strainTmp[4] + strainTenS[0, 1] = strainTmp[5] + strainTenS[2, 1] = strainTmp[3] + strainTenS[2, 0] = strainTmp[4] + strainTenS[1, 0] = strainTmp[5] + + + strainTenC=np.dot(np.dot(Rsc.T,strainTenS),Rsc) + strainVecC = mutil.strainTenToVec(strainTenC) + + + #Calculate stress + stressVecC=np.dot(c_mat_C,strainVecC) + stressTenC = mutil.stressVecToTen(stressVecC) + stressTenS = np.dot(np.dot(Rsc,stressTenC),Rsc.T) + stressVecS = mutil.stressTenToVec(stressTenS) + + #Calculate hydrostatic stress + hydrostaticStress=(stressVecS[:3].sum()/3) + + + #Calculate Von Mises Stress + devStressS=stressTenS-hydrostaticStress*np.identity(3) + vonMisesStress=np.sqrt((3/2)*(devStressS**2).sum()) + + + #Project on to slip systems + if schmid_T_list is not None: + for ii in np.arange(num_slip_systems): + RSS[jj,ii]=np.abs((stressTenC*schmid_T_list[ii,:,:]).sum()) + + + stress_S[jj,:]=stressVecS.flatten() + stress_C[jj,:]=stressVecC.flatten() + + hydrostatic[jj,0]=hydrostaticStress + pressure[jj,0]=-hydrostaticStress + von_mises[jj,0]=vonMisesStress + + stress_data=dict() + + stress_data['stress_S']=stress_S + stress_data['stress_C']=stress_C + stress_data['hydrostatic']=hydrostatic + stress_data['pressure']=pressure + stress_data['von_mises']=von_mises + + if schmid_T_list is not None: + stress_data['RSS']=RSS + + return stress_data + +#%% Command Line Access +if __name__ == '__main__': + """ + USAGE : python post_process_stress grains_file stiffness_mat_file output_file_stem schmid_tensors_file + """ + parser = argparse.ArgumentParser(description='Post Process HEXRD Grains File To Extract Stress Tensor and Associated Quantities (Assuming Small Strain, Linear Elasticty)') + + + parser.add_argument('grains_file', type=str) + parser.add_argument('stiffness_mat_file', type=str) + parser.add_argument('output_file_stem', type=str) + parser.add_argument('--schmid_tensors_file', type=str, default=None) + + args = vars(parser.parse_args(sys.argv[1:])) + + + grain_data=np.loadtxt(args['grains_file']) + + c_mat=np.loadtxt(args['stiffness_mat_file']) + + #Extract Schmid Tensors from txt file + if args['schmid_tensors_file'] is not None: + T_vec = np.atleast_2d(np.loadtxt(args['schmid_tensors_file'])) + num_ten=T_vec.shape[0] + T=np.zeros([num_ten,3,3]) + for i in np.arange(num_ten): + T[i,:,:]=T_vec[i,:].reshape([3,3]) + + stress_data=post_process_stress(grain_data,c_mat,T) + + else: + stress_data=post_process_stress(grain_data,c_mat) + + + cpl.dump(stress_data, open( args['output_file_stem']+'.cpl', "wb" ) ) diff --git a/scripts/stitch_grains_files.py b/scripts/stitch_grains_files.py new file mode 100644 index 00000000..a5b9b12c --- /dev/null +++ b/scripts/stitch_grains_files.py @@ -0,0 +1,218 @@ +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on downloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +#%% User Input +############################################################################### +hexrd_script_directory='###' + +material_file_loc='###' # hexrd material file in cpickle format +mat_name='###' + +grain_file_locs=['###',\ + '###']#Can be more than 2 + +output_data=True +output_dir='###' + + +#Position and Misorientation differences to merge grains +dist=0.05 #mm +misorientation=1. #degrees +completeness_diff=0.1 #if two grains are matched, the completenesses are checked, +#if the differences in completion are within completeness_diff, the grain values are averaged, +#if not, the data from the grain with higher completion is kept and the other data is discarded + + +#Offsets, these can be input as arguments +#Each dataset can have positional or rotation offsets +#Position offsets are in mm, 3 x n matrix where n is the number of grains.out files being stitched +#Rotation offsets exponential maps + +#3 x 2 examples +#pos_offset=np.array([[0.,0.],[0.,0.],[0.,0.]]) +#rot_offset=np.array([[0.,0.],[0.,0.],[0.,0.]]) +pos_offset=None +rot_offset=None + +############################################################################### + +#%% #Import Modules +import os + +import numpy as np + +import copy + +import cPickle as cpl + +from hexrd import matrixutil as mutil +from hexrd.xrd import rotations as rot +from hexrd.xrd import symmetry as sym + + + +#%% + +def remove_duplicate_grains(grain_data,qsyms,dist_thresh=0.01,misorient_thresh=0.1,comp_diff=0.1): + total_grains=grain_data.shape[0] + + all_comp=grain_data[:,1] + grain_quats=rot.quatOfExpMap(grain_data[:,3:6].T) + dup_list=np.array([]) + + print 'Removing duplicate grains' + for i in np.arange(total_grains-1): + cur_pos=grain_data[i,3:6] + other_pos=grain_data[(i+1):,3:6] + xdist=cur_pos[0]-other_pos[:,0] + ydist=cur_pos[1]-other_pos[:,1] + zdist=cur_pos[2]-other_pos[:,2] + + dist=np.sqrt(xdist**2.+ydist**2.+zdist**2.) + + if np.min(dist)0): + grain_data[i,:]=grain_data[np.argmin(dist)+i+1,:] + + grain_data=np.delete(grain_data,dup_list,axis=0) + + print 'Removed %d Grains' % (len(dup_list)) + + grain_data[:,0]=np.arange(grain_data.shape[0]) + + return grain_data + + + + +#%% + + +def assemble_grain_data(grain_data_list,pos_offset=None,rotation_offset=None): + num_grain_files=len(grain_data_list) + + num_grains_list=[None]*num_grain_files + + for i in np.arange(num_grain_files): + num_grains_list[i]=grain_data_list[i].shape[0] + + num_grains=np.sum(num_grains_list) + + grain_data=np.zeros([num_grains,grain_data_list[0].shape[1]]) + + for i in np.arange(num_grain_files): + + tmp=copy.copy(grain_data_list[i]) + + if pos_offset is not None: + pos_tile=np.tile(pos_offset[:,i],[num_grains_list[i],1]) + tmp[:,6:9]=tmp[:,6:9]+pos_tile + #Needs Testing + if rotation_offset is not None: + rot_tile=np.tile(np.atleast_2d(rotation_offset[:,i]).T,[1,num_grains_list[i]]) + quat_tile=rot.quatOfExpMap(rot_tile) + grain_quats=rot.quatOfExpMap(tmp[:,3:6].T) + new_quats=rot.quatProduct(grain_quats,quat_tile) + + sinang = mutil.columnNorm(new_quats[1:,:]) + ang=2.*np.arcsin(sinang) + axis = mutil.unitVector(new_quats[1:,:]) + tmp[:,3:6]=np.tile(np.atleast_2d(ang).T,[1,3])*axis.T + + + grain_data[int(np.sum(num_grains_list[:i])):int(np.sum(num_grains_list[:(i+1)])),:]=tmp + + + grain_data[:,0]=np.arange(num_grains) + return grain_data + + + +#%% Load data + +mat_list = cpl.load(open(material_file_loc, 'r')) +mat_idx = np.where([mat_list[i].name == mat_name for i in range(len(mat_list))])[0] + +# grab plane data, and useful things hanging off of it +pd = mat_list[mat_idx[0]].planeData +qsyms=sym.quatOfLaueGroup(pd.getLaueGroup()) + + +num_grain_files=len(grain_file_locs) + +grain_data_list=[None]*num_grain_files + +for i in np.arange(num_grain_files): + + + grain_data_list[i]=np.loadtxt(os.path.join(grain_file_locs[i],'grains.out')) + +grain_data=assemble_grain_data(grain_data_list,pos_offset,rot_offset) + +grain_data=remove_duplicate_grains(grain_data,qsyms,dist,misorientation,completeness_diff) + + +#%% Write data + +if output_data: + f = open(os.path.join(output_dir, 'grains.out'), 'w') + + header_items = ( + 'grain ID', 'completeness', 'chi2', + 'xi[0]', 'xi[1]', 'xi[2]', 'tVec_c[0]', 'tVec_c[1]', 'tVec_c[2]', + 'vInv_s[0]', 'vInv_s[1]', 'vInv_s[2]', 'vInv_s[4]*sqrt(2)', + 'vInv_s[5]*sqrt(2)', 'vInv_s[6]*sqrt(2)', 'ln(V[0,0])', + 'ln(V[1,1])', 'ln(V[2,2])', 'ln(V[1,2])', 'ln(V[0,2])', 'ln(V[0,1])', + ) + len_items = [] + for i in header_items[1:]: + temp = len(i) + len_items.append(temp if temp > 19 else 19) # for %19.12g + fmtstr = '#%13s ' + ' '.join(['%%%ds' % i for i in len_items]) + '\n' + f.write(fmtstr % header_items) + for i in grain_data.shape[0]: + res_items = ( + grain_data[i,0], grain_data[i,1], grain_data[i,2], grain_data[i,3], grain_data[i,4], grain_data[i,5], + grain_data[i,6], grain_data[i,7], grain_data[i,8], grain_data[i,9], + grain_data[i,10], grain_data[i,11], grain_data[i,12], grain_data[i,13], + grain_data[i,14], grain_data[i,15], grain_data[i,16], grain_data[i,17], grain_data[i,18], + grain_data[i,19], grain_data[i,20], + ) + fmtstr = ( + '%14d ' + ' '.join(['%%%d.12g' % i for i in len_items]) + '\n' + ) + f.write(fmtstr % res_items) + + f.close() diff --git a/scripts/strength_extraction.py b/scripts/strength_extraction.py new file mode 100644 index 00000000..d334e426 --- /dev/null +++ b/scripts/strength_extraction.py @@ -0,0 +1,159 @@ +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on downloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +#%% User Input +############################################################################### +hexrd_script_directory='###' #Needs post_process_stress.py from the scripts directory + +c_mat_C_file='###' # text file containing the stiffness matrix in the crystal coordinate system (6x6) + +schmid_tensor_directory='###' +schmid_tensor_files=['###',"###'] + +num_load_steps=### + +processed_data_directory='###' +analysis_stem='###' + + + +############################################################################### +#%% #Import Modules +import os, sys + +import numpy as np + +import copy + +from matplotlib import pyplot as plt + +import hexrd.fitting.fitpeak as fitpeaks + +from scipy.stats import gaussian_kde + +sys.path.append(hexrd_script_directory) +import post_process_stress as stress_proc + + + +#%% Function For Extracting Strength +############################################################################### + + + +def extract_strength(stress_data,ss_bnds,completeness_mat=None,threshold=0.8): + + tau_star=np.zeros(len(stress_data)) + w_tau=np.zeros(len(stress_data)) + for i in np.arange(len(stress_data)): + + if completeness_mat is not None: + grains_to_use=np.where(completeness_mat[i]>threshold)[0] + max_rss=np.max(stress_data[i]['RSS'][grains_to_use,ss_bnds[0]:ss_bnds[-1]],1) + else: + max_rss=np.max(stress_data[i]['RSS'][:,ss_bnds[0]:ss_bnds[-1]],1) + + tau=np.linspace(0., 1.5*np.max(max_rss), num=2000) + + G = gaussian_kde(max_rss) + tau_pdf = G.evaluate(tau) + + + maxPt=np.argmax(tau_pdf) + tmp_pdf=copy.copy(tau_pdf) + tmp_pdf[:maxPt]=np.max(tau_pdf) + + pfit=fitpeaks.fit_pk_parms_1d([np.max(tau_pdf),tau[maxPt]+1e7,45e6],tau,tmp_pdf,pktype='tanh_stepdown') + + tau_star[i]=pfit[1] + w_tau[i]=pfit[2] + + return tau_star, w_tau + + +#%% Function For Extracting Strength + +def plot_strength_curve(tau_star,w_tau,macro_strain=None,plot_color='blue'): + if macro_strain is None: + macro_strain=np.arange(len(tau_star)) + + strain_fine=np.linspace(macro_strain[0],macro_strain[-1],1000) + interp_tau_star=np.interp(strain_fine,macro_strain,tau_star) + interp_w_tau=np.interp(strain_fine,macro_strain,w_tau) + + + plt.errorbar(strain_fine,interp_tau_star,yerr=interp_w_tau,color=plot_color, capthick=0) + plt.plot(macro_strain,tau_star,'s--',markerfacecolor=plot_color,markeredgecolor='k',markeredgewidth=1,color='k') + plt.plot(strain_fine,interp_tau_star+interp_w_tau,'k--',linewidth=2) + plt.plot(strain_fine,interp_tau_star-interp_w_tau,'k--',linewidth=2) + + plt.grid() + + +#%% Loading Data +############################################################################### + +#Load Stiffness Matrix +c_mat_C=np.loadtxt(c_mat_C_file) + +#Load Schmid Tensors +num_ss_fams=len(schmid_tensor_files) +num_per_sys=[None]*num_ss_fams +T_vecs=[None]*num_ss_fams +for i in np.arange(num_ss_fams): + T_vecs[i] = np.atleast_2d(np.loadtxt(os.path.join(schmid_tensor_directory,schmid_tensor_files[i]))) + num_per_sys[i]=T_vecs[i].shape[0] + +num_ten=int(np.sum(num_per_sys)) +T=np.zeros([num_ten,3,3]) +counter=0 +for j in np.arange(num_ss_fams): + for i in np.arange(num_per_sys[j]): + T[counter,:,:]=T_vecs[j][i,:].reshape([3,3]) + counter+=1 + +#Load and Process Stress Data +stress_data=[None]*(num_load_steps) +completeness=[None]*(num_load_steps) + +for i in np.arange(num_load_steps): + print('Processing Load ' + str(i)) + grain_data=np.loadtxt(os.path.join(processed_data_directory,analysis_stem + '%03d'%(i),'grains.out')) + completeness[i]=grain_data[:,1] + stress_data[i]=stress_proc.post_process_stress(grain_data,c_mat_C,T) + + + +#%% Extract Strengths from Different Slip System Families +tau_star=[None]*num_ss_fams +w_tau=[None]*num_ss_fams +for i in np.arange(num_ss_fams): + tau_star[i], w_tau[i] = extract_strength(stress_data,[int(np.sum(num_per_sys[:i])),int(np.sum(num_per_sys[:(i+1)]))],completeness,0.8) + +#%% Plot Slip System Strength Curves +plt.close('all') +plot_strength_curve(tau_star[2],w_tau[2],macro_strain=None,plot_color='blue') + diff --git a/scripts/virtual_diffractometer.py b/scripts/virtual_diffractometer.py new file mode 100644 index 00000000..30eac31c --- /dev/null +++ b/scripts/virtual_diffractometer.py @@ -0,0 +1,261 @@ +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on downloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +#%% User Input +############################################################################### + +#File Locations +grains_file='###' #HEXRD Grains.out file +matl_file='###' #HEXRD Materials cpl +active_matl='###' +cfg_file='###' #HEXRD cfg yml +instr_file='###' #HEXRD instrument yml + + +#Output Image Location +output_location='###' +output_name='test'#Frame Cache Name + +#Script Options +det_psf_fwhm=2. +cts_per_event=1000. +delta_ome = 0.25 + + +############################################################################### +#%% + +import os + + +import cPickle as cpl + +import numpy as np +import scipy as sp + +import yaml + +from hexrd import config + +from hexrd.xrd import distortion as dFuncs +from hexrd.xrd import transforms_CAPI as xfcapi + + +from hexrd.gridutil import cellIndices + + +from hexrd.xrd.xrdutil import simulateGVecs + +#%% File Loading +cfg = config.open(cfg_file)[0] # NOTE: always a list of cfg objects + +instr_cfg = yaml.load(open(instr_file, 'r')) + +mat_list = cpl.load(open(matl_file, 'r')) + +grain_params_list=np.loadtxt(grains_file) + +#%% Extract Quantities from Loaded data + +#Instrument Info +# stack into array for later +detector_params = np.hstack([ + instr_cfg['detector']['transform']['tilt_angles'], + instr_cfg['detector']['transform']['t_vec_d'], + instr_cfg['oscillation_stage']['chi'], + instr_cfg['oscillation_stage']['t_vec_s'], + ]) + +# pixel pitches +pixel_pitch = cfg.instrument.detector.pixels.size + +# panel dimensions calculated from pixel pitches +row_dim = pixel_pitch[0]*cfg.instrument.detector.pixels.rows +col_dim = pixel_pitch[1]*cfg.instrument.detector.pixels.columns + +# panel is ( (xmin, ymin), (xmax, ymax) ) +panel_dims = ( + (-0.5*col_dim, -0.5*row_dim), + ( 0.5*col_dim, 0.5*row_dim), + ) + +detector_x_edges = np.arange(cfg.instrument.detector.pixels.columns+1)*pixel_pitch[1] + panel_dims[0][0] +detector_y_edges = np.arange(cfg.instrument.detector.pixels.rows+1)*pixel_pitch[0] + panel_dims[0][1] + +# UGH! hard-coded distortion... still needs fixing when detector is rewritten +if instr_cfg['detector']['distortion']['function_name'] == 'GE_41RT': + distortion = (dFuncs.GE_41RT, + instr_cfg['detector']['distortion']['parameters'], + ) +else: + distortion = None + +#Image Info +nrows = int((panel_dims[1][1] - panel_dims[0][1]) / float(pixel_pitch[0])) +ncols = int((panel_dims[1][0] - panel_dims[0][0]) / float(pixel_pitch[1])) +row_edges = (np.arange(nrows+1)*pixel_pitch[0] + panel_dims[0][1])[::-1] +col_edges = np.arange(ncols+1)*pixel_pitch[1] + panel_dims[0][0] + + +nframes = int(360./float(delta_ome)) +ome_edges = np.arange(nframes + 1)*delta_ome - 180. + + +#extract transform objects; rotations and translations +# detector first, rotation, then translation +# - rotation takes comps from det frame to lab +rMat_d = xfcapi.makeDetectorRotMat(detector_params[:3]) +tVec_d = np.r_[detector_params[3:6]] + +# rotation stage (omega) +# - chi is ccw tilt about lab X; rMat_s is omega dependent +# - takes comps in sample to lab frame +chi = detector_params[6] +tVec_s = np.zeros((3,1)) + +# crystal; this will be a list of things, computed from quaternions +# - trivial case here... +rMat_c = np.eye(3) +tVec_c = np.zeros((3,1)) + + + +#Material Info +mat_name = cfg.material.active # str that is the material name in database + +# need to find the index of the active material +mat_idx = np.where([mat_list[i].name == mat_name for i in range(len(mat_list))])[0] + +# grab plane data, and useful things hanging off of it +plane_data = mat_list[mat_idx].planeData +plane_data.tThMax=np.radians(20) +plane_data.set_exclusions(np.zeros(len(plane_data.exclusions), dtype=bool)) + + +#%% Filters For Point Spread +def make_gaussian_filter(size,fwhm): + sigma=fwhm/(2.*np.sqrt(2.*np.log(2.))) +# size=[5,5] +# sigma=1. + gaussFilter=np.zeros(size) + cenRow=size[0]/2. + cenCol=size[1]/2. + + pixRowCens=np.arange(size[0])+0.5 + pixColCens=np.arange(size[1])+0.5 + + y=cenRow-pixRowCens + x=pixColCens-cenCol + + xv, yv = np.meshgrid(x, y, sparse=False) + + r=np.sqrt(xv**2.+yv**2.) + gaussFilter=np.exp(-r**2./(2*sigma**2)) + gaussFilter=gaussFilter/gaussFilter.sum() + + + return gaussFilter + +def make_lorentzian_filter(size,fwhm): + + gamma=fwhm/2. + + lorentzianFilter=np.zeros(size) + cenRow=size[0]/2. + cenCol=size[1]/2. + + pixRowCens=np.arange(size[0])+0.5 + pixColCens=np.arange(size[1])+0.5 + + y=cenRow-pixRowCens + x=pixColCens-cenCol + + xv, yv = np.meshgrid(x, y, sparse=False) + + r=np.sqrt(xv**2.+yv**2.) + lorentzianFilter=gamma**2 / ((r)**2 + gamma**2) + lorentzianFilter=lorentzianFilter/lorentzianFilter.sum() + + + return lorentzianFilter + +#%% +#Calculate Intercepts for diffraction events from grains + +pixel_data = [] + +for ii in np.arange(grain_params_list.shape[0]): + print "processing grain %d..." %ii + + simg = simulateGVecs(plane_data, detector_params, grain_params_list[ii,3:15],distortion=None) + + valid_ids, valid_hkl, valid_ang, valid_xy, ang_ps = simg + + #ax.plot(valid_xy[:, 0], valid_xy[:, 1], 'b.', ms=2) + this_frame = sp.sparse.coo_matrix((nrows, ncols), np.uint16) + frame_indices = cellIndices(ome_edges, np.degrees(valid_ang[:, 2])) + i_row = cellIndices(row_edges, valid_xy[:, 1]) + j_col = cellIndices(col_edges, valid_xy[:, 0]) + pixel_data.append(np.vstack([i_row, j_col, frame_indices])) + + +pixd = np.hstack(pixel_data) + + + +#%% +#Build Images + +frame_cache=[sp.sparse.csc_matrix([2048,2048])]*nframes + +filter_size=np.round(det_psf_fwhm*5) +if filter_size % 2 == 0: + filter_size+=1 + +psf_filter=make_gaussian_filter([filter_size,filter_size],det_psf_fwhm) + +#Make pad four fast fourier tranform +filterPad=np.zeros((nrows, ncols), dtype=float) +filterPad[:psf_filter.shape[0],:psf_filter.shape[1]]=psf_filter +filterPadTransform=np.fft.fft2(filterPad) + + +#Build images and apply point spread +for i in np.arange(nframes): + print "processing frame %d of %d" % (i,nframes) + + this_frame = np.zeros((nrows, ncols), dtype=float) + these_ij = pixd[:2, pixd[2, :] == i] + + this_frame[these_ij[0], these_ij[1]] += cts_per_event + + + this_frame_transform=np.fft.fft2(this_frame) + this_frame_convolved=np.real(np.fft.ifft2(this_frame_transform*filterPadTransform)) + + frame_cache[i]=sp.sparse.csc_matrix(this_frame_convolved) + +np.savez_compressed(os.path.join(output_location,output_name+'.npz'),frame_cache=frame_cache) From 10b4466628fb2a01c6bc5e0510823654a2f6a608 Mon Sep 17 00:00:00 2001 From: darrencpagan Date: Mon, 10 Oct 2016 09:16:18 -0700 Subject: [PATCH 054/253] Bug fixes in virtual difffraction scripts for outputing frame caches. --- hexrd/matrixutil.py | 131 ++++++++++++++++++++++++++++++ hexrd/test.txt | 0 scripts/VirtualDiffraction.py | 48 ----------- scripts/virtual_diffractometer.py | 18 ++-- 4 files changed, 143 insertions(+), 54 deletions(-) delete mode 100644 hexrd/test.txt delete mode 100644 scripts/VirtualDiffraction.py diff --git a/hexrd/matrixutil.py b/hexrd/matrixutil.py index 0ac24396..b6638d74 100644 --- a/hexrd/matrixutil.py +++ b/hexrd/matrixutil.py @@ -44,6 +44,7 @@ sqr3i = 1./sqrt(3.) sqr2i = 1./sqrt(2.) sqr2 = sqrt(2.) +sqr3 = sqrt(3.) sqr2b3 = sqrt(2./3.) fpTol = finfo(float).eps # ~2.2e-16 @@ -663,3 +664,133 @@ def determinant3(mat): det = sum(mat[2,:] * v[:]) return det +def strainTenToVec(strainTen): + + strainVec = num.zeros(6, dtype='float64') + strainVec[0] = strainTen[0, 0] + strainVec[1] = strainTen[1, 1] + strainVec[2] = strainTen[2, 2] + strainVec[3] = 2*strainTen[1, 2] + strainVec[4] = 2*strainTen[0, 2] + strainVec[5] = 2*strainTen[0, 1] + + strainVec=num.atleast_2d(strainVec).T + + return strainVec + +def strainVecToTen(strainVec): + + strainTen = num.zeros((3, 3), dtype='float64') + strainTen[0, 0] = strainVec[0] + strainTen[1, 1] = strainVec[1] + strainTen[2, 2] = strainVec[2] + strainTen[1, 2] = strainVec[3] / 2 + strainTen[0, 2] = strainVec[4] / 2 + strainTen[0, 1] = strainVec[5] / 2 + strainTen[2, 1] = strainVec[3] / 2 + strainTen[2, 0] = strainVec[4] / 2 + strainTen[1, 0] = strainVec[5] / 2 + + return strainTen + + +def stressTenToVec(stressTen): + + stressVec = num.zeros(6, dtype='float64') + stressVec[0] = stressTen[0, 0] + stressVec[1] = stressTen[1, 1] + stressVec[2] = stressTen[2, 2] + stressVec[3] = stressTen[1, 2] + stressVec[4] = stressTen[0, 2] + stressVec[5] = stressTen[0, 1] + + stressVec=num.atleast_2d(stressVec).T + + return stressVec + + +def stressVecToTen(stressVec): + + stressTen = num.zeros((3, 3), dtype='float64') + stressTen[0, 0] = stressVec[0] + stressTen[1, 1] = stressVec[1] + stressTen[2, 2] = stressVec[2] + stressTen[1, 2] = stressVec[3] + stressTen[0, 2] = stressVec[4] + stressTen[0, 1] = stressVec[5] + stressTen[2, 1] = stressVec[3] + stressTen[2, 0] = stressVec[4] + stressTen[1, 0] = stressVec[5] + + return stressTen + + + + +def ale3dStrainOutToV(vecds): + #takes 5 components of evecd and the 6th component is lndetv + + + """convert from vecds representation to symmetry matrix""" + eps = num.zeros([3,3],dtype='float64') + #Akk_by_3 = sqr3i * vecds[5] # -p + a = num.exp(vecds[5])**(1./3.)# -p + t1 = sqr2i*vecds[0] + t2 = sqr6i*vecds[1] + + eps[0, 0] = t1 - t2 + eps[1, 1] = -t1 - t2 + eps[2, 2] = sqr2b3*vecds[1] + eps[1, 0] = vecds[2] * sqr2i + eps[2, 0] = vecds[3] * sqr2i + eps[2, 1] = vecds[4] * sqr2i + + eps[0, 1] = eps[1, 0] + eps[0, 2] = eps[2, 0] + eps[1, 2] = eps[2, 1] + + epstar=eps/a + + V=(num.identity(3)+epstar)*a + Vinv=(num.identity(3)-epstar)/a + + return V,Vinv + +def vecdsToSymm(vecds): + """convert from vecds representation to symmetry matrix""" + A = num.zeros([3,3],dtype='float64') + Akk_by_3 = sqr3i * vecds[5] # -p + t1 = sqr2i*vecds[0] + t2 = sqr6i*vecds[1] + + A[0, 0] = t1 - t2 + Akk_by_3 + A[1, 1] = -t1 - t2 + Akk_by_3 + A[2, 2] = sqr2b3*vecds[1] + Akk_by_3 + A[1, 0] = vecds[2] * sqr2i + A[2, 0] = vecds[3] * sqr2i + A[2, 1] = vecds[4] * sqr2i + + A[0, 1] = A[1, 0] + A[0, 2] = A[2, 0] + A[1, 2] = A[2, 1] + return A + +def traceToVecdsS(Akk): + return sqr3i * Akk + +def vecdsSToTrace(vecdsS): + return vecdsS * sqr3 + +def trace3(A): + return A[0,0]+A[1,1]+A[2,2] + +def symmToVecds(A): + """convert from symmetry matrix to vecds representation""" + vecds = num.zeros(6,dtype='float64') + vecds[0] = sqr2i * (A[0,0] - A[1,1]) + vecds[1] = sqr6i * (2. * A[2,2] - A[0,0] - A[1,1]) + vecds[2] = sqr2 * A[1,0] + vecds[3] = sqr2 * A[2,0] + vecds[4] = sqr2 * A[2,1] + vecds[5] = traceToVecdsS(trace3(A)) + return vecds \ No newline at end of file diff --git a/hexrd/test.txt b/hexrd/test.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/scripts/VirtualDiffraction.py b/scripts/VirtualDiffraction.py deleted file mode 100644 index 26a1eda1..00000000 --- a/scripts/VirtualDiffraction.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon Nov 9 11:03:24 2015 - -@author: pagan2 -""" - -#%% - -import numpy as np -import hexrd.xrd.material as mat -import hexrd.xrd.crystallography as crys -import hexrd.xrd.transforms_CAPI as trans -import multiprocessing as mp - - -#%% - - -material=mat.Material() -material.beamEnergy=15 -material.sgnum=227 -material.latticeParameters=[5.4310,] -material.name='Silicon' - -#%% - -samplePos=np.array([[0],[0],[0]]) -crysPos=np.array([[0],[0],[0]]) -rMat_c=np.identity(3) -bMat=material.planeData.latVecOps['B'] -wavelength=material.planeData.wavelength - -material.planeData.t - -#%% -omega0,omega1=trans.oscillAnglesOfHKLs(material.planeData.hkls.T, 0, rMat_c, bMat, wavelength) - - - - -#%% - -def VirtDiffWorker - - - - diff --git a/scripts/virtual_diffractometer.py b/scripts/virtual_diffractometer.py index 30eac31c..2c6517e9 100644 --- a/scripts/virtual_diffractometer.py +++ b/scripts/virtual_diffractometer.py @@ -43,6 +43,7 @@ det_psf_fwhm=2. cts_per_event=1000. delta_ome = 0.25 +min_I=5. ############################################################################### @@ -226,10 +227,7 @@ def make_lorentzian_filter(size,fwhm): -#%% -#Build Images - -frame_cache=[sp.sparse.csc_matrix([2048,2048])]*nframes +frame_cache_data=[sp.sparse.coo_matrix([2048,2048],dtype='uint16')]*nframes filter_size=np.round(det_psf_fwhm*5) if filter_size % 2 == 0: @@ -255,7 +253,15 @@ def make_lorentzian_filter(size,fwhm): this_frame_transform=np.fft.fft2(this_frame) this_frame_convolved=np.real(np.fft.ifft2(this_frame_transform*filterPadTransform)) - - frame_cache[i]=sp.sparse.csc_matrix(this_frame_convolved) + tmp=np.where(this_frame_convolved Date: Fri, 14 Oct 2016 21:45:01 -0700 Subject: [PATCH 055/253] added multiprocessing to fiber generation (tested) --- hexrd/findorientations.py | 104 +++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index c35d0ff6..eccfc6da 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -50,7 +50,7 @@ pass -def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndiv, filt_stdev=0.8): +def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndiv, filt_stdev=0.8, ncpus=1): """ From ome-eta maps and hklid spec, generate list of quaternions from fibers @@ -67,16 +67,24 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi # crystallography data from the pd object pd = eta_ome.planeData + hkls = pd.hkls tTh = pd.getTTh() bMat = pd.latVecOps['B'] csym = pd.getLaueGroup() + params = { + 'bMat':bMat, + 'chi':chi, + 'csym':csym, + 'fiber_ndiv':fiber_ndiv, + } + ############################################ ## Labeling of spots from seed hkls ## ############################################ qfib = [] - labels = [] + input_p = [] numSpots = [] coms = [] for i in seed_hkl_ids: @@ -94,52 +102,80 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi index=np.arange(1, np.amax(labels_t)+1) ) ) - labels.append(labels_t) + #labels.append(labels_t) numSpots.append(numSpots_t) coms.append(coms_t) pass - ############################################ - ## Generate discrete fibers from labels ## - ############################################ - for i in range(len(pd_hkl_ids)): - ii = 0 - qfib_tmp = np.empty((4, fiber_ndiv*numSpots[i])) for ispot in range(numSpots[i]): if not np.isnan(coms[i][ispot][0]): - ome_c = eta_ome.omeEdges[0] \ - + (0.5 + coms[i][ispot][0])*del_ome - eta_c = eta_ome.etaEdges[0] \ - + (0.5 + coms[i][ispot][1])*del_eta - - gVec_s = xfcapi.anglesToGVec( - np.atleast_2d( - [tTh[pd_hkl_ids[i]], eta_c, ome_c] - ), - chi=chi - ).T - - tmp = mutil.uniqueVectors( - rot.discreteFiber( - pd.hkls[:, pd_hkl_ids[i]].reshape(3, 1), - gVec_s, - B=bMat, - ndiv=fiber_ndiv, - invert=False, - csym=csym - )[0] + ome_c = eta_ome.omeEdges[0] + (0.5 + coms[i][ispot][0])*del_ome + eta_c = eta_ome.etaEdges[0] + (0.5 + coms[i][ispot][1])*del_eta + input_p.append( + np.hstack( + [hkls[:, pd_hkl_ids[i]], + tTh[pd_hkl_ids[i]], eta_c, ome_c] ) - jj = ii + tmp.shape[1] - qfib_tmp[:, ii:jj] = tmp - ii += tmp.shape[1] + ) pass pass - qfib.append(qfib_tmp[:, :ii]) pass + + # do the mapping + start = time.time() + qfib = None + if ncpus > 1: + # multiple process version + pool = mp.Pool(ncpus, discretefiber_init, (params, )) + qfib = pool.map(discretefiber_reduced, input_p) # chunksize=chunksize) + pool.close() + else: + # single process version. + global paramMP + discretefiber_init(params) # sets paramMP + qfib = map(discretefiber_reduced, input_p) + paramMP = None # clear paramMP + elapsed = (time.time() - start) + logger.info("fiber generation took %.3f seconds", elapsed) + return np.hstack(qfib) +def discretefiber_init(params): + global paramMP + paramMP = params + + +def discretefiber_reduced(params_in): + """ + input parameters are [hkl_id, com_ome, com_eta] + """ + bMat = paramMP['bMat'] + chi = paramMP['chi'] + csym = paramMP['csym'] + fiber_ndiv = paramMP['fiber_ndiv'] + + hkl = params_in[:3].reshape(3, 1) + + gVec_s = xfcapi.anglesToGVec( + np.atleast_2d(params_in[3:]), + chi=chi, + ).T + + tmp = mutil.uniqueVectors( + rot.discreteFiber( + hkl, + gVec_s, + B=bMat, + ndiv=fiber_ndiv, + invert=False, + csym=csym + )[0] + ) + return tmp + + def run_cluster(compl, qfib, qsym, cfg, min_samples=None, compl_thresh=None, radius=None): """ """ From d66f1b8b17717cd8c50c42a3affcb44b7feafb6f Mon Sep 17 00:00:00 2001 From: "Donald E. Boyce" Date: Wed, 19 Oct 2016 13:18:21 -0400 Subject: [PATCH 056/253] fixed omega-edges to expect omegas to be nframes x 2; handled case in which simulateGVecs returns empty list --- hexrd/findorientations.py | 20 +++++++++------- hexrd/xrd/xrdutil.py | 50 ++++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index c35d0ff6..19571398 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -266,7 +266,7 @@ def quat_distance(x, y): ).flatten() pass pass - + if (algorithm == 'dbscan' or algorithm == 'ort-dbscan') \ and qbar.size/4 > 1: logger.info("\tchecking for duplicate orientations...") @@ -275,7 +275,7 @@ def quat_distance(x, y): np.radians(cl_radius), criterion='distance', metric=quat_distance) - nblobs_new = len(np.unique(cl)) + nblobs_new = len(np.unique(cl)) if nblobs_new < nblobs: logger.info("\tfound %d duplicates within %f degrees" \ %(nblobs-nblobs_new, cl_radius)) @@ -289,7 +289,7 @@ def quat_distance(x, y): qbar = tmp pass pass - + logger.info("clustering took %f seconds", time.clock() - start) logger.info( "Found %d orientation clusters with >=%.1f%% completeness" @@ -354,7 +354,7 @@ def generate_eta_ome_maps(cfg, pd, image_series, hkls=None): ome_step=ome_step, threshold=cfg.find_orientations.orientation_maps.threshold ) - + fn = os.path.join( cfg.working_dir, cfg.find_orientations.orientation_maps.file @@ -390,7 +390,7 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): cfg.image_series.filename, fmt=cfg.image_series.format, **cfg.image_series.args) - + # need instrument cfg later on down... instr_cfg = get_instrument_parameters(cfg) detector_params = np.hstack([ @@ -501,6 +501,7 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): * mutil.unitVector(rand_q[1:, :]) refl_per_grain = np.zeros(ngrains) num_seed_refls = np.zeros(ngrains) + print('fo: hklids = ', hkl_ids) for i in range(ngrains): grain_params = np.hstack([rand_e[:, i], xf.zeroVec.flatten(), @@ -517,8 +518,11 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): distortion=distortion, ) refl_per_grain[i] = len(sim_results[0]) - num_seed_refls[i] = np.sum([sum(sim_results[0] == hkl_id) for hkl_id in hkl_ids]) - pass + # lines below fix bug when sim_results[0] is empty + if refl_per_grain[i] > 0: + num_seed_refls[i] = np.sum([sum(sim_results[0] == hkl_id) for hkl_id in hkl_ids]) + else: + num_seed_refls[i] = 0 #min_samples = 2 min_samples = max( int(np.floor(0.5*min_compl*min(num_seed_refls))), @@ -539,7 +543,7 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): cfg.analysis_name.strip().replace(' ', '-'), cfg.material.active.strip().replace(' ', '-'), ) - + np.savetxt( os.path.join( cfg.working_dir, diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index afc8c677..bd69e46b 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -1761,12 +1761,13 @@ def __init__(self, image_series, instrument_params, planeData, active_hkls, """ omegas come from image series directly """ - self._omegas = num.radians(num.average(image_series.omega, axis=0)) - self._omeEdges = num.radians(num.hstack([image_series.omega[0, :], - image_series.omega[1, -1] - ]) - ) - + # image series omegas have shape (nframes, 2) + self._omegas = num.radians(num.average(image_series.omega, axis=1)) + self._omeEdges = num.radians( + num.concatenate( + [image_series.omega[:, 0].flatten(), image_series.omega[-1, 1].flatten()] + ) + ) """ construct patches in place on init @@ -1848,6 +1849,7 @@ def __init__(self, image_series, instrument_params, planeData, active_hkls, maxval=image_series.nframes ).start() maps = num.zeros((len(active_hkls), len(self._omegas), len(self._etas))) + print "maps.shape: ", maps.shape for i_ome in range(image_series.nframes): pbar.update(i_ome) this_frame = image_series[i_ome] @@ -1921,18 +1923,18 @@ def save(self, filename): pass # end of class: GenerateEtaOmeMaps - + class EtaOmeMaps(object): """ find-orientations loads pickled eta-ome data, but CollapseOmeEta is not pickleable, because it holds a list of ReadGE, each of which holds a reference to an open file object, which is not pickleable. """ - + def __init__(self, ome_eta_archive): ome_eta = num.load(ome_eta_archive) - + planeData_args = ome_eta['planeData_args'] planeData_hkls = ome_eta['planeData_hkls'] self.planeData = crystallography.PlaneData(planeData_hkls, *planeData_args) @@ -1943,7 +1945,7 @@ def __init__(self, ome_eta_archive): self.omeEdges = ome_eta['omeEdges'] self.etas = ome_eta['etas'] self.omegas = ome_eta['omegas'] - + return pass # end of class: EtaOmeMaps @@ -1968,7 +1970,7 @@ def __init__(self, ome_eta_archive): # not ready # class BaseEtaOme(object): # not ready # """ # not ready # eta-ome map base class derived from new YAML config -# not ready # +# not ready # # not ready # ...for now... # not ready # # not ready # must provide: @@ -1990,25 +1992,25 @@ def __init__(self, ome_eta_archive): # not ready # """ # not ready # self.cfg = cfg # not ready # self.instr_cfg = get_instrument_parameters(cfg) -# not ready # +# not ready # # not ready # # currently hard-coded to do reader from npz frame cache # not ready # # kwarg *MUST* be 'new' style reader # not ready # if reader is None: # not ready # self.__reader = get_frames(reader, self.cfg) # not ready # else: # not ready # self.__reader = reader -# not ready # +# not ready # # not ready # # set eta_step IN DEGREES # not ready # if eta_step is None: # not ready # self._eta_step = self.cfg.image_series.omega.step # not ready # else: # not ready # self._eta_step = abs(eta_step) # just in case negative... -# not ready # +# not ready # # not ready # material_list = cPickle.load(open(cfg.material.definitions, 'r')) # not ready # material_names = [material_list[i].name for i in range(len(material_list))] # not ready # material_dict = dict(zip(material_names, material_list)) # not ready # self.planeData = material_dict[cfg.material.active].planeData -# not ready # +# not ready # # not ready # self._iHKLList = None # not ready # # not ready # self._etaEdges = None @@ -2028,7 +2030,7 @@ def __init__(self, ome_eta_archive): # not ready # """ # not ready # if ids is not None: # not ready # assert hasattr(ids, '__len__'), "ids must be a list or list-like object" -# not ready # +# not ready # # not ready # # start with all available # not ready # active_hkls = range(pd.hkls.shape[1]) # not ready # # check cfg file @@ -2054,7 +2056,7 @@ def __init__(self, ome_eta_archive): # not ready # @property # not ready # def eta_step(self): # not ready # return self._eta_step -# not ready # +# not ready # # not ready # @property # not ready # def etas(self): # not ready # return self._etas @@ -2089,10 +2091,10 @@ def __init__(self, ome_eta_archive): # not ready # def __init__(self, cfg, reader=None, eta_step=None, # not ready # omega=0., tVec_s=num.zeros(3), # not ready # npdiv=2): -# not ready # +# not ready # # not ready # # first init the base class # not ready # super( EtaOmeMaps, self ).__init__(cfg, reader=reader, eta_step=eta_step) -# not ready # +# not ready # # not ready # # grac relevant tolerances for patches # not ready # tth_tol = num.degrees(self.planeData.tThWidth) # not ready # eta_tol = num.degrees(abs(self.etas[1]-self.etas[0])) @@ -2125,7 +2127,7 @@ def __init__(self, ome_eta_archive): # not ready # # since making maps for all eta, must hand trivial crystal params # not ready # rMat_c = np.eye(3) # not ready # tVec_c = np.zeros(3) -# not ready # +# not ready # # not ready # # make angle arrays for patches # not ready # neta = len(self.etas) # not ready # nome = len(reader[0]) @@ -2144,7 +2146,7 @@ def __init__(self, ome_eta_archive): # not ready # xydet_ring = xfcapi.gvecToDetectorXY(gVec_ring_l, # not ready # rMat_d, rMat_s, rMat_c, # not ready # tVec_d, tVec_s, tVec_c) -# not ready # +# not ready # # not ready # if distortion is not None: # not ready # det_xy = distortion[0](xydet_ring, # not ready # distortion[1], @@ -2153,7 +2155,7 @@ def __init__(self, ome_eta_archive): # not ready # rMat_d, rMat_s, # not ready # tVec_d, tVec_s, tVec_c, # not ready # distortion=distortion) -# not ready # +# not ready # # not ready # patches = make_reflection_patches(self.instr_cfg, # not ready # angs[i_ring].T[:, :2], ang_ps, # not ready # omega=None, @@ -2161,7 +2163,7 @@ def __init__(self, ome_eta_archive): # not ready # distortion=distortion, # not ready # npdiv=npdiv, quiet=False, # not ready # compute_areas_func=gutil.compute_areas) -# not ready # +# not ready # # not ready # for i in range(nome): # not ready # this_frame = num.array(reader[0][i].todense()) # not ready # for j in range(neta): @@ -4057,7 +4059,7 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, # beam vector if beamVec is None: beamVec = xfcapi.bVec_ref - + # data to loop # ...WOULD IT BE CHEAPER TO CARRY ZEROS OR USE CONDITIONAL? if omega is None: From 5a304c9b48111fa3ee7f594ca7b9944bb46696a7 Mon Sep 17 00:00:00 2001 From: "Donald E. Boyce" Date: Wed, 19 Oct 2016 16:29:48 -0400 Subject: [PATCH 057/253] saves eta-omega maps in compressed format --- hexrd/xrd/xrdutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index bd69e46b..7e008346 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -1918,7 +1918,7 @@ def save(self, filename): 'planeData_args':args, 'planeData_hkls':hkls, } - num.savez(filename, **save_dict) + num.savez_compressed(filename, **save_dict) return pass # end of class: GenerateEtaOmeMaps From c6a29d000acff6e1761c2f63da1abbf62389a903 Mon Sep 17 00:00:00 2001 From: "Donald E. Boyce" Date: Wed, 19 Oct 2016 20:26:20 -0400 Subject: [PATCH 058/253] added new adapter for imagefiles type of imageseries, including a yaml metadata reader --- hexrd/imageseries/load/imagefiles.py | 231 +++++++++++++++++++++++++++ hexrd/imageseries/load/metadata.py | 35 ++++ 2 files changed, 266 insertions(+) create mode 100644 hexrd/imageseries/load/imagefiles.py create mode 100644 hexrd/imageseries/load/metadata.py diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py new file mode 100644 index 00000000..18c16a67 --- /dev/null +++ b/hexrd/imageseries/load/imagefiles.py @@ -0,0 +1,231 @@ +"""Adapter class for list of image files +""" +from __future__ import print_function + +import sys +import os +import logging +import glob + +# # Put this before fabio import and reset level if you +# # want to control its import warnings. +# logging.basicConfig(level=logging.INFO) + +import numpy as np +import fabio +import yaml + +from . import ImageSeriesAdapter +from .metadata import yamlmeta +from ..imageseriesiter import ImageSeriesIterator + + +class ImageFilesImageSeriesAdapter(ImageSeriesAdapter): + """collection of image files""" + + format = 'image-files' + + def __init__(self, fname, **kwargs): + """Constructor for image files image series + + *fname* - should be yaml file with files and metadata sections + *kwargs* - keyword arguments + . 'files' = a list of image files + . 'metadata' = a dictionary + """ + self._fname = fname + self._load_yml() + self._process_files() + + #@memoize + def __len__(self): + return self._nframes + + def __getitem__(self, key): + if self.singleframes: + imgf = self._files[key] + img = fabio.open(imgf) + else: + (fnum, frame) = self._file_and_frame(key) + imgf = self._files[fnum] + img0 = fabio.open(imgf) + img = img0.getframe(frame) + + return img.data + + def __iter__(self): + return ImageSeriesIterator(self) + + def __str__(self): + s = """==== imageseries from file list + fabio class: %s +number of files: %s + nframes: %s + dtype: %s + shape: %s + single frames: %s + """ % (self.fabioclass, len(self._files), len(self), + self.dtype, self.shape, self.singleframes) + return s + + def _load_yml(self): + EMPTY = 'empty-frames' + MAXF = 'max-frames' + + with open(self._fname, "r") as f: + d = yaml.load(f) + imgsd = d['image-files'] + dname = imgsd['directory'] + fglob = imgsd['files'] + self._files = [] + for g in fglob.split(): + self._files += glob.glob(os.path.join(dname, g)) + + self.optsd = d['options'] if 'options' else None + self._empty = self.optsd[EMPTY] if EMPTY in self.optsd else 0 + self._maxframes = self.optsd[MAXF] if MAXF in self.optsd else 0 + + self._meta = yamlmeta(d['meta']) + + def _process_files(self): + kw = {'empty': self._empty} + fcl = None + shp = None + dtp = None + nf = 0 + self._singleframes = True + infolist = [] + for imgf in self._files: + info = FileInfo(imgf, **kw) + infolist.append(info) + shp = self._checkvalue(shp, info.shape, + "inconsistent image shapes") + dtp = self._checkvalue(dtp, info.dtype, + "inconsistent image dtypes") + fcl = self._checkvalue(fcl, info.fabioclass, + "inconsistent image types") + nf += info.nframes + if info.nframes > 1: + self._singleframes = False + + + self._nframes = nf + self._shape = shp + self._dtype = dtp + self._fabioclass = fcl + self._infolist = infolist + + # from make_imageseries_h5 + @staticmethod + def _checkvalue(v, vtest, msg): + """helper: ensure value set conistently""" + if v is None: + val = vtest + else: + if vtest != v: + raise ValueError(msg) + else: + val = v + + return val + + def _file_and_frame(self, key): + """for multiframe images""" + # allow for negatives (just use [nframes + key]) + nf = len(self) + if key < -nf or key >= nf: + msg = "frame out of range: %s" % key + raise LookupError(msg) + k = key if key >= 0 else (nf + key) + + frame = -nf - 1 + fnum = 0 + for info in self.infolist: + if k < info.nframes: + frame = k + info.empty + break + else: + k -= info.nframes + fnum += 1 + + return fnum, frame + + # ======================================== API + + @property + def metadata(self): + """(read-only) Image sequence metadata + + Currently returns none + """ + return self._meta + + @property + def shape(self): + return self._shape + + @property + def dtype(self): + return self._dtype + + @property + def infolist(self): + return self._infolist + + @property + def fabioclass(self): + return self._fabioclass + + @property + def singleframes(self): + """indicates whether all files are single frames""" + return self._singleframes + + pass # end class + + +class FileInfo(object): + """class for managing individual file information""" + def __init__(self, filename, **kwargs): + self.filename = filename + img = fabio.open(filename) + self._fabioclass = img.classname + self._imgframes = img.nframes + self.dat = img.data + + d = kwargs.copy() + self._empty = d.pop('empty', 0) + if self._empty >= self._imgframes: + msg = "more empty frames than images: %s" % self.filename + raise ValueError(msg) + + def __str__(self): + s = """==== image file + name: %s +fabio class: %s + frames: %s + dtype: %s + shape: %s\n""" % (self.filename, self.fabioclass, + self.nframes, self.dtype, self.shape) + + return s + + @property + def empty(self): + return self._empty + + @property + def shape(self): + return self.dat.shape + + @property + def dtype(self): + return self.dat.dtype + + @property + def fabioclass(self): + return self._fabioclass + + @property + def nframes(self): + return self._imgframes - self.empty diff --git a/hexrd/imageseries/load/metadata.py b/hexrd/imageseries/load/metadata.py new file mode 100644 index 00000000..625acafa --- /dev/null +++ b/hexrd/imageseries/load/metadata.py @@ -0,0 +1,35 @@ +"""metadata tools for imageseries""" +import yaml +import numpy as np + +def yamlmeta(meta): + """ Image sequence metadata + +The usual yaml dictionary is returned with the exception that +if the first word of a multiword string is an exclamation mark ("!"), +it will trigger further processing determined by the rest of the string. +Currently only one trigger is used: + +! load-numpy-object + the returned value will the numpy object read from the file +""" + metad = {} + for k, v in meta.items(): + # check for triggers + istrigger = False + if isinstance(v, basestring): + words = v.split() + istrigger = (words[0] == "!") and (len(words) > 1) + + if v == '++np.array': # old way used in frame-cache (obsolescent) + newk = k + '-array' + metad[k] = np.array(self._meta.pop(newk)) + metad.pop(newk, None) + elif istrigger: + if words[1] == "load-numpy-array": + fname = words[2] + metad[k] = np.load(fname) + else: + metad[k] = v + + return metad From 875cd660cf98e63eed54dfc8a246c9e9cdbd6b7c Mon Sep 17 00:00:00 2001 From: "Donald E. Boyce" Date: Thu, 20 Oct 2016 13:59:51 -0400 Subject: [PATCH 059/253] added file omega.py for handling omega data; currently has OmegaWedges for generating metadata --- hexrd/imageseries/omega.py | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 hexrd/imageseries/omega.py diff --git a/hexrd/imageseries/omega.py b/hexrd/imageseries/omega.py new file mode 100644 index 00000000..7d48053e --- /dev/null +++ b/hexrd/imageseries/omega.py @@ -0,0 +1,74 @@ +"""Handle omega (specimen rotation) metadata + +* OmegaWedges class specifies omega metadata in wedges +""" +import numpy as np + +class OmegaWedges(object): + """piecewise linear omega ranges""" + def __init__(self, nframes): + """Constructor for OmegaWedge""" + self.nframes = nframes + self._wedges = [] + # + # ============================== API + # + @property + def omegas(self): + """n x 2 array of omega values, one per frame""" + if self.nframes != self.wframes: + msg = "number of frames (%s) does not match "\ + "number of wedge frames (%s)" %(self.nframes, self.wframes) + raise OmegaWedgesError(msg) + + oa = np.zeros((self.nframes, 2)) + wstart = 0 + for w in self.wedges: + ns = w['nsteps'] + wr = range(wstart, wstart + ns) + wa0 = np.linspace(w['ostart'], w['ostop'], ns + 1) + oa[wr, 0] = wa0[:-1] + oa[wr, 1] = wa0[1:] + + return oa + + @property + def nwedges(self): + """number of wedges""" + return len(self._wedges) + + @property + def wedges(self): + """list of wedges (dictionary)""" + return self._wedges + + def addwedge(self, ostart, ostop, nsteps, loc=None): + """add wedge to list""" + d = dict(ostart=ostart, ostop=ostop, nsteps=nsteps) + if loc is None: + loc = self.nwedges + + self.wedges.insert(loc, d) + + def delwedge(self, i): + """delete wedge number i""" + self.wedges.pop(i) + + @property + def wframes(self): + """number of frames in wedges""" + wf = [w['nsteps'] for w in self.wedges] + return np.int(np.sum(wf)) + + def save_omegas(self, fname): + """save omegas to text file""" + np.save(fname, self.omegas) + + pass # end class + + +class OmegaWedgesError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) From 59dd46b739eb29556e3313674411e2faa54917e0 Mon Sep 17 00:00:00 2001 From: "Donald E. Boyce" Date: Fri, 21 Oct 2016 20:31:51 -0400 Subject: [PATCH 060/253] added new OmegaImageSeries class and modified config files to deliver omega-imageseries directly * added "imageseries" and "omegaseries" properties to ImageSeriesConfig * added OmegaImageSeries class to imageseries.omega module * modified xrdutils.GenerateEtaOmeMaps to use imageseries API --- hexrd/config/imageseries.py | 20 ++++++++++++++++---- hexrd/findorientations.py | 16 ++++++---------- hexrd/imageseries/__init__.py | 3 ++- hexrd/imageseries/omega.py | 21 +++++++++++++++++++++ hexrd/xrd/xrdutil.py | 8 ++------ 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index 64d05050..287aba29 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -7,12 +7,24 @@ class ImageSeriesConfig(Config): - def _open(self): - self._imser = imageseries.open(self.filename, self.format, **self.args) + def __init__(self, cfg): + super(ImageSeriesConfig, self).__init__(cfg) + self._imser = None + self._omseries = None - def _meta(self): - pass # to be done later + @property + def imageseries(self): + """return the imageseries without checking for omega metadata""" + if self._imser is None: + self._imser = imageseries.open(self.filename, self.format, **self.args) + return self._imser + @property + def omegaseries(self): + """return the imageseries and ensure it has omega metadata""" + if self._omseries is None: + self._omseries = imageseries.omega.OmegaImageSeries(self.imageseries) + return self._omseries @property def filename(self): diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 06f1bced..78f9add3 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -20,7 +20,6 @@ from hexrd.xrd import symmetry as sym from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.xrd import image_io from hexrd.xrd import xrdutil @@ -74,7 +73,7 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi params = { 'bMat':bMat, - 'chi':chi, + 'chi':chi, 'csym':csym, 'fiber_ndiv':fiber_ndiv, } @@ -114,7 +113,7 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi eta_c = eta_ome.etaEdges[0] + (0.5 + coms[i][ispot][1])*del_eta input_p.append( np.hstack( - [hkls[:, pd_hkl_ids[i]], + [hkls[:, pd_hkl_ids[i]], tTh[pd_hkl_ids[i]], eta_c, ome_c] ) ) @@ -138,7 +137,7 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi paramMP = None # clear paramMP elapsed = (time.time() - start) logger.info("fiber generation took %.3f seconds", elapsed) - + return np.hstack(qfib) @@ -157,7 +156,7 @@ def discretefiber_reduced(params_in): fiber_ndiv = paramMP['fiber_ndiv'] hkl = params_in[:3].reshape(3, 1) - + gVec_s = xfcapi.anglesToGVec( np.atleast_2d(params_in[3:]), chi=chi, @@ -421,11 +420,8 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): md = dict(zip([matl[i].name for i in range(len(matl))], matl)) pd = md[cfg.material.active].planeData - # make image_series, which must be an OmegaImageSeries - image_series = image_io.OmegaImageSeries( - cfg.image_series.filename, - fmt=cfg.image_series.format, - **cfg.image_series.args) + # make image_series + image_series = cfg.image_series.omegaseries # need instrument cfg later on down... instr_cfg = get_instrument_parameters(cfg) diff --git a/hexrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py index 9a48a8e5..6f6a919f 100644 --- a/hexrd/imageseries/__init__.py +++ b/hexrd/imageseries/__init__.py @@ -6,9 +6,10 @@ """ from .baseclass import ImageSeries from . import load -from . import process from . import save from . import stats +from . import process +from . import omega def open(filename, format=None, **kwargs): # find the appropriate adapter based on format specified diff --git a/hexrd/imageseries/omega.py b/hexrd/imageseries/omega.py index 7d48053e..d597ad30 100644 --- a/hexrd/imageseries/omega.py +++ b/hexrd/imageseries/omega.py @@ -4,6 +4,27 @@ """ import numpy as np +from .baseclass import ImageSeries + +OMEGA_KEY = 'omega' + +class OmegaImageSeries(ImageSeries): + """ImageSeries with omega metadata""" + def __init__(self, ims): + """This class is initialized with an existing imageseries""" + # check for omega metadata + if OMEGA_KEY not in ims.metadata: + raise RuntimeError('Imageseries has no omega metadata') + + # use the imageseries as the adapter, as it may be a processed imageseries + super(OmegaImageSeries, self).__init__(ims) + + @property + def omega(self): + """return omega range array (nframes, 2)""" + return self.metadata[OMEGA_KEY] + + class OmegaWedges(object): """piecewise linear omega ranges""" def __init__(self, nframes): diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 7e008346..46c19623 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -1839,18 +1839,14 @@ def __init__(self, image_series, instrument_params, planeData, active_hkls, npdiv=npdiv, quiet=False, compute_areas_func=gutil.compute_areas) ij_patches.append(patches) - # DEBUGGING # mxf = num.amax([image_series[i] for i in range(image_series.nframes)], axis=0) - # DEBUGGING # xs = num.hstack([ij_patches[0][i][-1][1] for i in num.linspace(0, 1436, num=360, dtype=int)]) - # DEBUGGING # ys = num.hstack([ij_patches[0][i][-1][0] for i in num.linspace(0, 1436, num=360, dtype=int)]) - # DEBUGGING # import pdb; pdb.set_trace() # initialize maps and loop pbar = ProgressBar( widgets=[Bar('>'), ' ', ETA(), ' ', ReverseBar('<')], - maxval=image_series.nframes + maxval=len(image_series) ).start() maps = num.zeros((len(active_hkls), len(self._omegas), len(self._etas))) print "maps.shape: ", maps.shape - for i_ome in range(image_series.nframes): + for i_ome in range(len(image_series)): pbar.update(i_ome) this_frame = image_series[i_ome] if threshold is not None: From d878cb206eb4af50f828425c22e3a8848e40facb Mon Sep 17 00:00:00 2001 From: "Donald E. Boyce" Date: Sun, 23 Oct 2016 14:51:48 -0400 Subject: [PATCH 061/253] added imageseries processing to config file * config/imageseries.py + added process config section and applied processing to imageseries * image_io.py + _OmegaImageSeries now instantiates with an existing imageseries + ReadGE can instantiate with an existing imageseries + ReadGE.makeNew() method now raises NotImplementedError * coreutil.py + set up to call ReadGE with an imageseries --- hexrd/config/imageseries.py | 39 ++++++++++++++++++++++++++++++++++- hexrd/coreutil.py | 14 +------------ hexrd/imageseries/__init__.py | 1 + hexrd/xrd/image_io.py | 22 +++++++++++++------- 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index 287aba29..2e86215c 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -1,6 +1,8 @@ import glob import os +import numpy as np + from .config import Config from hexrd import imageseries @@ -17,6 +19,9 @@ def imageseries(self): """return the imageseries without checking for omega metadata""" if self._imser is None: self._imser = imageseries.open(self.filename, self.format, **self.args) + plist = self.process.process_list + if plist: + self._imser = imageseries.process.ProcessedImageSeries(self._imser, plist) return self._imser @property @@ -26,6 +31,8 @@ def omegaseries(self): self._omseries = imageseries.omega.OmegaImageSeries(self.imageseries) return self._omseries + # ========== yaml inputs + @property def filename(self): temp = self._cfg.get('image_series:filename') @@ -39,12 +46,42 @@ def format(self): @property def args(self): - return self._cfg.get('image_series:args') + return self._cfg.get('image_series:args', default={}) + + # ========== Other Configs @property def omega(self): return OmegaConfig(self._cfg) + @property + def process(self): + return ProcessConfig(self._cfg) + + +class ProcessConfig(Config): + + @property + def process_list(self): + plist = [] + dark = self.dark + if self.dark is not None: + plist.append(('dark', dark)) + flip = self.flip + if self.flip is not None: + plist.append(('flip', flip)) + + return plist + + @property + def flip(self): + return self._cfg.get('image_series:process:flip', default=None) + + @property + def dark(self): + fname = self._cfg.get('image_series:process:dark', default=None) + return np.load(fname) + class OmegaConfig(Config): diff --git a/hexrd/coreutil.py b/hexrd/coreutil.py index 955a69d3..fa7652c8 100644 --- a/hexrd/coreutil.py +++ b/hexrd/coreutil.py @@ -168,21 +168,9 @@ def initialize_experiment(cfg): pd = ws.activeMaterial.planeData - isfile = cfg.image_series.filename - isfmt = cfg.image_series.format - isargs = cfg.image_series.args # detector data try: - reader = ReadGE(isfile, fmt=isfmt, **isargs) - #reader = ReadGE( - # [(f, image_start) for f in cfg.image_series.files], - # np.radians(cfg.image_series.omega.start), - # np.radians(cfg.image_series.omega.step), - # subtractDark=dark is not None, # TODO: get rid of this - # dark=dark, - # doFlip=flip is not None, - # flipArg=flip, # TODO: flip=flip - # ) + reader = ReadGE(cfg.image_series.omegaseries) except IOError: logger.info("raw data not found, skipping reader init") reader = None diff --git a/hexrd/imageseries/__init__.py b/hexrd/imageseries/__init__.py index 6f6a919f..d8cbbdf4 100644 --- a/hexrd/imageseries/__init__.py +++ b/hexrd/imageseries/__init__.py @@ -5,6 +5,7 @@ data formats are managed in the "load" subpackage. """ from .baseclass import ImageSeries +from . import imageseriesabc from . import load from . import save from . import stats diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index 70eda82c..97ff1e9e 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -35,14 +35,14 @@ def __str__(self): -class OmegaImageSeries(object): +class _OmegaImageSeries(object): """Facade for frame_series class, replacing other readers, primarily ReadGE""" OMEGA_TAG = 'omega' - def __init__(self, fname, fmt='hdf5', **kwargs): + def __init__(self, ims, fmt='hdf5', **kwargs): """Initialize frame readerOmegaFrameReader - *fileinfo* is a string + *ims* is either an imageseries instance or a filename *fmt* is the format to be passed to imageseries.open() *kwargs* is the option list to be passed to imageseries.open() @@ -50,7 +50,10 @@ def __init__(self, fname, fmt='hdf5', **kwargs): * The shape returned from imageseries is cast to int from numpy.uint64 to allow for addition of indices with regular ints """ - self._imseries = imageseries.open(fname, fmt, **kwargs) + if isinstance(ims, imageseries.imageseriesabc.ImageSeriesABC): + self._imseries = ims + else: + self._imseries = imageseries.open(ims, fmt, **kwargs) self._nframes = len(self._imseries) self._shape = self._imseries.shape self._meta = self._imseries.metadata @@ -206,17 +209,19 @@ class ReadGE(Framer2DRC,OmegaFramer): def __init__(self, file_info, *args, **kwargs): """Initialize the reader - *file_info* is now just the filename + *file_info* is now just the filename or an existing omegaimageseries *kwargs* is a dictionary - keys include: "path" path in hdf5 file + keys include: 'fmt' which provides imageseries format + other keys depend on the format Of original kwargs, only using "mask" """ self._fname = file_info self._kwargs = kwargs self._format = kwargs.pop('fmt', None) + try: - self._omis = OmegaImageSeries(file_info, fmt=self._format, **kwargs) + self._omis = _OmegaImageSeries(file_info, fmt=self._format, **kwargs) Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) # note: Omegas are expected in radians, but input in degrees OmegaFramer.__init__(self, (np.pi/180.)*self._omis.omega) @@ -239,7 +244,8 @@ def __call__(self, *args, **kwargs): @classmethod def makeNew(cls): """return another copy of this reader""" - return cls(self._fname, **self._kwargs) + raise NotImplementedError('this method to be removed') + return None def getWriter(self, filename): return None From 60e0e08a64f3ec814c6c6418ac99cb418e49a7ad Mon Sep 17 00:00:00 2001 From: "Donald E. Boyce" Date: Thu, 27 Oct 2016 14:59:41 -0400 Subject: [PATCH 062/253] corrections in refactored ReadGE.read() --- hexrd/xrd/image_io.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index 97ff1e9e..53cef386 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -341,7 +341,7 @@ def read(self, nskip=0, nframes=1, sumImg=False): # Now, operate on frames consecutively op = sumImg if sumimg_callable else np.add - ifrm = self.iFrame + 1 + ifrm = self.iFrame[0] img = self._omis[ifrm] for i in range(1, nframes): @@ -353,6 +353,11 @@ def read(self, nskip=0, nframes=1, sumImg=False): if self.mask is not None: img[self.mask] = 0 + # reset iframe to single value of last frame read + self.iFrame = self.iFrame[-1] + if self.iFrame + 1 == self.getNFrames: + self.iFrame = -1 + return img def close(self): From 981cbe3da6bffc5345018baf85999d64e1277255 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 6 Dec 2016 14:45:29 -0600 Subject: [PATCH 063/253] fixed bug in imageseries processing for dark files in config file --- hexrd/config/imageseries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index 2e86215c..3731d893 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -80,7 +80,8 @@ def flip(self): @property def dark(self): fname = self._cfg.get('image_series:process:dark', default=None) - return np.load(fname) + if fname is not None: + return np.load(fname) class OmegaConfig(Config): From 59272172f3c0d34abdea0744da1690960c733145 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 6 Dec 2016 14:57:19 -0600 Subject: [PATCH 064/253] added comment about last commit --- hexrd/config/imageseries.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index 3731d893..20493b7c 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -79,6 +79,7 @@ def flip(self): @property def dark(self): + # fixed bug that returned np.load(None) fname = self._cfg.get('image_series:process:dark', default=None) if fname is not None: return np.load(fname) From e52a60f5f22c36a658f2208dc7979f810ee5de72 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 15 Aug 2016 11:39:23 -0500 Subject: [PATCH 065/253] added default pk func kwarg --- hexrd/fitting/fitpeak.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index d9901d97..74302fe1 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -36,7 +36,7 @@ #### 1-D Peak Fitting -def estimate_pk_parms_1d(x,f,pktype): +def estimate_pk_parms_1d(x,f,pktype='pvoigt'): """ Gives initial guess of parameters for analytic fit of one dimensional peak data. @@ -98,7 +98,7 @@ def estimate_pk_parms_1d(x,f,pktype): return p -def fit_pk_parms_1d(p0,x,f,pktype): +def fit_pk_parms_1d(p0,x,f,pktype='pvoigt'): """ Performs least squares fit to find parameters for 1d analytic functions fit to diffraction data @@ -353,4 +353,4 @@ def goodness_of_fit(f,f0): return R, Rw - \ No newline at end of file + From b07ee8aa886bd85533ad76fba3de6f4f8c11521e Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 7 Dec 2016 13:20:57 -0600 Subject: [PATCH 066/253] fixed relative path issues in imageseries loading and metadata --- hexrd/imageseries/load/framecache.py | 14 ++++++++++---- hexrd/imageseries/load/imagefiles.py | 2 +- hexrd/imageseries/load/metadata.py | 14 ++++++++++++-- hexrd/xrd/xrdutil.py | 3 ++- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/hexrd/imageseries/load/framecache.py b/hexrd/imageseries/load/framecache.py index 763b175f..52cba432 100644 --- a/hexrd/imageseries/load/framecache.py +++ b/hexrd/imageseries/load/framecache.py @@ -1,12 +1,15 @@ """Adapter class for frame caches """ -from . import ImageSeriesAdapter -from ..imageseriesiter import ImageSeriesIterator +import os import numpy as np from scipy.sparse import csr_matrix import yaml +from . import ImageSeriesAdapter +from ..imageseriesiter import ImageSeriesIterator +from .metadata import yamlmeta + class FrameCacheImageSeriesAdapter(ImageSeriesAdapter): """collection of images in HDF5 format""" @@ -30,11 +33,13 @@ def _load_yml(self): self._nframes = datad['nframes'] self._shape = tuple(datad['shape']) self._dtype = np.dtype(datad['dtype']) - self._meta = self.load_metadata(d['meta']) + self._meta = yamlmeta(d['meta'], path=self._cache) def _load_cache(self): """load into list of csr sparse matrices""" - arrs = np.load(self._cache) + bpath = os.path.dirname(self._fname) + arrs = np.load(os.path.join(bpath, self._cache)) + print bpath, os.path.join(bpath, self._cache) self._framelist = [] for i in range(self._nframes): @@ -56,6 +61,7 @@ def load_metadata(self, indict): Currently returns none """ + #### Currently not used: saved temporarily for np.array trigger metad = {} for k, v in indict.items(): if v == '++np.array': diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py index 18c16a67..8a738a5b 100644 --- a/hexrd/imageseries/load/imagefiles.py +++ b/hexrd/imageseries/load/imagefiles.py @@ -85,7 +85,7 @@ def _load_yml(self): self._empty = self.optsd[EMPTY] if EMPTY in self.optsd else 0 self._maxframes = self.optsd[MAXF] if MAXF in self.optsd else 0 - self._meta = yamlmeta(d['meta']) + self._meta = yamlmeta(d['meta'], path=imgsd) def _process_files(self): kw = {'empty': self._empty} diff --git a/hexrd/imageseries/load/metadata.py b/hexrd/imageseries/load/metadata.py index 625acafa..94d6ea2a 100644 --- a/hexrd/imageseries/load/metadata.py +++ b/hexrd/imageseries/load/metadata.py @@ -1,10 +1,15 @@ """metadata tools for imageseries""" +import os + import yaml import numpy as np -def yamlmeta(meta): +def yamlmeta(meta, path=None): """ Image sequence metadata + *path* is a full path or directory used to find the relative location + of files loaded via the trigger mechanism + The usual yaml dictionary is returned with the exception that if the first word of a multiword string is an exclamation mark ("!"), it will trigger further processing determined by the rest of the string. @@ -13,6 +18,11 @@ def yamlmeta(meta): ! load-numpy-object the returned value will the numpy object read from the file """ + if path is not None: + path = os.path.dirname(path) + else: + path = '.' + metad = {} for k, v in meta.items(): # check for triggers @@ -27,7 +37,7 @@ def yamlmeta(meta): metad.pop(newk, None) elif istrigger: if words[1] == "load-numpy-array": - fname = words[2] + fname = os.path.join(path, words[2]) metad[k] = np.load(fname) else: metad[k] = v diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 46c19623..e54fbcb1 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -1417,7 +1417,8 @@ def save(self, *args, **kwargs): self.p.save(*args, **kwargs) class CollapseOmeEta(object): - """ + """ MARKED FOR DELETION + Can pass a mask to use in addition to whatever the readers are already set up to do; with frames set zero where mask is True From 08cef1e3e06612be21e34a16a2e5f5bfb16f9ef8 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sun, 11 Dec 2016 15:22:43 -0500 Subject: [PATCH 067/253] added wedge determination to omega series and fixed relative path issues in imageseries and metadata load --- hexrd/config/tests/test_image_series.py | 6 -- hexrd/imageseries/load/framecache.py | 7 +- hexrd/imageseries/load/metadata.py | 2 +- hexrd/imageseries/omega.py | 64 ++++++++++++++++-- hexrd/imageseries/save.py | 10 ++- hexrd/imageseries/tests/test_formats.py | 1 + hexrd/imageseries/tests/test_omega.py | 89 +++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 hexrd/imageseries/tests/test_omega.py diff --git a/hexrd/config/tests/test_image_series.py b/hexrd/config/tests/test_image_series.py index f4069ade..86891334 100644 --- a/hexrd/config/tests/test_image_series.py +++ b/hexrd/config/tests/test_image_series.py @@ -32,7 +32,6 @@ def get_reference_data(cls): def test_filename(self): - self.assertRaises( RuntimeError, getattr, self.cfgs[0].image_series, 'filename' @@ -43,11 +42,6 @@ def test_filename(self): getattr, self.cfgs[0].image_series, 'format' ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].image_series, 'args' - ) - self.assertEqual( self.cfgs[1].image_series.filename, os.path.join(test_data['tempdir'], test_data['nonexistent_file']) diff --git a/hexrd/imageseries/load/framecache.py b/hexrd/imageseries/load/framecache.py index 52cba432..35de0e06 100644 --- a/hexrd/imageseries/load/framecache.py +++ b/hexrd/imageseries/load/framecache.py @@ -38,8 +38,11 @@ def _load_yml(self): def _load_cache(self): """load into list of csr sparse matrices""" bpath = os.path.dirname(self._fname) - arrs = np.load(os.path.join(bpath, self._cache)) - print bpath, os.path.join(bpath, self._cache) + if os.path.isabs(self._cache): + cachepath = self._cache + else: + cachepath = os.path.join(bpath, self._cache) + arrs = np.load(cachepath) self._framelist = [] for i in range(self._nframes): diff --git a/hexrd/imageseries/load/metadata.py b/hexrd/imageseries/load/metadata.py index 94d6ea2a..380a0e5a 100644 --- a/hexrd/imageseries/load/metadata.py +++ b/hexrd/imageseries/load/metadata.py @@ -33,7 +33,7 @@ def yamlmeta(meta, path=None): if v == '++np.array': # old way used in frame-cache (obsolescent) newk = k + '-array' - metad[k] = np.array(self._meta.pop(newk)) + metad[k] = np.array(meta.pop(newk)) metad.pop(newk, None) elif istrigger: if words[1] == "load-numpy-array": diff --git a/hexrd/imageseries/omega.py b/hexrd/imageseries/omega.py index d597ad30..f0ade6a1 100644 --- a/hexrd/imageseries/omega.py +++ b/hexrd/imageseries/omega.py @@ -10,19 +10,68 @@ class OmegaImageSeries(ImageSeries): """ImageSeries with omega metadata""" + DFLT_TOL = 1.0e-6 + def __init__(self, ims): """This class is initialized with an existing imageseries""" # check for omega metadata - if OMEGA_KEY not in ims.metadata: - raise RuntimeError('Imageseries has no omega metadata') + if OMEGA_KEY in ims.metadata: + self._omega = ims.metadata[OMEGA_KEY] + if len(ims) != self._omega.shape[0]: + msg = 'omega array mismatch: array has %s frames, expecting %s' + msg = msg % (self._omega.shape[0], len(ims)) + raise OmegaSeriesError(msg) + else: + raise OmegaSeriesError('Imageseries has no omega metadata') - # use the imageseries as the adapter, as it may be a processed imageseries super(OmegaImageSeries, self).__init__(ims) + self._make_wedges() + + def _make_wedges(self, tol=DFLT_TOL): + nf = len(self) + om = self.omega + + # find the frames where the wedges break + starts = [0] + delta = om[0, 1] - om[0, 0] + omlast = om[0, 1] + for f in range(1, nf): + if delta <= 0: + raise OmegaSeriesError('omega array must be increasing') + # check whether delta changes or ranges not contiguous + d = om[f,1] - om[f,0] + if (np.abs(d - delta) > tol) or (np.abs(om[f,0] - omlast) > tol): + starts.append(f) + delta = d + omlast = om[f, 1] + starts.append(nf) + + self._omegawedges = OmegaWedges(nf) + for s in range(len(starts) - 1): + ostart = om[starts[s], 0] + ostop = om[starts[s + 1] - 1, 1] + steps = starts[s+1] - starts[s] + self._omegawedges.addwedge(ostart, ostop, steps) @property def omega(self): """return omega range array (nframes, 2)""" - return self.metadata[OMEGA_KEY] + return self._omega + + @property + def omegawedges(self): + return self._omegawedges + + @property + def nwedges(self): + return self.omegawedges.nwedges + + def wedge(self, i): + """return i'th wedge as a dictionary""" + d = self.omegawedges.wedges[i] + delta = (d['ostop'] - d['ostart'])/d['nsteps'] + d.update(delta=delta) + return d class OmegaWedges(object): @@ -40,7 +89,7 @@ def omegas(self): if self.nframes != self.wframes: msg = "number of frames (%s) does not match "\ "number of wedge frames (%s)" %(self.nframes, self.wframes) - raise OmegaWedgesError(msg) + raise OmegaSeriesError(msg) oa = np.zeros((self.nframes, 2)) wstart = 0 @@ -50,6 +99,7 @@ def omegas(self): wa0 = np.linspace(w['ostart'], w['ostop'], ns + 1) oa[wr, 0] = wa0[:-1] oa[wr, 1] = wa0[1:] + wstart += ns return oa @@ -60,7 +110,7 @@ def nwedges(self): @property def wedges(self): - """list of wedges (dictionary)""" + """list of wedges (dictionaries)""" return self._wedges def addwedge(self, ostart, ostop, nsteps, loc=None): @@ -88,7 +138,7 @@ def save_omegas(self, fname): pass # end class -class OmegaWedgesError(Exception): +class OmegaSeriesError(Exception): def __init__(self, value): self.value = value def __str__(self): diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 7425a20e..84ec4d25 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -1,6 +1,7 @@ """Write imageseries to various formats""" from __future__ import print_function import abc +import os import numpy as np import h5py @@ -91,7 +92,7 @@ def write(self): for i in range(self._nframes): ds[i, :, :] = self._ims[i] - + # add metadata for k, v in self._meta.items(): g.attrs[k] = v @@ -131,7 +132,12 @@ def __init__(self, ims, fname, **kwargs): """ Writer.__init__(self, ims, fname, **kwargs) self._thresh = self._opts['threshold'] - self._cache = kwargs['cache_file'] + cf = kwargs['cache_file'] + if os.path.isabs(cf): + self._cache = cf + else: + cdir = os.path.dirname(fname) + self._cache = os.path.join(cdir, cf) def _process_meta(self): d = {} diff --git a/hexrd/imageseries/tests/test_formats.py b/hexrd/imageseries/tests/test_formats.py index 39f4fe61..b1138c9f 100644 --- a/hexrd/imageseries/tests/test_formats.py +++ b/hexrd/imageseries/tests/test_formats.py @@ -87,6 +87,7 @@ def setUp(self): def tearDown(self): os.remove(self.fcfile) + os.remove(os.path.join(self.tmpdir, self.cache_file)) def test_fmtfc(self): diff --git a/hexrd/imageseries/tests/test_omega.py b/hexrd/imageseries/tests/test_omega.py new file mode 100644 index 00000000..fa5672b3 --- /dev/null +++ b/hexrd/imageseries/tests/test_omega.py @@ -0,0 +1,89 @@ +import numpy as np + +from .common import ImageSeriesTest + +from hexrd import imageseries +from hexrd.imageseries.omega import OmegaSeriesError, OmegaImageSeries + +class TestOmegaSeries(ImageSeriesTest): + + @staticmethod + def make_ims(nf, meta): + a = np.zeros((nf, 2, 2)) + ims = imageseries.open(None, 'array', data=a, meta=meta) + return ims + + def test_no_omega(self): + ims = self.make_ims(2, {}) + with self.assertRaises(OmegaSeriesError): + oms = OmegaImageSeries(ims) + + def test_nframes_mismatch(self): + m = dict(omega=np.zeros((3, 2))) + ims = self.make_ims(2, m) + with self.assertRaises(OmegaSeriesError): + oms = OmegaImageSeries(ims) + + def test_negative_delta(self): + om = np.zeros((3, 2)) + om[0,1] = -0.5 + m = dict(omega=om, dtype=np.float) + ims = self.make_ims(3, m) + with self.assertRaises(OmegaSeriesError): + oms = OmegaImageSeries(ims) + + def test_one_wedge(self): + nf = 5 + a = np.linspace(0, nf+1, nf+1) + om = np.zeros((nf, 2)) + om[:,0] = a[:-1] + om[:,1] = a[1:] + m = dict(omega=om, dtype=np.float) + ims = self.make_ims(nf, m) + oms = OmegaImageSeries(ims) + self.assertEqual(oms.nwedges, 1) + + def test_two_wedges(self): + nf = 5 + a = np.linspace(0, nf+1, nf+1) + om = np.zeros((nf, 2)) + om[:,0] = a[:-1] + om[:,1] = a[1:] + om[3:, :] += 0.1 + m = dict(omega=om, dtype=np.float) + ims = self.make_ims(nf, m) + oms = OmegaImageSeries(ims) + self.assertEqual(oms.nwedges, 2) + + def test_compare_omegas(self): + nf = 5 + a = np.linspace(0, nf+1, nf+1) + om = np.zeros((nf, 2)) + om[:,0] = a[:-1] + om[:,1] = a[1:] + om[3:, :] += 0.1 + m = dict(omega=om, dtype=np.float) + ims = self.make_ims(nf, m) + oms = OmegaImageSeries(ims) + domega = om - oms.omegawedges.omegas + dnorm = np.linalg.norm(domega) + + msg='omegas from wedges do not match originals' + self.assertAlmostEqual(dnorm, 0., msg=msg) + + def test_wedge_delta(self): + nf = 5 + a = np.linspace(0, nf+1, nf+1) + om = np.zeros((nf, 2)) + om[:,0] = a[:-1] + om[:,1] = a[1:] + om[3:, :] += 0.1 + m = dict(omega=om, dtype=np.float) + ims = self.make_ims(nf, m) + oms = OmegaImageSeries(ims) + + mydelta =om[nf - 1, 1] - om[nf - 1, 0] + d = oms.wedge(oms.nwedges - 1) + self.assertAlmostEqual(d['delta'], mydelta) + + # end class From dabefe0aae9106341ebfc057a586a1ed9e33d95f Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 23 Jan 2017 08:10:22 -0800 Subject: [PATCH 068/253] added instrument module --- hexrd/instrument.py | 591 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 591 insertions(+) create mode 100644 hexrd/instrument.py diff --git a/hexrd/instrument.py b/hexrd/instrument.py new file mode 100644 index 00000000..08e92518 --- /dev/null +++ b/hexrd/instrument.py @@ -0,0 +1,591 @@ +# -*- coding: utf-8 -*- +#! /usr/bin/env python +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on dowloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +""" +Created on Fri Dec 9 13:05:27 2016 + +@author: bernier2 +""" + +import numpy as np + +from hexrd import gridutil as gutil +from hexrd import matrixutil as mutil +from hexrd.xrd.transforms_CAPI import anglesToGVec, \ + detectorXYToGvec, \ + gvecToDetectorXY, \ + makeDetectorRotMat, \ + mapAngle +from hexrd.xrd import xrdutil +from hexrd import constants as cnst + +from hexrd.xrd.distortion import GE_41RT # BAD, VERY BAD!!! FIX!!! + +beam_energy_DFLT = 65.351 +beam_vec_DFLT = cnst.beam_vec + +eta_vec_DFLT = cnst.eta_vec + +panel_id_DFLT = "generic" +nrows_DFLT = 2048 +ncols_DFLT = 2048 +pixel_size_DFLT = (0.2, 0.2) + +tilt_angles_DFLT = np.zeros(3) +t_vec_d_DFLT = np.r_[0., 0., -1000.] + +chi_DFLT = 0. +t_vec_s_DFLT = np.zeros(3) + +def calc_beam_vec(azim, pola): + """ + Calculate unit beam propagation vector from + spherical coordinate spec in DEGREES + + ...MAY CHANGE; THIS IS ALSO LOCATED IN XRDUTIL! + """ + tht = np.radians(azim) + phi = np.radians(pola) + bv = np.r_[ + np.sin(phi)*np.cos(tht), + np.cos(phi), + np.sin(phi)*np.sin(tht)] + return -bv + +class HEDMInstrument(object): + """ + * Distortion needs to be moved to a class with registry; tuple unworkable + * where should reference eta be defined? currently set to default config + """ + def __init__(self, instrument_config=None, + image_series=None, + instrument_name="instrument", + ): + self._id = instrument_name + + if instrument_config is None: + self._num_panels = 1 + self._beam_energy = beam_energy_DFLT + self._beam_vector = beam_vec_DFLT + + self._eta_vector = eta_vec_DFLT + + self._detectors = { + panel_id_DFLT:PlanarDetector( + rows=nrows_DFLT, cols=ncols_DFLT, + pixel_size=pixel_size_DFLT, + tvec=t_vec_d_DFLT, + tilt=tilt_angles_DFLT, + bvec=self._beam_vector, + evec=self._eta_vector, + distortion=None), + } + + self._t_vec_s = t_vec_s_DFLT + self._chi = chi_DFLT + else: + self._num_panels = len(instrument_config['detectors']) + self._beam_energy = instrument_config['beam']['energy'] # keV + self._beam_vector = calc_beam_vec( + instrument_config['beam']['vector']['azimuth'], + instrument_config['beam']['vector']['polar_angle'], + ) + cnst.eta_vec + # now build detector dict + detector_ids = instrument_config['detectors'].keys() + pixel_info = [instrument_config['detectors'][i]['pixels'] for i in detector_ids] + affine_info = [instrument_config['detectors'][i]['transform'] for i in detector_ids] + distortion = [] + for i in detector_ids: + try: + distortion.append( + instrument_config['detectors'][i]['distortion'] + ) + except KeyError: + distortion.append(None) + det_list = [] + for pix, xform, dist in zip(pixel_info, affine_info, distortion): + # HARD CODED GE DISTORTION !!! FIX + dist_tuple = None + if dist is not None: dist_tuple = (GE_41RT, dist['parameters']) + + det_list.append( + PlanarDetector( + rows=pix['rows'], cols=pix['columns'], + pixel_size=pix['size'], + tvec=xform['t_vec_d'], + tilt=xform['tilt_angles'], + bvec=self._beam_vector, + evec=cnst.eta_vec, + distortion=dist_tuple) + ) + pass + self._detectors = dict(zip(detector_ids, det_list)) + + self._t_vec_s = instrument_config['oscillation_stage']['t_vec_s'] + self._chi = instrument_config['oscillation_stage']['chi'] + + return + + # properties for physical size of rectangular detector + @property + def id(self): + return self._id + @property + def num_panels(self): + return self._num_panels + @property + def detectors(self): + return self._detectors + + @property + def tvec(self): + return self._t_vec_s + @tvec.setter + def tvec(self, x): + x = np.array(x).flatten() + assert len(x) == 3, 'input must have length = 3' + self._t_vec_s = x + + @property + def chi(self): + return self._chi + @chi.setter + def chi(self, x): + self._chi = float(x) + + @property + def beam_energy(self): + return self._beam_energy + @beam_energy.setter + def beam_energy(self, x): + self._beam_energy = float(x) + + @property + def beam_vector(self): + return self._beam_vector + @beam_vector.setter + def beam_vector(self, x): + x = np.array(x).flatten() + assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + 'input must have length = 3 and have unit magnitude' + self._beam_vector = x + # ...maybe change dictionary item behavior for 3.x compatibility? + for detector_id in self._detectors: + panel = self._detectors[detector_id] + panel.bvec = self._beam_vector + + @property + def eta_vector(self): + return self._eta_vector + @eta_vector.setter + def eta_vector(self, x): + x = np.array(x).flatten() + assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + 'input must have length = 3 and have unit magnitude' + self._eta_vector = x + # ...maybe change dictionary item behavior for 3.x compatibility? + for detector_id in self._detectors: + panel = self._detectors[detector_id] + panel.evec = self._eta_vector + + # methods + pass # end class: HEDMInstrument + +class PlanarDetector(object): + """ + base class for 2D row-column detector + """ + + __pixelPitchUnit = 'mm' + __delta_eta = np.radians(10.) + + def __init__(self, + rows=2048, cols=2048, + pixel_size=(0.2, 0.2), + tvec=np.r_[0., 0., -1000.], + tilt=cnst.zeros_3, + bvec=cnst.beam_vec, + evec=cnst.eta_vec, + distortion=None): + """ + """ + self._rows = rows + self._cols = cols + + self.pixel_size_row = pixel_size[0] + self.pixel_size_col = pixel_size[1] + + self._tvec = np.array(tvec).flatten() + self._tilt = tilt + + self._bvec = np.array(bvec).flatten() + self._evec = np.array(evec).flatten() + + self._distortion = distortion + return + + # properties for physical size of rectangular detector + @property + def rows(self): + return self._rows + @rows.setter + def rows(self, x): + assert isinstance(x, int) + self._rows = x + @property + def cols(self): + return self._cols + @cols.setter + def cols(self, x): + assert isinstance(x, int) + self._cols = x + + @property + def row_dim(self): + return self.rows * self.pixel_size_row + @property + def col_dim(self): + return self.cols * self.pixel_size_col + + @property + def row_pixel_vec(self): + return self.pixel_size_row*(0.5*(self.rows-1)-np.arange(self.rows)) + @property + def row_edge_vec(self): + return self.pixel_size_row*(0.5*self.rows-np.arange(self.rows+1)) + @property + def col_pixel_vec(self): + return self.pixel_size_col*(np.arange(self.cols)-0.5*(self.cols-1)) + @property + def col_edge_vec(self): + return self.pixel_size_col*(np.arange(self.cols+1)-0.5*self.cols) + + @property + def corner_ul(self): + return np.r_[-0.5 * self.col_dim, 0.5 * self.row_dim] + @property + def corner_ll(self): + return np.r_[-0.5 * self.col_dim, -0.5 * self.row_dim] + @property + def corner_lr(self): + return np.r_[ 0.5 * self.col_dim, -0.5 * self.row_dim] + @property + def corner_ur(self): + return np.r_[ 0.5 * self.col_dim, 0.5 * self.row_dim] + + @property + def tvec(self): + return self._tvec + @tvec.setter + def tvec(self, x): + x = np.array(x).flatten() + assert len(x) == 3, 'input must have length = 3' + self._tvec = x + + @property + def tilt(self): + return self._tilt + @tilt.setter + def tilt(self, x): + assert len(x) == 3, 'input must have length = 3' + self._tilt = np.array(x).squeeze() + + @property + def bvec(self): + return self._bvec + @bvec.setter + def bvec(self, x): + x = np.array(x).flatten() + assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + 'input must have length = 3 and have unit magnitude' + self._bvec = x + + @property + def evec(self): + return self._evec + @evec.setter + def evec(self, x): + x = np.array(x).flatten() + assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + 'input must have length = 3 and have unit magnitude' + self._evec = x + + @property + def distortion(self): + return self._distortion + @distortion.setter + def distortion(self, x): + """ + Probably should make distortion a class... + """ + assert len(x) == 2 and hasattr(x[0], '__call__'), \ + 'distortion must be a tuple: (, params)' + self._distortion = x + + @property + def rmat(self): + return makeDetectorRotMat(self.tilt) + + @property + def normal(self): + return self.rmat[:, 2] + + @property + def beam_position(self): + """ + returns the coordinates of the beam in the cartesian detector + frame {Xd, Yd, Zd}. NaNs if no intersection. + """ + output = np.nan * np.ones(2) + b_dot_n = np.dot(self.bvec, self.normal) + if np.logical_and( + abs(b_dot_n) > cnst.sqrt_epsf, + np.sign(b_dot_n) == -1 + ): + u = np.dot(self.normal, self.tvec) / b_dot_n + p2_l = u*self.bvec + p2_d = np.dot(self.rmat.T, p2_l - self.tvec) + output = p2_d[:2] + return output + + @property + def pixel_coords(self): + pix_i, pix_j = np.meshgrid( + self.row_pixel_vec, self.col_pixel_vec, + indexing='ij') + return pix_i, pix_j + + @property + def pixel_angles(self): + pix_i, pix_j = self.pixel_coords + xy = np.ascontiguousarray( + np.vstack([ + pix_j.flatten(), pix_i.flatten() + ]).T + ) + angs, g_vec = detectorXYToGvec( + xy, self.rmat, cnst.identity_3x3, + self.tvec, cnst.zeros_3, cnst.zeros_3, + beamVec=self.bvec, etaVec=self.evec) + del(g_vec) + tth = angs[0].reshape(self.rows, self.cols) + eta = angs[1].reshape(self.rows, self.cols) + return tth, eta + + + """ + ##################### METHODS + """ + def cartToPixel(self, xy_det, pixels=False): + """ + Convert vstacked array or list of [x,y] points in the center-based + cartesian frame {Xd, Yd, Zd} to (i, j) edge-based indices + + i is the row index, measured from the upper-left corner + j is the col index, measured from the upper-left corner + + if pixels=True, then (i,j) are integer pixel indices. + else (i,j) are continuous coords + """ + xy_det = np.atleast_2d(xy_det) + + npts = len(xy_det) + + tmp_ji = xy_det - np.tile(self.corner_ul, (npts, 1)) + i_pix = -tmp_ji[:, 1] / self.pixel_size_row - 0.5 + j_pix = tmp_ji[:, 0] / self.pixel_size_col - 0.5 + + ij_det = np.vstack([i_pix, j_pix]).T + if pixels: + ij_det = np.array(np.round(ij_det), dtype=int) + return ij_det + + def pixelToCart(self, ij_det): + """ + Convert vstacked array or list of [i,j] pixel indices + (or UL corner-based points) and convert to (x,y) in the + cartesian frame {Xd, Yd, Zd} + """ + ij_det = np.atleast_2d(ij_det) + + x = (ij_det[:, 1] + 0.5)*self.pixel_size_col + self.corner_ll[0] + y = (self.rows - ij_det[:, 0] - 0.5)*self.pixel_size_row + self.corner_ll[1] + return np.vstack([x, y]).T + + def angularPixelSize(self, xy, rMat_s=None, tVec_s=None, tVec_c=None): + """ + Wraps xrdutil.angularPixelSize + """ + # munge kwargs + if rMat_s is None: rMat_s = cnst.identity_3x3 + if tVec_s is None: tVec_s = cnst.zeros_3x1 + if tVec_c is None: tVec_c = cnst.zeros_3x1 + + # call function + ang_ps = xrdutil.angularPixelSize( + xy, (self.pixel_size_row, self.pixel_size_col), + self.rmat, rMat_s, + self.tvec, tVec_s, tVec_c, + distortion=self.distortion, + beamVec=self.bvec, etaVec=self.evec) + return ang_ps + + def clip_to_panel(self, xy, buffer_edges=False): + """ + """ + xy = np.atleast_2d(xy) + xlim = 0.5*self.col_dim + if buffer_edges: + xlim -= 0.5*self.pixel_size_col + ylim = 0.5*self.row_dim + if buffer_edges: + ylim -= 0.5*self.pixel_size_row + on_panel_x = np.logical_and(xy[:, 0] >= -xlim, xy[:, 0] <= xlim) + on_panel_y = np.logical_and(xy[:, 1] >= -ylim, xy[:, 1] <= ylim) + on_panel = np.where(np.logical_and(on_panel_x, on_panel_y))[0] + return xy[on_panel, :], on_panel + + def interpolate_bilinear(self, xy, img, pad_with_nans=True): + """ + """ + is_2d = img.ndim == 2 + right_shape = img.shape[0] == self.rows and img.shape[1] == self.cols + assert is_2d and right_shape, \ + "input image must be 2-d with shape (%d, %d)" %(self.rows, self.cols) + + # initialize output with nans + if pad_with_nans: + int_xy = np.nan*np.ones(len(xy)) + else: + int_xy = np.zeros(len(xy)) + + # clip away points too close to or off the edges of the detector + xy_clip, on_panel = self.clip_to_panel(xy, buffer_edges=True) + + # grab fractional pixel indices of clipped points + ij_frac = self.cartToPixel(xy_clip) + + # get floors/ceils from array of pixel _centers_ + i_floor = gutil.cellIndices(self.row_pixel_vec, xy_clip[:, 1]) + j_floor = gutil.cellIndices(self.col_pixel_vec, xy_clip[:, 0]) + i_ceil = i_floor + 1 + j_ceil = j_floor + 1 + + # first interpolate at top/bottom rows + row_floor_int = \ + (j_ceil - ij_frac[:, 1])*img[i_floor, j_floor] \ + + (ij_frac[:, 1] - j_floor)*img[i_floor, j_ceil] + row_ceil_int = \ + (j_ceil - ij_frac[:, 1])*img[i_ceil, j_floor] \ + + (ij_frac[:, 1] - j_floor)*img[i_ceil, j_ceil] + + # next interpolate across cols + int_vals = \ + (i_ceil - ij_frac[:, 0])*row_floor_int \ + + (ij_frac[:, 0] - i_floor)*row_ceil_int + int_xy[on_panel] = int_vals + return int_xy + + def make_powder_rings( + self, pd, merge_hkls=False, delta_eta=None, eta_period=None, + rmat_s=cnst.identity_3x3, tvec_s=cnst.zeros_3, + tvec_c=cnst.zeros_3 + ): + """ + """ + + # for generating rings + if delta_eta is None: delta_eta=self.__delta_eta + if eta_period is None: eta_period = (-np.pi, np.pi) + + neta = int(360./float(delta_eta)) + eta = mapAngle( + np.radians(delta_eta*np.linspace(0, neta-1, num=neta)) + eta_period[0], + eta_period + ) + + if merge_hkls: + tth_idx, tth_ranges = pd.getMergedRanges() + tth = [0.5*sum(i) for i in tth_ranges] + else: + tth = pd.getTTh() + angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] + + # need xy coords and pixel sizes + valid_ang = [] + valid_xy = [] + for i_ring in range(len(angs)): + these_angs = angs[i_ring].T + gVec_ring_l = anglesToGVec(these_angs, bHat_l=self.bvec) + xydet_ring = gvecToDetectorXY( + gVec_ring_l, + self.rmat, rmat_s, cnst.identity_3x3, + self.tvec, tvec_s, tvec_c) + # + xydet_ring, on_panel = self.clip_to_panel(xydet_ring) + # + valid_ang.append(these_angs[on_panel, :2]) + valid_xy.append(xydet_ring) + pass + return valid_ang, valid_xy + + def map_to_plane(self, pts, rmat, tvec): + """ + map detctor points to specified plane + + by convention + + n * (u*pts_l - tvec) = 0 + + [pts]_l = rmat*[pts]_m + tvec + """ + + # arg munging + pts = np.atleast_2d(pts); npts = len(pts) + + # map plane normal & translation vector, LAB FRAME + nvec_map_lab = rmat[:, 2].reshape(3, 1) + tvec_map_lab = np.atleast_2d(tvec).reshape(3, 1) + tvec_d_lab = np.atleast_2d(self.tvec).reshape(3, 1) + + # put pts as 3-d in panel CS and transform to 3-d lab coords + pts_det = np.hstack([pts, np.zeros((npts, 1))]) + pts_lab = np.dot(self.rmat, pts_det.T) + tvec_d_lab + + # scaling along pts vectors to hit map plane + u = np.dot(nvec_map_lab.T, tvec_map_lab) \ + / np.dot(nvec_map_lab.T, pts_lab) + + # pts on map plane, in LAB FRAME + pts_map_lab = np.tile(u, (3, 1)) * pts_lab + + return np.dot(rmat.T, pts_map_lab - tvec_map_lab)[:2, :].T + + + From 4af6e4bb329f88ea3e1ddb95b5728142ca809424 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 23 Jan 2017 14:47:28 -0800 Subject: [PATCH 069/253] fix branch spec in conda recipe --- conda.recipe/meta.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 85732094..270f37f9 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -5,8 +5,7 @@ package: source: git_url: https://github.com/joelvbernier/hexrd.git - #git_tag: master # edit to point to specific branch or tag - git_tag: v0.3.x + git_tag: master # edit to point to specific branch or tag build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} From cb04b8e240c8c8f6ae8841256c91ddd1d27e3dd9 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 25 Jan 2017 11:27:17 -0800 Subject: [PATCH 070/253] Update hdf5.py Added option to specify data attribute name, with the default being "images" --- hexrd/imageseries/load/hdf5.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hexrd/imageseries/load/hdf5.py b/hexrd/imageseries/load/hdf5.py index 935a470c..78f633c9 100644 --- a/hexrd/imageseries/load/hdf5.py +++ b/hexrd/imageseries/load/hdf5.py @@ -19,7 +19,8 @@ def __init__(self, fname, **kwargs): """ self.__h5name = fname self.__path = kwargs['path'] - self.__images = '/'.join([self.__path, 'images']) + self.__dataname = kwargs.pop('dataname', 'images') + self.__images = '/'.join([self.__path, self.__dataname]) self._meta = self._getmeta() def __getitem__(self, key): From 64cbc9de09a5d1c1926ba6fbbcdf781ec724fbdc Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 26 Jan 2017 12:59:43 -0500 Subject: [PATCH 071/253] fixed shape property on processed imageseries --- hexrd/imageseries/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index f688731a..b196660e 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -87,7 +87,7 @@ def dtype(self): @property def shape(self): - return self._imser.shape + return self[0].shape @property def metadata(self): From 09129546c66b78667f711f2038c819d8183f6a8e Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 26 Jan 2017 13:00:57 -0500 Subject: [PATCH 072/253] added omega_to_frame and omega_ranges_to_frames methods --- hexrd/imageseries/omega.py | 56 +++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/hexrd/imageseries/omega.py b/hexrd/imageseries/omega.py index f0ade6a1..cf2092d0 100644 --- a/hexrd/imageseries/omega.py +++ b/hexrd/imageseries/omega.py @@ -11,6 +11,7 @@ class OmegaImageSeries(ImageSeries): """ImageSeries with omega metadata""" DFLT_TOL = 1.0e-6 + TAU = 360 def __init__(self, ims): """This class is initialized with an existing imageseries""" @@ -46,12 +47,23 @@ def _make_wedges(self, tol=DFLT_TOL): omlast = om[f, 1] starts.append(nf) + nw = len(starts) - 1 + nf0 = 0 + self._wedge_om = np.zeros((nw, 3)) + self._wedge_f = np.zeros((nw, 2), dtype=int) self._omegawedges = OmegaWedges(nf) - for s in range(len(starts) - 1): + for s in range(nw): ostart = om[starts[s], 0] ostop = om[starts[s + 1] - 1, 1] steps = starts[s+1] - starts[s] self._omegawedges.addwedge(ostart, ostop, steps) + # + delta = (ostop - ostart)/steps + self._wedge_om[s, :] = (ostart, ostop, delta) + self._wedge_f[s, 0] = nf0 + self._wedge_f[s, 1] = steps + nf0 += steps + assert(nf0 == nf) @property def omega(self): @@ -73,6 +85,48 @@ def wedge(self, i): d.update(delta=delta) return d + def omega_to_frame(self, om): + """Return frame and wedge which includes given omega, -1 if not found""" + f = -1 + w = -1 + f0 = 0 + for i in range(len(self._wedge_om)): + omin = self._wedge_om[i, 0] + omax = self._wedge_om[i, 1] + omcheck = omin + np.mod(om - omin, self.TAU) + if omcheck < omax: + odel = self._wedge_om[i, 2] + f = self._wedge_f[i,0] + int(np.floor(omcheck - omin)/odel) + w = i + break + + return f, w + + def omegarange_to_frames(self, omin, omax): + """Return list of frames for range of omegas""" + noframes = () + f0, w0 = self.omega_to_frame(omin) + if w0 < 0: + return noframes + f1, w1 = self.omega_to_frame(omax) + if w1 < 0: + return noframes + + # if same wedge, require frames be increasing + if (w0 == w1) and (f1 > f0): + return range(f0, f1+1) + + # case: adjacent wedges with 2pi jump in omega + w0max = self._wedge_om[w0, 1] + w1min = self._wedge_om[w1, 0] + + if np.mod(np.abs(w1min - w0max), self.TAU) < self.DFLT_TOL: + r0 = range(f0, self._wedge_f[w0, 0] + self._wedge_f[w0, 1]) + r1 = range(self._wedge_f[w1, 0], f1 + 1) + return r0 + r1 + + return noframes + class OmegaWedges(object): """piecewise linear omega ranges""" From 678712ecab3d240d8b1bf602a669a7cd11ac2829 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 26 Jan 2017 13:01:43 -0500 Subject: [PATCH 073/253] added angles_in_ranges() function intended to replace validateAngleRanges() --- hexrd/xrd/transforms.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/hexrd/xrd/transforms.py b/hexrd/xrd/transforms.py index 08fcde4f..f82893a8 100644 --- a/hexrd/xrd/transforms.py +++ b/hexrd/xrd/transforms.py @@ -789,7 +789,7 @@ def _unitVectorMulti(a, b): else: for i in range(n): b[i, j] = a[i, j] - + def unitVector(a): """ @@ -893,7 +893,7 @@ def _makeEtaFrameRotMat(bHat_l, eHat_l, out): # bHat_l and eHat_l CANNOT have 0 magnitude! # must catch this case as well as colinear bHat_l/eHat_l elsewhere... bHat_mag = np.sqrt(bHat_l[0]**2 + bHat_l[1]**2 + bHat_l[2]**2) - + # assign Ze as -bHat_l for i in range(3): out[i, 2] = -bHat_l[i] / bHat_mag @@ -933,10 +933,10 @@ def makeEtaFrameRotMat(bHat_l, eHat_l): def makeEtaFrameRotMat(bHat_l, eHat_l): """ make eta basis COB matrix with beam antiparallel with Z - + takes components from ETA frame to LAB """ - # normalize input + # normalize input bHat_l = unitVector(bHat_l.reshape(3, 1)) eHat_l = unitVector(eHat_l.reshape(3, 1)) @@ -945,11 +945,35 @@ def makeEtaFrameRotMat(bHat_l, eHat_l): if np.sqrt(np.sum(Ye*Ye)) < 1e-8: raise RuntimeError, "bHat_l and eHat_l must NOT be colinear!" Ye = unitVector(Ye.reshape(3, 1)) - + # find Xe as cross(bHat_l, Ye) Xe = np.cross(bHat_l.flatten(), Ye.flatten()).reshape(3, 1) return np.hstack([Xe, Ye, -bHat_l]) +def angles_in_range(angles, starts, stops, degrees=True): + """Determine whether angles lie in or out of specified ranges + + *angles* - a list/array of angles + *starts* - a list of range starts + *stops* - a list of range stops + + OPTIONAL ARGS: + *degrees* - [True] angles & ranges in degrees (or radians) +""" + TAU = 360.0 if degrees else 2*np.pi + nw = len(starts) + na = len(angles) + in_range = np.zeros((na), dtype=bool) + for i in range(nw): + amin = starts[i] + amax = stops[i] + for j in range(na): + a = angles[j] + acheck = amin + np.mod(a - amin, TAU) + if acheck <= amax: + in_range[j] = True + + return in_range def validateAngleRanges(angList, startAngs, stopAngs, ccw=True): """ From 16ba3062dfc9fb0586f1db27be01df9249bc7051 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 27 Jan 2017 09:30:24 -0500 Subject: [PATCH 074/253] Fixes to image browser behavior --- hexrd/imageseries/process.py | 2 +- hexrd/wx/canvaspanel.py | 11 +++++++++-- hexrd/wx/gereader.py | 4 ++-- hexrd/xrd/detector.py | 23 ++++++++++++++++------- hexrd/xrd/image_io.py | 32 ++++++++++++++++++++++++-------- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index f688731a..b196660e 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -87,7 +87,7 @@ def dtype(self): @property def shape(self): - return self._imser.shape + return self[0].shape @property def metadata(self): diff --git a/hexrd/wx/canvaspanel.py b/hexrd/wx/canvaspanel.py index 344df48c..6b296ff9 100644 --- a/hexrd/wx/canvaspanel.py +++ b/hexrd/wx/canvaspanel.py @@ -289,12 +289,14 @@ def update(self, **kwargs): li = kwargs['loadImage'] ui = kwargs['updateImage'] oninit = kwargs['onInit'] + print 'li: ', li # # Show image if box is checked. # app = wx.GetApp() exp = app.ws img = exp.active_img + if img is None: # no active image, but possibly one on the axes @@ -304,9 +306,11 @@ def update(self, **kwargs): img0.set_visible(False) else: si = self.showImage_box.IsChecked() - + if ni or ui: # not using axes image list + if ni: self.axes.set_autoscale_on(True) + self.axes.images = [] self.axes.imshow(img, @@ -339,7 +343,10 @@ def update(self, **kwargs): # #rcho = self.rings_cho #ResetChoice(rcho, exp.matNames, rcho.GetStringSelection) - # + if li: + print 'loading image: working with axes' + self.axes.set_autoscale_on(True) + self.draw() # # Update image list diff --git a/hexrd/wx/gereader.py b/hexrd/wx/gereader.py index 12d8b58e..1ca137a4 100644 --- a/hexrd/wx/gereader.py +++ b/hexrd/wx/gereader.py @@ -419,7 +419,7 @@ def OnBrowseSpin(self, e): app = wx.GetApp() exp = app.ws exp.readImage(self.browse_spn.GetValue()) - app.getCanvas().update(newImage=True) + app.getCanvas().update(updateImage=True) return @@ -456,7 +456,7 @@ def OnImgBut(self, e): """Load image file names with file dialogue NOTE: converts filenames to str from unicode -""" + """ dlg = ReaderInfoDialog(self, -1) if dlg.ShowModal() == wx.ID_OK: d = dlg.GetInfo() diff --git a/hexrd/xrd/detector.py b/hexrd/xrd/detector.py index e25a764e..f855852e 100644 --- a/hexrd/xrd/detector.py +++ b/hexrd/xrd/detector.py @@ -79,9 +79,14 @@ ####### # GE, Perkin -NROWS = 2048 -NCOLS = 2048 -PIXEL = 0.2 +#NROWS = 2048 +#NCOLS = 2048 +#PIXEL = 0.2 + +# dexela, horizontal +NROWS = 3072 +NCOLS = 3888 +PIXEL = 0.0748 # CHESS retiga #NROWS = 2048 @@ -3746,10 +3751,10 @@ class DetectorGeomGE(Detector2DRC): __vfu = 0.2 # made up __vdk = 1800 # made up # 200 x 200 micron pixels - __pixelPitch = 0.2 # in mm - __idim = 2048 - __nrows = 2048 - __ncols = 2048 + __pixelPitch = PIXEL # in mm + __idim = max(NROWS, NCOLS) + __nrows = NROWS + __ncols = NCOLS __dParamDflt = [ 0.0, 0.0, 0.0, 2.0, 2.0, 2.0] __dParamZero = [ 0.0, 0.0, 0.0, 2.0, 2.0, 2.0] __dParamScalings = [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] @@ -3764,6 +3769,10 @@ def __init__(self, *args, **kwargs): else: self.__nrows = self.__idim = reader.nrows self.__ncols = reader.ncols + # self.__pixelPitch = kwargs.pop('pixelPitch', 0.2) + # self.__nrows = kwargs.pop('nrows', 2048) + # self.__ncols = kwargs.pop('ncols', 2048) + # self.__idim = max(self.__nrows, self.__ncols) Detector2DRC.__init__(self, self.__ncols, self.__nrows, self.__pixelPitch, diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index 53cef386..1debe25b 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -23,6 +23,8 @@ from hexrd import imageseries +import detector + #logging.basicConfig(level=logging.WARNING) warnings.filterwarnings('always', '', DeprecationWarning) @@ -59,7 +61,8 @@ def __init__(self, ims, fmt='hdf5', **kwargs): self._meta = self._imseries.metadata if self.OMEGA_TAG not in self._meta: - raise RuntimeError('No omega data found in data file') + #raise ImageIOError('No omega data found in data file') + pass def __getitem__(self, k): return self._imseries[k] @@ -82,7 +85,10 @@ def ncols(self): @property def omega(self): """ (get-only) array of omega begin/end per frame""" - return self._meta[self.OMEGA_TAG] + if self.OMEGA_TAG in self._meta: + return self._meta[self.OMEGA_TAG] + else: + return np.zeros((self.nframes,2)) @@ -91,8 +97,8 @@ class Framer2DRC(object): """ def __init__(self, ncols, nrows, dtypeDefault='int16', dtypeRead='uint16', dtypeFloat='float64'): - self.__nrows = nrows - self.__ncols = ncols + self._nrows = nrows + self._ncols = ncols self.__frame_dtype_dflt = dtypeDefault self.__frame_dtype_read = dtypeRead self.__frame_dtype_float = dtypeFloat @@ -102,11 +108,11 @@ def __init__(self, ncols, nrows, return def get_nrows(self): - return self.__nrows + return self._nrows nrows = property(get_nrows, None, None) def get_ncols(self): - return self.__ncols + return self._ncols ncols = property(get_ncols, None, None) def get_nbytesFrame(self): @@ -219,16 +225,20 @@ def __init__(self, file_info, *args, **kwargs): self._fname = file_info self._kwargs = kwargs self._format = kwargs.pop('fmt', None) - + self._nrows = detector.NROWS + self._ncols = detector.NCOLS try: self._omis = _OmegaImageSeries(file_info, fmt=self._format, **kwargs) Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) # note: Omegas are expected in radians, but input in degrees OmegaFramer.__init__(self, (np.pi/180.)*self._omis.omega) - except: + except(TypeError, IOError): logging.info('READGE initializations failed') if file_info is not None: raise self._omis = None + except ImageIOError: + self._omis = None + pass self.mask = None @@ -395,3 +405,9 @@ def newGenericReader(ncols, nrows, *args, **kwargs): retval = ReadGeneric(filename, ncols, nrows, *args, **kwargs) return retval + +class ImageIOError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) From 06cf8c88114decb8210cd7653a67f10ee08bb179 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 27 Jan 2017 14:25:54 -0500 Subject: [PATCH 075/253] added Dexela preprocessing files to share --- share/pp_dexela.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++ share/pp_init.py | 25 +++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 share/pp_dexela.py create mode 100644 share/pp_init.py diff --git a/share/pp_dexela.py b/share/pp_dexela.py new file mode 100644 index 00000000..7508e8de --- /dev/null +++ b/share/pp_dexela.py @@ -0,0 +1,76 @@ +import time + +from hexrd import imageseries + +PIS = imageseries.process.ProcessedImageSeries + +class PP_Dexela(object): + """PP_Dexela""" + PROCFMT = 'frame-cache' + RAWFMT = 'hdf5' + RAWPATH = '/imageseries' + DARKPCTILE = 50 + + def __init__(self, fname, omw, frame_start=0): + """Constructor for PP_Dexela""" + # + self.fname = fname + self.omwedges = omw + self.frame_start = frame_start + self.use_frame_list = (self.frame_start > 0) + self.raw = imageseries.open(self.fname, self.RAWFMT, path=self.RAWPATH) + self._dark = None + + return + + @property + def oplist(self): + return [('dark', self.dark ), ('flip', 't'), ('flip', 'hv') ] + + @property + def framelist(self): + return range(self.frame_start, self.nframes+self.frame_start) + # + # ============================== API + # + @property + def nframes(self): + return self.omwedges.nframes + + def omegas(self): + return self.omwedges.omegas + + def save_omegas(self, fname): + self.omwedges.save_omegas(fname) + + def processed(self): + if self.use_frame_list: + kw = dict(frame_list=self.framelist) + + return PIS(self.raw, self.oplist, **kw) + + @property + def dark(self, nframes=50): + """build and return dark image""" + if self._dark is None: + print "building dark images using %s frames (may take a while) ... " % nframes + start = time.clock() + self._dark = imageseries.stats.percentile( + self.raw, self.DARKPCTILE, nframes=nframes + ) + elapsed = (time.clock() - start) + print "done building background (dakr) image: elapsed time is %f seconds" \ + % elapsed + + return self._dark + + def save_processed(self, name, threshold): + fcname = '%s-fcache.yml' % name + cache = '%s-cachefile.npz' % name + omname = '%s-omegas.npy' % name + imageseries.write(self.processed(), fcname, self.PROCFMT, + threshold=threshold, + cache_file=cache) + self.save_omegas(omname) + + pass # end class diff --git a/share/pp_init.py b/share/pp_init.py new file mode 100644 index 00000000..106f1444 --- /dev/null +++ b/share/pp_init.py @@ -0,0 +1,25 @@ +from hexrd.imageseries import omega +from pp_dexla import PP_Dexela + +raw_fname = '/nfs/chess/raw/current/f2/shade-560-1/LSHR-6/%d/ff/ff2_%05d.h5' +raw_scannumber = 32 +raw_filenumber = 35 + +input_name = raw_fname %(raw_scannumber, raw_filenumber) +output_name = input_name.split('/')[-1].split('.')[0] + +nframes = 1440 +ostart = 0 +ostep = 0.25 +fstart = 5 +threshold = 150 + +# ==================== End Inputs (should not need to alter below this line) + +ostop = ostart + nframes*ostep +omw = omega.OmegaWedges(nframes) +omw.addwedge(ostart, ostop, nframes) + + +ppd = PP_Dexela(input_name, omw, frame_start=fstart) +ppd.save_processed(output_name, threshold) From 9ac3b02cefc125c5e821fea9063d6802e60f64db Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Fri, 27 Jan 2017 16:36:59 -0500 Subject: [PATCH 076/253] fixed frame-cache write (cache filename in yaml file); cleaned up pp_dexela --- hexrd/imageseries/save.py | 3 ++- share/pp_dexela.py | 39 ++++++++++++++++++++++++++++++++------- share/pp_init.py | 24 ++++++++++++++++++------ 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 84ec4d25..e93b989a 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -138,6 +138,7 @@ def __init__(self, ims, fname, **kwargs): else: cdir = os.path.dirname(fname) self._cache = os.path.join(cdir, cf) + self._cachename = cf def _process_meta(self): d = {} @@ -151,7 +152,7 @@ def _process_meta(self): return d def _write_yml(self): - datad = {'file': self._cache, 'dtype': str(self._ims.dtype), + datad = {'file': self._cachename, 'dtype': str(self._ims.dtype), 'nframes': len(self._ims), 'shape': list(self._ims.shape)} info = {'data': datad, 'meta': self._process_meta()} with open(self._fname, "w") as f: diff --git a/share/pp_dexela.py b/share/pp_dexela.py index 7508e8de..4bbce863 100644 --- a/share/pp_dexela.py +++ b/share/pp_dexela.py @@ -1,4 +1,5 @@ import time +import os from hexrd import imageseries @@ -11,25 +12,28 @@ class PP_Dexela(object): RAWPATH = '/imageseries' DARKPCTILE = 50 - def __init__(self, fname, omw, frame_start=0): + def __init__(self, fname, omw, flips, frame_start=0): """Constructor for PP_Dexela""" # self.fname = fname self.omwedges = omw + self.flips = flips self.frame_start = frame_start self.use_frame_list = (self.frame_start > 0) self.raw = imageseries.open(self.fname, self.RAWFMT, path=self.RAWPATH) self._dark = None + print 'On Init: ', self.nframes, self.fname, self.omwedges.nframes,\ + len(self.raw) return @property def oplist(self): - return [('dark', self.dark ), ('flip', 't'), ('flip', 'hv') ] + return [('dark', self.dark)] + self.flips @property def framelist(self): - return range(self.frame_start, self.nframes+self.frame_start) + return range(self.frame_start, self.nframes) # # ============================== API # @@ -53,10 +57,12 @@ def processed(self): def dark(self, nframes=50): """build and return dark image""" if self._dark is None: - print "building dark images using %s frames (may take a while) ... " % nframes + usenframes = min(nframes, self.nframes) + print "building dark images using %s frames (may take a while)"\ + " ... " % usenframes start = time.clock() self._dark = imageseries.stats.percentile( - self.raw, self.DARKPCTILE, nframes=nframes + self.raw, self.DARKPCTILE, nframes=usenframes ) elapsed = (time.clock() - start) print "done building background (dakr) image: elapsed time is %f seconds" \ @@ -65,12 +71,31 @@ def dark(self, nframes=50): return self._dark def save_processed(self, name, threshold): + dname = '%s-fcache-dir' % name + tcname = '%s-fcache-tmp.yml' % name fcname = '%s-fcache.yml' % name cache = '%s-cachefile.npz' % name omname = '%s-omegas.npy' % name - imageseries.write(self.processed(), fcname, self.PROCFMT, + + pname = lambda s: os.path.join(dname, s) # prepend fc directory + + os.mkdir(dname) + + # Steps: + # * write frame cache with no omegas to temporary file + # * write omegas to file + # * modify temporary file to include omegas + imageseries.write(self.processed(), pname(tcname), self.PROCFMT, threshold=threshold, cache_file=cache) - self.save_omegas(omname) + self.save_omegas(pname(omname)) + # modify yaml + with open(pname(tcname), 'r') as f: + s = f.read() + m0 = 'meta: {}' + m1 = 'meta:\n omega: ! load-numpy-array %s' % omname + with open(pname(fcname), 'w') as f: + f.write(s.replace(m0, m1)) + os.remove(pname(tcname)) pass # end class diff --git a/share/pp_init.py b/share/pp_init.py index 106f1444..44249243 100644 --- a/share/pp_init.py +++ b/share/pp_init.py @@ -1,14 +1,24 @@ +import os + from hexrd.imageseries import omega -from pp_dexla import PP_Dexela +from pp_dexela import PP_Dexela + +CHESS_BASE = '/nfs/chess/raw/current/f2/shade-560-1/LSHR-6' +CHESS_TMPL = '/%d/ff/ff2_%05d.h5' + +def h5name(scan, file, base=CHESS_BASE): + path = CHESS_TMPL % (scan, file) + return os.path.join(base, path) + +# ==================== Inputs (should not need to alter above this line) -raw_fname = '/nfs/chess/raw/current/f2/shade-560-1/LSHR-6/%d/ff/ff2_%05d.h5' raw_scannumber = 32 raw_filenumber = 35 -input_name = raw_fname %(raw_scannumber, raw_filenumber) -output_name = input_name.split('/')[-1].split('.')[0] +flips = [('flip', 't'), ('flip', 'hv') ] nframes = 1440 + ostart = 0 ostep = 0.25 fstart = 5 @@ -16,10 +26,12 @@ # ==================== End Inputs (should not need to alter below this line) +input_name = h5name(raw_scannumber, raw_filenumber) +output_name = input_name.split('/')[-1].split('.')[0] + ostop = ostart + nframes*ostep omw = omega.OmegaWedges(nframes) omw.addwedge(ostart, ostop, nframes) - -ppd = PP_Dexela(input_name, omw, frame_start=fstart) +ppd = PP_Dexela(input_name, omw, flips, frame_start=fstart) ppd.save_processed(output_name, threshold) From 34fd2c93ad6fcece72ff00b3f5078718be787fe3 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 27 Jan 2017 17:28:31 -0500 Subject: [PATCH 077/253] added average to imageseries.stats --- hexrd/imageseries/stats.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index 0efaac32..df4045eb 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -9,6 +9,13 @@ def max(ims, nframes=0): imgmax = np.maximum(imgmax, ims[i]) return imgmax +def average(ims, nframes=0): + """return image with average values over all frames""" + # could be done by rectangle by rectangle if full series + # too big for memory + nf = _nframes(ims, nframes) + return np.average(_toarray(ims, nf), axis=0) + def median(ims, nframes=0): """return image with median values over all frames""" # could be done by rectangle by rectangle if full series From 869b08e09e2d78378fe36f671417cc163652f8eb Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sat, 28 Jan 2017 20:33:55 -0500 Subject: [PATCH 078/253] Update instrument.py --- hexrd/instrument.py | 60 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 08e92518..1f1846c0 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -84,8 +84,7 @@ class HEDMInstrument(object): """ def __init__(self, instrument_config=None, image_series=None, - instrument_name="instrument", - ): + instrument_name="instrument"): self._id = instrument_name if instrument_config is None: @@ -232,8 +231,11 @@ def __init__(self, tilt=cnst.zeros_3, bvec=cnst.beam_vec, evec=cnst.eta_vec, + panel_buffer=None, distortion=None): """ + panel buffer is in pixels... + """ self._rows = rows self._cols = cols @@ -241,6 +243,8 @@ def __init__(self, self.pixel_size_row = pixel_size[0] self.pixel_size_col = pixel_size[1] + self._panel_buffer = panel_buffer + self._tvec = np.array(tvec).flatten() self._tilt = tilt @@ -266,6 +270,15 @@ def cols(self, x): assert isinstance(x, int) self._cols = x + @property + def panel_buffer(self): + return self._panel_buffer + @panel_buffer.setter + def panel_buffer(self, x): + """if not None, a buffer in pixels (rows, cols)""" + if x is not None: assert len(x) == 2 + self._panel_buffer = x + @property def row_dim(self): return self.rows * self.pixel_size_row @@ -273,6 +286,19 @@ def row_dim(self): def col_dim(self): return self.cols * self.pixel_size_col + @property + def panel_dims(self): + if self.panel_buffer is None: + pb = (0, 0) + pb = (self.pixel_size_row * pb[0], + self.pixel_size_col * pb[1]) + # is [(xmin, xmax), (ymin, ymax)] + pdim_buffered = [ + (-0.5*self.col_dim + pb[1], -0.5*self.row_dim + pb[0]), + ( 0.5*self.col_dim + pb[1], 0.5*self.row_dim + pb[0]), + ] + return pdim_buffered + @property def row_pixel_vec(self): return self.pixel_size_row*(0.5*(self.rows-1)-np.arange(self.rows)) @@ -530,11 +556,15 @@ def make_powder_rings( eta_period ) - if merge_hkls: - tth_idx, tth_ranges = pd.getMergedRanges() - tth = [0.5*sum(i) for i in tth_ranges] + # in case you want to give it tth angles directly + if hasattr(pd, '__len__'): + tth = np.array(pd).flatten() else: - tth = pd.getTTh() + if merge_hkls: + tth_idx, tth_ranges = pd.getMergedRanges() + tth = [0.5*sum(i) for i in tth_ranges] + else: + tth = pd.getTTh() angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] # need xy coords and pixel sizes @@ -554,7 +584,21 @@ def make_powder_rings( valid_xy.append(xydet_ring) pass return valid_ang, valid_xy - + """ + def make_reflection_patches(self, tth_eta, angular_pixel_size, + tth_tol=0.2, eta_tol=1.0, + rMat_c=None, tVec_c=None, + distortion=None, + npdiv=1): + make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, + omega=None, + tth_tol=0.2, eta_tol=1.0, + rMat_c=num.eye(3), tVec_c=num.zeros((3, 1)), + distortion=None, + npdiv=1, quiet=False, compute_areas_func=gutil.compute_areas, + beamVec=None) + return + """ def map_to_plane(self, pts, rmat, tvec): """ map detctor points to specified plane @@ -587,5 +631,3 @@ def map_to_plane(self, pts, rmat, tvec): return np.dot(rmat.T, pts_map_lab - tvec_map_lab)[:2, :].T - - From 3709327463bf446c49360e7d5f3c20f11dc4af3f Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sat, 28 Jan 2017 20:36:31 -0500 Subject: [PATCH 079/253] Added powder ring integration script. --- share/collapse_powder_ring.py | 224 ++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 share/collapse_powder_ring.py diff --git a/share/collapse_powder_ring.py b/share/collapse_powder_ring.py new file mode 100644 index 00000000..2c2a6b36 --- /dev/null +++ b/share/collapse_powder_ring.py @@ -0,0 +1,224 @@ +import os, copy + +import yaml + +import numpy as np + +from skimage import io + +from hexrd.xrd import transforms_CAPI as xfc +from hexrd.xrd import material + +from hexrd import constants, imageseries + +import instrument + +from matplotlib import pyplot as plt +from matplotlib import cm + +import hexrd.fitting.fitpeak +import hexrd.fitting.peakfunctions as pkfuncs +import scipy.optimize as optimize + +def make_matl(mat_name, sgnum, lparms, hkl_ssq_max=100): + matl = material.Material(mat_name) + matl.sgnum = sgnum + matl.latticeParameters = lparms + matl.hklMax = hkl_ssq_max + + nhkls = len(matl.planeData.exclusions) + matl.planeData.set_exclusions(np.zeros(nhkls, dtype=bool)) + return matl + +#%% +#instr_cfg_file = open('./dexela2_new.yml', 'r') +#instr_cfg = yaml.load(instr_cfg_file) +#instr = instrument.HEDMInstrument(instr_cfg) +# +#data_path = './' +#img_series_root = 'ff2_00047' +instr_cfg_file = open('./ge_detector_new.yml', 'r') +instr_cfg = yaml.load(instr_cfg_file) +instr = instrument.HEDMInstrument(instr_cfg) + +data_path = './' +img_series_root = 'ge_scan_114' +img_series = imageseries.open( + os.path.join( + img_series_root + '-fcache-dir', img_series_root + '-fcache.yml' + ) + , 'frame-cache') + +nframes = 240 + +average_frame = imageseries.stats.average(img_series) + +#%% +wlen = constants.keVToAngstrom(instr_cfg['beam']['energy']) + +matl = make_matl('LSHR', 225, [3.5905,]) + +pd = matl.planeData +pd.wavelength = instr_cfg['beam']['energy'] # takes keV +pd.exclusions = np.zeros_like(pd.exclusions, dtype=bool) + +set_by_tth_max = False +if set_by_tth_max: + pd.tThMax = np.radians(6.75) + tth_del = np.radians(0.75) + tth_avg = np.average(pd.getTTh()) + tth_lo = pd.getTTh()[0] - tth_del + tth_hi = pd.getTTh()[-1] + tth_del +else: + tth_lo = np.radians(5.) + tth_hi = np.radians(7.) + tth_avg = 0.5*(tth_hi + tth_lo) + excl = np.logical_or(pd.getTTh() <= tth_lo, pd.getTTh() >= tth_hi) + pd.exclusions = excl + +panel_id = instr_cfg['detectors'].keys()[0] + +d = instr.detectors[panel_id] + +pangs, pxys = d.make_powder_rings([tth_avg, ]) + +#tth, peta = d.pixel_angles +#Y, X = d.pixel_coords +#xy = np.vstack([X.flatten(), Y.flatten()]).T +aps = d.angularPixelSize(pxys[0]) + +print "min angular pixel sizes: %.4f, %.4f" \ + %(np.degrees(np.min(aps[:, 0])), np.degrees(np.min(aps[:, 1]))) + +#%% set from looking at GUI +tth_size = np.degrees(np.min(aps[:, 0])) +eta_size = np.degrees(np.min(aps[:, 1])) + +tth0 = np.degrees(tth_avg) +eta0 = 0. + +tth_range = np.degrees(tth_hi - tth_lo) +eta_range = 360. + +ntth = int(tth_range/tth_size) +neta = int(eta_range/eta_size) + +tth_vec = tth_size*(np.arange(ntth) - 0.5*ntth - 1) + tth0 +eta_vec = eta_size*(np.arange(neta) - 0.5*neta - 1) + eta0 + +angpts = np.meshgrid(eta_vec, tth_vec, indexing='ij') +gpts = xfc.anglesToGVec( + np.vstack([ + np.radians(angpts[1].flatten()), + np.radians(angpts[0].flatten()), + np.zeros(neta*ntth) + ]).T, bHat_l=d.bvec) + +xypts = xfc.gvecToDetectorXY( + gpts, + d.rmat, np.eye(3), np.eye(3), + d.tvec, np.zeros(3), np.zeros(3), + beamVec=d.bvec) + +img2 = d.interpolate_bilinear(xypts, average_frame).reshape(neta, ntth) +img3 = copy.deepcopy(img2) +borders = np.isnan(img2) +img2[borders] = 0. +img3[borders] = 0. +img3 += np.min(img3) + 1 +img3 = np.log(img3) +img3[borders] = np.nan + +extent = ( + np.min(angpts[1]), np.max(angpts[1]), + np.min(angpts[0]), np.max(angpts[0]) +) + +fig, ax = plt.subplots(2, 1, sharex=True, sharey=False) +ax[0].imshow(img3.reshape(neta, ntth), + interpolation='nearest', + cmap=cm.plasma, vmax=None, + extent=extent, + origin='lower') +ax[1].plot(angpts[1][0, :], np.sum(img2, axis=0)/img2.size) +ax[0].axis('tight') +ax[0].grid(True) +ax[1].grid(True) +ax[0].set_ylabel(r'$\eta$ [deg]', size=18) +ax[1].set_xlabel(r'$2\theta$ [deg]', size=18) +ax[1].set_ylabel(r'Intensity (arbitrary)', size=18) + +plt.show() + + + +#%% Multipeak Kludge + +def fit_pk_obj_1d_mpeak(p,x,f0,pktype,num_pks): + + f=np.zeros(len(x)) + p=np.reshape(p,[num_pks,p.shape[0]/num_pks]) + for ii in np.arange(num_pks): + if pktype == 'gaussian': + f=f+pkfuncs._gaussian1d_no_bg(p[ii],x) + elif pktype == 'lorentzian': + f=f+pkfuncs._lorentzian1d_no_bg(p[ii],x) + elif pktype == 'pvoigt': + f=f+pkfuncs._pvoigt1d_no_bg(p[ii],x) + elif pktype == 'split_pvoigt': + f=f+pkfuncs._split_pvoigt1d_no_bg(p[ii],x) + + + resd = f-f0 + return resd + + + +#%% +#plt.close('all') + +num_tth=len(pd.getTTh()) + +x=angpts[1][0, :] +f=np.sum(img2, axis=0)/img2.size +pktype='pvoigt' +num_pks=num_tth + +ftol=1e-6 +xtol=1e-6 + +fitArgs=(x,f,pktype,num_pks) + +tth=matl.planeData.getTTh()*180./np.pi + + +p0=np.zeros([num_tth,4]) + +for ii in np.arange(num_tth): + pt=np.argmin(np.abs(x-tth[ii])) + + p0[ii,:]=[f[pt],tth[ii],0.1,0.5] + + + +p, outflag = optimize.leastsq(fit_pk_obj_1d_mpeak, p0, args=fitArgs,ftol=ftol,xtol=xtol) + +p=np.reshape(p,[num_pks,p.shape[0]/num_pks]) +f_fit=np.zeros(len(x)) + +for ii in np.arange(num_pks): + f_fit=f_fit+pkfuncs._pvoigt1d_no_bg(p[ii],x) + + +#plt.plot(x,f,'x') +#plt.hold('true') +#plt.plot(x,f_fit) +ax[1].plot(x, f_fit, 'm+', ms=1) + +#%% +fit_tths = p[:, 1] +fit_dsps = 0.5*wlen/np.sin(0.5*np.radians(fit_tths)) +nrml_strains = fit_dsps/pd.getPlaneSpacings() - 1. + +print nrml_strains +print "avg normal strain: %.3e" %np.average(nrml_strains) \ No newline at end of file From 09f6c44af477830fb59849a49669e23e0a976d91 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 30 Jan 2017 14:04:54 -0500 Subject: [PATCH 080/253] tweaks to imageseries for pre-processing --- hexrd/imageseries/load/imagefiles.py | 2 +- hexrd/imageseries/stats.py | 42 +++++++++++++++++++++++++--- share/pp_init.py | 21 +++++++++++--- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py index 8a738a5b..77bccc6a 100644 --- a/hexrd/imageseries/load/imagefiles.py +++ b/hexrd/imageseries/load/imagefiles.py @@ -85,7 +85,7 @@ def _load_yml(self): self._empty = self.optsd[EMPTY] if EMPTY in self.optsd else 0 self._maxframes = self.optsd[MAXF] if MAXF in self.optsd else 0 - self._meta = yamlmeta(d['meta'], path=imgsd) + self._meta = yamlmeta(d['meta']) #, path=imgsd) def _process_files(self): kw = {'empty': self._empty} diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index df4045eb..39d1ed88 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -2,6 +2,11 @@ import numpy as np import logging +from hexrd.imageseries.process import ProcessedImageSeries as PIS + +# Default Buffer: 100 MB +STATS_BUFFER = 1.e8 + def max(ims, nframes=0): nf = _nframes(ims, nframes) imgmax = ims[0] @@ -11,10 +16,11 @@ def max(ims, nframes=0): def average(ims, nframes=0): """return image with average values over all frames""" - # could be done by rectangle by rectangle if full series - # too big for memory nf = _nframes(ims, nframes) - return np.average(_toarray(ims, nf), axis=0) + avg = np.array(ims[0], dtype=float) + for i in range(1, nf): + avg += ims[i] + return avg/nf def median(ims, nframes=0): """return image with median values over all frames""" @@ -28,7 +34,18 @@ def percentile(ims, pct, nframes=0): # could be done by rectangle by rectangle if full series # too big for memory nf = _nframes(ims, nframes) - return np.percentile(_toarray(ims, nf), pct, axis=0) + dt = ims.dtype + (nr, nc) = ims.shape + nrpb = _rows_in_buffer(nframes, nf*nc*dt.itemsize) + print 'rows per buffer: ', nrpb + # now build the result a rectangle at a time + img = np.zeros_like(ims[0]) + for rr in _row_ranges(nr, nrpb): + rect = np.array([[rr[0], rr[1]], [0, nc]]) + pims = PIS(ims, [('rectangle', rect)]) + print 'pims: ', len(pims), pims.shape + img[rr[0]:rr[1], :] = np.percentile(_toarray(pims, nf), pct, axis=0) + return img # # ==================== Utilities @@ -46,3 +63,20 @@ def _toarray(ims, nframes): a[i] = ims[i] return a + +def _row_ranges(n, m): + """return row ranges, representing m rows or remainder, until exhausted""" + i = 0 + while i < n: + imax = i+m + if imax <= n: + yield (i, imax) + else: + yield (i, n) + i = imax + +def _rows_in_buffer(ncol, rsize): + """number of rows in buffer + + NOTE: Use ceiling to make sure at it has at least one row""" + return int(np.ceil(STATS_BUFFER/rsize)) diff --git a/share/pp_init.py b/share/pp_init.py index 44249243..3c2cf66f 100644 --- a/share/pp_init.py +++ b/share/pp_init.py @@ -4,7 +4,7 @@ from pp_dexela import PP_Dexela CHESS_BASE = '/nfs/chess/raw/current/f2/shade-560-1/LSHR-6' -CHESS_TMPL = '/%d/ff/ff2_%05d.h5' +CHESS_TMPL = '%d/ff/ff2_%05d.h5' def h5name(scan, file, base=CHESS_BASE): path = CHESS_TMPL % (scan, file) @@ -12,12 +12,25 @@ def h5name(scan, file, base=CHESS_BASE): # ==================== Inputs (should not need to alter above this line) -raw_scannumber = 32 -raw_filenumber = 35 +## Room temp +#raw_scannumber = 32 +#raw_filenumber = 35 + +# ROOM TEMP +raw_scannumber = 81 +raw_filenumber = 45 + +## 100C +#raw_scannumber = 82 +#raw_filenumber = 46 + +## 300C +#raw_scannumber = 83 +#raw_filenumber = 47 flips = [('flip', 't'), ('flip', 'hv') ] -nframes = 1440 +nframes = 100 ostart = 0 ostep = 0.25 From e82c5afda80134940308597da00929e9a952b087 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 20 Mar 2017 11:04:25 -0500 Subject: [PATCH 081/253] Update omega.py Fixed omega_to_frame; missing parenthesis --- hexrd/imageseries/omega.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/imageseries/omega.py b/hexrd/imageseries/omega.py index cf2092d0..02b6a345 100644 --- a/hexrd/imageseries/omega.py +++ b/hexrd/imageseries/omega.py @@ -96,7 +96,7 @@ def omega_to_frame(self, om): omcheck = omin + np.mod(om - omin, self.TAU) if omcheck < omax: odel = self._wedge_om[i, 2] - f = self._wedge_f[i,0] + int(np.floor(omcheck - omin)/odel) + f = self._wedge_f[i,0] + int(np.floor((omcheck - omin)/odel)) w = i break From b0e11de9f16d7bb3f3381ff9e3ebc664f55a9c7d Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 21 Mar 2017 13:59:13 -0500 Subject: [PATCH 082/253] added pull_spots to instrument --- hexrd/gridutil.py | 15 + hexrd/instrument.py | 695 +++++++++++++++++++++++++++++++++++-------- hexrd/xrd/xrdutil.py | 56 ++-- 3 files changed, 620 insertions(+), 146 deletions(-) diff --git a/hexrd/gridutil.py b/hexrd/gridutil.py index 5628e26e..5b3ae184 100644 --- a/hexrd/gridutil.py +++ b/hexrd/gridutil.py @@ -178,6 +178,21 @@ def computeArea(polygon): area += 0.5 * cross(tvp[:2], tvp[2:]) return area +def make_tolerance_grid(bin_width, window_width, num_subdivisions, + adjust_window=False, one_sided=False): + if bin_width > window_width: + bin_width = window_width + if adjust_window: + window_width = np.ceil(window_width/bin_width)*bin_width + if one_sided: + ndiv = abs(int(window_width/bin_width)) + grid = (np.arange(0, 2*ndiv+1) - ndiv)*bin_width + ndiv = 2*ndiv + else: + ndiv = int(num_subdivisions*np.ceil(window_width/float(bin_width))) + grid = np.arange(0, ndiv+1)*window_width/float(ndiv) - 0.5*window_width + return ndiv, grid + def computeIntersection(line1, line2): """ compute intersection of two-dimensional line intersection diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 1f1846c0..b962a660 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -12,9 +12,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the Free +# Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -31,25 +31,36 @@ @author: bernier2 """ +from __future__ import print_function + +import os + +import yaml import numpy as np -from hexrd import gridutil as gutil +from scipy import ndimage + +from gridutil import cellIndices, make_tolerance_grid from hexrd import matrixutil as mutil from hexrd.xrd.transforms_CAPI import anglesToGVec, \ detectorXYToGvec, \ gvecToDetectorXY, \ makeDetectorRotMat, \ - mapAngle + makeOscillRotMat, \ + makeEtaFrameRotMat, \ + makeRotMatOfExpMap, \ + mapAngle, \ + oscillAnglesOfHKLs from hexrd.xrd import xrdutil -from hexrd import constants as cnst +from hexrd import constants as ct -from hexrd.xrd.distortion import GE_41RT # BAD, VERY BAD!!! FIX!!! +from hexrd.xrd.distortion import GE_41RT # BAD, VERY BAD!!! FIX!!! beam_energy_DFLT = 65.351 -beam_vec_DFLT = cnst.beam_vec +beam_vec_DFLT = ct.beam_vec -eta_vec_DFLT = cnst.eta_vec +eta_vec_DFLT = ct.eta_vec panel_id_DFLT = "generic" nrows_DFLT = 2048 @@ -62,9 +73,10 @@ chi_DFLT = 0. t_vec_s_DFLT = np.zeros(3) + def calc_beam_vec(azim, pola): """ - Calculate unit beam propagation vector from + Calculate unit beam propagation vector from spherical coordinate spec in DEGREES ...MAY CHANGE; THIS IS ALSO LOCATED IN XRDUTIL! @@ -77,6 +89,38 @@ def calc_beam_vec(azim, pola): np.sin(phi)*np.sin(tht)] return -bv + +def calc_angles_from_beam_vec(bvec): + """ + Return the azimuth and polar angle from a beam + vector + """ + bvec = np.atleast_2d(bvec).reshape(3, 1) + nvec = mutil.unitVector(-bvec) + azim = float( + np.degrees( + 0.5*np.pi + np.arctan2(nvec[0], nvec[2]) + ) + ) + pola = float(np.degrees(np.arccos(nvec[1]))) + return azim, pola + + + + +def migrate_instrument_config(instrument_config): + """utility function to generate old instrument config dictionary""" + cfg_list = [] + for detector_id in instrument_config['detectors']: + cfg_list.append( + dict( + detector=instrument_config['detectors'][detector_id], + oscillation_stage=instrument_config['oscillation_stage'], + ) + ) + return cfg_list + + class HEDMInstrument(object): """ * Distortion needs to be moved to a class with registry; tuple unworkable @@ -91,11 +135,11 @@ def __init__(self, instrument_config=None, self._num_panels = 1 self._beam_energy = beam_energy_DFLT self._beam_vector = beam_vec_DFLT - + self._eta_vector = eta_vec_DFLT - - self._detectors = { - panel_id_DFLT:PlanarDetector( + + self._detectors = dict( + panel_id_DFLT=PlanarDetector( rows=nrows_DFLT, cols=ncols_DFLT, pixel_size=pixel_size_DFLT, tvec=t_vec_d_DFLT, @@ -103,23 +147,25 @@ def __init__(self, instrument_config=None, bvec=self._beam_vector, evec=self._eta_vector, distortion=None), - } - - self._t_vec_s = t_vec_s_DFLT + ) + + self._tvec = t_vec_s_DFLT self._chi = chi_DFLT else: self._num_panels = len(instrument_config['detectors']) - self._beam_energy = instrument_config['beam']['energy'] # keV + self._beam_energy = instrument_config['beam']['energy'] # keV self._beam_vector = calc_beam_vec( instrument_config['beam']['vector']['azimuth'], instrument_config['beam']['vector']['polar_angle'], ) - cnst.eta_vec + ct.eta_vec # now build detector dict detector_ids = instrument_config['detectors'].keys() - pixel_info = [instrument_config['detectors'][i]['pixels'] for i in detector_ids] - affine_info = [instrument_config['detectors'][i]['transform'] for i in detector_ids] - distortion = [] + pixel_info = [instrument_config['detectors'][i]['pixels'] + for i in detector_ids] + affine_info = [instrument_config['detectors'][i]['transform'] + for i in detector_ids] + distortion = [] for i in detector_ids: try: distortion.append( @@ -131,8 +177,9 @@ def __init__(self, instrument_config=None, for pix, xform, dist in zip(pixel_info, affine_info, distortion): # HARD CODED GE DISTORTION !!! FIX dist_tuple = None - if dist is not None: dist_tuple = (GE_41RT, dist['parameters']) - + if dist is not None: + dist_tuple = (GE_41RT, dist['parameters']) + det_list.append( PlanarDetector( rows=pix['rows'], cols=pix['columns'], @@ -140,40 +187,44 @@ def __init__(self, instrument_config=None, tvec=xform['t_vec_d'], tilt=xform['tilt_angles'], bvec=self._beam_vector, - evec=cnst.eta_vec, + evec=ct.eta_vec, distortion=dist_tuple) ) pass self._detectors = dict(zip(detector_ids, det_list)) - self._t_vec_s = instrument_config['oscillation_stage']['t_vec_s'] - self._chi = instrument_config['oscillation_stage']['chi'] - + self._tvec = np.r_[instrument_config['oscillation_stage']['t_vec_s']] + self._chi = instrument_config['oscillation_stage']['chi'] + return - + # properties for physical size of rectangular detector @property def id(self): return self._id + @property def num_panels(self): return self._num_panels + @property def detectors(self): return self._detectors @property def tvec(self): - return self._t_vec_s + return self._tvec + @tvec.setter def tvec(self, x): x = np.array(x).flatten() assert len(x) == 3, 'input must have length = 3' - self._t_vec_s = x + self._tvec = x @property def chi(self): return self._chi + @chi.setter def chi(self, x): self._chi = float(x) @@ -181,44 +232,334 @@ def chi(self, x): @property def beam_energy(self): return self._beam_energy + @beam_energy.setter def beam_energy(self, x): self._beam_energy = float(x) - + + @property + def beam_wavelength(self): + return ct.keVToAngstrom(self.beam_energy) + @property def beam_vector(self): return self._beam_vector + @beam_vector.setter def beam_vector(self, x): x = np.array(x).flatten() - assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + assert len(x) == 3 and sum(x*x) > 1-ct.sqrt_epsf, \ 'input must have length = 3 and have unit magnitude' self._beam_vector = x # ...maybe change dictionary item behavior for 3.x compatibility? - for detector_id in self._detectors: - panel = self._detectors[detector_id] + for detector_id in self.detectors: + panel = self.detectors[detector_id] panel.bvec = self._beam_vector @property def eta_vector(self): return self._eta_vector + @eta_vector.setter def eta_vector(self, x): x = np.array(x).flatten() - assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + assert len(x) == 3 and sum(x*x) > 1-ct.sqrt_epsf, \ 'input must have length = 3 and have unit magnitude' self._eta_vector = x # ...maybe change dictionary item behavior for 3.x compatibility? - for detector_id in self._detectors: - panel = self._detectors[detector_id] + for detector_id in self.detectors: + panel = self.detectors[detector_id] panel.evec = self._eta_vector # methods - pass # end class: HEDMInstrument + def write_config(self, filename, calibration_dict={}): + """ WRITE OUT YAML FILE """ + # initialize output dictionary + + par_dict = {} + + azim, pola = calc_angles_from_beam_vec(self.beam_vector) + beam = dict( + energy=self.beam_energy, + vector=dict( + azimuth=azim, + polar_angle=pola, + ) + ) + par_dict['beam'] = beam + + if calibration_dict: + par_dict['calibration_crystal'] = calibration_dict + + ostage = dict( + chi=self.chi, + t_vec_s=self.tvec.tolist() + ) + par_dict['oscillation_stage'] = ostage + + det_names = self.detectors.keys() + det_dict = dict.fromkeys(det_names) + for det_name in det_names: + panel = self.detectors[det_name] + pdict = panel.config_dict(self.chi, self.tvec) + det_dict[det_name] = pdict['detector'] + par_dict['detectors'] = det_dict + with open(filename, 'w') as f: + yaml.dump(par_dict, stream=f) + return par_dict + + + def pull_spots(self, plane_data, grain_params, + imgser_dict, + tth_tol=0.25, eta_tol=1., ome_tol=1., + npdiv=1, threshold=10, + dirname='results', filename=None, save_spot_list=False, + quiet=True, lrank=1): + + + '''first find valid G-vectors''' + bMat = plane_data.latVecOps['B'] + + rMat_c = makeRotMatOfExpMap(grain_params[:3]) + tVec_c = grain_params[3:6] + vInv_s = grain_params[6:] + + # vstacked G-vector id, h, k, l + full_hkls = xrdutil._fetch_hkls_from_planedata(plane_data) + + # All possible bragg conditions as vstacked [tth, eta, ome] for + # each omega solution + angList = np.vstack( + oscillAnglesOfHKLs( + full_hkls[:, 1:], self.chi, + rMat_c, bMat, self.beam_wavelength, + vInv=vInv_s, + ) + ) + + # grab omega ranges from first imageseries + # ...NOTE THAT THEY ARE ALL ASSUMED TO HAVE SAME OMEGAS + oims0 = imgser_dict[imgser_dict.keys()[0]] + ome_ranges = [(ct.d2r*i['ostart'], ct.d2r*i['ostop']) + for i in oims0.omegawedges.wedges] + + # delta omega in DEGREES grabbed from first imageseries + # ...put in a check that they are all the same??? + delta_ome = oims0.omega[0, 1] - oims0.omega[0, 0] + + # make omega grid for frame expansion around reference frame + ndiv_ome, ome_del = make_tolerance_grid( + delta_ome, ome_tol, 1, adjust_window=True, + ) + + # generate structuring element for connected component labeling + if len(ome_del) == 1: + label_struct = ndimage.generate_binary_structure(2, lrank) + else: + label_struct = ndimage.generate_binary_structure(3, lrank) + + # filter by eta and omega ranges + allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( + full_hkls, angList, [(-np.pi, np.pi), ], ome_ranges + ) + + # dilate angles tth and eta to patch corners + nangs = len(allAngs) + tol_vec = 0.5*np.radians( + [-tth_tol, -eta_tol, + -tth_tol, eta_tol, + tth_tol, eta_tol, + tth_tol, -eta_tol]) + patch_vertices = (np.tile(allAngs[:, :2], (1, 4)) \ + + np.tile(tol_vec, (nangs, 1))).reshape(4*nangs, 2) + ome_dupl = np.tile(allAngs[:, 2], (4, 1)).T.reshape(len(patch_vertices), 1) + + '''loop over panels''' + iRefl = 0 + for detector_id in self.detectors: + # initialize output writer + if filename is not None: + output_dir = os.path.join( + dirname, detector_id + ) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + this_filename = os.path.join( + output_dir, filename + ) + pw = PatchDataWriter(this_filename) + + # grab panel + panel = self.detectors[detector_id] + instr_cfg = panel.config_dict(self.chi, self.tvec) + native_area = panel.pixel_area # pixel ref area + + # find points that fall on the panel + det_xy, rMat_s = xrdutil._project_on_detector_plane( + np.hstack([patch_vertices, ome_dupl]), + panel.rmat, rMat_c, self.chi, + panel.tvec, tVec_c, self.tvec, + panel.distortion + ) + tmp_xy, on_panel = panel.clip_to_panel(det_xy) + + # all vertices must be on... + patch_is_on = np.all(on_panel.reshape(nangs, 4), axis=1) + #nrefl_p += sum(patch_is_on) + + # grab hkls and gvec ids for this panel + hkls_p = allHKLs[patch_is_on, 1:] + hkl_ids = allHKLs[patch_is_on, 0] + + # reflection angles (voxel centers) and pixel size in (tth, eta) + ang_centers = allAngs[patch_is_on, :] + ang_pixel_size = panel.angularPixelSize(tmp_xy) + + # make the tth,eta patches for interpolation + patches = xrdutil.make_reflection_patches( + instr_cfg, ang_centers[:, :2], ang_pixel_size, + tth_tol=tth_tol, eta_tol=eta_tol, + rMat_c=rMat_c, tVec_c=tVec_c, + distortion=panel.distortion, + npdiv=npdiv, quiet=True, + beamVec=self.beam_vector) + + # pull out the OmegaImageSeries for this panel from input dict + ome_imgser = imgser_dict[detector_id] + + # grand loop over reflections for this panel + for i_pt, patch in enumerate(patches): + + # grab hkl info + hkl = hkls_p[i_pt, :] + hkl_id = hkl_ids[i_pt] + + # strip relevant objects out of current patch + vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch + prows, pcols = areas.shape + + tth_edges = vtx_angs[0][0, :] + delta_tth = tth_edges[1] - tth_edges[0] + + eta_edges = vtx_angs[1][:, 0] + delta_eta = eta_edges[1] - eta_edges[0] + + # need to reshape eval pts for interpolation + xy_eval = np.vstack([xy_eval[0].flatten(), + xy_eval[1].flatten()]).T + + # the evaluation omegas; + # expand about the central value using tol vector + ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del + + # ...vectorize the omega_to_frame function to avoid loop? + frame_indices = [ + ome_imgser.omega_to_frame(ome)[0] for ome in ome_eval + ] + if np.any(frame_indices == -1): + if not quiet: + msg = "window for (%d%d%d) falls outside omega range"\ + % tuple(hkl) + print(msg) + continue + else: + peak_id = -999 + sum_int = None + max_int = None + meas_angs = None + meas_xy = None + + patch_data = np.zeros((len(frame_indices), prows, pcols)) + ome_edges = np.hstack( + [ome_imgser.omega[frame_indices][:, 0], + ome_imgser.omega[frame_indices][-1, 1]] + ) + for i, i_frame in enumerate(frame_indices): + patch_data[i] = \ + panel.interpolate_bilinear( + xy_eval, + ome_imgser[i_frame], + ).reshape(prows, pcols)*(areas/float(native_area)) + pass + + # now have interpolated patch data... + labels, num_peaks = ndimage.label( + patch_data > threshold, structure=label_struct + ) + slabels = np.arange(1, num_peaks + 1) + if num_peaks > 0: + peak_id = iRefl + coms = np.array( + ndimage.center_of_mass( + patch_data, labels=labels, index=slabels + ) + ) + if num_peaks > 1: + center = np.r_[patch_data.shape]*0.5 + com_diff = coms - np.tile(center, (num_peaks, 1)) + closest_peak_idx = np.argmin(np.sum(com_diff**2, axis=1)) + else: + closest_peak_idx = 0 + pass # end multipeak conditional + coms = coms[closest_peak_idx] + meas_angs = np.hstack([ + tth_edges[0] + (0.5 + coms[2])*delta_tth, + eta_edges[0] + (0.5 + coms[1])*delta_eta, + np.radians(ome_edges[0] + (0.5 + coms[0])*delta_ome) + ]) + + # intensities + # - summed is 'integrated' over interpolated data + # - max is max of raw input data + sum_int = np.sum( + patch_data[labels == slabels[closest_peak_idx]] + ) + max_int = np.max( + [ome_imgser[i][ijs[0], ijs[1]] for i in frame_indices] + ) + #max_int = np.max( + # patch_data[labels == slabels[closest_peak_idx]] + # ) + + # need xy coords + gvec_c = anglesToGVec( + meas_angs, + chi=self.chi, + rMat_c=rMat_c, + bHat_l=self.beam_vector) + rMat_s = makeOscillRotMat([self.chi, meas_angs[2]]) + meas_xy = gvecToDetectorXY( + gvec_c, + panel.rmat, rMat_s, rMat_c, + panel.tvec, self.tvec, tVec_c, + beamVec=self.beam_vector) + if panel.distortion is not None: + """...FIX THIS!!!""" + meas_xy = panel.distortion[0]( + np.atleast_2d(meas_xy), + panel.distortion[1], + invert=True).flatten() + pass + pass + + # write output + if filename is not None: + pw.dump_patch( + peak_id, hkl_id, hkl, sum_int, max_int, + ang_centers[i_pt], meas_angs, meas_xy) + iRefl += 1 + pass # end patch conditional + pass # end patch loop + if filename is not None: + del(pw) + pass # end detector loop + return + pass # end class: HEDMInstrument + class PlanarDetector(object): """ - base class for 2D row-column detector + base class for 2D planar, rectangular row-column detector """ __pixelPitchUnit = 'mm' @@ -228,9 +569,9 @@ def __init__(self, rows=2048, cols=2048, pixel_size=(0.2, 0.2), tvec=np.r_[0., 0., -1000.], - tilt=cnst.zeros_3, - bvec=cnst.beam_vec, - evec=cnst.eta_vec, + tilt=ct.zeros_3, + bvec=ct.beam_vec, + evec=ct.eta_vec, panel_buffer=None, distortion=None): """ @@ -240,8 +581,8 @@ def __init__(self, self._rows = rows self._cols = cols - self.pixel_size_row = pixel_size[0] - self.pixel_size_col = pixel_size[1] + self._pixel_size_row = pixel_size[0] + self._pixel_size_col = pixel_size[1] self._panel_buffer = panel_buffer @@ -252,62 +593,79 @@ def __init__(self, self._evec = np.array(evec).flatten() self._distortion = distortion + return # properties for physical size of rectangular detector @property def rows(self): return self._rows + @rows.setter def rows(self, x): assert isinstance(x, int) self._rows = x + @property def cols(self): return self._cols + @cols.setter def cols(self, x): assert isinstance(x, int) self._cols = x + @property + def pixel_size_row(self): + return self._pixel_size_row + + @pixel_size_row.setter + def pixel_size_row(self, x): + self._pixel_size_row = float(x) + + @property + def pixel_size_col(self): + return self._pixel_size_col + + @pixel_size_col.setter + def pixel_size_col(self, x): + self._pixel_size_col = float(x) + + @property + def pixel_area(self): + return self.pixel_size_row * self.pixel_size_col + @property def panel_buffer(self): return self._panel_buffer + @panel_buffer.setter def panel_buffer(self, x): - """if not None, a buffer in pixels (rows, cols)""" - if x is not None: assert len(x) == 2 + """if not None, a buffer in mm (x, y)""" + if x is not None: + assert len(x) == 2 self._panel_buffer = x @property def row_dim(self): return self.rows * self.pixel_size_row + @property def col_dim(self): return self.cols * self.pixel_size_col - @property - def panel_dims(self): - if self.panel_buffer is None: - pb = (0, 0) - pb = (self.pixel_size_row * pb[0], - self.pixel_size_col * pb[1]) - # is [(xmin, xmax), (ymin, ymax)] - pdim_buffered = [ - (-0.5*self.col_dim + pb[1], -0.5*self.row_dim + pb[0]), - ( 0.5*self.col_dim + pb[1], 0.5*self.row_dim + pb[0]), - ] - return pdim_buffered - @property def row_pixel_vec(self): return self.pixel_size_row*(0.5*(self.rows-1)-np.arange(self.rows)) + @property def row_edge_vec(self): return self.pixel_size_row*(0.5*self.rows-np.arange(self.rows+1)) + @property def col_pixel_vec(self): return self.pixel_size_col*(np.arange(self.cols)-0.5*(self.cols-1)) + @property def col_edge_vec(self): return self.pixel_size_col*(np.arange(self.cols+1)-0.5*self.cols) @@ -315,19 +673,23 @@ def col_edge_vec(self): @property def corner_ul(self): return np.r_[-0.5 * self.col_dim, 0.5 * self.row_dim] + @property def corner_ll(self): return np.r_[-0.5 * self.col_dim, -0.5 * self.row_dim] + @property def corner_lr(self): - return np.r_[ 0.5 * self.col_dim, -0.5 * self.row_dim] + return np.r_[0.5 * self.col_dim, -0.5 * self.row_dim] + @property def corner_ur(self): - return np.r_[ 0.5 * self.col_dim, 0.5 * self.row_dim] + return np.r_[0.5 * self.col_dim, 0.5 * self.row_dim] @property def tvec(self): return self._tvec + @tvec.setter def tvec(self, x): x = np.array(x).flatten() @@ -337,6 +699,7 @@ def tvec(self, x): @property def tilt(self): return self._tilt + @tilt.setter def tilt(self, x): assert len(x) == 3, 'input must have length = 3' @@ -345,30 +708,34 @@ def tilt(self, x): @property def bvec(self): return self._bvec + @bvec.setter def bvec(self, x): x = np.array(x).flatten() - assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + assert len(x) == 3 and sum(x*x) > 1-ct.sqrt_epsf, \ 'input must have length = 3 and have unit magnitude' self._bvec = x @property def evec(self): return self._evec + @evec.setter def evec(self, x): x = np.array(x).flatten() - assert len(x) == 3 and sum(x*x) > 1-cnst.sqrt_epsf, \ + assert len(x) == 3 and sum(x*x) > 1-ct.sqrt_epsf, \ 'input must have length = 3 and have unit magnitude' self._evec = x @property def distortion(self): return self._distortion + @distortion.setter def distortion(self, x): """ Probably should make distortion a class... + ***FIX THIS*** """ assert len(x) == 2 and hasattr(x[0], '__call__'), \ 'distortion must be a tuple: (, params)' @@ -391,15 +758,16 @@ def beam_position(self): output = np.nan * np.ones(2) b_dot_n = np.dot(self.bvec, self.normal) if np.logical_and( - abs(b_dot_n) > cnst.sqrt_epsf, + abs(b_dot_n) > ct.sqrt_epsf, np.sign(b_dot_n) == -1 - ): + ): u = np.dot(self.normal, self.tvec) / b_dot_n p2_l = u*self.bvec p2_d = np.dot(self.rmat.T, p2_l - self.tvec) output = p2_d[:2] return output + # ...memoize??? @property def pixel_coords(self): pix_i, pix_j = np.meshgrid( @@ -407,6 +775,7 @@ def pixel_coords(self): indexing='ij') return pix_i, pix_j + # ...memoize??? @property def pixel_angles(self): pix_i, pix_j = self.pixel_coords @@ -416,14 +785,46 @@ def pixel_angles(self): ]).T ) angs, g_vec = detectorXYToGvec( - xy, self.rmat, cnst.identity_3x3, - self.tvec, cnst.zeros_3, cnst.zeros_3, + xy, self.rmat, ct.identity_3x3, + self.tvec, ct.zeros_3, ct.zeros_3, beamVec=self.bvec, etaVec=self.evec) del(g_vec) tth = angs[0].reshape(self.rows, self.cols) eta = angs[1].reshape(self.rows, self.cols) return tth, eta + def config_dict(self, chi, t_vec_s, sat_level=None): + """ + """ + t_vec_s = np.atleast_1d(t_vec_s) + + d = dict( + detector=dict( + transform=dict( + tilt_angles=self.tilt, + t_vec_d=self.tvec.tolist(), + ), + pixels=dict( + rows=self.rows, + columns=self.cols, + size=[self.pixel_size_row, self.pixel_size_col], + ), + ), + oscillation_stage=dict( + chi=chi, + t_vec_s=t_vec_s.tolist(), + ), + ) + if sat_level is not None: + d['detector']['saturation_level'] = sat_level + if self.distortion is not None: + """...HARD CODED DISTORTION! FIX THIS!!!""" + dist_d = dict( + function_name='GE_41RT', + parameters=self.distortion[1] + ) + d['detector']['distortion'] = dist_d + return d """ ##################### METHODS @@ -445,7 +846,7 @@ def cartToPixel(self, xy_det, pixels=False): tmp_ji = xy_det - np.tile(self.corner_ul, (npts, 1)) i_pix = -tmp_ji[:, 1] / self.pixel_size_row - 0.5 - j_pix = tmp_ji[:, 0] / self.pixel_size_col - 0.5 + j_pix = tmp_ji[:, 0] / self.pixel_size_col - 0.5 ij_det = np.vstack([i_pix, j_pix]).T if pixels: @@ -460,8 +861,10 @@ def pixelToCart(self, ij_det): """ ij_det = np.atleast_2d(ij_det) - x = (ij_det[:, 1] + 0.5)*self.pixel_size_col + self.corner_ll[0] - y = (self.rows - ij_det[:, 0] - 0.5)*self.pixel_size_row + self.corner_ll[1] + x = (ij_det[:, 1] + 0.5)*self.pixel_size_col\ + + self.corner_ll[0] + y = (self.rows - ij_det[:, 0] - 0.5)*self.pixel_size_row\ + + self.corner_ll[1] return np.vstack([x, y]).T def angularPixelSize(self, xy, rMat_s=None, tVec_s=None, tVec_c=None): @@ -469,32 +872,34 @@ def angularPixelSize(self, xy, rMat_s=None, tVec_s=None, tVec_c=None): Wraps xrdutil.angularPixelSize """ # munge kwargs - if rMat_s is None: rMat_s = cnst.identity_3x3 - if tVec_s is None: tVec_s = cnst.zeros_3x1 - if tVec_c is None: tVec_c = cnst.zeros_3x1 + if rMat_s is None: + rMat_s = ct.identity_3x3 + if tVec_s is None: + tVec_s = ct.zeros_3x1 + if tVec_c is None: + tVec_c = ct.zeros_3x1 # call function ang_ps = xrdutil.angularPixelSize( xy, (self.pixel_size_row, self.pixel_size_col), self.rmat, rMat_s, self.tvec, tVec_s, tVec_c, - distortion=self.distortion, + distortion=self.distortion, beamVec=self.bvec, etaVec=self.evec) return ang_ps - def clip_to_panel(self, xy, buffer_edges=False): + def clip_to_panel(self, xy, buffer_edges=True): """ """ xy = np.atleast_2d(xy) xlim = 0.5*self.col_dim - if buffer_edges: - xlim -= 0.5*self.pixel_size_col ylim = 0.5*self.row_dim - if buffer_edges: - ylim -= 0.5*self.pixel_size_row + if buffer_edges and self.panel_buffer is not None: + xlim -= self.panel_buffer[0] + ylim -= self.panel_buffer[1] on_panel_x = np.logical_and(xy[:, 0] >= -xlim, xy[:, 0] <= xlim) on_panel_y = np.logical_and(xy[:, 1] >= -ylim, xy[:, 1] <= ylim) - on_panel = np.where(np.logical_and(on_panel_x, on_panel_y))[0] + on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel def interpolate_bilinear(self, xy, img, pad_with_nans=True): @@ -502,15 +907,16 @@ def interpolate_bilinear(self, xy, img, pad_with_nans=True): """ is_2d = img.ndim == 2 right_shape = img.shape[0] == self.rows and img.shape[1] == self.cols - assert is_2d and right_shape, \ - "input image must be 2-d with shape (%d, %d)" %(self.rows, self.cols) + assert is_2d and right_shape,\ + "input image must be 2-d with shape (%d, %d)"\ + % (self.rows, self.cols) # initialize output with nans if pad_with_nans: int_xy = np.nan*np.ones(len(xy)) else: int_xy = np.zeros(len(xy)) - + # clip away points too close to or off the edges of the detector xy_clip, on_panel = self.clip_to_panel(xy, buffer_edges=True) @@ -518,8 +924,8 @@ def interpolate_bilinear(self, xy, img, pad_with_nans=True): ij_frac = self.cartToPixel(xy_clip) # get floors/ceils from array of pixel _centers_ - i_floor = gutil.cellIndices(self.row_pixel_vec, xy_clip[:, 1]) - j_floor = gutil.cellIndices(self.col_pixel_vec, xy_clip[:, 0]) + i_floor = cellIndices(self.row_pixel_vec, xy_clip[:, 1]) + j_floor = cellIndices(self.col_pixel_vec, xy_clip[:, 0]) i_ceil = i_floor + 1 j_ceil = j_floor + 1 @@ -539,23 +945,25 @@ def interpolate_bilinear(self, xy, img, pad_with_nans=True): return int_xy def make_powder_rings( - self, pd, merge_hkls=False, delta_eta=None, eta_period=None, - rmat_s=cnst.identity_3x3, tvec_s=cnst.zeros_3, - tvec_c=cnst.zeros_3 - ): + self, pd, merge_hkls=False, delta_eta=None, eta_period=None, + rmat_s=ct.identity_3x3, tvec_s=ct.zeros_3, + tvec_c=ct.zeros_3): """ """ - + # for generating rings - if delta_eta is None: delta_eta=self.__delta_eta - if eta_period is None: eta_period = (-np.pi, np.pi) - + if delta_eta is None: + delta_eta = self.__delta_eta + if eta_period is None: + eta_period = (-np.pi, np.pi) + neta = int(360./float(delta_eta)) eta = mapAngle( - np.radians(delta_eta*np.linspace(0, neta-1, num=neta)) + eta_period[0], - eta_period + np.radians( + delta_eta*np.linspace(0, neta-1, num=neta) + ) + eta_period[0], eta_period ) - + # in case you want to give it tth angles directly if hasattr(pd, '__len__'): tth = np.array(pd).flatten() @@ -575,8 +983,9 @@ def make_powder_rings( gVec_ring_l = anglesToGVec(these_angs, bHat_l=self.bvec) xydet_ring = gvecToDetectorXY( gVec_ring_l, - self.rmat, rmat_s, cnst.identity_3x3, - self.tvec, tvec_s, tvec_c) + self.rmat, rmat_s, ct.identity_3x3, + self.tvec, tvec_s, tvec_c, + beamVec=self.bvec) # xydet_ring, on_panel = self.clip_to_panel(xydet_ring) # @@ -584,50 +993,86 @@ def make_powder_rings( valid_xy.append(xydet_ring) pass return valid_ang, valid_xy - """ - def make_reflection_patches(self, tth_eta, angular_pixel_size, - tth_tol=0.2, eta_tol=1.0, - rMat_c=None, tVec_c=None, - distortion=None, - npdiv=1): - make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, - omega=None, - tth_tol=0.2, eta_tol=1.0, - rMat_c=num.eye(3), tVec_c=num.zeros((3, 1)), - distortion=None, - npdiv=1, quiet=False, compute_areas_func=gutil.compute_areas, - beamVec=None) - return - """ + def map_to_plane(self, pts, rmat, tvec): """ map detctor points to specified plane - by convention - + by convention + n * (u*pts_l - tvec) = 0 - + [pts]_l = rmat*[pts]_m + tvec """ - # arg munging - pts = np.atleast_2d(pts); npts = len(pts) + pts = np.atleast_2d(pts) + npts = len(pts) # map plane normal & translation vector, LAB FRAME - nvec_map_lab = rmat[:, 2].reshape(3, 1) + nvec_map_lab = rmat[:, 2].reshape(3, 1) tvec_map_lab = np.atleast_2d(tvec).reshape(3, 1) tvec_d_lab = np.atleast_2d(self.tvec).reshape(3, 1) - + # put pts as 3-d in panel CS and transform to 3-d lab coords pts_det = np.hstack([pts, np.zeros((npts, 1))]) pts_lab = np.dot(self.rmat, pts_det.T) + tvec_d_lab - + # scaling along pts vectors to hit map plane u = np.dot(nvec_map_lab.T, tvec_map_lab) \ / np.dot(nvec_map_lab.T, pts_lab) - + # pts on map plane, in LAB FRAME pts_map_lab = np.tile(u, (3, 1)) * pts_lab - + return np.dot(rmat.T, pts_map_lab - tvec_map_lab)[:2, :].T - + + +"""UTILITIES""" + + +class PatchDataWriter(object): + """ + """ + def __init__(self, filename): + xy_str = '{:18}\t{:18}\t{:18}' + ang_str = xy_str + '\t' + self._header = \ + '{:6}\t{:6}\t'.format('# ID', 'PID') + \ + '{:3}\t{:3}\t{:3}\t'.format('H', 'K', 'L') + \ + '{:12}\t{:12}\t'.format('sum(int)', 'max(int)') + \ + ang_str.format('pred tth', 'pred eta', 'pred ome') + \ + ang_str.format('meas tth', 'meas eta', 'meas ome') + \ + xy_str.format('meas X', 'meas Y', 'meas ome') + if isinstance(filename, file): + self.fid = filename + else: + self.fid = open(filename, 'w') + print(self._header, file=self.fid) + + def __del__(self): + self.close() + + def close(self): + self.fid.close() + + def dump_patch(self, peak_id, hkl_id, + hkl, spot_int, max_int, + pangs, mangs, xy): + nans_tabbed_12 = '{:^12}\t{:^12}\t' + nans_tabbed_18 = '{:^18}\t{:^18}\t{:^18}\t{:^18}\t{:^18}' + output_str = \ + '{:<6d}\t{:<6d}\t'.format(int(peak_id), int(hkl_id)) + \ + '{:<3d}\t{:<3d}\t{:<3d}\t'.format(*np.array(hkl, dtype=int)) + if peak_id >= 0: + output_str += \ + '{:<1.6e}\t{:<1.6e}\t'.format(spot_int, max_int) + \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*pangs) + \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*mangs) + \ + '{:<1.12e}\t{:<1.12e}'.format(xy[0], xy[1]) + else: + output_str += \ + nans_tabbed_12.format(*np.ones(2)*np.nan) + \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*pangs) + \ + nans_tabbed_18.format(*np.ones(5)*np.nan) + print(output_str, file=self.fid) + return output_str diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 5628e8bf..77fa4608 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -499,13 +499,14 @@ def makePathVariantPoles(rMatRef, fromPhase, return qVecList + def displayPathVariants(data, rMatRef, fromPhase, pathList, planeDataDict, detectorGeom, omeMin, omeMax, phaseForDfltPD=None, - markerList = markerListDflt, - hklList = None, + markerList=markerListDflt, + hklList=None, color=None, pointKWArgs={}, hklIDs=None, pw=None): @@ -3891,10 +3892,11 @@ def _compute_max(tth, eta, result): return result - def angularPixelSize(xy_det, xy_pixelPitch, - rMat_d, rMat_s, - tVec_d, tVec_s, tVec_c, - distortion=None, beamVec=None, etaVec=None): + def angularPixelSize( + xy_det, xy_pixelPitch, + rMat_d, rMat_s, + tVec_d, tVec_s, tVec_c, + distortion=None, beamVec=None, etaVec=None): """ * choices to beam vector and eta vector specs have been supressed * assumes xy_det in UNWARPED configuration @@ -4134,17 +4136,29 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, col_indices = gutil.cellIndices(col_edges, xy_eval[:, 0]) # append patch data to list - patches.append(((gVec_angs_vtx[:, 0].reshape(m_tth.shape), - gVec_angs_vtx[:, 1].reshape(m_tth.shape)), - (xy_eval_vtx[:, 0].reshape(m_tth.shape), - xy_eval_vtx[:, 1].reshape(m_tth.shape)), - conn, - areas.reshape(sdims[0], sdims[1]), - (row_indices.reshape(sdims[0], sdims[1]), - col_indices.reshape(sdims[0], sdims[1])) - ) - ) - pass + patches.append( + ( + ( + gVec_angs_vtx[:, 0].reshape(m_tth.shape), + gVec_angs_vtx[:, 1].reshape(m_tth.shape), + ), + ( + xy_eval_vtx[:, 0].reshape(m_tth.shape), + xy_eval_vtx[:, 1].reshape(m_tth.shape), + ), + conn, + areas.reshape(sdims[0], sdims[1]), + ( + xy_eval[:, 0].reshape(sdims[0], sdims[1]), + xy_eval[:, 1].reshape(sdims[0], sdims[1]), + ), + ( + row_indices.reshape(sdims[0], sdims[1]), + col_indices.reshape(sdims[0], sdims[1]), + ), + ) + ) + pass # close loop over angles return patches def pullSpots(pd, detector_params, grain_params, reader, @@ -4570,11 +4584,11 @@ def extract_detector_transformation(detector_params): """ # extract variables for convenience if isinstance(detector_params, dict): rMat_d = xfcapi.makeDetectorRotMat( - instr_cfg['detector']['transform']['tilt_angles'] + detector_params['detector']['transform']['tilt_angles'] ) - tVec_d = num.r_[instr_cfg['detector']['transform']['t_vec_d']] - chi = instr_cfg['oscillation_stage']['chi'] - tVec_s = num.r_[instr_cfg['oscillation_stage']['t_vec_s']] + tVec_d = num.r_[detector_params['detector']['transform']['t_vec_d']] + chi = detector_params['oscillation_stage']['chi'] + tVec_s = num.r_[detector_params['oscillation_stage']['t_vec_s']] else: assert len(detector_params >= 10), \ "list of detector parameters must have length >= 10" From e41723702902064bbae649f78a5f2131767a55c2 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 22 Mar 2017 14:01:52 -0500 Subject: [PATCH 083/253] added line extraction and tth range plotting opt --- hexrd/instrument.py | 314 +++++++++++++++++++++++++++++--------------- 1 file changed, 210 insertions(+), 104 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index b962a660..a62e8b38 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -41,7 +41,7 @@ from scipy import ndimage -from gridutil import cellIndices, make_tolerance_grid +from hexrd.gridutil import cellIndices, make_tolerance_grid from hexrd import matrixutil as mutil from hexrd.xrd.transforms_CAPI import anglesToGVec, \ detectorXYToGvec, \ @@ -104,8 +104,8 @@ def calc_angles_from_beam_vec(bvec): ) pola = float(np.degrees(np.arccos(nvec[1]))) return azim, pola - - + + def migrate_instrument_config(instrument_config): @@ -287,16 +287,16 @@ def write_config(self, filename, calibration_dict={}): ) ) par_dict['beam'] = beam - + if calibration_dict: par_dict['calibration_crystal'] = calibration_dict - + ostage = dict( chi=self.chi, t_vec_s=self.tvec.tolist() ) par_dict['oscillation_stage'] = ostage - + det_names = self.detectors.keys() det_dict = dict.fromkeys(det_names) for det_name in det_names: @@ -308,13 +308,104 @@ def write_config(self, filename, calibration_dict={}): yaml.dump(par_dict, stream=f) return par_dict - + + def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1., npdiv=2): + """ + """ + tol_vec = 0.5*np.radians( + [-tth_tol, -eta_tol, + -tth_tol, eta_tol, + tth_tol, eta_tol, + tth_tol, -eta_tol]) + panel_data = [] + for detector_id in self.detectors: + # grab panel + panel = self.detectors[detector_id] + instr_cfg = panel.config_dict(self.chi, self.tvec) + native_area = panel.pixel_area # pixel ref area + + # pull out the image for this panel from input dict + image = image_dict[detector_id] + + # make rings + pow_angs, pow_xys = panel.make_powder_rings( + plane_data, merge_hkls=True, delta_eta=eta_tol) + n_rings = len(pow_angs) + + ring_data = [] + for i_ring in range(n_rings): + these_angs = pow_angs[i_ring] + + # make sure no one falls off... + npts = len(these_angs) + patch_vertices = (np.tile(these_angs, (1, 4)) \ + + np.tile(tol_vec, (npts, 1))).reshape(4*npts, 2) + + # find points that fall on the panel + det_xy, rMat_s = xrdutil._project_on_detector_plane( + np.hstack([patch_vertices, np.zeros((4*npts, 1))]), + panel.rmat, ct.identity_3x3, self.chi, + panel.tvec, ct.zeros_3, self.tvec, + panel.distortion + ) + tmp_xy, on_panel = panel.clip_to_panel(det_xy) + + # all vertices must be on... + patch_is_on = np.all(on_panel.reshape(npts, 4), axis=1) + + # reflection angles (voxel centers) and pixel size in (tth, eta) + ang_centers = these_angs[patch_is_on] + ang_pixel_size = panel.angularPixelSize(tmp_xy[::4, :]) + + # make the tth,eta patches for interpolation + patches = xrdutil.make_reflection_patches( + instr_cfg, ang_centers, ang_pixel_size, + tth_tol=tth_tol, eta_tol=eta_tol, + distortion=panel.distortion, + npdiv=npdiv, quiet=True, + beamVec=self.beam_vector) + + # loop over patches + patch_data = [] + for patch in patches: + # strip relevant objects out of current patch + vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch + prows, pcols = areas.shape + + # need to reshape eval pts for interpolation + xy_eval = np.vstack([ + xy_eval[0].flatten(), + xy_eval[1].flatten()]).T + + # edge arrays + tth_edges = vtx_angs[0][0, :] + delta_tth = tth_edges[1] - tth_edges[0] + eta_edges = vtx_angs[1][:, 0] + delta_eta = eta_edges[1] - eta_edges[0] + + # interpolate + patch_data.append( + panel.interpolate_bilinear( + xy_eval, + image, + ).reshape(prows, pcols)*(areas/float(native_area)) + ) + + # + pass # close patch loop + ring_data.append(patch_data) + pass # close ring loop + panel_data.append(ring_data) + pass # close panel loop + return panel_data + + def pull_spots(self, plane_data, grain_params, imgser_dict, tth_tol=0.25, eta_tol=1., ome_tol=1., - npdiv=1, threshold=10, + npdiv=2, threshold=10, dirname='results', filename=None, save_spot_list=False, - quiet=True, lrank=1): + quiet=True, lrank=1, check_only=False): '''first find valid G-vectors''' @@ -376,6 +467,7 @@ def pull_spots(self, plane_data, grain_params, '''loop over panels''' iRefl = 0 + compl = [] for detector_id in self.detectors: # initialize output writer if filename is not None: @@ -405,7 +497,6 @@ def pull_spots(self, plane_data, grain_params, # all vertices must be on... patch_is_on = np.all(on_panel.reshape(nangs, 4), axis=1) - #nrefl_p += sum(patch_is_on) # grab hkls and gvec ids for this panel hkls_p = allHKLs[patch_is_on, 1:] @@ -413,7 +504,7 @@ def pull_spots(self, plane_data, grain_params, # reflection angles (voxel centers) and pixel size in (tth, eta) ang_centers = allAngs[patch_is_on, :] - ang_pixel_size = panel.angularPixelSize(tmp_xy) + ang_pixel_size = panel.angularPixelSize(tmp_xy[::4, :]) # make the tth,eta patches for interpolation patches = xrdutil.make_reflection_patches( @@ -430,17 +521,17 @@ def pull_spots(self, plane_data, grain_params, # grand loop over reflections for this panel for i_pt, patch in enumerate(patches): - # grab hkl info - hkl = hkls_p[i_pt, :] - hkl_id = hkl_ids[i_pt] - # strip relevant objects out of current patch vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch prows, pcols = areas.shape + # grab hkl info + hkl = hkls_p[i_pt, :] + hkl_id = hkl_ids[i_pt] + + # edge arrays tth_edges = vtx_angs[0][0, :] delta_tth = tth_edges[1] - tth_edges[0] - eta_edges = vtx_angs[1][:, 0] delta_eta = eta_edges[1] - eta_edges[0] @@ -463,97 +554,106 @@ def pull_spots(self, plane_data, grain_params, print(msg) continue else: - peak_id = -999 - sum_int = None - max_int = None - meas_angs = None - meas_xy = None - - patch_data = np.zeros((len(frame_indices), prows, pcols)) - ome_edges = np.hstack( - [ome_imgser.omega[frame_indices][:, 0], - ome_imgser.omega[frame_indices][-1, 1]] - ) - for i, i_frame in enumerate(frame_indices): - patch_data[i] = \ - panel.interpolate_bilinear( - xy_eval, - ome_imgser[i_frame], - ).reshape(prows, pcols)*(areas/float(native_area)) - pass - - # now have interpolated patch data... - labels, num_peaks = ndimage.label( - patch_data > threshold, structure=label_struct - ) - slabels = np.arange(1, num_peaks + 1) - if num_peaks > 0: - peak_id = iRefl - coms = np.array( - ndimage.center_of_mass( - patch_data, labels=labels, index=slabels - ) - ) - if num_peaks > 1: - center = np.r_[patch_data.shape]*0.5 - com_diff = coms - np.tile(center, (num_peaks, 1)) - closest_peak_idx = np.argmin(np.sum(com_diff**2, axis=1)) - else: - closest_peak_idx = 0 - pass # end multipeak conditional - coms = coms[closest_peak_idx] - meas_angs = np.hstack([ - tth_edges[0] + (0.5 + coms[2])*delta_tth, - eta_edges[0] + (0.5 + coms[1])*delta_eta, - np.radians(ome_edges[0] + (0.5 + coms[0])*delta_ome) - ]) - - # intensities - # - summed is 'integrated' over interpolated data - # - max is max of raw input data - sum_int = np.sum( - patch_data[labels == slabels[closest_peak_idx]] + contains_signal = False + for i_frame in frame_indices: + contains_signal = contains_signal or np.any( + ome_imgser[i_frame][ijs[0], ijs[1]] > threshold ) - max_int = np.max( - [ome_imgser[i][ijs[0], ijs[1]] for i in frame_indices] + compl.append(contains_signal) + if not check_only: + peak_id = -999 + sum_int = None + max_int = None + meas_angs = None + meas_xy = None + + patch_data = np.zeros((len(frame_indices), prows, pcols)) + ome_edges = np.hstack( + [ome_imgser.omega[frame_indices][:, 0], + ome_imgser.omega[frame_indices][-1, 1]] ) - #max_int = np.max( - # patch_data[labels == slabels[closest_peak_idx]] - # ) - - # need xy coords - gvec_c = anglesToGVec( - meas_angs, - chi=self.chi, - rMat_c=rMat_c, - bHat_l=self.beam_vector) - rMat_s = makeOscillRotMat([self.chi, meas_angs[2]]) - meas_xy = gvecToDetectorXY( - gvec_c, - panel.rmat, rMat_s, rMat_c, - panel.tvec, self.tvec, tVec_c, - beamVec=self.beam_vector) - if panel.distortion is not None: - """...FIX THIS!!!""" - meas_xy = panel.distortion[0]( - np.atleast_2d(meas_xy), - panel.distortion[1], - invert=True).flatten() - pass - pass - - # write output - if filename is not None: - pw.dump_patch( - peak_id, hkl_id, hkl, sum_int, max_int, - ang_centers[i_pt], meas_angs, meas_xy) + for i, i_frame in enumerate(frame_indices): + patch_data[i] = \ + panel.interpolate_bilinear( + xy_eval, + ome_imgser[i_frame], + ).reshape(prows, pcols)*(areas/float(native_area)) + pass + + # now have interpolated patch data... + labels, num_peaks = ndimage.label( + patch_data > threshold, structure=label_struct + ) + slabels = np.arange(1, num_peaks + 1) + if num_peaks > 0: + peak_id = iRefl + coms = np.array( + ndimage.center_of_mass( + patch_data, labels=labels, index=slabels + ) + ) + if num_peaks > 1: + center = np.r_[patch_data.shape]*0.5 + com_diff = coms - np.tile(center, (num_peaks, 1)) + closest_peak_idx = np.argmin(np.sum(com_diff**2, axis=1)) + else: + closest_peak_idx = 0 + pass # end multipeak conditional + coms = coms[closest_peak_idx] + meas_angs = np.hstack([ + tth_edges[0] + (0.5 + coms[2])*delta_tth, + eta_edges[0] + (0.5 + coms[1])*delta_eta, + np.radians(ome_edges[0] + (0.5 + coms[0])*delta_ome) + ]) + + # intensities + # - summed is 'integrated' over interpolated data + # - max is max of raw input data + sum_int = np.sum( + patch_data[labels == slabels[closest_peak_idx]] + ) + max_int = np.max( + [ome_imgser[i][ijs[0], ijs[1]] for i in frame_indices] + ) + #max_int = np.max( + # patch_data[labels == slabels[closest_peak_idx]] + # ) + + # need xy coords + gvec_c = anglesToGVec( + meas_angs, + chi=self.chi, + rMat_c=rMat_c, + bHat_l=self.beam_vector) + rMat_s = makeOscillRotMat([self.chi, meas_angs[2]]) + meas_xy = gvecToDetectorXY( + gvec_c, + panel.rmat, rMat_s, rMat_c, + panel.tvec, self.tvec, tVec_c, + beamVec=self.beam_vector) + if panel.distortion is not None: + """...FIX THIS!!!""" + meas_xy = panel.distortion[0]( + np.atleast_2d(meas_xy), + panel.distortion[1], + invert=True).flatten() + pass + pass + + # write output + if filename is not None: + pw.dump_patch( + peak_id, hkl_id, hkl, sum_int, max_int, + ang_centers[i_pt], meas_angs, meas_xy) + pass # end conditional on write output + pass # end conditional on check only iRefl += 1 pass # end patch conditional pass # end patch loop if filename is not None: - del(pw) + pw.close() pass # end detector loop - return + return compl pass # end class: HEDMInstrument @@ -797,7 +897,7 @@ def config_dict(self, chi, t_vec_s, sat_level=None): """ """ t_vec_s = np.atleast_1d(t_vec_s) - + d = dict( detector=dict( transform=dict( @@ -947,7 +1047,7 @@ def interpolate_bilinear(self, xy, img, pad_with_nans=True): def make_powder_rings( self, pd, merge_hkls=False, delta_eta=None, eta_period=None, rmat_s=ct.identity_3x3, tvec_s=ct.zeros_3, - tvec_c=ct.zeros_3): + tvec_c=ct.zeros_3, output_ranges=False): """ """ @@ -970,9 +1070,15 @@ def make_powder_rings( else: if merge_hkls: tth_idx, tth_ranges = pd.getMergedRanges() - tth = [0.5*sum(i) for i in tth_ranges] + if output_ranges: + tth = np.r_[tth_ranges].flatten() + else: + tth = np.array([0.5*sum(i) for i in tth_ranges]) else: - tth = pd.getTTh() + if output_ranges: + tth = pd.getTTh() + else: + tth = pd.getTThRanges().flatten() angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] # need xy coords and pixel sizes From 9a52b8fbb90dc1f2bc8eac8f12a089c88457cd7b Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 23 Mar 2017 10:56:35 -0500 Subject: [PATCH 084/253] default pixel buffer --- hexrd/instrument.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index a62e8b38..b7f1d96c 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -684,10 +684,11 @@ def __init__(self, self._pixel_size_row = pixel_size[0] self._pixel_size_col = pixel_size[1] - self._panel_buffer = panel_buffer + if panel_buffer is None: + self._panel_buffer = [self._pixel_size_col, self._pixel_size_row] self._tvec = np.array(tvec).flatten() - self._tilt = tilt + self._tilt = np.array(tilt).flatten() self._bvec = np.array(bvec).flatten() self._evec = np.array(evec).flatten() @@ -1002,6 +1003,20 @@ def clip_to_panel(self, xy, buffer_edges=True): on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel + + def cart_to_angles(self, xy_data, + rmat_s=ct.identity_3x3, + tvec_s=ct.zeros_3, tvec_c=ct.zeros_3): + """ + """ + angs, g_vec = detectorXYToGvec( + xy_data, self.rmat, rmat_s, + self.tvec, tvec_s, tvec_c, + beamVec=self.bvec, etaVec=self.evec) + tth_eta = np.vstack([angs[0], angs[1]]).T + return tth_eta, g_vec + + def interpolate_bilinear(self, xy, img, pad_with_nans=True): """ """ @@ -1076,9 +1091,9 @@ def make_powder_rings( tth = np.array([0.5*sum(i) for i in tth_ranges]) else: if output_ranges: - tth = pd.getTTh() - else: tth = pd.getTThRanges().flatten() + else: + tth = pd.getTTh() angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] # need xy coords and pixel sizes From c96ac61da7e2c76a3cd34c776ecabbe768045ea2 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 23 Mar 2017 16:26:00 -0500 Subject: [PATCH 085/253] fixed way frame-cache writes numpy array metadata --- hexrd/imageseries/save.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index e93b989a..a133f06e 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -57,6 +57,13 @@ def __init__(self, ims, fname, **kwargs): self._fname = fname self._opts = kwargs + # split filename into components + tmp = os.path.split(fname) + self._fname_dir = tmp[0] + tmp = os.path.splitext(tmp[1]) + self._fname_base = tmp[0] + self._fname_suff = tmp[1] + pass # end class class WriteH5(Writer): @@ -144,8 +151,18 @@ def _process_meta(self): d = {} for k, v in self._meta.items(): if isinstance(v, np.ndarray): - d[k] = '++np.array' - d[k + '-array'] = v.tolist() + # Save as a numpy array file + # if file does not exist (careful about directory) + # create new file + + cdir = os.path.dirname(self._cache) + b = self._fname_base + fname = os.path.join(cdir, "%s-%s.npy" % (b,k)) + if not os.path.exists(fname): + np.save(fname, v) + + # add trigger in yml file + d[k] = "! load-numpy-array %s" % fname else: d[k] = v From 5179077f535b0b1fdaf989b3c8439ad1d04d4ade Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sun, 26 Mar 2017 03:37:56 -0500 Subject: [PATCH 086/253] added rotation series simulator at panel level --- hexrd/instrument.py | 223 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 185 insertions(+), 38 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index b7f1d96c..54543fae 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -37,6 +37,8 @@ import yaml +import h5py + import numpy as np from scipy import ndimage @@ -48,7 +50,6 @@ gvecToDetectorXY, \ makeDetectorRotMat, \ makeOscillRotMat, \ - makeEtaFrameRotMat, \ makeRotMatOfExpMap, \ mapAngle, \ oscillAnglesOfHKLs @@ -106,8 +107,6 @@ def calc_angles_from_beam_vec(bvec): return azim, pola - - def migrate_instrument_config(instrument_config): """utility function to generate old instrument config dictionary""" cfg_list = [] @@ -193,7 +192,9 @@ def __init__(self, instrument_config=None, pass self._detectors = dict(zip(detector_ids, det_list)) - self._tvec = np.r_[instrument_config['oscillation_stage']['t_vec_s']] + self._tvec = np.r_[ + instrument_config['oscillation_stage']['t_vec_s'] + ] self._chi = instrument_config['oscillation_stage']['chi'] return @@ -308,8 +309,8 @@ def write_config(self, filename, calibration_dict={}): yaml.dump(par_dict, stream=f) return par_dict - - def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1., npdiv=2): + def extract_line_positions(self, plane_data, image_dict, + tth_tol=0.25, eta_tol=1., npdiv=2): """ """ tol_vec = 0.5*np.radians( @@ -326,20 +327,21 @@ def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1 # pull out the image for this panel from input dict image = image_dict[detector_id] - + # make rings pow_angs, pow_xys = panel.make_powder_rings( plane_data, merge_hkls=True, delta_eta=eta_tol) n_rings = len(pow_angs) - + ring_data = [] for i_ring in range(n_rings): these_angs = pow_angs[i_ring] - + # make sure no one falls off... npts = len(these_angs) - patch_vertices = (np.tile(these_angs, (1, 4)) \ - + np.tile(tol_vec, (npts, 1))).reshape(4*npts, 2) + patch_vertices = (np.tile(these_angs, (1, 4)) + + np.tile(tol_vec, (npts, 1)) + ).reshape(4*npts, 2) # find points that fall on the panel det_xy, rMat_s = xrdutil._project_on_detector_plane( @@ -349,14 +351,15 @@ def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1 panel.distortion ) tmp_xy, on_panel = panel.clip_to_panel(det_xy) - + # all vertices must be on... patch_is_on = np.all(on_panel.reshape(npts, 4), axis=1) - - # reflection angles (voxel centers) and pixel size in (tth, eta) + + # reflection angles (voxel centers) and + # pixel size in (tth, eta) ang_centers = these_angs[patch_is_on] ang_pixel_size = panel.angularPixelSize(tmp_xy[::4, :]) - + # make the tth,eta patches for interpolation patches = xrdutil.make_reflection_patches( instr_cfg, ang_centers, ang_pixel_size, @@ -364,7 +367,7 @@ def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1 distortion=panel.distortion, npdiv=npdiv, quiet=True, beamVec=self.beam_vector) - + # loop over patches patch_data = [] for patch in patches: @@ -376,13 +379,15 @@ def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1 xy_eval = np.vstack([ xy_eval[0].flatten(), xy_eval[1].flatten()]).T - + + ''' Maybe need these # edge arrays tth_edges = vtx_angs[0][0, :] delta_tth = tth_edges[1] - tth_edges[0] eta_edges = vtx_angs[1][:, 0] delta_eta = eta_edges[1] - eta_edges[0] - + ''' + # interpolate patch_data.append( panel.interpolate_bilinear( @@ -391,7 +396,7 @@ def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1 ).reshape(prows, pcols)*(areas/float(native_area)) ) - # + # pass # close patch loop ring_data.append(patch_data) pass # close ring loop @@ -399,6 +404,10 @@ def extract_line_positions(self, plane_data, image_dict, tth_tol=0.25, eta_tol=1 pass # close panel loop return panel_data + def simulate_rotation_series(self, plane_data, grain_param_list): + """ + """ + return NotImplementedError def pull_spots(self, plane_data, grain_params, imgser_dict, @@ -407,7 +416,6 @@ def pull_spots(self, plane_data, grain_params, dirname='results', filename=None, save_spot_list=False, quiet=True, lrank=1, check_only=False): - '''first find valid G-vectors''' bMat = plane_data.latVecOps['B'] @@ -461,9 +469,13 @@ def pull_spots(self, plane_data, grain_params, -tth_tol, eta_tol, tth_tol, eta_tol, tth_tol, -eta_tol]) - patch_vertices = (np.tile(allAngs[:, :2], (1, 4)) \ - + np.tile(tol_vec, (nangs, 1))).reshape(4*nangs, 2) - ome_dupl = np.tile(allAngs[:, 2], (4, 1)).T.reshape(len(patch_vertices), 1) + + patch_vertices = ( + np.tile(allAngs[:, :2], (1, 4)) + np.tile(tol_vec, (nangs, 1)) + ).reshape(4*nangs, 2) + ome_dupl = np.tile( + allAngs[:, 2], (4, 1) + ).T.reshape(len(patch_vertices), 1) '''loop over panels''' iRefl = 0 @@ -476,7 +488,7 @@ def pull_spots(self, plane_data, grain_params, ) if not os.path.exists(output_dir): os.makedirs(output_dir) - this_filename = os.path.join( + this_filename = os.path.join( output_dir, filename ) pw = PatchDataWriter(this_filename) @@ -524,6 +536,7 @@ def pull_spots(self, plane_data, grain_params, # strip relevant objects out of current patch vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch prows, pcols = areas.shape + nrm_fac = areas/float(native_area) # grab hkl info hkl = hkls_p[i_pt, :] @@ -567,7 +580,9 @@ def pull_spots(self, plane_data, grain_params, meas_angs = None meas_xy = None - patch_data = np.zeros((len(frame_indices), prows, pcols)) + patch_data = np.zeros( + (len(frame_indices), prows, pcols) + ) ome_edges = np.hstack( [ome_imgser.omega[frame_indices][:, 0], ome_imgser.omega[frame_indices][-1, 1]] @@ -577,7 +592,7 @@ def pull_spots(self, plane_data, grain_params, panel.interpolate_bilinear( xy_eval, ome_imgser[i_frame], - ).reshape(prows, pcols)*(areas/float(native_area)) + ).reshape(prows, pcols)*nrm_fac pass # now have interpolated patch data... @@ -594,16 +609,21 @@ def pull_spots(self, plane_data, grain_params, ) if num_peaks > 1: center = np.r_[patch_data.shape]*0.5 - com_diff = coms - np.tile(center, (num_peaks, 1)) - closest_peak_idx = np.argmin(np.sum(com_diff**2, axis=1)) + center_t = np.tile(center, (num_peaks, 1)) + com_diff = coms - center_t + closest_peak_idx = np.argmin( + np.sum(com_diff**2, axis=1) + ) else: closest_peak_idx = 0 pass # end multipeak conditional coms = coms[closest_peak_idx] + meas_omes = ome_edges[0] + \ + (0.5 + coms[0])*delta_ome meas_angs = np.hstack([ tth_edges[0] + (0.5 + coms[2])*delta_tth, eta_edges[0] + (0.5 + coms[1])*delta_eta, - np.radians(ome_edges[0] + (0.5 + coms[0])*delta_ome) + np.radians(meas_omes), ]) # intensities @@ -613,11 +633,13 @@ def pull_spots(self, plane_data, grain_params, patch_data[labels == slabels[closest_peak_idx]] ) max_int = np.max( - [ome_imgser[i][ijs[0], ijs[1]] for i in frame_indices] + patch_data[labels == slabels[closest_peak_idx]] ) - #max_int = np.max( - # patch_data[labels == slabels[closest_peak_idx]] - # ) + ''' ...ONLY USE LABELED PIXELS? + max_int = np.max( + patch_data[labels == slabels[closest_peak_idx]] + ) + ''' # need xy coords gvec_c = anglesToGVec( @@ -670,6 +692,7 @@ def __init__(self, pixel_size=(0.2, 0.2), tvec=np.r_[0., 0., -1000.], tilt=ct.zeros_3, + name='default', bvec=ct.beam_vec, evec=ct.eta_vec, panel_buffer=None, @@ -678,6 +701,8 @@ def __init__(self, panel buffer is in pixels... """ + self._name = name + self._rows = rows self._cols = cols @@ -697,6 +722,16 @@ def __init__(self, return + # detector ID + @property + def name(self): + return self._name + + @name.setter + def name(self, s): + assert isinstance(s, (str, unicode)), "requires string input" + self._name = s + # properties for physical size of rectangular detector @property def rows(self): @@ -902,7 +937,7 @@ def config_dict(self, chi, t_vec_s, sat_level=None): d = dict( detector=dict( transform=dict( - tilt_angles=self.tilt, + tilt_angles=self.tilt.tolist(), t_vec_d=self.tvec.tolist(), ), pixels=dict( @@ -1003,10 +1038,9 @@ def clip_to_panel(self, xy, buffer_edges=True): on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel - def cart_to_angles(self, xy_data, - rmat_s=ct.identity_3x3, - tvec_s=ct.zeros_3, tvec_c=ct.zeros_3): + rmat_s=ct.identity_3x3, + tvec_s=ct.zeros_3, tvec_c=ct.zeros_3): """ """ angs, g_vec = detectorXYToGvec( @@ -1016,7 +1050,6 @@ def cart_to_angles(self, xy_data, tth_eta = np.vstack([angs[0], angs[1]]).T return tth_eta, g_vec - def interpolate_bilinear(self, xy, img, pad_with_nans=True): """ """ @@ -1147,6 +1180,77 @@ def map_to_plane(self, pts, rmat, tvec): return np.dot(rmat.T, pts_map_lab - tvec_map_lab)[:2, :].T + def simulate_rotation_series(self, plane_data, grain_param_list, + ome_ranges, chi=0., tVec_s=ct.zeros_3, + wavelength=None): + """ + """ + + # grab B-matrix from plane data + bMat = plane_data.latVecOps['B'] + + # reconcile wavelength + # * added sanity check on exclusions here; possible to + # * make some reflections invalid (NaN) + if wavelength is None: + wavelength = plane_data.wavelength + else: + if plane_data.wavelength != wavelength: + plane_data.wavelength = ct.keVToAngstrom(wavelength) + assert not np.any(np.isnan(plane_data.getTTh())),\ + "plane data exclusions incompatible with wavelength" + + # vstacked G-vector id, h, k, l + full_hkls = xrdutil._fetch_hkls_from_planedata(plane_data) + + """ LOOP OVER GRAINS """ + valid_ids = [] + valid_hkls = [] + valid_angs = [] + valid_xys = [] + ang_pixel_size = [] + for gparm in grain_param_list: + + # make useful parameters + rMat_c = makeRotMatOfExpMap(gparm[:3]) + tVec_c = gparm[3:6] + vInv_s = gparm[6:] + + # All possible bragg conditions as vstacked [tth, eta, ome] + # for each omega solution + angList = np.vstack( + oscillAnglesOfHKLs( + full_hkls[:, 1:], chi, + rMat_c, bMat, wavelength, + vInv=vInv_s, + ) + ) + + # filter by eta and omega ranges + # get eta range from detector? + allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( + full_hkls, angList, [(-np.pi, np.pi), ], ome_ranges + ) + + # find points that fall on the panel + det_xy, rMat_s = xrdutil._project_on_detector_plane( + allAngs, + self.rmat, rMat_c, chi, + self.tvec, tVec_c, tVec_s, + self.distortion + ) + xys_p, on_panel = self.clip_to_panel(det_xy) + valid_xys.append(xys_p) + + # grab hkls and gvec ids for this panel + valid_hkls.append(allHKLs[on_panel, 1:]) + valid_ids.append(allHKLs[on_panel, 0]) + + # reflection angles (voxel centers) and pixel size in (tth, eta) + valid_angs.append(allAngs[on_panel, :]) + ang_pixel_size.append(self.angularPixelSize(xys_p)) + return valid_ids, valid_hkls, valid_angs, valid_xys, ang_pixel_size + """UTILITIES""" @@ -1197,3 +1301,46 @@ def dump_patch(self, peak_id, hkl_id, nans_tabbed_18.format(*np.ones(5)*np.nan) print(output_str, file=self.fid) return output_str + + +class PatchDataWriter_h5(object): + """ + """ + def __init__(self, filename, instr_cfg, panel_id): + if isinstance(filename, h5py.File): + self.fid = filename + else: + self.fid = h5py.File(filename + ".hdf5", "w") + icfg = {} + icfg.update(instr_cfg) + + # add instrument groups and attributes + grp = self.fid.create_group('instrument') + unwrap_dict_to_h5(grp, icfg, asattr=True) + + grp = self.fid.create_group("data") + grp.attrs.create("panel_id", panel_id) + + def __del__(self): + self.close() + + def close(self): + self.fid.close() + + def dump_patch(self, peak_id, hkl_id, + hkl, spot_int, max_int, + pangs, mangs, xy): + return NotImplementedError + + +def unwrap_dict_to_h5(grp, d, asattr=True): + while len(d) > 0: + key, item = d.popitem() + if isinstance(item, dict): + subgrp = grp.create_group(key) + unwrap_dict_to_h5(subgrp, item) + else: + if asattr: + grp.attrs.create(key, item) + else: + grp.create_dataset(key, data=np.atleast_1d(item)) From 46fcb270dc692f13292fe02ce0609e9f78449971 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 27 Mar 2017 21:00:04 -0500 Subject: [PATCH 087/253] changes related to implementing serial multipanel fit_grains --- hexrd/instrument.py | 108 ++++++++-- hexrd/matrixutil.py | 394 +++++++++++++++++----------------- hexrd/xrd/fitting.py | 417 ++++++++++++++++++++---------------- hexrd/xrd/xrdutil.py | 9 +- scripts/makeOverlapTable.py | 6 +- 5 files changed, 545 insertions(+), 389 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 54543fae..f859adde 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -42,6 +42,7 @@ import numpy as np from scipy import ndimage +from scipy.linalg.matfuncs import logm from hexrd.gridutil import cellIndices, make_tolerance_grid from hexrd import matrixutil as mutil @@ -212,6 +213,13 @@ def num_panels(self): def detectors(self): return self._detectors + @property + def detector_parameters(self): + pdict = {} + for key, panel in self.detectors.iteritems(): + pdict[key] = panel.config_dict(self.chi, self.tvec) + return pdict + @property def tvec(self): return self._tvec @@ -413,10 +421,13 @@ def pull_spots(self, plane_data, grain_params, imgser_dict, tth_tol=0.25, eta_tol=1., ome_tol=1., npdiv=2, threshold=10, + eta_ranges=None, dirname='results', filename=None, save_spot_list=False, quiet=True, lrank=1, check_only=False): - '''first find valid G-vectors''' + if eta_ranges is None: + eta_ranges = [(-np.pi, np.pi), ] + bMat = plane_data.latVecOps['B'] rMat_c = makeRotMatOfExpMap(grain_params[:3]) @@ -459,7 +470,7 @@ def pull_spots(self, plane_data, grain_params, # filter by eta and omega ranges allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( - full_hkls, angList, [(-np.pi, np.pi), ], ome_ranges + full_hkls, angList, eta_ranges, ome_ranges ) # dilate angles tth and eta to patch corners @@ -480,6 +491,7 @@ def pull_spots(self, plane_data, grain_params, '''loop over panels''' iRefl = 0 compl = [] + output = dict.fromkeys(self.detectors) for detector_id in self.detectors: # initialize output writer if filename is not None: @@ -531,6 +543,7 @@ def pull_spots(self, plane_data, grain_params, ome_imgser = imgser_dict[detector_id] # grand loop over reflections for this panel + patch_output = [] for i_pt, patch in enumerate(patches): # strip relevant objects out of current patch @@ -635,7 +648,8 @@ def pull_spots(self, plane_data, grain_params, max_int = np.max( patch_data[labels == slabels[closest_peak_idx]] ) - ''' ...ONLY USE LABELED PIXELS? + # IDEA: Should this only use labeled pixels ??? + ''' max_int = np.max( patch_data[labels == slabels[closest_peak_idx]] ) @@ -654,7 +668,7 @@ def pull_spots(self, plane_data, grain_params, panel.tvec, self.tvec, tVec_c, beamVec=self.beam_vector) if panel.distortion is not None: - """...FIX THIS!!!""" + # FIXME: distortion handling meas_xy = panel.distortion[0]( np.atleast_2d(meas_xy), panel.distortion[1], @@ -670,13 +684,21 @@ def pull_spots(self, plane_data, grain_params, pass # end conditional on write output pass # end conditional on check only iRefl += 1 + patch_output.append([ + peak_id, hkl_id, hkl, sum_int, max_int, + ang_centers[i_pt], meas_angs, meas_xy, + ]) pass # end patch conditional pass # end patch loop + output[detector_id] = patch_output if filename is not None: pw.close() pass # end detector loop - return compl - pass # end class: HEDMInstrument + return compl, output + + """def fit_grain(self, grain_params, data_dir='results'): + + pass # end class: HEDMInstrument""" class PlanarDetector(object): @@ -1241,7 +1263,7 @@ def simulate_rotation_series(self, plane_data, grain_param_list, ) xys_p, on_panel = self.clip_to_panel(det_xy) valid_xys.append(xys_p) - + # grab hkls and gvec ids for this panel valid_hkls.append(allHKLs[on_panel, 1:]) valid_ids.append(allHKLs[on_panel, 0]) @@ -1259,15 +1281,14 @@ class PatchDataWriter(object): """ """ def __init__(self, filename): - xy_str = '{:18}\t{:18}\t{:18}' - ang_str = xy_str + '\t' + dp3_str = '{:18}\t{:18}\t{:18}' self._header = \ '{:6}\t{:6}\t'.format('# ID', 'PID') + \ '{:3}\t{:3}\t{:3}\t'.format('H', 'K', 'L') + \ '{:12}\t{:12}\t'.format('sum(int)', 'max(int)') + \ - ang_str.format('pred tth', 'pred eta', 'pred ome') + \ - ang_str.format('meas tth', 'meas eta', 'meas ome') + \ - xy_str.format('meas X', 'meas Y', 'meas ome') + dp3_str.format('pred tth', 'pred eta', 'pred ome') + '\t' + \ + dp3_str.format('meas tth', 'meas eta', 'meas ome') + '\t' + \ + dp3_str.format('meas X', 'meas Y', 'meas ome') if isinstance(filename, file): self.fid = filename else: @@ -1303,10 +1324,69 @@ def dump_patch(self, peak_id, hkl_id, return output_str -class PatchDataWriter_h5(object): +class GrainDataWriter(object): + """ + """ + def __init__(self, filename): + sp3_str = '{:12}\t{:12}\t{:12}' + dp3_str = '{:18}\t{:18}\t{:18}' + self._header = \ + sp3_str.format( + '# grain ID', 'completeness', 'chi^2') + '\t' + \ + dp3_str.format( + 'exp_map_c[0]', 'exp_map_c[1]', 'exp_map_c[2]') + '\t' + \ + dp3_str.format( + 't_vec_c[0]', 't_vec_c[1]', 't_vec_c[2]') + '\t' + \ + dp3_str.format( + 'inv(V_s)[0, 0]', + 'inv(V_s)[1, 1]', + 'inv(V_s)[2, 2]') + '\t' + \ + dp3_str.format( + 'inv(V_s)[1, 2]*√2', 'inv(V_s)[0, 2]*√2', 'inv(V_s)[0, 2]*√2' + ) + '\t' + dp3_str.format( + 'ln(V_s)[0, 0]', 'ln(V_s)[1, 1]', 'ln(V_s)[2, 2]') + '\t' + \ + dp3_str.format( + 'ln(V_s)[1, 2]', 'ln(V_s)[0, 2]', 'ln(V_s)[0, 1]') + if isinstance(filename, file): + self.fid = filename + else: + self.fid = open(filename, 'w') + print(self._header, file=self.fid) + + def __del__(self): + self.close() + + def close(self): + self.fid.close() + + def dump_grain(self, grain_id, completeness, chisq, + grain_params): + assert len(grain_params) == 12, \ + "len(grain_params) must be 12, not %d" % len(grain_params) + + # extract strain + emat = logm(np.linalg.inv(mutil.vecMVToSymm(grain_params[6:]))) + evec = mutil.symmToVecMV(emat, scale=False) + + dp3_e_str = '{:<1.12e}\t{:<1.12e}\t{:<1.12e}' + dp6_e_str = \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}' + output_str = \ + '{:<12d}\t{:<12f}\t{:<12d}\t'.format( + grain_id, completeness, chisq) + \ + dp3_e_str.format(*grain_params[:3]) + '\t' + \ + dp3_e_str.format(*grain_params[3:6]) + '\t' + \ + dp6_e_str.format(*grain_params[6:]) + '\t' + \ + dp6_e_str.format(*evec) + print(output_str, file=self.fid) + return output_str + + +class GrainDataWriter_h5(object): """ """ def __init__(self, filename, instr_cfg, panel_id): + use_attr = True if isinstance(filename, h5py.File): self.fid = filename else: @@ -1316,7 +1396,7 @@ def __init__(self, filename, instr_cfg, panel_id): # add instrument groups and attributes grp = self.fid.create_group('instrument') - unwrap_dict_to_h5(grp, icfg, asattr=True) + unwrap_dict_to_h5(grp, icfg, asattr=use_attr) grp = self.fid.create_group("data") grp.attrs.create("panel_id", panel_id) diff --git a/hexrd/matrixutil.py b/hexrd/matrixutil.py index b6638d74..1ddb4937 100644 --- a/hexrd/matrixutil.py +++ b/hexrd/matrixutil.py @@ -1,24 +1,24 @@ # ============================================================ -# Copyright (c) 2007-2012, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. -# Written by Joel Bernier and others. -# LLNL-CODE-529294. +# Copyright (c) 2007-2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. # All rights reserved. -# +# # This file is part of HEXRD. For details on dowloading the source, # see the file COPYING. -# +# # Please also see the file LICENSE. -# +# # This program is free software; you can redistribute it and/or modify it under the # terms of the GNU Lesser General Public License (as published by the Free Software # Foundation) version 2.1 dated February 1999. -# +# # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this program (see file LICENSE); if not, write to # the Free Software Foundation, Inc., 59 Temple Place, Suite 330, @@ -57,9 +57,9 @@ def columnNorm(a): """ if len(a.shape) > 2: raise RuntimeError, "incorrect shape: arg must be 1-d or 2-d, yours is %d" %(len(a.shape)) - + cnrma = sqrt(sum(asarray(a)**2, 0)) - + return cnrma def rowNorm(a): @@ -68,9 +68,9 @@ def rowNorm(a): """ if len(a.shape) > 2: raise RuntimeError, "incorrect shape: arg must be 1-d or 2-d, yours is %d" %(len(a.shape)) - + cnrma = sqrt(sum(asarray(a)**2, 1)) - + return cnrma def unitVector(a): @@ -78,19 +78,19 @@ def unitVector(a): normalize array of column vectors (hstacked, axis = 0) """ assert a.ndim in [1, 2], "incorrect arg shape; must be 1-d or 2-d, yours is %d-d" % (a.ndim) - + ztol = 1.0e-14 - + m = a.shape[0]; n = 1 - + nrm = tile(sqrt(sum(asarray(a)**2, 0)), (m, n)) - + # prevent divide by zero zchk = nrm <= ztol nrm[zchk] = 1.0 - + nrma = a/nrm - + return nrma def nullSpace(A, tol=vTol): @@ -98,14 +98,14 @@ def nullSpace(A, tol=vTol): computes the null space of the real matrix A """ assert A.ndim == 2, 'input must be 2-d; yours is %d-d' % (A.ndim) - + n, m = A.shape if n > m : return nullSpace(A.T, tol).T U, S, V = svd(A) - + S = hstack([S, zeros(m-n)]) null_mask = (S <= tol) @@ -119,103 +119,111 @@ def blockSparseOfMatArray(matArray): Constructs a block diagonal sparse matrix (csc format) from a (p, m, n) ndarray of p (m, n) arrays - + ...maybe optional args to pick format type? """ # if isinstance(args[0], str): # a = args[0] # if a == 'csc': ... - + if len(matArray.shape) != 3: raise RuntimeError, "input array is not the correct shape!" - + l = matArray.shape[0] m = matArray.shape[1] n = matArray.shape[2] - + mn = m*n; jmax = l*n; imax = l*m; ntot = l*m*n; - - rl = asarray(range(l), 'int') + + rl = asarray(range(l), 'int') rm = asarray(range(m), 'int') rjmax = asarray(range(jmax), 'int') - + sij = matArray.transpose(0, 2, 1).reshape(1, ntot).squeeze() j = reshape(tile(rjmax, (m, 1)).T, (1, ntot)) i = reshape(tile(rm, (1, jmax)), (1, ntot)) + reshape(tile(m*rl, (mn, 1)).T, (1, ntot)) - + ij = concatenate((i, j), 0) - + smat = sparse.csc_matrix((sij, ij), shape=(imax, jmax)) # syntax as of scipy-0.7.0 - + return smat -def symmToVecMV(A): +def symmToVecMV(A, scale=True): """ convert from symmetric matrix to Mandel-Voigt vector representation (JVB) - """ + """ + if scale: + fac = sqr2 + else: + fac = 1. mvvec = zeros(6, dtype='float64') mvvec[0] = A[0,0] mvvec[1] = A[1,1] mvvec[2] = A[2,2] - mvvec[3] = sqr2 * A[1,2] - mvvec[4] = sqr2 * A[0,2] - mvvec[5] = sqr2 * A[0,1] + mvvec[3] = fac * A[1,2] + mvvec[4] = fac * A[0,2] + mvvec[5] = fac * A[0,1] return mvvec -def vecMVToSymm(A): +def vecMVToSymm(A, scale=True): """ - convert from Mandel-Voigt vector to symmetric matrix - representation (JVB) - """ + convert from Mandel-Voigt vector to symmetric matrix + representation (JVB) + """ + if scale: + fac = sqr2 + else: + fac = 1. symm = zeros((3, 3), dtype='float64') symm[0, 0] = A[0] symm[1, 1] = A[1] symm[2, 2] = A[2] - symm[1, 2] = A[3] / sqr2 - symm[0, 2] = A[4] / sqr2 - symm[0, 1] = A[5] / sqr2 - symm[2, 1] = A[3] / sqr2 - symm[2, 0] = A[4] / sqr2 - symm[1, 0] = A[5] / sqr2 + symm[1, 2] = A[3] / fac + symm[0, 2] = A[4] / fac + symm[0, 1] = A[5] / fac + symm[2, 1] = A[3] / fac + symm[2, 0] = A[4] / fac + symm[1, 0] = A[5] / fac return symm def vecMVCOBMatrix(R): """ GenerateS array of 6 x 6 basis transformation matrices for the - Mandel-Voigt tensor representation in 3-D given by: - + Mandel-Voigt tensor representation in 3-D given by: + [A] = [[A_11, A_12, A_13], [A_12, A_22, A_23], [A_13, A_23, A_33]] - + {A} = [A_11, A_22, A_33, sqrt(2)*A_23, sqrt(2)*A_13, sqrt(2)*A_12] - + where the operation :math:`R*A*R.T` (in tensor notation) is obtained by the matrix-vector product [T]*{A}. - + USAGE - + T = vecMVCOBMatrix(R) - + INPUTS - + 1) R is (3, 3) an ndarray representing a change of basis matrix - + OUTPUTS - + 1) T is (6, 6), an ndarray of transformation matrices as described above - + NOTES - + 1) Compoments of symmetric 4th-rank tensors transform in a manner analogous to symmetric 2nd-rank tensors in full - matrix notation. + matrix notation. SEE ALSO @@ -231,10 +239,10 @@ def vecMVCOBMatrix(R): raise RuntimeError, \ "R array must be (3, 3) or (n, 3, 3); input has dimension %d" \ % (rdim) - + T = zeros((nrot, 6, 6), dtype='float64') - - T[:, 0, 0] = R[:, 0, 0]**2 + + T[:, 0, 0] = R[:, 0, 0]**2 T[:, 0, 1] = R[:, 0, 1]**2 T[:, 0, 2] = R[:, 0, 2]**2 T[:, 0, 3] = sqr2 * R[:, 0, 1] * R[:, 0, 2] @@ -270,7 +278,7 @@ def vecMVCOBMatrix(R): T[:, 5, 3] = R[:, 0, 2] * R[:, 1, 1] + R[:, 0, 1] * R[:, 1, 2] T[:, 5, 4] = R[:, 0, 0] * R[:, 1, 2] + R[:, 0, 2] * R[:, 1, 0] T[:, 5, 5] = R[:, 0, 1] * R[:, 1, 0] + R[:, 0, 0] * R[:, 1, 1] - + if nrot == 1: T = T.squeeze() @@ -284,68 +292,68 @@ def nrmlProjOfVecMV(vec): Nvec = normalProjectionOfMV(vec) *) the input vector array need not be normalized; it is performed in place - + """ # normalize in place... col vectors! n = unitVector(vec) - - nmat = array([n[0, :]**2, - n[1, :]**2, - n[2, :]**2, - sqr2 * n[1, :] * n[2, :], - sqr2 * n[0, :] * n[2, :], - sqr2 * n[0, :] * n[1, :]], + + nmat = array([n[0, :]**2, + n[1, :]**2, + n[2, :]**2, + sqr2 * n[1, :] * n[2, :], + sqr2 * n[0, :] * n[2, :], + sqr2 * n[0, :] * n[1, :]], dtype='float64') - + return nmat.T def rankOneMatrix(vec1, *args): """ Create rank one matrices (dyadics) from vectors. - + r1mat = rankOneMatrix(vec1) r1mat = rankOneMatrix(vec1, vec2) - + vec1 is m1 x n, an array of n hstacked m1 vectors vec2 is m2 x n, (optional) another array of n hstacked m2 vectors - + r1mat is n x m1 x m2, an array of n rank one matrices formed as c1*c2' from columns c1 and c2 - + With one argument, the second vector is taken to the same as the first. - + Notes: - - *) This routine loops on the dimension m, assuming this + + *) This routine loops on the dimension m, assuming this is much smaller than the number of points, n. """ if len(vec1.shape) > 2: raise RuntimeError, "input vec1 is the wrong shape" - + if (len(args) == 0): vec2 = vec1.copy() else: vec2 = args[0] if len(vec1.shape) > 2: raise RuntimeError, "input vec2 is the wrong shape" - + m1, n1 = asmatrix(vec1).shape m2, n2 = asmatrix(vec2).shape - + if (n1 != n2): raise RuntimeError, "Number of vectors differ in arguments." - + m1m2 = m1 * m2 - + r1mat = zeros((m1m2, n1), dtype='float64') - + mrange = asarray(range(m1), dtype='int') - + for i in range(m2): r1mat[mrange, :] = vec1 * tile(vec2[i, :], (m1, 1)) mrange = mrange + m1 - + r1mat = reshape(r1mat.T, (n1, m2, m1)).transpose(0, 2, 1) return squeeze(r1mat) @@ -363,7 +371,7 @@ def skew(A): if m != n: raise RuntimeError, "this function only works for square arrays; " \ + "yours is (%d, %d)" %(m, n) - A.resize(1, m, n) + A.resize(1, m, n) elif A.ndim == 3: m = A.shape[1] n = A.shape[2] @@ -371,13 +379,13 @@ def skew(A): raise RuntimeError, "this function only works for square arrays" else: raise RuntimeError, "this function only works for square arrays" - + return squeeze(0.5*(A - A.transpose(0, 2, 1))) - + def symm(A): """ symmetric decomposition of n square (m, m) ndarrays. Result - is a (squeezed) (n, m, m) ndarray. + is a (squeezed) (n, m, m) ndarray. """ if not isinstance(A, ndarray): raise RuntimeError, "input argument is of incorrect type; should be numpy ndarray." @@ -388,7 +396,7 @@ def symm(A): if m != n: raise RuntimeError, "this function only works for square arrays; " \ + "yours is (%d, %d)" %(m, n) - A.resize(1, m, n) + A.resize(1, m, n) elif A.ndim == 3: m = A.shape[1] n = A.shape[2] @@ -396,7 +404,7 @@ def symm(A): raise RuntimeError, "this function only works for square arrays" else: raise RuntimeError, "this function only works for square arrays" - + return squeeze(0.5*(A + A.transpose(0, 2, 1))) def skewMatrixOfVector(w): @@ -424,7 +432,7 @@ def skewMatrixOfVector(w): stackdim = w.shape[1] else: raise RuntimeError, 'input is incorrect shape; expecting ndim = 1 or 2' - + zs = zeros((1, stackdim), dtype='float64') W = vstack([ zs, -w[2, :], @@ -435,9 +443,9 @@ def skewMatrixOfVector(w): -w[1, :], w[0, :], zs ]) - + return squeeze(reshape(W.T, (stackdim, 3, 3))) - + def vectorOfSkewMatrix(W): """ vectorOfSkewMatrix(W) @@ -460,11 +468,11 @@ def vectorOfSkewMatrix(W): stackdim = W.shape[0] else: raise RuntimeError, 'input is incorrect shape; expecting (n, 3, 3)' - + w = zeros((3, stackdim), dtype='float64') for i in range(stackdim): w[:, i] = r_[-W[i, 1, 2], W[i, 0, 2], -W[i, 0, 1]] - + return w def multMatArray(ma1, ma2): @@ -473,8 +481,8 @@ def multMatArray(ma1, ma2): """ shp1 = ma1.shape shp2 = ma2.shape - - + + if len(shp1) != 3 or len(shp2) != 3: raise RuntimeError, 'input is incorrect shape; ' \ + 'expecting len(ma1).shape = len(ma2).shape = 3' @@ -484,7 +492,7 @@ def multMatArray(ma1, ma2): if shp1[2] != shp2[1]: raise RuntimeError, 'mismatch on internal matrix dimensions' - + prod = zeros((shp1[0], shp1[1], shp2[2])) for j in range(shp1[0]): prod[j, :, :] = dot( ma1[j, :, :], ma2[j, :, :] ) @@ -496,17 +504,17 @@ def uniqueVectors(v, tol=1.0e-12): Sort vectors and discard duplicates. USAGE: - + uvec = uniqueVectors(vec, tol=1.0e-12) - v -- + v -- tol -- (optional) comparison tolerance D. E. Boyce 2010-03-18 """ - + vdims = v.shape - + iv = zeros(vdims) iv2 = zeros(vdims, dtype="bool") bsum = zeros((vdims[1], ), dtype="bool") @@ -514,18 +522,18 @@ def uniqueVectors(v, tol=1.0e-12): tmpord = num.argsort(v[row, :]).tolist() tmpsrt = v[ix_([row], tmpord)].squeeze() tmpcmp = abs(tmpsrt[1:] - tmpsrt[0:-1]) - indep = num.hstack([True, tmpcmp > tol]) # independent values + indep = num.hstack([True, tmpcmp > tol]) # independent values rowint = indep.cumsum() iv[ix_([row], tmpord)] = rowint pass - + # # Dictionary sort from bottom up # iNum = num.lexsort(iv) ivSrt = iv[:, iNum] vSrt = v[:, iNum] - + ivInd = zeros(vdims[1], dtype='int') nUniq = 1; ivInd[0] = 0 for col in range(1, vdims[1]): @@ -534,61 +542,61 @@ def uniqueVectors(v, tol=1.0e-12): nUniq += 1 pass pass - + return vSrt[:, ivInd[0:nUniq]] def findDuplicateVectors(vec, tol=vTol, equivPM=False): """ Find vectors in an array that are equivalent to within a specified tolerance - + USAGE: - + eqv = DuplicateVectors(vec, *tol) - + INPUT: - + 1) vec is n x m, a double array of m horizontally concatenated n-dimensional vectors. *2) tol is 1 x 1, a scalar tolerance. If not specified, the default tolerance is 1e-14. *3) set equivPM to True if vec and -vec are to be treated as equivalent - + OUTPUT: - + 1) eqv is 1 x p, a list of p equivalence relationships. - + NOTES: - + Each equivalence relationship is a 1 x q vector of indices that represent the locations of duplicate columns/entries in the array vec. For example: - + | 1 2 2 2 1 2 7 | vec = | | | 2 3 5 3 2 3 3 | - + eqv = [[1x2 double] [1x3 double]], where - + eqv[0] = [0 4] eqv[1] = [1 3 5] """ - + vlen = vec.shape[1] vlen0 = vlen orid = asarray(range(vlen), dtype="int") torid = orid.copy() tvec = vec.copy() - + eqv = [] eqvTot = 0 uid = 0 - + ii = 1 while vlen > 1 and ii < vlen0: dupl = tile(tvec[:, 0], (vlen, 1)) - + if not equivPM: diff = abs(tvec - dupl.T).sum(0) match = abs(diff[1:]) <= tol # logical to find duplicates @@ -598,25 +606,25 @@ def findDuplicateVectors(vec, tol=vTol, equivPM=False): diffp = abs(tvec + dupl.T).sum(0) matchp = abs(diffp[1:]) <= tol match = matchn + matchp - + kick = hstack([True, match]) # pick self too - + if kick.sum() > 1: eqv += [torid[kick].tolist()] eqvTot = hstack( [ eqvTot, torid[kick] ] ) uid = hstack( [ uid, torid[kick][0] ] ) - + cmask = ones((vlen,)) cmask[kick] = 0 - cmask = cmask != 0 - + cmask = cmask != 0 + tvec = tvec[:, cmask] torid = torid[cmask] - + vlen = tvec.shape[1] - ii += 1 + ii += 1 if len(eqv) == 0: eqvTot = [] @@ -624,7 +632,7 @@ def findDuplicateVectors(vec, tol=vTol, equivPM=False): else: eqvTot = eqvTot[1:].tolist() uid = uid[1:].tolist() - + # find all single-instance vectors singles = sort( setxor1d( eqvTot, range(vlen0) ) ) @@ -633,7 +641,7 @@ def findDuplicateVectors(vec, tol=vTol, equivPM=False): # make sure is a 1D list if not hasattr(uid,'__len__'): uid = [uid] - + return eqv, uid def normvec(v): @@ -665,72 +673,72 @@ def determinant3(mat): return det def strainTenToVec(strainTen): - + strainVec = num.zeros(6, dtype='float64') strainVec[0] = strainTen[0, 0] strainVec[1] = strainTen[1, 1] strainVec[2] = strainTen[2, 2] - strainVec[3] = 2*strainTen[1, 2] - strainVec[4] = 2*strainTen[0, 2] - strainVec[5] = 2*strainTen[0, 1] - - strainVec=num.atleast_2d(strainVec).T - + strainVec[3] = 2*strainTen[1, 2] + strainVec[4] = 2*strainTen[0, 2] + strainVec[5] = 2*strainTen[0, 1] + + strainVec=num.atleast_2d(strainVec).T + return strainVec - -def strainVecToTen(strainVec): - + +def strainVecToTen(strainVec): + strainTen = num.zeros((3, 3), dtype='float64') strainTen[0, 0] = strainVec[0] strainTen[1, 1] = strainVec[1] strainTen[2, 2] = strainVec[2] strainTen[1, 2] = strainVec[3] / 2 - strainTen[0, 2] = strainVec[4] / 2 - strainTen[0, 1] = strainVec[5] / 2 - strainTen[2, 1] = strainVec[3] / 2 - strainTen[2, 0] = strainVec[4] / 2 - strainTen[1, 0] = strainVec[5] / 2 - + strainTen[0, 2] = strainVec[4] / 2 + strainTen[0, 1] = strainVec[5] / 2 + strainTen[2, 1] = strainVec[3] / 2 + strainTen[2, 0] = strainVec[4] / 2 + strainTen[1, 0] = strainVec[5] / 2 + return strainTen - - + + def stressTenToVec(stressTen): - + stressVec = num.zeros(6, dtype='float64') stressVec[0] = stressTen[0, 0] stressVec[1] = stressTen[1, 1] stressVec[2] = stressTen[2, 2] - stressVec[3] = stressTen[1, 2] - stressVec[4] = stressTen[0, 2] - stressVec[5] = stressTen[0, 1] - - stressVec=num.atleast_2d(stressVec).T - + stressVec[3] = stressTen[1, 2] + stressVec[4] = stressTen[0, 2] + stressVec[5] = stressTen[0, 1] + + stressVec=num.atleast_2d(stressVec).T + return stressVec - - -def stressVecToTen(stressVec): - + + +def stressVecToTen(stressVec): + stressTen = num.zeros((3, 3), dtype='float64') stressTen[0, 0] = stressVec[0] stressTen[1, 1] = stressVec[1] stressTen[2, 2] = stressVec[2] stressTen[1, 2] = stressVec[3] - stressTen[0, 2] = stressVec[4] - stressTen[0, 1] = stressVec[5] + stressTen[0, 2] = stressVec[4] + stressTen[0, 1] = stressVec[5] stressTen[2, 1] = stressVec[3] stressTen[2, 0] = stressVec[4] - stressTen[1, 0] = stressVec[5] - + stressTen[1, 0] = stressVec[5] + return stressTen - - - - -def ale3dStrainOutToV(vecds): + + + + +def ale3dStrainOutToV(vecds): #takes 5 components of evecd and the 6th component is lndetv - - + + """convert from vecds representation to symmetry matrix""" eps = num.zeros([3,3],dtype='float64') #Akk_by_3 = sqr3i * vecds[5] # -p @@ -738,22 +746,22 @@ def ale3dStrainOutToV(vecds): t1 = sqr2i*vecds[0] t2 = sqr6i*vecds[1] - eps[0, 0] = t1 - t2 - eps[1, 1] = -t1 - t2 - eps[2, 2] = sqr2b3*vecds[1] - eps[1, 0] = vecds[2] * sqr2i - eps[2, 0] = vecds[3] * sqr2i - eps[2, 1] = vecds[4] * sqr2i + eps[0, 0] = t1 - t2 + eps[1, 1] = -t1 - t2 + eps[2, 2] = sqr2b3*vecds[1] + eps[1, 0] = vecds[2] * sqr2i + eps[2, 0] = vecds[3] * sqr2i + eps[2, 1] = vecds[4] * sqr2i eps[0, 1] = eps[1, 0] eps[0, 2] = eps[2, 0] eps[1, 2] = eps[2, 1] - + epstar=eps/a - + V=(num.identity(3)+epstar)*a Vinv=(num.identity(3)-epstar)/a - + return V,Vinv def vecdsToSymm(vecds): @@ -763,18 +771,18 @@ def vecdsToSymm(vecds): t1 = sqr2i*vecds[0] t2 = sqr6i*vecds[1] - A[0, 0] = t1 - t2 + Akk_by_3 - A[1, 1] = -t1 - t2 + Akk_by_3 + A[0, 0] = t1 - t2 + Akk_by_3 + A[1, 1] = -t1 - t2 + Akk_by_3 A[2, 2] = sqr2b3*vecds[1] + Akk_by_3 - A[1, 0] = vecds[2] * sqr2i - A[2, 0] = vecds[3] * sqr2i - A[2, 1] = vecds[4] * sqr2i + A[1, 0] = vecds[2] * sqr2i + A[2, 0] = vecds[3] * sqr2i + A[2, 1] = vecds[4] * sqr2i A[0, 1] = A[1, 0] A[0, 2] = A[2, 0] A[1, 2] = A[2, 1] return A - + def traceToVecdsS(Akk): return sqr3i * Akk @@ -783,14 +791,14 @@ def vecdsSToTrace(vecdsS): def trace3(A): return A[0,0]+A[1,1]+A[2,2] - + def symmToVecds(A): """convert from symmetry matrix to vecds representation""" vecds = num.zeros(6,dtype='float64') vecds[0] = sqr2i * (A[0,0] - A[1,1]) - vecds[1] = sqr6i * (2. * A[2,2] - A[0,0] - A[1,1]) + vecds[1] = sqr6i * (2. * A[2,2] - A[0,0] - A[1,1]) vecds[2] = sqr2 * A[1,0] vecds[3] = sqr2 * A[2,0] vecds[4] = sqr2 * A[2,1] vecds[5] = traceToVecdsS(trace3(A)) - return vecds \ No newline at end of file + return vecds diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index 8c2aafb7..44c23822 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -28,10 +28,6 @@ import numpy as np -# try: -# from scipy.optimize import basinhopping -# except: -# from scipy.optimize import leastsq from scipy import optimize return_value_flag = None @@ -41,23 +37,29 @@ from hexrd.xrd import transforms_CAPI as xfcapi from hexrd.xrd import distortion as dFuncs -epsf = np.finfo(float).eps # ~2.2e-16 -sqrt_epsf = np.sqrt(epsf) # ~1.5e-8 +from hexrd.xrd.xrdutil import extract_detector_transformation -# ###################################################################### -# Module Data +epsf = np.finfo(float).eps # ~2.2e-16 +sqrt_epsf = np.sqrt(epsf) # ~1.5e-8 + +# ============================================================================= +# ############## MODULE PARAMETERS +# ============================================================================= + +# FIXME: pull these things from hexrd.constants instead d2r = np.pi/180. r2d = 180./np.pi -bVec_ref = xf.bVec_ref -eta_ref = xf.eta_ref -vInv_ref = np.r_[1., 1., 1., 0., 0., 0.] +bVec_ref = xf.bVec_ref +eta_ref = xf.eta_ref +vInv_ref = np.r_[1., 1., 1., 0., 0., 0.] # for distortion -dFunc_ref = dFuncs.GE_41RT +# FIXME: distortion implementation must change +dFunc_ref = dFuncs.GE_41RT dParams_ref = [0., 0., 0., 2., 2., 2] -dFlag_ref = np.array([0, 0, 0, 0, 0, 0], dtype=bool) -dScl_ref = np.array([1, 1, 1, 1, 1, 1], dtype=float) +dFlag_ref = np.array([0, 0, 0, 0, 0, 0], dtype=bool) +dScl_ref = np.array([1, 1, 1, 1, 1, 1], dtype=float) # for sx detector cal pFlag_ref = np.array( @@ -84,21 +86,21 @@ ) # for grain parameters -gFlag_ref = np.ones(12, dtype=bool) -gScl_ref = np.ones(12, dtype=bool) +gFlag_ref = np.ones(12, dtype=bool) +gScl_ref = np.ones(12, dtype=bool) + + +# ============================================================================= +# ############## UTILITY FUNCTIONS +# ============================================================================= -""" -###################################################################### -############## UTILITY FUNCTIONS ############## -###################################################################### -""" def matchOmegas(xyo_det, hkls_idx, chi, rMat_c, bMat, wavelength, vInv=vInv_ref, beamVec=bVec_ref, etaVec=eta_ref, omePeriod=None): """ - For a given list of (x, y, ome) points, outputs the index into the results from - oscillAnglesOfHKLs, including the calculated omega values. + For a given list of (x, y, ome) points, outputs the index into the results + from oscillAnglesOfHKLs, including the calculated omega values. """ # get omegas for rMat_s calculation if omePeriod is not None: @@ -106,47 +108,52 @@ def matchOmegas(xyo_det, hkls_idx, chi, rMat_c, bMat, wavelength, else: meas_omes = xyo_det[:, 2] - oangs0, oangs1 = xfcapi.oscillAnglesOfHKLs(hkls_idx.T, chi, rMat_c, bMat, wavelength, - vInv=vInv, - beamVec=beamVec, - etaVec=etaVec) + oangs0, oangs1 = xfcapi.oscillAnglesOfHKLs( + hkls_idx.T, chi, rMat_c, bMat, wavelength, + vInv=vInv, + beamVec=beamVec, + etaVec=etaVec) if np.any(np.isnan(oangs0)): + import pdb; pdb.set_trace() nanIdx = np.where(np.isnan(oangs0[:, 0]))[0] errorString = "Infeasible parameters for hkls:\n" for i in range(len(nanIdx)): errorString += "%d %d %d\n" % tuple(hkls_idx[:, nanIdx[i]]) - raise RuntimeError, errorString + raise RuntimeError(errorString) else: # CAPI version gives vstacked angles... must be (2, nhkls) calc_omes = np.vstack([oangs0[:, 2], oangs1[:, 2]]) if omePeriod is not None: - calc_omes = np.vstack([xf.mapAngle(oangs0[:, 2], omePeriod), - xf.mapAngle(oangs1[:, 2], omePeriod)]) + calc_omes = np.vstack([xf.mapAngle(oangs0[:, 2], omePeriod), + xf.mapAngle(oangs1[:, 2], omePeriod)]) # do angular difference - diff_omes = xf.angularDifference(np.tile(meas_omes, (2, 1)), calc_omes) + diff_omes = xf.angularDifference(np.tile(meas_omes, (2, 1)), calc_omes) match_omes = np.argsort(diff_omes, axis=0) == 0 - calc_omes = calc_omes.T.flatten()[match_omes.T.flatten()] + calc_omes = calc_omes.T.flatten()[match_omes.T.flatten()] return match_omes, calc_omes + def geomParamsToInput(wavelength, tiltAngles, chi, expMap_c, tVec_d, tVec_s, tVec_c, dParams): """ + helper routing to format data into parameter list for + calibrateDetectorFromSX """ p = np.zeros(17) - p[0] = wavelength - p[1] = tiltAngles[0] - p[2] = tiltAngles[1] - p[3] = tiltAngles[2] - p[4] = tVec_d[0] - p[5] = tVec_d[1] - p[6] = tVec_d[2] - p[7] = chi - p[8] = tVec_s[0] - p[9] = tVec_s[1] + p[0] = wavelength + p[1] = tiltAngles[0] + p[2] = tiltAngles[1] + p[3] = tiltAngles[2] + p[4] = tVec_d[0] + p[5] = tVec_d[1] + p[6] = tVec_d[2] + p[7] = chi + p[8] = tVec_s[0] + p[9] = tVec_s[1] p[10] = tVec_s[2] p[11] = expMap_c[0] p[12] = expMap_c[1] @@ -157,71 +164,75 @@ def geomParamsToInput(wavelength, return np.hstack([p, dParams]) + def inputToGeomParams(p): """ + helper routing for packing parameter list from calibrateDetectorFromSX + into a dictionary """ retval = {} retval['wavelength'] = p[0] retval['tiltAngles'] = (p[1], p[2], p[3]) - retval['tVec_d'] = np.c_[p[4], p[5], p[6]].T - retval['chi'] = p[7] - retval['tVec_s'] = np.c_[p[8], p[9], p[10]].T - retval['expMap_c'] = np.c_[p[11], p[12], p[13]].T - retval['tVec_c'] = np.c_[p[14], p[15], p[16]].T - retval['dParams'] = p[17:] + retval['tVec_d'] = np.c_[p[4], p[5], p[6]].T + retval['chi'] = p[7] + retval['tVec_s'] = np.c_[p[8], p[9], p[10]].T + retval['expMap_c'] = np.c_[p[11], p[12], p[13]].T + retval['tVec_c'] = np.c_[p[14], p[15], p[16]].T + retval['dParams'] = p[17:] return retval -""" -###################################################################### -############## CALIBRATION ############## -###################################################################### -""" +# ============================================================================= +# ############## CALIBRATION FUNCTIONS +# ============================================================================= + def calibrateDetectorFromSX( - xyo_det, hkls_idx, bMat, wavelength, - tiltAngles, chi, expMap_c, - tVec_d, tVec_s, tVec_c, - vInv=vInv_ref, - beamVec=bVec_ref, etaVec=eta_ref, - distortion=(dFunc_ref, dParams_ref, dFlag_ref, dScl_ref), - pFlag=pFlag_ref, pScl=pScl_ref, - omePeriod=None, - factor=0.1, - xtol=sqrt_epsf, ftol=sqrt_epsf, - ): + xyo_det, hkls_idx, bMat, wavelength, + tiltAngles, chi, expMap_c, + tVec_d, tVec_s, tVec_c, + vInv=vInv_ref, + beamVec=bVec_ref, etaVec=eta_ref, + distortion=(dFunc_ref, dParams_ref, dFlag_ref, dScl_ref), + pFlag=pFlag_ref, pScl=pScl_ref, + omePeriod=None, + factor=0.1, + xtol=sqrt_epsf, ftol=sqrt_epsf): """ """ if omePeriod is not None: xyo_det[:, 2] = xf.mapAngle(xyo_det[:, 2], omePeriod) - dFunc = distortion[0] + # FIXME: this format for distortion needs to go away ASAP + dFunc = distortion[0] dParams = distortion[1] - dFlag = distortion[2] - dScl = distortion[3] - - # p = np.zeros(17) - # - # p[0] = wavelength - # p[1] = tiltAngles[0] - # p[2] = tiltAngles[1] - # p[3] = tiltAngles[2] - # p[4] = tVec_d[0] - # p[5] = tVec_d[1] - # p[6] = tVec_d[2] - # p[7] = chi - # p[8] = tVec_s[0] - # p[9] = tVec_s[1] - # p[10] = tVec_s[2] - # p[11] = expMap_c[0] - # p[12] = expMap_c[1] - # p[13] = expMap_c[2] - # p[14] = tVec_c[0] - # p[15] = tVec_c[1] - # p[16] = tVec_c[2] - # - # pFull = np.hstack([p, dParams]) + dFlag = distortion[2] + dScl = distortion[3] + + """ + p = np.zeros(17) + + p[0] = wavelength + p[1] = tiltAngles[0] + p[2] = tiltAngles[1] + p[3] = tiltAngles[2] + p[4] = tVec_d[0] + p[5] = tVec_d[1] + p[6] = tVec_d[2] + p[7] = chi + p[8] = tVec_s[0] + p[9] = tVec_s[1] + p[10] = tVec_s[2] + p[11] = expMap_c[0] + p[12] = expMap_c[1] + p[13] = expMap_c[2] + p[14] = tVec_c[0] + p[15] = tVec_c[1] + p[16] = tVec_c[2] + + pFull = np.hstack([p, dParams]) + """ pFull = geomParamsToInput( wavelength, @@ -230,11 +241,12 @@ def calibrateDetectorFromSX( dParams ) + # TODO: check scaling refineFlag = np.hstack([pFlag, dFlag]) - scl = np.hstack([pScl, dScl]) - pFit = pFull[refineFlag] - fitArgs = (pFull, pFlag, dFunc, dFlag, xyo_det, hkls_idx, - bMat, vInv, beamVec, etaVec, omePeriod) + scl = np.hstack([pScl, dScl]) + pFit = pFull[refineFlag] + fitArgs = (pFull, pFlag, dFunc, dFlag, xyo_det, hkls_idx, + bMat, vInv, beamVec, etaVec, omePeriod) results = optimize.leastsq(objFuncSX, pFit, args=fitArgs, diag=1./scl[refineFlag].flatten(), @@ -246,13 +258,14 @@ def calibrateDetectorFromSX( retval[refineFlag] = pFit_opt return retval + def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, xyo_det, hkls_idx, bMat, vInv, bVec, eVec, omePeriod, simOnly=False, return_value_flag=return_value_flag): """ """ - npts = len(xyo_det) + npts = len(xyo_det) refineFlag = np.hstack([pFlag, dFlag]) @@ -269,21 +282,29 @@ def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, tVec_d = pFull[4:7].reshape(3, 1) # sample quantities - chi = pFull[7] + chi = pFull[7] tVec_s = pFull[8:11].reshape(3, 1) # crystal quantities rMat_c = xf.makeRotMatOfExpMap(pFull[11:14]) tVec_c = pFull[14:17].reshape(3, 1) + # stretch tensor comp matrix from MV notation in SAMPLE frame + vMat_s = mutil.vecMVToSymm(vInv) + + # g-vectors: + # 1. calculate full g-vector components in CRYSTAL frame from B + # 2. rotate into SAMPLE frame and apply stretch + # 3. rotate back into CRYSTAL frame and normalize to unit magnitude + # IDEA: make a function for this sequence of operations with option for + # choosing ouput frame (i.e. CRYSTAL vs SAMPLE vs LAB) gVec_c = np.dot(bMat, hkls_idx) - vMat_s = mutil.vecMVToSymm(vInv) # stretch tensor comp matrix from MV notation in SAMPLE frame - gVec_s = np.dot(vMat_s, np.dot(rMat_c, gVec_c)) # reciprocal lattice vectors in SAMPLE frame - gHat_s = mutil.unitVector(gVec_s) # unit reciprocal lattice vectors in SAMPLE frame - gHat_c = np.dot(rMat_c.T, gHat_s) # unit reciprocal lattice vectors in CRYSTAL frame + gVec_s = np.dot(vMat_s, np.dot(rMat_c, gVec_c)) + gHat_c = mutil.unitVector(np.dot(rMat_c.T, gVec_s)) - match_omes, calc_omes = matchOmegas(xyo_det, hkls_idx, chi, rMat_c, bMat, wavelength, - vInv=vInv, beamVec=bVec, etaVec=eVec, omePeriod=omePeriod) + match_omes, calc_omes = matchOmegas( + xyo_det, hkls_idx, chi, rMat_c, bMat, wavelength, + vInv=vInv, beamVec=bVec, etaVec=eVec, omePeriod=omePeriod) calc_xy = np.zeros((npts, 2)) for i in range(npts): @@ -294,64 +315,58 @@ def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, beamVec=bVec).flatten() pass if np.any(np.isnan(calc_xy)): - print "infeasible pFull: may want to scale back finite difference step size" + raise RuntimeError( + "infeasible pFull: may want to scale" + + "back finite difference step size") # return values if simOnly: + # return simulated values retval = np.hstack([calc_xy, calc_omes.reshape(npts, 1)]) else: + # return residual vector + # IDEA: try angles instead of xys? diff_vecs_xy = calc_xy - xy_unwarped[:, :2] - diff_ome = xf.angularDifference( calc_omes, xyo_det[:, 2] ) + diff_ome = xf.angularDifference(calc_omes, xyo_det[:, 2]) retval = np.hstack([diff_vecs_xy, diff_ome.reshape(npts, 1) ]).flatten() if return_value_flag == 1: - retval = sum( abs(retval) ) + # return scalar sum of squared residuals + retval = sum(abs(retval)) elif return_value_flag == 2: + # return DOF-normalized chisq + # TODO: check this calculation denom = npts - len(pFit) - 1. if denom != 0: nu_fac = 1. / denom else: nu_fac = 1. nu_fac = 1 / (npts - len(pFit) - 1.) - retval = nu_fac * sum(retval**2 / abs(np.hstack([calc_xy, calc_omes.reshape(npts, 1)]).flatten())) + retval = nu_fac * sum(retval**2) return retval -""" -###################################################################### -############## GRAIN FITTING ############## -###################################################################### -""" +# ============================================================================= +# ############## GRAIN FITTING FUNCTIONS +# ============================================================================= + -def fitGrain(xyo_det, hkls_idx, bMat, wavelength, - detectorParams, - expMap_c, tVec_c, vInv, +def fitGrain(gFull, instrument, reflections_dict, + bMat, wavelength, beamVec=bVec_ref, etaVec=eta_ref, - distortion=(dFunc_ref, dParams_ref), gFlag=gFlag_ref, gScl=gScl_ref, omePeriod=None, factor=0.1, xtol=sqrt_epsf, ftol=sqrt_epsf): """ """ + # FIXME: will currently fail if omePeriod is specifed if omePeriod is not None: xyo_det[:, 2] = xf.mapAngle(xyo_det[:, 2], omePeriod) - dFunc = distortion[0] - dParams = distortion[1] - - gFull = np.hstack([expMap_c.flatten(), - tVec_c.flatten(), - vInv.flatten()]) - - gFit = gFull[gFlag] - - fitArgs = (gFull, gFlag, - detectorParams, - xyo_det, hkls_idx, bMat, wavelength, - beamVec, etaVec, - dFunc, dParams, - omePeriod) + gFit = gFull[gFlag] + fitArgs = (gFull, gFlag, instrument, reflections_dict, + bMat, wavelength, beamVec, etaVec, omePeriod) results = optimize.leastsq(objFuncFitGrain, gFit, args=fitArgs, diag=1./gScl[gFlag].flatten(), factor=0.1, xtol=xtol, ftol=ftol) @@ -362,11 +377,12 @@ def fitGrain(xyo_det, hkls_idx, bMat, wavelength, retval[gFlag] = gFit_opt return retval + def objFuncFitGrain(gFit, gFull, gFlag, - detectorParams, - xyo_det, hkls_idx, bMat, wavelength, + instrument, + reflections_dict, + bMat, wavelength, bVec, eVec, - dFunc, dParams, omePeriod, simOnly=False, return_value_flag=return_value_flag): """ @@ -383,72 +399,115 @@ def objFuncFitGrain(gFit, gFull, gFlag, gFull[10] = vInv_MV[4] gFull[11] = vInv_MV[5] - detectorParams[0] = tiltAngles[0] - detectorParams[1] = tiltAngles[1] - detectorParams[2] = tiltAngles[2] - detectorParams[3] = tVec_d[0] - detectorParams[4] = tVec_d[1] - detectorParams[5] = tVec_d[2] - detectorParams[6] = chi - detectorParams[7] = tVec_s[0] - detectorParams[8] = tVec_s[1] - detectorParams[9] = tVec_s[2] + OLD CALL + objFuncFitGrain(gFit, gFull, gFlag, + detectorParams, + xyo_det, hkls_idx, bMat, wavelength, + bVec, eVec, + dFunc, dParams, + omePeriod, + simOnly=False, return_value_flag=return_value_flag) """ - npts = len(xyo_det) - + + # fill out parameters gFull[gFlag] = gFit - xy_unwarped = dFunc(xyo_det[:, :2], dParams) - - rMat_d = xfcapi.makeDetectorRotMat(detectorParams[:3]) - tVec_d = detectorParams[3:6].reshape(3, 1) - chi = detectorParams[6] - tVec_s = detectorParams[7:10].reshape(3, 1) - + # map parameters to functional arrays rMat_c = xfcapi.makeRotMatOfExpMap(gFull[:3]) tVec_c = gFull[3:6].reshape(3, 1) vInv_s = gFull[6:] - vMat_s = mutil.vecMVToSymm(vInv_s) # NOTE: Inverse of V from F = V * R - - gVec_c = np.dot(bMat, hkls_idx) # gVecs with magnitudes in CRYSTAL frame - gVec_s = np.dot(vMat_s, np.dot(rMat_c, gVec_c)) # stretched gVecs in SAMPLE frame - gHat_c = mutil.unitVector( - np.dot(rMat_c.T, gVec_s)) # unit reciprocal lattice vectors in CRYSTAL frame - - match_omes, calc_omes = matchOmegas(xyo_det, hkls_idx, chi, rMat_c, bMat, wavelength, - vInv=vInv_s, beamVec=bVec, etaVec=eVec, - omePeriod=omePeriod) - - rMat_s = xfcapi.makeOscillRotMatArray(chi, calc_omes) - calc_xy = xfcapi.gvecToDetectorXYArray(gHat_c.T, - rMat_d, rMat_s, rMat_c, - tVec_d, tVec_s, tVec_c, - beamVec=bVec) + vMat_s = mutil.vecMVToSymm(vInv_s) # NOTE: Inverse of V from F = V * R + + # loop over instrument panels + calc_omes_all = [] + calc_xy_all = [] + meas_xyo_all = [] + for det_key, panel in instrument.detectors.iteritems(): + rMat_d, tVec_d, chi, tVec_s = extract_detector_transformation( + instrument.detector_parameters[det_key]) + + results = reflections_dict[det_key] + + """ + extract data from results list + fields: + refl_id, gvec_id, hkl, sum_int, max_int, pred_ang, meas_ang, meas_xy + """ + + # WARNING: hkls and derived vectors below must be columnwise; + # strictly necessary??? change affected APIs instead? + # + hkls = np.atleast_2d( + np.vstack([x[2] for x in results]) + ).T + xyo_det = np.atleast_2d( + np.vstack([np.r_[x[7], x[6][-1]] for x in results]) + ) + + # FIXME: distortion handling must change to class-based + xy_unwarped = panel.distortion[0]( + xyo_det[:, :2], panel.distortion[1]) + meas_omes = xyo_det[:, 2] + meas_xyo = np.vstack([xy_unwarped.T, meas_omes]).T + + # g-vectors: + # 1. calculate full g-vector components in CRYSTAL frame from B + # 2. rotate into SAMPLE frame and apply stretch + # 3. rotate back into CRYSTAL frame and normalize to unit magnitude + # IDEA: make a function for this sequence of operations with option for + # choosing ouput frame (i.e. CRYSTAL vs SAMPLE vs LAB) + gVec_c = np.dot(bMat, hkls) + gVec_s = np.dot(vMat_s, np.dot(rMat_c, gVec_c)) + gHat_c = mutil.unitVector(np.dot(rMat_c.T, gVec_s)) + + # !!!: check that this operates on UNWARPED xy + match_omes, calc_omes = matchOmegas( + meas_xyo, hkls, chi, rMat_c, bMat, wavelength, + vInv=vInv_s, beamVec=bVec, etaVec=eVec, + omePeriod=omePeriod) + + # TODO: try Numba implementations + rMat_s = xfcapi.makeOscillRotMatArray(chi, calc_omes) + calc_xy = xfcapi.gvecToDetectorXYArray(gHat_c.T, + rMat_d, rMat_s, rMat_c, + tVec_d, tVec_s, tVec_c, + beamVec=bVec) + calc_omes_all.append(calc_omes) + calc_xy_all.append(calc_xy) + meas_xyo_all.append(meas_xyo) + pass + calc_omes_all = np.hstack(calc_omes_all) + calc_xy_all = np.vstack(calc_xy_all) + meas_xyo_all = np.vstack(meas_xyo_all) + npts = len(meas_xyo_all) if np.any(np.isnan(calc_xy)): - print "infeasible pFull" + raise RuntimeError( + "infeasible pFull: may want to scale" + + "back finite difference step size") # return values if simOnly: - retval = np.hstack([calc_xy, calc_omes.reshape(npts, 1)]) + # return simulated values + retval = np.hstack([calc_xy_all, calc_omes_all.reshape(npts, 1)]) else: - diff_vecs_xy = calc_xy - xy_unwarped[:, :2] - diff_ome = xf.angularDifference( calc_omes, xyo_det[:, 2] ) + # return residual vector + # IDEA: try angles instead of xys? + diff_vecs_xy = calc_xy_all - meas_xyo_all[:, :2] + diff_ome = xf.angularDifference(calc_omes_all, meas_xyo_all[:, 2]) retval = np.hstack([diff_vecs_xy, diff_ome.reshape(npts, 1) ]).flatten() if return_value_flag == 1: - retval = sum( abs(retval) ) + # return scalar sum of squared residuals + retval = sum(abs(retval)) elif return_value_flag == 2: - denom = npts - len(gFit) - 1. + # return DOF-normalized chisq + # TODO: check this calculation + denom = 3*npts - len(gFit) - 1. if denom != 0: nu_fac = 1. / denom else: nu_fac = 1. - retval = nu_fac * sum(retval**2 / abs(np.hstack([calc_xy, calc_omes.reshape(npts, 1)]).flatten())) + retval = nu_fac * sum(retval**2) return retval - -# def accept_test(f_new=f_new, x_new=x_new, f_old=fold, x_old=x_old): -# """ -# """ -# return not np.any(np.isnan(f_new)) diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 5f1b48b8..334452cd 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -4016,7 +4016,7 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, pixel_pitch is [row_size, column_size] in mm - DISTORTION HANDING IS STILL A KLUDGE + FIXME: DISTORTION HANDING IS STILL A KLUDGE!!! patches are: @@ -4031,6 +4031,13 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, t | x | x | x | ... | x | x | x | a ------------- ... ------------- + outputs are: + (tth_vtx, eta_vtx), + (x_vtx, y_vtx), + connectivity, + subpixel_areas, + (x_center, y_center), + (i_row, j_col) """ npts = len(tth_eta) diff --git a/scripts/makeOverlapTable.py b/scripts/makeOverlapTable.py index b3ceccff..30d80748 100755 --- a/scripts/makeOverlapTable.py +++ b/scripts/makeOverlapTable.py @@ -181,7 +181,8 @@ def build_discrete_cmap(ngrains): #%% if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Make overlap table from cfg file') + parser = argparse.ArgumentParser( + description='Make overlap table from cfg file') parser.add_argument( 'cfg', metavar='cfg_filename', type=str, help='a YAML config filename') @@ -195,7 +196,8 @@ def build_discrete_cmap(ngrains): cfg = config.open(args['cfg'])[0] print "loaded config file %s" %args['cfg'] overlap_table = build_overlap_table(cfg) - np.savez(os.path.join(cfg.analysis_dir, 'overlap_table.npz'), *overlap_table) + np.savez(os.path.join(cfg.analysis_dir, 'overlap_table.npz'), + *overlap_table) #%% #fig = plt.figure() #ax = fig.add_subplot(111, projection='3d') From f05733e585c3e2eb0076b7bf0602582fc8813bde Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 27 Mar 2017 23:09:27 -0500 Subject: [PATCH 088/253] sparseness check on frame-cache and casting for median --- hexrd/imageseries/save.py | 3 +++ hexrd/imageseries/stats.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index a133f06e..44b36b33 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -182,6 +182,9 @@ def _write_frames(self): for i in range(self._nframes): frame = self._ims[i] mask = frame > self._thresh + # FIXME: formalize this a little better??? + if sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.05: + raise Warning("frame %d is less than 95%% sparse" %i) row, col = mask.nonzero() arrd['%d_data' % i] = frame[mask] arrd['%d_row' % i] = row diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index 39d1ed88..c7a0df70 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -27,7 +27,8 @@ def median(ims, nframes=0): # could be done by rectangle by rectangle if full series # too big for memory nf = _nframes(ims, nframes) - return np.median(_toarray(ims, nf), axis=0) + out = np.empty(ims.shape, dtype=ims.dtype) + return np.median(_toarray(ims, nf), axis=0, out=out) def percentile(ims, pct, nframes=0): """return image with given percentile values over all frames""" From 3457644add7048a0940583ce0b037ec2d7fab2dc Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Sat, 1 Apr 2017 10:53:27 -0400 Subject: [PATCH 089/253] added max-file-frames and max-total-frames to imagefiles imageseries type --- hexrd/imageseries/load/imagefiles.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py index 77bccc6a..24128aeb 100644 --- a/hexrd/imageseries/load/imagefiles.py +++ b/hexrd/imageseries/load/imagefiles.py @@ -39,7 +39,10 @@ def __init__(self, fname, **kwargs): #@memoize def __len__(self): - return self._nframes + if self._maxframes_tot > 0: + return min(self._nframes, self._maxframes_tot) + else: + return self._nframes def __getitem__(self, key): if self.singleframes: @@ -70,7 +73,8 @@ def __str__(self): def _load_yml(self): EMPTY = 'empty-frames' - MAXF = 'max-frames' + MAXTOTF = 'max-total-frames' + MAXFILF = 'max-file-frames' with open(self._fname, "r") as f: d = yaml.load(f) @@ -83,12 +87,13 @@ def _load_yml(self): self.optsd = d['options'] if 'options' else None self._empty = self.optsd[EMPTY] if EMPTY in self.optsd else 0 - self._maxframes = self.optsd[MAXF] if MAXF in self.optsd else 0 + self._maxframes_tot = self.optsd[MAXTOTF] if MAXTOTF in self.optsd else 0 + self._maxframes_file = self.optsd[MAXFILF] if MAXFILF in self.optsd else 0 self._meta = yamlmeta(d['meta']) #, path=imgsd) def _process_files(self): - kw = {'empty': self._empty} + kw = {'empty': self._empty, 'max_frames': self._maxframes_file} fcl = None shp = None dtp = None @@ -195,6 +200,10 @@ def __init__(self, filename, **kwargs): d = kwargs.copy() self._empty = d.pop('empty', 0) + # user may set max-frames to 0, indicating use all frames + self._maxframes = d.pop('max_frames', 0) + if self._maxframes == 0: + self._maxframes = self._imgframes if self._empty >= self._imgframes: msg = "more empty frames than images: %s" % self.filename raise ValueError(msg) @@ -228,4 +237,4 @@ def fabioclass(self): @property def nframes(self): - return self._imgframes - self.empty + return min(self._maxframes, self._imgframes - self.empty) From 457518e203564b696408e90c18c41b0d6bd02849 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 7 Apr 2017 11:50:06 -0700 Subject: [PATCH 090/253] changes to enable mutliwedge fitting --- hexrd/imageseries/save.py | 2 +- hexrd/instrument.py | 32 ++++++++++++++++++-------------- hexrd/xrd/fitting.py | 16 +++++++++------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 44b36b33..46f19040 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -183,7 +183,7 @@ def _write_frames(self): frame = self._ims[i] mask = frame > self._thresh # FIXME: formalize this a little better??? - if sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.05: + if np.sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.05: raise Warning("frame %d is less than 95%% sparse" %i) row, col = mask.nonzero() arrd['%d_data' % i] = frame[mask] diff --git a/hexrd/instrument.py b/hexrd/instrument.py index f859adde..5929e173 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -127,17 +127,20 @@ class HEDMInstrument(object): * where should reference eta be defined? currently set to default config """ def __init__(self, instrument_config=None, - image_series=None, + image_series=None, eta_vector=None, instrument_name="instrument"): self._id = instrument_name + if eta_vector is None: + self._eta_vector = eta_vec_DFLT + else: + self._eta_vector = eta_vector + if instrument_config is None: self._num_panels = 1 self._beam_energy = beam_energy_DFLT self._beam_vector = beam_vec_DFLT - self._eta_vector = eta_vec_DFLT - self._detectors = dict( panel_id_DFLT=PlanarDetector( rows=nrows_DFLT, cols=ncols_DFLT, @@ -421,7 +424,7 @@ def pull_spots(self, plane_data, grain_params, imgser_dict, tth_tol=0.25, eta_tol=1., ome_tol=1., npdiv=2, threshold=10, - eta_ranges=None, + eta_ranges=None, ome_period=(-np.pi, np.pi), dirname='results', filename=None, save_spot_list=False, quiet=True, lrank=1, check_only=False): @@ -472,6 +475,7 @@ def pull_spots(self, plane_data, grain_params, allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( full_hkls, angList, eta_ranges, ome_ranges ) + allAngs[:, 2] = mapAngle(allAngs[:, 2], ome_period) # dilate angles tth and eta to patch corners nangs = len(allAngs) @@ -573,7 +577,7 @@ def pull_spots(self, plane_data, grain_params, frame_indices = [ ome_imgser.omega_to_frame(ome)[0] for ome in ome_eval ] - if np.any(frame_indices == -1): + if -1 in frame_indices: if not quiet: msg = "window for (%d%d%d) falls outside omega range"\ % tuple(hkl) @@ -1288,7 +1292,7 @@ def __init__(self, filename): '{:12}\t{:12}\t'.format('sum(int)', 'max(int)') + \ dp3_str.format('pred tth', 'pred eta', 'pred ome') + '\t' + \ dp3_str.format('meas tth', 'meas eta', 'meas ome') + '\t' + \ - dp3_str.format('meas X', 'meas Y', 'meas ome') + '{:18}\t{:18}'.format('meas X', 'meas Y') if isinstance(filename, file): self.fid = filename else: @@ -1329,7 +1333,7 @@ class GrainDataWriter(object): """ def __init__(self, filename): sp3_str = '{:12}\t{:12}\t{:12}' - dp3_str = '{:18}\t{:18}\t{:18}' + dp3_str = '{:19}\t{:19}\t{:19}' self._header = \ sp3_str.format( '# grain ID', 'completeness', 'chi^2') + '\t' + \ @@ -1338,15 +1342,15 @@ def __init__(self, filename): dp3_str.format( 't_vec_c[0]', 't_vec_c[1]', 't_vec_c[2]') + '\t' + \ dp3_str.format( - 'inv(V_s)[0, 0]', - 'inv(V_s)[1, 1]', - 'inv(V_s)[2, 2]') + '\t' + \ + 'inv(V_s)[0,0]', + 'inv(V_s)[1,1]', + 'inv(V_s)[2,2]') + '\t' + \ dp3_str.format( - 'inv(V_s)[1, 2]*√2', 'inv(V_s)[0, 2]*√2', 'inv(V_s)[0, 2]*√2' + 'inv(V_s)[1,2]*√2', 'inv(V_s)[0,2]*√2', 'inv(V_s)[0,2]*√2' ) + '\t' + dp3_str.format( - 'ln(V_s)[0, 0]', 'ln(V_s)[1, 1]', 'ln(V_s)[2, 2]') + '\t' + \ + 'ln(V_s)[0,0]', 'ln(V_s)[1,1]', 'ln(V_s)[2,2]') + '\t' + \ dp3_str.format( - 'ln(V_s)[1, 2]', 'ln(V_s)[0, 2]', 'ln(V_s)[0, 1]') + 'ln(V_s)[1,2]', 'ln(V_s)[0,2]', 'ln(V_s)[0,1]') if isinstance(filename, file): self.fid = filename else: @@ -1372,7 +1376,7 @@ def dump_grain(self, grain_id, completeness, chisq, dp6_e_str = \ '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}' output_str = \ - '{:<12d}\t{:<12f}\t{:<12d}\t'.format( + '{:<12d}\t{:<12f}\t{:<12f}\t'.format( grain_id, completeness, chisq) + \ dp3_e_str.format(*grain_params[:3]) + '\t' + \ dp3_e_str.format(*grain_params[3:6]) + '\t' + \ diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index 44c23822..69ab0fb7 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -29,16 +29,16 @@ import numpy as np from scipy import optimize -return_value_flag = None from hexrd import matrixutil as mutil -from hexrd.xrd import transforms as xf +from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.xrd import distortion as dFuncs +from hexrd.xrd import distortion as dFuncs from hexrd.xrd.xrdutil import extract_detector_transformation +return_value_flag = None epsf = np.finfo(float).eps # ~2.2e-16 sqrt_epsf = np.sqrt(epsf) # ~1.5e-8 @@ -353,7 +353,6 @@ def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, def fitGrain(gFull, instrument, reflections_dict, bMat, wavelength, - beamVec=bVec_ref, etaVec=eta_ref, gFlag=gFlag_ref, gScl=gScl_ref, omePeriod=None, factor=0.1, xtol=sqrt_epsf, ftol=sqrt_epsf): @@ -361,12 +360,13 @@ def fitGrain(gFull, instrument, reflections_dict, """ # FIXME: will currently fail if omePeriod is specifed if omePeriod is not None: - xyo_det[:, 2] = xf.mapAngle(xyo_det[:, 2], omePeriod) + # xyo_det[:, 2] = xf.mapAngle(xyo_det[:, 2], omePeriod) + raise(RuntimeError, "ome period must be specified") gFit = gFull[gFlag] fitArgs = (gFull, gFlag, instrument, reflections_dict, - bMat, wavelength, beamVec, etaVec, omePeriod) + bMat, wavelength, omePeriod) results = optimize.leastsq(objFuncFitGrain, gFit, args=fitArgs, diag=1./gScl[gFlag].flatten(), factor=0.1, xtol=xtol, ftol=ftol) @@ -382,7 +382,6 @@ def objFuncFitGrain(gFit, gFull, gFlag, instrument, reflections_dict, bMat, wavelength, - bVec, eVec, omePeriod, simOnly=False, return_value_flag=return_value_flag): """ @@ -409,6 +408,9 @@ def objFuncFitGrain(gFit, gFull, gFlag, simOnly=False, return_value_flag=return_value_flag) """ + bVec = instrument.beam_vector + eVec = instrument.eta_vector + # fill out parameters gFull[gFlag] = gFit From 9b1c66926c538870bf8ec847e2df45509e1066b8 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 11 Apr 2017 16:53:32 -0700 Subject: [PATCH 091/253] added first pass at multipanel eta-omega generation --- conda.recipe/meta.yaml | 6 +- hexrd/imageseries/omega.py | 1 - hexrd/instrument.py | 288 +++++++++++++++++++++++++++++++------ 3 files changed, 248 insertions(+), 47 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 86697150..76f29d69 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -3,9 +3,9 @@ package: version: master source: - git_url: https://github.com/joelvbernier/hexrd.git - git_tag: master # edit to point to specific branch or tag - + #git_url: https://github.com/joelvbernier/hexrd.git + #git_tag: instrument # edit to point to specific branch or tag + path: ../ build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} # detect_binary_files_with_prefix: true diff --git a/hexrd/imageseries/omega.py b/hexrd/imageseries/omega.py index 02b6a345..3cda9f3c 100644 --- a/hexrd/imageseries/omega.py +++ b/hexrd/imageseries/omega.py @@ -89,7 +89,6 @@ def omega_to_frame(self, om): """Return frame and wedge which includes given omega, -1 if not found""" f = -1 w = -1 - f0 = 0 for i in range(len(self._wedge_om)): omin = self._wedge_om[i, 0] omax = self._wedge_om[i, 1] diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 5929e173..77191392 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -53,11 +53,15 @@ makeOscillRotMat, \ makeRotMatOfExpMap, \ mapAngle, \ - oscillAnglesOfHKLs + oscillAnglesOfHKLs, \ + validateAngleRanges from hexrd.xrd import xrdutil from hexrd import constants as ct -from hexrd.xrd.distortion import GE_41RT # BAD, VERY BAD!!! FIX!!! +# from hexrd.utils.progressbar import ProgressBar, Bar, ETA, ReverseBar + +# FIXME: distortion kludge +from hexrd.xrd.distortion import GE_41RT # BAD, VERY BAD!!! beam_energy_DFLT = 65.351 beam_vec_DFLT = ct.beam_vec @@ -121,6 +125,25 @@ def migrate_instrument_config(instrument_config): return cfg_list +def angle_in_range(angle, ranges, ccw=True, units='degrees'): + """ + Return the index of the first wedge the angle is found in + + WARNING: always clockwise; assumes wedges are not overlapping + """ + tau = 360. + if units.lower() == 'radians': + tau = 2*np.pi + w = np.nan + for i, wedge in enumerate(ranges): + amin = wedge[0] + amax = wedge[1] + check = amin + np.mod(angle - amin, tau) + if check < amax: + w = i + break + return w + class HEDMInstrument(object): """ * Distortion needs to be moved to a class with registry; tuple unworkable @@ -320,25 +343,111 @@ def write_config(self, filename, calibration_dict={}): yaml.dump(par_dict, stream=f) return par_dict - def extract_line_positions(self, plane_data, image_dict, - tth_tol=0.25, eta_tol=1., npdiv=2): + + def extract_polar_maps(self, plane_data, imgser_dict, + tth_tol=None, eta_tol=0.25): + """ + Quick and dirty way to histogram angular patch data for make + pole figures suitable for fiber generation + + TODO: streamline projection code + TODO: normalization + """ + if tth_tol is not None: + plane_data.tThWidth = np.radians(tth_tol) + else: + tth_tol = np.degrees(plane_data.tThWidth) + tth_ranges = plane_data.getTThRanges() + + # need this for making eta ranges + eta_tol_vec = 0.5*np.radians([-eta_tol, eta_tol]) + + ring_maps_panel = dict.fromkeys(self.detectors) + for i_d, det_key in enumerate(self.detectors): + print("working on detector '%s'..." %det_key) + + # grab panel + panel = self.detectors[det_key] + # native_area = panel.pixel_area # pixel ref area + + # make rings clipped to panel + pow_angs, pow_xys, eta_idx, full_etas = panel.make_powder_rings( + plane_data, + merge_hkls=False, delta_eta=eta_tol, + output_etas=True) + + ptth, peta = panel.pixel_angles + ring_maps = [] + for i_r, tthr in enumerate(tth_ranges): + print("working on ring %d..." %i_r) + rtth_idx = np.where( + np.logical_and(ptth >= tthr[0], ptth <= tthr[1]) + ) + etas = pow_angs[i_r][:, 1] + netas = len(etas) + eta_ranges = np.tile(etas, (2, 1)).T \ + + np.tile(eta_tol_vec, (netas, 1)) + ring_map = [] + for i_e, etar in enumerate(eta_ranges): + # WARNING: assuming start/stop + emin = np.r_[etar[0]] + emax = np.r_[etar[1]] + reta_idx = np.where( + validateAngleRanges(peta[rtth_idx], emin, emax) + ) + ijs = (rtth_idx[0][reta_idx], + rtth_idx[1][reta_idx]) + ring_map.append(ijs) + pass + + try: + omegas = imgser_dict[det_key].metadata['omega'] + except(KeyError): + msg = "imageseries for '%s' has no omega info" %det_key + raise RuntimeError(msg) + # ome_edges = np.r_[omegas[:, 0], omegas[-1, 1]] + nrows_ome = len(omegas) + ncols_eta = len(full_etas) + this_map = np.nan*np.ones((nrows_ome, ncols_eta)) + for i_row, image in enumerate(imgser_dict[det_key]): + #import pdb; pdb.set_trace() + this_map[i_row, eta_idx[i_r]] = [ + np.sum(image[k[0], k[1]]) for k in ring_map + ] + ring_maps.append(this_map) + pass + ring_maps_panel[det_key] = ring_maps + return ring_maps_panel + + def extract_line_positions(self, plane_data, imgser_dict, + tth_tol=None, eta_tol=1., npdiv=2, + collapse_tth=False, do_interpolation=True): """ + TODO: handle wedge boundaries """ + if tth_tol is None: + tth_tol = np.degrees(plane_data.tThWidth) tol_vec = 0.5*np.radians( [-tth_tol, -eta_tol, -tth_tol, eta_tol, tth_tol, eta_tol, tth_tol, -eta_tol]) - panel_data = [] - for detector_id in self.detectors: + # + # pbar = ProgressBar( + # widgets=[Bar('>'), ' ', ETA(), ' ', ReverseBar('<')], + # maxval=self.num_panels, + # ).start() + # + panel_data = dict.fromkeys(self.detectors) + for i_det, detector_id in enumerate(self.detectors): + print("working on detector '%s'..." %detector_id) + # pbar.update(i_det + 1) # grab panel panel = self.detectors[detector_id] instr_cfg = panel.config_dict(self.chi, self.tvec) native_area = panel.pixel_area # pixel ref area - - # pull out the image for this panel from input dict - image = image_dict[detector_id] - + n_images = len(imgser_dict[detector_id]) + # make rings pow_angs, pow_xys = panel.make_powder_rings( plane_data, merge_hkls=True, delta_eta=eta_tol) @@ -346,6 +455,7 @@ def extract_line_positions(self, plane_data, image_dict, ring_data = [] for i_ring in range(n_rings): + print("working on ring %d..." %i_ring) these_angs = pow_angs[i_ring] # make sure no one falls off... @@ -355,6 +465,7 @@ def extract_line_positions(self, plane_data, image_dict, ).reshape(4*npts, 2) # find points that fall on the panel + # WARNING: ignoring effect of crystal tvec det_xy, rMat_s = xrdutil._project_on_detector_plane( np.hstack([patch_vertices, np.zeros((4*npts, 1))]), panel.rmat, ct.identity_3x3, self.chi, @@ -380,39 +491,55 @@ def extract_line_positions(self, plane_data, image_dict, beamVec=self.beam_vector) # loop over patches - patch_data = [] - for patch in patches: + # FIXME: fix initialization + if collapse_tth: + patch_data = np.zeros((len(ang_centers), n_images)) + else: + patch_data = [] + for i_p, patch in enumerate(patches): # strip relevant objects out of current patch vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch + if collapse_tth: + ang_data = (vtx_angs[0][0, [0, -1]], + vtx_angs[1][[0, -1], 0]) + else: + ang_data = (vtx_angs[0][0, :], + ang_centers[i_p][-1]) prows, pcols = areas.shape - + area_fac = areas/float(native_area) # need to reshape eval pts for interpolation xy_eval = np.vstack([ xy_eval[0].flatten(), xy_eval[1].flatten()]).T - ''' Maybe need these - # edge arrays - tth_edges = vtx_angs[0][0, :] - delta_tth = tth_edges[1] - tth_edges[0] - eta_edges = vtx_angs[1][:, 0] - delta_eta = eta_edges[1] - eta_edges[0] - ''' - # interpolate - patch_data.append( - panel.interpolate_bilinear( - xy_eval, - image, - ).reshape(prows, pcols)*(areas/float(native_area)) - ) - - # + if not collapse_tth: + ims_data = [] + for j_p, image in enumerate(imgser_dict[detector_id]): + # catch interpolation type + if do_interpolation: + tmp = panel.interpolate_bilinear( + xy_eval, + image, + ).reshape(prows, pcols)*area_fac + else: + tmp = image[ijs[0], ijs[1]]*area_fac + + # catch collapsing options + if collapse_tth: + patch_data[i_p, j_p] = np.sum(tmp) + #ims_data.append(np.sum(tmp)) + else: + ims_data.append(np.sum(tmp, axis=0)) + pass # close image loop + if not collapse_tth: + patch_data.append((ang_data, ims_data)) pass # close patch loop ring_data.append(patch_data) pass # close ring loop - panel_data.append(ring_data) + panel_data[detector_id] = ring_data pass # close panel loop + # pbar.finish() return panel_data def simulate_rotation_series(self, plane_data, grain_param_list): @@ -700,9 +827,9 @@ def pull_spots(self, plane_data, grain_params, pass # end detector loop return compl, output - """def fit_grain(self, grain_params, data_dir='results'): + """def fit_grain(self, grain_params, data_dir='results'):""" - pass # end class: HEDMInstrument""" + pass # end class: HEDMInstrument class PlanarDetector(object): @@ -711,7 +838,6 @@ class PlanarDetector(object): """ __pixelPitchUnit = 'mm' - __delta_eta = np.radians(10.) def __init__(self, rows=2048, cols=2048, @@ -1119,22 +1245,31 @@ def interpolate_bilinear(self, xy, img, pad_with_nans=True): return int_xy def make_powder_rings( - self, pd, merge_hkls=False, delta_eta=None, eta_period=None, + self, pd, merge_hkls=False, delta_tth=None, + delta_eta=10., eta_period=None, rmat_s=ct.identity_3x3, tvec_s=ct.zeros_3, - tvec_c=ct.zeros_3, output_ranges=False): + tvec_c=ct.zeros_3, output_ranges=False, output_etas=False): """ """ + if delta_tth is not None: + pd.tThWidth = np.radians(delta_tth) + else: + delta_tth = np.degrees(pd.tThWidth) + sector_vec = 0.5*np.radians( + [-delta_tth, -delta_eta, + -delta_tth, delta_eta, + delta_tth, delta_eta, + delta_tth, -delta_eta] + ) # for generating rings - if delta_eta is None: - delta_eta = self.__delta_eta if eta_period is None: eta_period = (-np.pi, np.pi) neta = int(360./float(delta_eta)) eta = mapAngle( np.radians( - delta_eta*np.linspace(0, neta-1, num=neta) + delta_eta*np.linspace(0, neta - 1, num=neta) ) + eta_period[0], eta_period ) @@ -1158,6 +1293,7 @@ def make_powder_rings( # need xy coords and pixel sizes valid_ang = [] valid_xy = [] + map_indices = [] for i_ring in range(len(angs)): these_angs = angs[i_ring].T gVec_ring_l = anglesToGVec(these_angs, bHat_l=self.bvec) @@ -1166,13 +1302,45 @@ def make_powder_rings( self.rmat, rmat_s, ct.identity_3x3, self.tvec, tvec_s, tvec_c, beamVec=self.bvec) - # xydet_ring, on_panel = self.clip_to_panel(xydet_ring) - # - valid_ang.append(these_angs[on_panel, :2]) - valid_xy.append(xydet_ring) + nangs_r = len(xydet_ring) + + # now expand and check to see which sectors (patches) fall off + patch_vertices = ( + np.tile(these_angs[on_panel, :2], (1, 4)) + + np.tile(sector_vec, (nangs_r, 1)) + ).reshape(4*nangs_r, 2) + + # duplicate ome array + ome_dupl = np.tile( + these_angs[on_panel, 2], (4, 1) + ).T.reshape(len(patch_vertices), 1) + + # find vertices that fall on the panel + gVec_ring_l = anglesToGVec( + np.hstack([patch_vertices, ome_dupl]), + bHat_l=self.bvec) + tmp_xy = gvecToDetectorXY( + gVec_ring_l, + self.rmat, rmat_s, ct.identity_3x3, + self.tvec, tvec_s, tvec_c, + beamVec=self.bvec) + tmp_xy, on_panel_p= self.clip_to_panel(tmp_xy) + + # all vertices must be on... + patch_is_on = np.all(on_panel_p.reshape(nangs_r, 4), axis=1) + + idx = np.where(on_panel)[0][patch_is_on] + + valid_ang.append(these_angs[idx, :2]) + valid_xy.append(xydet_ring[patch_is_on]) + map_indices.append(idx) pass - return valid_ang, valid_xy + if output_etas: + return valid_ang, valid_xy, map_indices, eta + else: + return valid_ang, valid_xy + def map_to_plane(self, pts, rmat, tvec): """ @@ -1428,3 +1596,37 @@ def unwrap_dict_to_h5(grp, d, asattr=True): grp.attrs.create(key, item) else: grp.create_dataset(key, data=np.atleast_1d(item)) + + +''' +class GenerateEtaOmeMaps(object): + """ + eta-ome map class derived from new image_series and YAML config + + ...for now... + + must provide: + + self.dataStore + self.planeData + self.iHKLList + self.etaEdges # IN RADIANS + self.omeEdges # IN RADIANS + self.etas # IN RADIANS + self.omegas # IN RADIANS + + """ + def __init__(self, image_series_dict, instrument, plane_data, active_hkls, + ome_step=None, eta_step=None, threshold=None): + """ + image_series must be OmegaImageSeries class + instrument_params must be a dict (loaded from yaml spec) + active_hkls must be a list (required for now) + """ + + # ...TO DO: change name of iHKLList? + self._iHKLList = active_hkls + self._planeData = planeData + + eta_mapping = instr.extract_polar_maps(plane_data, image_series_dict) +''' \ No newline at end of file From 72b03f26648f04c2c8c753728dca5ab63dd627ed Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 12 Apr 2017 23:03:00 -0400 Subject: [PATCH 092/253] more fixes for multipanel indexing --- hexrd/findorientations.py | 55 +++++---- hexrd/instrument.py | 238 ++++++++++++++++++++++++++++---------- hexrd/xrd/xrdutil.py | 7 +- 3 files changed, 210 insertions(+), 90 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index a3845e99..d4578bd2 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -9,7 +9,7 @@ import yaml import numpy as np -#np.seterr(over='ignore', invalid='ignore') +# np.seterr(over='ignore', invalid='ignore') import scipy.cluster as cluster from scipy import ndimage @@ -17,24 +17,18 @@ from hexrd import matrixutil as mutil from hexrd.xrd import indexer as idx from hexrd.xrd import rotations as rot -from hexrd.xrd import symmetry as sym from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.xrd import xrdutil - -from hexrd.xrd.detector import ReadGE from hexrd.xrd.xrdutil import GenerateEtaOmeMaps, EtaOmeMaps, simulateGVecs -from hexrd.xrd.xrdutil import simulateGVecs - from hexrd.xrd import distortion as dFuncs from hexrd.fitgrains import get_instrument_parameters logger = logging.getLogger(__name__) -save_as_ascii = False # FIX LATER... +save_as_ascii = False # FIX LATER... # just require scikit-learn? have_sklearn = False @@ -49,7 +43,9 @@ pass -def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndiv, filt_stdev=0.8, ncpus=1): +def generate_orientation_fibers( + eta_ome, chi, threshold, seed_hkl_ids, fiber_ndiv, + filt_stdev=0.8, ncpus=1): """ From ome-eta maps and hklid spec, generate list of quaternions from fibers @@ -67,28 +63,28 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi # crystallography data from the pd object pd = eta_ome.planeData hkls = pd.hkls - tTh = pd.getTTh() + tTh = pd.getTTh() bMat = pd.latVecOps['B'] csym = pd.getLaueGroup() - params = { - 'bMat':bMat, - 'chi':chi, - 'csym':csym, - 'fiber_ndiv':fiber_ndiv, - } + params = dict( + bMat=bMat, + chi=chi, + csym=csym, + fiber_ndiv=fiber_ndiv) - ############################################ - ## Labeling of spots from seed hkls ## - ############################################ + # ========================================================================= + # Labeling of spots from seed hkls + # ========================================================================= - qfib = [] - input_p = [] + qfib = [] + input_p = [] numSpots = [] - coms = [] + coms = [] for i in seed_hkl_ids: # First apply filter - this_map_f = -ndimage.filters.gaussian_laplace(eta_ome.dataStore[i], filt_stdev) + this_map_f = -ndimage.filters.gaussian_laplace( + eta_ome.dataStore[i], filt_stdev) labels_t, numSpots_t = ndimage.label( this_map_f > threshold, @@ -101,7 +97,6 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi index=np.arange(1, np.amax(labels_t)+1) ) ) - #labels.append(labels_t) numSpots.append(numSpots_t) coms.append(coms_t) pass @@ -126,18 +121,18 @@ def generate_orientation_fibers(eta_ome, chi, threshold, seed_hkl_ids, fiber_ndi qfib = None if ncpus > 1: # multiple process version + # QUESTION: Need a chunksize? pool = mp.Pool(ncpus, discretefiber_init, (params, )) - qfib = pool.map(discretefiber_reduced, input_p) # chunksize=chunksize) + qfib = pool.map(discretefiber_reduced, input_p) # chunksize=chunksize) pool.close() else: # single process version. global paramMP - discretefiber_init(params) # sets paramMP + discretefiber_init(params) # sets paramMP qfib = map(discretefiber_reduced, input_p) - paramMP = None # clear paramMP + paramMP = None # clear paramMP elapsed = (time.time() - start) logger.info("fiber generation took %.3f seconds", elapsed) - return np.hstack(qfib) @@ -466,7 +461,9 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): # or doing a seeded search? logger.info("Defaulting to seeded search") hkl_seeds = cfg.find_orientations.seed_search.hkl_seeds - hkl_ids = [eta_ome.planeData.hklDataList[i]['hklID'] for i in hkl_seeds] + hkl_ids = [ + eta_ome.planeData.hklDataList[i]['hklID'] for i in hkl_seeds + ] hklseedstr = ', '.join( [str(i) for i in eta_ome.planeData.hkls.T[hkl_seeds]] ) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 77191392..7b7310d2 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -46,6 +46,7 @@ from hexrd.gridutil import cellIndices, make_tolerance_grid from hexrd import matrixutil as mutil +from hexrd.valunits import valWUnit from hexrd.xrd.transforms_CAPI import anglesToGVec, \ detectorXYToGvec, \ gvecToDetectorXY, \ @@ -127,8 +128,8 @@ def migrate_instrument_config(instrument_config): def angle_in_range(angle, ranges, ccw=True, units='degrees'): """ - Return the index of the first wedge the angle is found in - + Return the index of the first wedge the angle is found in + WARNING: always clockwise; assumes wedges are not overlapping """ tau = 360. @@ -144,6 +145,7 @@ def angle_in_range(angle, ranges, ccw=True, units='degrees'): break return w + class HEDMInstrument(object): """ * Distortion needs to be moved to a class with registry; tuple unworkable @@ -343,13 +345,13 @@ def write_config(self, filename, calibration_dict={}): yaml.dump(par_dict, stream=f) return par_dict - def extract_polar_maps(self, plane_data, imgser_dict, + active_hkls=None, threshold=None, tth_tol=None, eta_tol=0.25): """ Quick and dirty way to histogram angular patch data for make pole figures suitable for fiber generation - + TODO: streamline projection code TODO: normalization """ @@ -357,29 +359,34 @@ def extract_polar_maps(self, plane_data, imgser_dict, plane_data.tThWidth = np.radians(tth_tol) else: tth_tol = np.degrees(plane_data.tThWidth) + tth_ranges = plane_data.getTThRanges() - + if active_hkls is not None: + assert hasattr(active_hkls, '__len__'), \ + "active_hkls must be an iterable with __len__" + tth_ranges = tth_ranges[active_hkls] + # need this for making eta ranges eta_tol_vec = 0.5*np.radians([-eta_tol, eta_tol]) - + ring_maps_panel = dict.fromkeys(self.detectors) for i_d, det_key in enumerate(self.detectors): - print("working on detector '%s'..." %det_key) - + print("working on detector '%s'..." % det_key) + # grab panel panel = self.detectors[det_key] # native_area = panel.pixel_area # pixel ref area - + # make rings clipped to panel pow_angs, pow_xys, eta_idx, full_etas = panel.make_powder_rings( - plane_data, - merge_hkls=False, delta_eta=eta_tol, + plane_data, + merge_hkls=False, delta_eta=eta_tol, output_etas=True) - + ptth, peta = panel.pixel_angles ring_maps = [] for i_r, tthr in enumerate(tth_ranges): - print("working on ring %d..." %i_r) + print("working on ring %d..." % i_r) rtth_idx = np.where( np.logical_and(ptth >= tthr[0], ptth <= tthr[1]) ) @@ -395,32 +402,38 @@ def extract_polar_maps(self, plane_data, imgser_dict, reta_idx = np.where( validateAngleRanges(peta[rtth_idx], emin, emax) ) - ijs = (rtth_idx[0][reta_idx], + ijs = (rtth_idx[0][reta_idx], rtth_idx[1][reta_idx]) ring_map.append(ijs) pass - + try: omegas = imgser_dict[det_key].metadata['omega'] except(KeyError): - msg = "imageseries for '%s' has no omega info" %det_key + msg = "imageseries for '%s' has no omega info" % det_key raise RuntimeError(msg) # ome_edges = np.r_[omegas[:, 0], omegas[-1, 1]] nrows_ome = len(omegas) ncols_eta = len(full_etas) this_map = np.nan*np.ones((nrows_ome, ncols_eta)) for i_row, image in enumerate(imgser_dict[det_key]): - #import pdb; pdb.set_trace() - this_map[i_row, eta_idx[i_r]] = [ - np.sum(image[k[0], k[1]]) for k in ring_map - ] + psum = np.zeros(len(ring_map)) + for i_k, k in enumerate(ring_map): + pdata = image[k[0], k[1]] + if threshold: + pdata[pdata <= threshold] = 0 + psum[i_k] = np.average(pdata) + this_map[i_row, eta_idx[i_r]] = psum + # this_map[i_row, eta_idx[i_r]] = [ + # np.sum(image[k[0], k[1]]) for k in ring_map + # ] ring_maps.append(this_map) pass ring_maps_panel[det_key] = ring_maps - return ring_maps_panel + return ring_maps_panel, full_etas def extract_line_positions(self, plane_data, imgser_dict, - tth_tol=None, eta_tol=1., npdiv=2, + tth_tol=None, eta_tol=1., npdiv=2, collapse_tth=False, do_interpolation=True): """ TODO: handle wedge boundaries @@ -440,14 +453,14 @@ def extract_line_positions(self, plane_data, imgser_dict, # panel_data = dict.fromkeys(self.detectors) for i_det, detector_id in enumerate(self.detectors): - print("working on detector '%s'..." %detector_id) + print("working on detector '%s'..." % detector_id) # pbar.update(i_det + 1) # grab panel panel = self.detectors[detector_id] instr_cfg = panel.config_dict(self.chi, self.tvec) native_area = panel.pixel_area # pixel ref area n_images = len(imgser_dict[detector_id]) - + # make rings pow_angs, pow_xys = panel.make_powder_rings( plane_data, merge_hkls=True, delta_eta=eta_tol) @@ -455,7 +468,7 @@ def extract_line_positions(self, plane_data, imgser_dict, ring_data = [] for i_ring in range(n_rings): - print("working on ring %d..." %i_ring) + print("working on ring %d..." % i_ring) these_angs = pow_angs[i_ring] # make sure no one falls off... @@ -500,10 +513,10 @@ def extract_line_positions(self, plane_data, imgser_dict, # strip relevant objects out of current patch vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch if collapse_tth: - ang_data = (vtx_angs[0][0, [0, -1]], + ang_data = (vtx_angs[0][0, [0, -1]], vtx_angs[1][[0, -1], 0]) else: - ang_data = (vtx_angs[0][0, :], + ang_data = (vtx_angs[0][0, :], ang_centers[i_p][-1]) prows, pcols = areas.shape area_fac = areas/float(native_area) @@ -525,10 +538,10 @@ def extract_line_positions(self, plane_data, imgser_dict, else: tmp = image[ijs[0], ijs[1]]*area_fac - # catch collapsing options + # catch collapsing options if collapse_tth: patch_data[i_p, j_p] = np.sum(tmp) - #ims_data.append(np.sum(tmp)) + # ims_data.append(np.sum(tmp)) else: ims_data.append(np.sum(tmp, axis=0)) pass # close image loop @@ -542,10 +555,20 @@ def extract_line_positions(self, plane_data, imgser_dict, # pbar.finish() return panel_data - def simulate_rotation_series(self, plane_data, grain_param_list): + def simulate_rotation_series(self, plane_data, grain_param_list, + ome_ranges=[(-np.pi, np.pi), ], + wavelength=None): """ + TODO: revisit output; dict, or concatenated list? """ - return NotImplementedError + results = dict.fromkeys(self.detectors) + for det_key, panel in self.detectors.iteritems(): + results[det_key] = panel.simulate_rotation_series( + plane_data, grain_param_list, + ome_ranges, + chi=self.chi, tVec_s=self.tvec, + wavelength=wavelength) + return results def pull_spots(self, plane_data, grain_params, imgser_dict, @@ -762,12 +785,13 @@ def pull_spots(self, plane_data, grain_params, closest_peak_idx = 0 pass # end multipeak conditional coms = coms[closest_peak_idx] - meas_omes = ome_edges[0] + \ + meas_omes = \ + ome_edges[0] +\ (0.5 + coms[0])*delta_ome meas_angs = np.hstack([ tth_edges[0] + (0.5 + coms[2])*delta_tth, eta_edges[0] + (0.5 + coms[1])*delta_eta, - np.radians(meas_omes), + mapAngle(np.radians(meas_omes), ome_period), ]) # intensities @@ -814,11 +838,11 @@ def pull_spots(self, plane_data, grain_params, ang_centers[i_pt], meas_angs, meas_xy) pass # end conditional on write output pass # end conditional on check only - iRefl += 1 - patch_output.append([ - peak_id, hkl_id, hkl, sum_int, max_int, - ang_centers[i_pt], meas_angs, meas_xy, - ]) + iRefl += 1 + patch_output.append([ + peak_id, hkl_id, hkl, sum_int, max_int, + ang_centers[i_pt], meas_angs, meas_xy, + ]) pass # end patch conditional pass # end patch loop output[detector_id] = patch_output @@ -1245,7 +1269,7 @@ def interpolate_bilinear(self, xy, img, pad_with_nans=True): return int_xy def make_powder_rings( - self, pd, merge_hkls=False, delta_tth=None, + self, pd, merge_hkls=False, delta_tth=None, delta_eta=10., eta_period=None, rmat_s=ct.identity_3x3, tvec_s=ct.zeros_3, tvec_c=ct.zeros_3, output_ranges=False, output_etas=False): @@ -1268,9 +1292,8 @@ def make_powder_rings( neta = int(360./float(delta_eta)) eta = mapAngle( - np.radians( - delta_eta*np.linspace(0, neta - 1, num=neta) - ) + eta_period[0], eta_period + np.radians(delta_eta*(np.linspace(0, neta - 1, num=neta) + 0.5)) + + eta_period[0], eta_period ) # in case you want to give it tth angles directly @@ -1307,8 +1330,8 @@ def make_powder_rings( # now expand and check to see which sectors (patches) fall off patch_vertices = ( - np.tile(these_angs[on_panel, :2], (1, 4)) - + np.tile(sector_vec, (nangs_r, 1)) + np.tile(these_angs[on_panel, :2], (1, 4)) + + np.tile(sector_vec, (nangs_r, 1)) ).reshape(4*nangs_r, 2) # duplicate ome array @@ -1318,20 +1341,20 @@ def make_powder_rings( # find vertices that fall on the panel gVec_ring_l = anglesToGVec( - np.hstack([patch_vertices, ome_dupl]), + np.hstack([patch_vertices, ome_dupl]), bHat_l=self.bvec) tmp_xy = gvecToDetectorXY( gVec_ring_l, self.rmat, rmat_s, ct.identity_3x3, self.tvec, tvec_s, tvec_c, beamVec=self.bvec) - tmp_xy, on_panel_p= self.clip_to_panel(tmp_xy) + tmp_xy, on_panel_p = self.clip_to_panel(tmp_xy) # all vertices must be on... patch_is_on = np.all(on_panel_p.reshape(nangs_r, 4), axis=1) idx = np.where(on_panel)[0][patch_is_on] - + valid_ang.append(these_angs[idx, :2]) valid_xy.append(xydet_ring[patch_is_on]) map_indices.append(idx) @@ -1341,7 +1364,6 @@ def make_powder_rings( else: return valid_ang, valid_xy - def map_to_plane(self, pts, rmat, tvec): """ map detctor points to specified plane @@ -1553,7 +1575,7 @@ def dump_grain(self, grain_id, completeness, chisq, print(output_str, file=self.fid) return output_str - + class GrainDataWriter_h5(object): """ """ @@ -1598,7 +1620,6 @@ def unwrap_dict_to_h5(grp, d, asattr=True): grp.create_dataset(key, data=np.atleast_1d(item)) -''' class GenerateEtaOmeMaps(object): """ eta-ome map class derived from new image_series and YAML config @@ -1616,17 +1637,118 @@ class GenerateEtaOmeMaps(object): self.omegas # IN RADIANS """ - def __init__(self, image_series_dict, instrument, plane_data, active_hkls, - ome_step=None, eta_step=None, threshold=None): + def __init__(self, image_series_dict, instrument, plane_data, + active_hkls=None, eta_step=0.25, threshold=None): """ image_series must be OmegaImageSeries class instrument_params must be a dict (loaded from yaml spec) active_hkls must be a list (required for now) """ - - # ...TO DO: change name of iHKLList? - self._iHKLList = active_hkls - self._planeData = planeData - eta_mapping = instr.extract_polar_maps(plane_data, image_series_dict) -''' \ No newline at end of file + self._planeData = plane_data + + # ???: change name of iHKLList? + # ???: can we change the behavior of iHKLList? + if active_hkls is None: + n_rings = len(plane_data.getTTh()) + self._iHKLList = range(n_rings) + else: + self._iHKLList = active_hkls + n_rings = len(active_hkls) + + # ???: need to pass a threshold? + eta_mapping, etas = instrument.extract_polar_maps( + plane_data, image_series_dict, + active_hkls=active_hkls, threshold=threshold, + tth_tol=None, eta_tol=eta_step) + + # grab a det key + # WARNING: this process assumes that the imageseries for all panels + # have the same length and omegas + det_key = eta_mapping.keys()[0] + data_store = [] + for i_ring in range(n_rings): + full_map = np.zeros_like(eta_mapping[det_key][i_ring]) + nan_mask_full = np.zeros( + (len(eta_mapping), full_map.shape[0], full_map.shape[1]) + ) + i_p = 0 + for det_key, eta_map in eta_mapping.iteritems(): + nan_mask = ~np.isnan(eta_map[i_ring]) + nan_mask_full[i_p] = nan_mask + full_map[nan_mask] += eta_map[i_ring][nan_mask] + i_p += 1 + re_nan_these = np.sum(nan_mask_full, axis=0) == 0 + full_map[re_nan_these] = np.nan + data_store.append(full_map) + self._dataStore = data_store + + # handle omegas + omegas_array = image_series_dict[det_key].metadata['omega'] + self._omegas = mapAngle( + np.radians(np.average(omegas_array, axis=1)) + ) + self._omeEdges = mapAngle( + np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]) + ) + + # handle etas + # WARNING: unlinke the omegas in imageseries metadata, + # these are in RADIANS and represent bin centers + self._etas = etas + self._etaEdges = np.r_[ + etas - 0.5*np.radians(eta_step), + etas[-1] + 0.5*np.radians(eta_step)] + + @property + def dataStore(self): + return self._dataStore + + @property + def planeData(self): + return self._planeData + + @property + def iHKLList(self): + return np.atleast_1d(self._iHKLList).flatten() + + @property + def etaEdges(self): + return self._etaEdges + + @property + def omeEdges(self): + return self._omeEdges + + @property + def etas(self): + return self._etas + + @property + def omegas(self): + return self._omegas + + def save(self, filename): + """ + self.dataStore + self.planeData + self.iHKLList + self.etaEdges + self.omeEdges + self.etas + self.omegas + """ + args = np.array(self.planeData.getParams())[:4] + args[2] = valWUnit('wavelength', 'length', args[2], 'angstrom') + hkls = self.planeData.hkls + save_dict = {'dataStore': self.dataStore, + 'etas': self.etas, + 'etaEdges': self.etaEdges, + 'iHKLList': self.iHKLList, + 'omegas': self.omegas, + 'omeEdges': self.omeEdges, + 'planeData_args': args, + 'planeData_hkls': hkls} + np.savez_compressed(filename, **save_dict) + return + pass # end of class: GenerateEtaOmeMaps diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 334452cd..39e70524 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -4581,10 +4581,11 @@ def pullSpots(pd, detector_params, grain_params, reader, pass iRefl += 1 pass - if filename is not None: fid.close() - + if filename is not None: + fid.close() return spot_list + def extract_detector_transformation(detector_params): """ goes from 10 vector of detector parames OR instrument config dictionary @@ -4602,6 +4603,6 @@ def extract_detector_transformation(detector_params): "list of detector parameters must have length >= 10" rMat_d = xfcapi.makeDetectorRotMat(detector_params[:3]) tVec_d = num.ascontiguousarray(detector_params[3:6]) - chi = detector_params[6] + chi = detector_params[6] tVec_s = num.ascontiguousarray(detector_params[7:10]) return rMat_d, tVec_d, chi, tVec_s From 548bfade87251cb8820fb93e806c839f1a0f7dcb Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 13 Apr 2017 09:56:24 -0400 Subject: [PATCH 093/253] fixed non-int index in gui --- hexrd/wx/canvaspanel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/wx/canvaspanel.py b/hexrd/wx/canvaspanel.py index 6b296ff9..31161095 100644 --- a/hexrd/wx/canvaspanel.py +++ b/hexrd/wx/canvaspanel.py @@ -148,8 +148,8 @@ def on_press(event): mainFrame = wx.GetApp().GetTopWindow() if hasattr(event, 'xdata') and event.xdata: - x = event.xdata; xadj = x + 0.5; xint = numpy.floor(xadj) - y = event.ydata; yadj = y + 0.5; yint = numpy.floor(yadj) + x = event.xdata; xadj = x + 0.5; xint = int(numpy.floor(xadj)) + y = event.ydata; yadj = y + 0.5; yint = int(numpy.floor(yadj)) tth, eta = numpy.array(det.xyoToAng(y, x)) cartx, carty = det.cartesianCoordsOfPixelIndices(y, x) cx = (cartx - det.xc)/det.pixelPitch From 01491b48248fa66a8ba753593d601e8308a3cdbe Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 14 Apr 2017 01:32:51 -0400 Subject: [PATCH 094/253] minor fixes --- hexrd/instrument.py | 14 ++++++++++---- hexrd/xrd/fitting.py | 32 +++++++++++++++++++------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 7b7310d2..d62849b8 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -736,9 +736,12 @@ def pull_spots(self, plane_data, grain_params, else: contains_signal = False for i_frame in frame_indices: - contains_signal = contains_signal or np.any( - ome_imgser[i_frame][ijs[0], ijs[1]] > threshold - ) + try: + contains_signal = contains_signal or np.any( + ome_imgser[i_frame][ijs[0], ijs[1]] > threshold + ) + except(IndexError): + import pdb;pdb.set_trace() compl.append(contains_signal) if not check_only: peak_id = -999 @@ -829,6 +832,8 @@ def pull_spots(self, plane_data, grain_params, panel.distortion[1], invert=True).flatten() pass + # FIXME: why is this suddenly necessary??? + meas_xy = meas_xy.squeeze() pass # write output @@ -886,7 +891,8 @@ def __init__(self, self._pixel_size_col = pixel_size[1] if panel_buffer is None: - self._panel_buffer = [self._pixel_size_col, self._pixel_size_row] + self._panel_buffer = 25*np.r_[self._pixel_size_col, + self._pixel_size_row] self._tvec = np.array(tvec).flatten() self._tilt = np.array(tilt).flatten() diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index 69ab0fb7..3604479e 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -242,7 +242,7 @@ def calibrateDetectorFromSX( ) # TODO: check scaling - refineFlag = np.hstack([pFlag, dFlag]) + refineFlag = np.array(np.hstack([pFlag, dFlag]), dtyp=bool) scl = np.hstack([pScl, dScl]) pFit = pFull[refineFlag] fitArgs = (pFull, pFlag, dFunc, dFlag, xyo_det, hkls_idx, @@ -267,14 +267,18 @@ def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, """ npts = len(xyo_det) - refineFlag = np.hstack([pFlag, dFlag]) - + refineFlag = np.array(np.hstack([pFlag, dFlag]), dtype=bool) + print refineFlag + # pFull[refineFlag] = pFit/scl[refineFlag] pFull[refineFlag] = pFit - dParams = pFull[-len(dFlag):] - xy_unwarped = dFunc(xyo_det[:, :2], dParams) - + if dFunc is not None: + dParams = pFull[-len(dFlag):] + xys = dFunc(xyo_det[:, :2], dParams) + else: + xys = xyo_det[:, :2] + # detector quantities wavelength = pFull[0] @@ -326,7 +330,7 @@ def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, else: # return residual vector # IDEA: try angles instead of xys? - diff_vecs_xy = calc_xy - xy_unwarped[:, :2] + diff_vecs_xy = calc_xy - xys[:, :2] diff_ome = xf.angularDifference(calc_omes, xyo_det[:, 2]) retval = np.hstack([diff_vecs_xy, diff_ome.reshape(npts, 1) @@ -442,15 +446,17 @@ def objFuncFitGrain(gFit, gFull, gFlag, hkls = np.atleast_2d( np.vstack([x[2] for x in results]) ).T - xyo_det = np.atleast_2d( + meas_xyo = np.atleast_2d( np.vstack([np.r_[x[7], x[6][-1]] for x in results]) ) - + # FIXME: distortion handling must change to class-based - xy_unwarped = panel.distortion[0]( - xyo_det[:, :2], panel.distortion[1]) - meas_omes = xyo_det[:, 2] - meas_xyo = np.vstack([xy_unwarped.T, meas_omes]).T + if panel.distortion is not None: + meas_omes = meas_xyo[:, 2] + xy_unwarped = panel.distortion[0]( + meas_xyo[:, :2], panel.distortion[1]) + meas_xyo = np.vstack([xy_unwarped.T, meas_omes]).T + # g-vectors: # 1. calculate full g-vector components in CRYSTAL frame from B From 018ad5c48767636f35c9bbe6fd1351626419f5bf Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 14 Apr 2017 12:20:09 -0400 Subject: [PATCH 095/253] added polygon testing to pull_spots --- hexrd/instrument.py | 203 ++++++++++++++++++++++++++++---------------- 1 file changed, 129 insertions(+), 74 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index d62849b8..351931f2 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -64,6 +64,8 @@ # FIXME: distortion kludge from hexrd.xrd.distortion import GE_41RT # BAD, VERY BAD!!! +from skimage.draw import polygon + beam_energy_DFLT = 65.351 beam_vec_DFLT = ct.beam_vec @@ -664,6 +666,9 @@ def pull_spots(self, plane_data, grain_params, instr_cfg = panel.config_dict(self.chi, self.tvec) native_area = panel.pixel_area # pixel ref area + # pull out the OmegaImageSeries for this panel from input dict + ome_imgser = imgser_dict[detector_id] + # find points that fall on the panel det_xy, rMat_s = xrdutil._project_on_detector_plane( np.hstack([patch_vertices, ome_dupl]), @@ -671,85 +676,117 @@ def pull_spots(self, plane_data, grain_params, panel.tvec, tVec_c, self.tvec, panel.distortion ) - tmp_xy, on_panel = panel.clip_to_panel(det_xy) + scrap, on_panel = panel.clip_to_panel(det_xy) # all vertices must be on... patch_is_on = np.all(on_panel.reshape(nangs, 4), axis=1) + patch_xys = det_xy.reshape(nangs, 4, 2)[patch_is_on] # grab hkls and gvec ids for this panel hkls_p = allHKLs[patch_is_on, 1:] hkl_ids = allHKLs[patch_is_on, 0] - # reflection angles (voxel centers) and pixel size in (tth, eta) + # reflection angles (voxel centers) ang_centers = allAngs[patch_is_on, :] - ang_pixel_size = panel.angularPixelSize(tmp_xy[::4, :]) - - # make the tth,eta patches for interpolation - patches = xrdutil.make_reflection_patches( - instr_cfg, ang_centers[:, :2], ang_pixel_size, - tth_tol=tth_tol, eta_tol=eta_tol, - rMat_c=rMat_c, tVec_c=tVec_c, - distortion=panel.distortion, - npdiv=npdiv, quiet=True, - beamVec=self.beam_vector) - - # pull out the OmegaImageSeries for this panel from input dict - ome_imgser = imgser_dict[detector_id] - - # grand loop over reflections for this panel - patch_output = [] - for i_pt, patch in enumerate(patches): - - # strip relevant objects out of current patch - vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch - prows, pcols = areas.shape - nrm_fac = areas/float(native_area) - - # grab hkl info - hkl = hkls_p[i_pt, :] - hkl_id = hkl_ids[i_pt] - - # edge arrays - tth_edges = vtx_angs[0][0, :] - delta_tth = tth_edges[1] - tth_edges[0] - eta_edges = vtx_angs[1][:, 0] - delta_eta = eta_edges[1] - eta_edges[0] - - # need to reshape eval pts for interpolation - xy_eval = np.vstack([xy_eval[0].flatten(), - xy_eval[1].flatten()]).T - - # the evaluation omegas; - # expand about the central value using tol vector - ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del - - # ...vectorize the omega_to_frame function to avoid loop? - frame_indices = [ - ome_imgser.omega_to_frame(ome)[0] for ome in ome_eval - ] - if -1 in frame_indices: - if not quiet: - msg = "window for (%d%d%d) falls outside omega range"\ - % tuple(hkl) - print(msg) - continue - else: - contains_signal = False - for i_frame in frame_indices: - try: + + # calculate angular (tth, eta) pixel size using + # first vertex of each + ang_pixel_size = panel.angularPixelSize(patch_xys[:, 0, :]) + + # TODO: add polygon testing right here! + if check_only: + patch_output = [] + for i_pt, angs in enumerate(ang_centers): + # the evaluation omegas; + # expand about the central value using tol vector + ome_eval = np.degrees(angs[2]) + ome_del + + # ...vectorize the omega_to_frame function to avoid loop? + frame_indices = [ + ome_imgser.omega_to_frame(ome)[0] for ome in ome_eval + ] + if -1 in frame_indices: + if not quiet: + msg = """ + window for (%d%d%d) falls outside omega range + """ % tuple(hkls_p[i_pt, :]) + print(msg) + continue + else: + these_vertices = patch_xys[i_pt] + ijs = panel.cartToPixel(these_vertices) + ii, jj = polygon(ijs[:, 0], ijs[:, 1]) + contains_signal = False + for i_frame in frame_indices: + contains_signal = contains_signal or np.any( + ome_imgser[i_frame][ii, jj] > threshold + ) + compl.append(contains_signal) + patch_output.append((ii, jj, frame_indices)) + else: + # make the tth,eta patches for interpolation + patches = xrdutil.make_reflection_patches( + instr_cfg, ang_centers[:, :2], ang_pixel_size, + tth_tol=tth_tol, eta_tol=eta_tol, + rMat_c=rMat_c, tVec_c=tVec_c, + distortion=panel.distortion, + npdiv=npdiv, quiet=True, + beamVec=self.beam_vector) + + # GRAND LOOP over reflections for this panel + patch_output = [] + for i_pt, patch in enumerate(patches): + + # strip relevant objects out of current patch + vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch + prows, pcols = areas.shape + nrm_fac = areas/float(native_area) + + # grab hkl info + hkl = hkls_p[i_pt, :] + hkl_id = hkl_ids[i_pt] + + # edge arrays + tth_edges = vtx_angs[0][0, :] + delta_tth = tth_edges[1] - tth_edges[0] + eta_edges = vtx_angs[1][:, 0] + delta_eta = eta_edges[1] - eta_edges[0] + + # need to reshape eval pts for interpolation + xy_eval = np.vstack([xy_eval[0].flatten(), + xy_eval[1].flatten()]).T + + # the evaluation omegas; + # expand about the central value using tol vector + ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del + + # ...vectorize the omega_to_frame function to avoid loop? + frame_indices = [ + ome_imgser.omega_to_frame(ome)[0] for ome in ome_eval + ] + if -1 in frame_indices: + if not quiet: + msg = """ + window for (%d%d%d) falls outside omega range + """ % tuple(hkl) + print(msg) + continue + else: + contains_signal = False + for i_frame in frame_indices: contains_signal = contains_signal or np.any( ome_imgser[i_frame][ijs[0], ijs[1]] > threshold ) - except(IndexError): - import pdb;pdb.set_trace() - compl.append(contains_signal) - if not check_only: + compl.append(contains_signal) + + # initialize spot data parameters peak_id = -999 sum_int = None max_int = None meas_angs = None meas_xy = None - + + # initialize patch data array for intensities patch_data = np.zeros( (len(frame_indices), prows, pcols) ) @@ -764,7 +801,7 @@ def pull_spots(self, plane_data, grain_params, ome_imgser[i_frame], ).reshape(prows, pcols)*nrm_fac pass - + # now have interpolated patch data... labels, num_peaks = ndimage.label( patch_data > threshold, structure=label_struct @@ -796,7 +833,7 @@ def pull_spots(self, plane_data, grain_params, eta_edges[0] + (0.5 + coms[1])*delta_eta, mapAngle(np.radians(meas_omes), ome_period), ]) - + # intensities # - summed is 'integrated' over interpolated data # - max is max of raw input data @@ -812,7 +849,7 @@ def pull_spots(self, plane_data, grain_params, patch_data[labels == slabels[closest_peak_idx]] ) ''' - + # need xy coords gvec_c = anglesToGVec( meas_angs, @@ -1584,8 +1621,9 @@ def dump_grain(self, grain_id, completeness, chisq, class GrainDataWriter_h5(object): """ + TODO: add material spec """ - def __init__(self, filename, instr_cfg, panel_id): + def __init__(self, filename, instr_cfg): use_attr = True if isinstance(filename, h5py.File): self.fid = filename @@ -1595,11 +1633,14 @@ def __init__(self, filename, instr_cfg, panel_id): icfg.update(instr_cfg) # add instrument groups and attributes - grp = self.fid.create_group('instrument') - unwrap_dict_to_h5(grp, icfg, asattr=use_attr) + self.instr_grp = self.fid.create_group('instrument') + unwrap_dict_to_h5(self.instr_grp, icfg, asattr=use_attr) - grp = self.fid.create_group("data") - grp.attrs.create("panel_id", panel_id) + data_key = 'reflection_data' + self.data_grp = self.fid.create_group(data_key) + + for det_key in self.instr_grp['detectors'].keys(): + self.data_grp.create_group(det_key) def __del__(self): self.close() @@ -1607,10 +1648,24 @@ def __del__(self): def close(self): self.fid.close() - def dump_patch(self, peak_id, hkl_id, - hkl, spot_int, max_int, - pangs, mangs, xy): - return NotImplementedError + def dump_patch(self, panel_id, + peak_id, hkl_id, hkl, + tth_edges, eta_edges, ome_edges, spot_data, + pangs, mangs, xys, ijs): + panel_grp = self.data_grp[panel_id] + spot_grp = panel_grp.create_group("spot_%05d" % peak_id) + spot_grp.attrs.create('hkl_id', hkl_id) + spot_grp.attrs.create('hkl', hkl) + spot_grp.attrs.create('predicted_angles', pangs) + spot_grp.attrs.create('measured_angles', mangs) + + spot_grp.create_dataset('tth_vertices', data=tth_edges) + spot_grp.create_dataset('eta_vertices', data=eta_edges) + spot_grp.create_dataset('ome_edges', data=ome_edges) + spot_grp.create_dataset('xy_centers', data=xys) + spot_grp.create_dataset('ij_centers', data=ijs) + spot_grp.create_dataset('intensities', data=spot_data) + return def unwrap_dict_to_h5(grp, d, asattr=True): From 98a9af03c23f28753c0f4c11cc485fbbc2b5a6ac Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sat, 15 Apr 2017 14:14:47 -0400 Subject: [PATCH 096/253] catch for cse of empty panel data --- hexrd/xrd/fitting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index 3604479e..e297bbab 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -433,6 +433,8 @@ def objFuncFitGrain(gFit, gFull, gFlag, instrument.detector_parameters[det_key]) results = reflections_dict[det_key] + if len(results) == 0: + continue """ extract data from results list From 79c1b35abfc55953c027d3331d5e9e9a8037ffbf Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 18 Apr 2017 11:02:16 -0700 Subject: [PATCH 097/253] added HDF5 patch dump, start for including pixel size in refactored detector --- hexrd/imageseries/save.py | 31 ++-- hexrd/instrument.py | 368 +++++++++++++++++++++++-------------- hexrd/wx/detectorpanel.py | 61 +++++- hexrd/wx/gereader.py | 6 +- hexrd/wx/mainapp.py | 60 +++--- hexrd/wx/readerinfo_dlg.py | 28 ++- hexrd/xrd/detector.py | 13 +- hexrd/xrd/experiment.py | 202 ++++++++++---------- hexrd/xrd/image_io.py | 16 +- 9 files changed, 480 insertions(+), 305 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 46f19040..76e0554b 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -7,6 +7,7 @@ import h5py import yaml + def write(ims, fname, fmt, **kwargs): """write imageseries to file with options @@ -19,14 +20,15 @@ def write(ims, fname, fmt, **kwargs): w = wcls(ims, fname, **kwargs) w.write() -# Registry +# Registry class _RegisterWriter(abc.ABCMeta): def __init__(cls, name, bases, attrs): abc.ABCMeta.__init__(cls, name, bases, attrs) _Registry.register(cls) + class _Registry(object): """Registry for imageseries writers""" writer_registry = dict() @@ -44,10 +46,12 @@ def getwriter(cls, name): # pass # end class + class Writer(object): """Base class for writers""" __metaclass__ = _RegisterWriter fmt = None + def __init__(self, ims, fname, **kwargs): self._ims = ims self._shape = ims.shape @@ -64,7 +68,8 @@ def __init__(self, ims, fname, **kwargs): self._fname_base = tmp[0] self._fname_suff = tmp[1] - pass # end class + pass # end class + class WriteH5(Writer): fmt = 'hdf5' @@ -80,7 +85,7 @@ def __init__(self, ims, fname, **kwargs): Options: gzip - 0-9; 0 turns off compression; 4 is default chunk_rows - number of rows per chunk; default is all -""" + """ Writer.__init__(self, ims, fname, **kwargs) self._path = self._opts['path'] @@ -92,7 +97,6 @@ def write(self): f = h5py.File(self._fname, "a") g = f.create_group(self._path) s0, s1 = self._shape - chnk = (1,) + self._shape ds = g.create_dataset('images', (self._nframes, s0, s1), self._dtype, **self.h5opts) @@ -124,11 +128,13 @@ def h5opts(self): return d - pass # end class + pass # end class + class WriteFrameCache(Writer): """info from yml file""" fmt = 'frame-cache' + def __init__(self, ims, fname, **kwargs): """write yml file with frame cache info @@ -157,7 +163,7 @@ def _process_meta(self): cdir = os.path.dirname(self._cache) b = self._fname_base - fname = os.path.join(cdir, "%s-%s.npy" % (b,k)) + fname = os.path.join(cdir, "%s-%s.npy" % (b, k)) if not os.path.exists(fname): np.save(fname, v) @@ -178,20 +184,19 @@ def _write_yml(self): def _write_frames(self): """also save shape array as originally done (before yaml)""" arrd = dict() - sh = None - for i in range(self._nframes): - frame = self._ims[i] + for i, frame in enumerate(self._ims): mask = frame > self._thresh # FIXME: formalize this a little better??? if np.sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.05: - raise Warning("frame %d is less than 95%% sparse" %i) + raise Warning("frame %d is less than 95%% sparse" % i) row, col = mask.nonzero() arrd['%d_data' % i] = frame[mask] arrd['%d_row' % i] = row arrd['%d_col' % i] = col - if sh is None: - arrd['shape'] = np.array(frame.shape) - + arrd['shape'] = self._ims.shape + arrd['nframes'] = len(self._ims) + arrd['dtype'] = self._ims.dtype + arrd.update(self._process_meta()) np.savez_compressed(self._cache, **arrd) def write(self): diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 351931f2..71f70b79 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -66,6 +66,10 @@ from skimage.draw import polygon +# ============================================================================= +# PARAMETERS +# ============================================================================= + beam_energy_DFLT = 65.351 beam_vec_DFLT = ct.beam_vec @@ -83,6 +87,11 @@ t_vec_s_DFLT = np.zeros(3) +# ============================================================================= +# UTILITY METHODS +# ============================================================================= + + def calc_beam_vec(azim, pola): """ Calculate unit beam propagation vector from @@ -148,6 +157,17 @@ def angle_in_range(angle, ranges, ccw=True, units='degrees'): return w +# ???: move to gridutil? +def centers_of_edge_vec(edges): + assert np.r_[edges].ndim == 1, "edges must be 1-d" + return np.average(np.vstack([edges[:-1], edges[1:]]), axis=0) + + +# ============================================================================= +# CLASSES +# ============================================================================= + + class HEDMInstrument(object): """ * Distortion needs to be moved to a class with registry; tuple unworkable @@ -310,8 +330,11 @@ def eta_vector(self, x): panel = self.detectors[detector_id] panel.evec = self._eta_vector - # methods - def write_config(self, filename, calibration_dict={}): + # ========================================================================= + # METHODS + # ========================================================================= + + def write_config(self, filename=None, calibration_dict={}): """ WRITE OUT YAML FILE """ # initialize output dictionary @@ -343,8 +366,9 @@ def write_config(self, filename, calibration_dict={}): pdict = panel.config_dict(self.chi, self.tvec) det_dict[det_name] = pdict['detector'] par_dict['detectors'] = det_dict - with open(filename, 'w') as f: - yaml.dump(par_dict, stream=f) + if filename is not None: + with open(filename, 'w') as f: + yaml.dump(par_dict, stream=f) return par_dict def extract_polar_maps(self, plane_data, imgser_dict, @@ -577,7 +601,8 @@ def pull_spots(self, plane_data, grain_params, tth_tol=0.25, eta_tol=1., ome_tol=1., npdiv=2, threshold=10, eta_ranges=None, ome_period=(-np.pi, np.pi), - dirname='results', filename=None, save_spot_list=False, + dirname='results', filename=None, output_format='text', + save_spot_list=False, quiet=True, lrank=1, check_only=False): if eta_ranges is None: @@ -603,13 +628,14 @@ def pull_spots(self, plane_data, grain_params, ) # grab omega ranges from first imageseries - # ...NOTE THAT THEY ARE ALL ASSUMED TO HAVE SAME OMEGAS + # + # WARNING: all imageseries AND all wedges within are assumed to have + # the same omega values; put in a check that they are all the same??? oims0 = imgser_dict[imgser_dict.keys()[0]] - ome_ranges = [(ct.d2r*i['ostart'], ct.d2r*i['ostop']) + ome_ranges = [np.radians([i['ostart'], i['ostop']]) for i in oims0.omegawedges.wedges] - # delta omega in DEGREES grabbed from first imageseries - # ...put in a check that they are all the same??? + # delta omega in DEGREES grabbed from first imageseries in the dict delta_ome = oims0.omega[0, 1] - oims0.omega[0, 0] # make omega grid for frame expansion around reference frame @@ -644,13 +670,21 @@ def pull_spots(self, plane_data, grain_params, allAngs[:, 2], (4, 1) ).T.reshape(len(patch_vertices), 1) - '''loop over panels''' + if filename is not None and output_format.lower() == 'hdf5': + this_filename = os.path.join(dirname, filename) + writer = GrainDataWriter_h5( + os.path.join(dirname, filename), + self.write_config()) + + # ===================================================================== + # LOOP OVER PANELS + # ===================================================================== iRefl = 0 compl = [] output = dict.fromkeys(self.detectors) for detector_id in self.detectors: - # initialize output writer - if filename is not None: + # initialize text-based output writer + if filename is not None and output_format.lower() == 'text': output_dir = os.path.join( dirname, detector_id ) @@ -659,7 +693,7 @@ def pull_spots(self, plane_data, grain_params, this_filename = os.path.join( output_dir, filename ) - pw = PatchDataWriter(this_filename) + writer = PatchDataWriter(this_filename) # grab panel panel = self.detectors[detector_id] @@ -683,13 +717,13 @@ def pull_spots(self, plane_data, grain_params, patch_xys = det_xy.reshape(nangs, 4, 2)[patch_is_on] # grab hkls and gvec ids for this panel - hkls_p = allHKLs[patch_is_on, 1:] - hkl_ids = allHKLs[patch_is_on, 0] + hkls_p = np.array(allHKLs[patch_is_on, 1:], dtype=int) + hkl_ids = np.array(allHKLs[patch_is_on, 0], dtype=int) # reflection angles (voxel centers) ang_centers = allAngs[patch_is_on, :] - - # calculate angular (tth, eta) pixel size using + + # calculate angular (tth, eta) pixel size using # first vertex of each ang_pixel_size = panel.angularPixelSize(patch_xys[:, 0, :]) @@ -717,13 +751,13 @@ def pull_spots(self, plane_data, grain_params, ijs = panel.cartToPixel(these_vertices) ii, jj = polygon(ijs[:, 0], ijs[:, 1]) contains_signal = False - for i_frame in frame_indices: + for i_frame in frame_indices: contains_signal = contains_signal or np.any( ome_imgser[i_frame][ii, jj] > threshold ) compl.append(contains_signal) patch_output.append((ii, jj, frame_indices)) - else: + else: # make the tth,eta patches for interpolation patches = xrdutil.make_reflection_patches( instr_cfg, ang_centers[:, :2], ang_pixel_size, @@ -732,35 +766,35 @@ def pull_spots(self, plane_data, grain_params, distortion=panel.distortion, npdiv=npdiv, quiet=True, beamVec=self.beam_vector) - + # GRAND LOOP over reflections for this panel patch_output = [] for i_pt, patch in enumerate(patches): - + # strip relevant objects out of current patch vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch prows, pcols = areas.shape nrm_fac = areas/float(native_area) - + # grab hkl info hkl = hkls_p[i_pt, :] hkl_id = hkl_ids[i_pt] - + # edge arrays tth_edges = vtx_angs[0][0, :] delta_tth = tth_edges[1] - tth_edges[0] eta_edges = vtx_angs[1][:, 0] delta_eta = eta_edges[1] - eta_edges[0] - + # need to reshape eval pts for interpolation xy_eval = np.vstack([xy_eval[0].flatten(), xy_eval[1].flatten()]).T - + # the evaluation omegas; # expand about the central value using tol vector ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del - - # ...vectorize the omega_to_frame function to avoid loop? + + # ???: vectorize the omega_to_frame function to avoid loop? frame_indices = [ ome_imgser.omega_to_frame(ome)[0] for ome in ome_eval ] @@ -772,125 +806,150 @@ def pull_spots(self, plane_data, grain_params, print(msg) continue else: - contains_signal = False - for i_frame in frame_indices: - contains_signal = contains_signal or np.any( - ome_imgser[i_frame][ijs[0], ijs[1]] > threshold - ) - compl.append(contains_signal) - # initialize spot data parameters peak_id = -999 sum_int = None max_int = None meas_angs = None meas_xy = None - - # initialize patch data array for intensities - patch_data = np.zeros( - (len(frame_indices), prows, pcols) - ) - ome_edges = np.hstack( - [ome_imgser.omega[frame_indices][:, 0], - ome_imgser.omega[frame_indices][-1, 1]] - ) - for i, i_frame in enumerate(frame_indices): - patch_data[i] = \ - panel.interpolate_bilinear( - xy_eval, - ome_imgser[i_frame], - ).reshape(prows, pcols)*nrm_fac - pass - - # now have interpolated patch data... - labels, num_peaks = ndimage.label( - patch_data > threshold, structure=label_struct - ) - slabels = np.arange(1, num_peaks + 1) - if num_peaks > 0: - peak_id = iRefl - coms = np.array( - ndimage.center_of_mass( - patch_data, labels=labels, index=slabels - ) - ) - if num_peaks > 1: - center = np.r_[patch_data.shape]*0.5 - center_t = np.tile(center, (num_peaks, 1)) - com_diff = coms - center_t - closest_peak_idx = np.argmin( - np.sum(com_diff**2, axis=1) - ) - else: - closest_peak_idx = 0 - pass # end multipeak conditional - coms = coms[closest_peak_idx] - meas_omes = \ - ome_edges[0] +\ - (0.5 + coms[0])*delta_ome - meas_angs = np.hstack([ - tth_edges[0] + (0.5 + coms[2])*delta_tth, - eta_edges[0] + (0.5 + coms[1])*delta_eta, - mapAngle(np.radians(meas_omes), ome_period), - ]) - - # intensities - # - summed is 'integrated' over interpolated data - # - max is max of raw input data - sum_int = np.sum( - patch_data[labels == slabels[closest_peak_idx]] + + # quick check for intensity + contains_signal = False + for i_frame in frame_indices: + contains_signal = contains_signal or np.any( + ome_imgser[i_frame][ijs[0], ijs[1]] > threshold + ) + compl.append(contains_signal) + if contains_signal: + + # initialize patch data array for intensities + patch_data = np.zeros( + (len(frame_indices), prows, pcols) ) - max_int = np.max( - patch_data[labels == slabels[closest_peak_idx]] + ome_edges = np.hstack( + [ome_imgser.omega[frame_indices][:, 0], + ome_imgser.omega[frame_indices][-1, 1]] ) - # IDEA: Should this only use labeled pixels ??? - ''' - max_int = np.max( - patch_data[labels == slabels[closest_peak_idx]] - ) - ''' - - # need xy coords - gvec_c = anglesToGVec( - meas_angs, - chi=self.chi, - rMat_c=rMat_c, - bHat_l=self.beam_vector) - rMat_s = makeOscillRotMat([self.chi, meas_angs[2]]) - meas_xy = gvecToDetectorXY( - gvec_c, - panel.rmat, rMat_s, rMat_c, - panel.tvec, self.tvec, tVec_c, - beamVec=self.beam_vector) - if panel.distortion is not None: - # FIXME: distortion handling - meas_xy = panel.distortion[0]( - np.atleast_2d(meas_xy), - panel.distortion[1], - invert=True).flatten() + for i, i_frame in enumerate(frame_indices): + patch_data[i] = \ + panel.interpolate_bilinear( + xy_eval, + ome_imgser[i_frame], + ).reshape(prows, pcols)*nrm_fac pass - # FIXME: why is this suddenly necessary??? - meas_xy = meas_xy.squeeze() - pass + # now have interpolated patch data... + labels, num_peaks = ndimage.label( + patch_data > threshold, structure=label_struct + ) + slabels = np.arange(1, num_peaks + 1) + if num_peaks > 0: + peak_id = iRefl + coms = np.array( + ndimage.center_of_mass( + patch_data, + labels=labels, + index=slabels + ) + ) + if num_peaks > 1: + center = np.r_[patch_data.shape]*0.5 + center_t = np.tile(center, (num_peaks, 1)) + com_diff = coms - center_t + closest_peak_idx = np.argmin( + np.sum(com_diff**2, axis=1) + ) + else: + closest_peak_idx = 0 + pass # end multipeak conditional + coms = coms[closest_peak_idx] + meas_omes = \ + ome_edges[0] + (0.5 + coms[0])*delta_ome + meas_angs = np.hstack( + [tth_edges[0] + (0.5 + coms[2])*delta_tth, + eta_edges[0] + (0.5 + coms[1])*delta_eta, + mapAngle( + np.radians(meas_omes), ome_period + ) + ] + ) + + # intensities + # - summed is 'integrated' over interpolated + # data + # - max is max of raw input data + sum_int = np.sum( + patch_data[ + labels == slabels[closest_peak_idx] + ] + ) + max_int = np.max( + patch_data[ + labels == slabels[closest_peak_idx] + ] + ) + # ???: Should this only use labeled pixels? + # max_int = np.max( + # patch_data[ + # labels == slabels[closest_peak_idx] + # ] + # ) + + # need xy coords + gvec_c = anglesToGVec( + meas_angs, + chi=self.chi, + rMat_c=rMat_c, + bHat_l=self.beam_vector) + rMat_s = makeOscillRotMat( + [self.chi, meas_angs[2]] + ) + meas_xy = gvecToDetectorXY( + gvec_c, + panel.rmat, rMat_s, rMat_c, + panel.tvec, self.tvec, tVec_c, + beamVec=self.beam_vector) + if panel.distortion is not None: + # FIXME: distortion handling + meas_xy = panel.distortion[0]( + np.atleast_2d(meas_xy), + panel.distortion[1], + invert=True).flatten() + pass + # FIXME: why is this suddenly necessary??? + meas_xy = meas_xy.squeeze() + pass # end num_peaks > 0 + pass # end contains_signal # write output if filename is not None: - pw.dump_patch( - peak_id, hkl_id, hkl, sum_int, max_int, - ang_centers[i_pt], meas_angs, meas_xy) + if output_format.lower() == 'text': + writer.dump_patch( + peak_id, hkl_id, hkl, sum_int, max_int, + ang_centers[i_pt], meas_angs, meas_xy) + elif output_format.lower() == 'hdf5': + xyc_arr = xy_eval.reshape( + prows, pcols, 2 + ).transpose(2, 0, 1) + writer.dump_patch( + detector_id, iRefl, peak_id, hkl_id, hkl, + tth_edges, eta_edges, ome_edges, + xyc_arr, ijs, patch_data, + ang_centers[i_pt], meas_angs, meas_xy) pass # end conditional on write output pass # end conditional on check only - iRefl += 1 patch_output.append([ peak_id, hkl_id, hkl, sum_int, max_int, ang_centers[i_pt], meas_angs, meas_xy, ]) + iRefl += 1 pass # end patch conditional pass # end patch loop output[detector_id] = patch_output - if filename is not None: - pw.close() + if filename is not None and output_format.lower() == 'text': + writer.close() pass # end detector loop + if filename is not None and output_format.lower() == 'hdf5': + writer.close() return compl, output """def fit_grain(self, grain_params, data_dir='results'):""" @@ -928,7 +987,7 @@ def __init__(self, self._pixel_size_col = pixel_size[1] if panel_buffer is None: - self._panel_buffer = 25*np.r_[self._pixel_size_col, + self._panel_buffer = 25*np.r_[self._pixel_size_col, self._pixel_size_row] self._tvec = np.array(tvec).flatten() @@ -1511,7 +1570,9 @@ def simulate_rotation_series(self, plane_data, grain_param_list, return valid_ids, valid_hkls, valid_angs, valid_xys, ang_pixel_size -"""UTILITIES""" +# ============================================================================= +# UTILITIES +# ============================================================================= class PatchDataWriter(object): @@ -1638,33 +1699,58 @@ def __init__(self, filename, instr_cfg): data_key = 'reflection_data' self.data_grp = self.fid.create_group(data_key) - + for det_key in self.instr_grp['detectors'].keys(): self.data_grp.create_group(det_key) - def __del__(self): - self.close() + # FIXME: throws exception when called after close method + # def __del__(self): + # self.close() def close(self): self.fid.close() - def dump_patch(self, panel_id, - peak_id, hkl_id, hkl, - tth_edges, eta_edges, ome_edges, spot_data, - pangs, mangs, xys, ijs): + def dump_patch(self, panel_id, + i_refl, peak_id, hkl_id, hkl, + tth_edges, eta_edges, ome_edges, + xy_centers, ijs, spot_data, + pangs, mangs, mxy, gzip=9): + """ + to be called inside loop over patches + + default GZIP level for data arrays is 9 + """ panel_grp = self.data_grp[panel_id] - spot_grp = panel_grp.create_group("spot_%05d" % peak_id) + spot_grp = panel_grp.create_group("spot_%05d" % i_refl) + spot_grp.attrs.create('peak_id', peak_id) spot_grp.attrs.create('hkl_id', hkl_id) spot_grp.attrs.create('hkl', hkl) spot_grp.attrs.create('predicted_angles', pangs) + if mangs is None: + mangs = np.nan*np.ones(3) spot_grp.attrs.create('measured_angles', mangs) - - spot_grp.create_dataset('tth_vertices', data=tth_edges) - spot_grp.create_dataset('eta_vertices', data=eta_edges) - spot_grp.create_dataset('ome_edges', data=ome_edges) - spot_grp.create_dataset('xy_centers', data=xys) - spot_grp.create_dataset('ij_centers', data=ijs) - spot_grp.create_dataset('intensities', data=spot_data) + if mxy is None: + mxy = np.nan*np.ones(3) + spot_grp.attrs.create('measured_xy', mxy) + + # get centers crds from edge arrays + ome_crd, eta_crd, tth_crd = np.meshgrid( + centers_of_edge_vec(ome_edges), + centers_of_edge_vec(eta_edges), + centers_of_edge_vec(tth_edges), + indexing='ij') + spot_grp.create_dataset('tth_crd', data=tth_crd, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('eta_crd', data=eta_crd, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('ome_crd', data=ome_crd, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('xy_centers', data=xy_centers, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('ij_centers', data=ijs, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('intensities', data=spot_data, + compression="gzip", compression_opts=gzip) return diff --git a/hexrd/wx/detectorpanel.py b/hexrd/wx/detectorpanel.py index 88e83941..76e5ea6c 100644 --- a/hexrd/wx/detectorpanel.py +++ b/hexrd/wx/detectorpanel.py @@ -128,6 +128,11 @@ def __makeObjects(self): app = wx.GetApp() det = app.ws.detector + self.nrows_txt = wx.TextCtrl(self, wx.NewId(), value=str(det.nrows), style=wx.RAISED_BORDER) + self.ncols_txt = wx.TextCtrl(self, wx.NewId(), value=str(det.ncols), style=wx.RAISED_BORDER) + self.pixel_txt = wx.TextCtrl(self, wx.NewId(), value=str(det.pixelPitch), style=wx.RAISED_BORDER) + self.pixel_txt_s = wx.TextCtrl(self, wx.NewId(), value=str(det.pixelPitch), style=wx.RAISED_BORDER|wx.TE_READONLY) + name = 'x Center' self.cbox_xc = wx.CheckBox(self, wx.NewId(), name) self.float_xc = FloatControl(self, wx.NewId()) @@ -237,6 +242,10 @@ def __makeBindings(self): self.Bind(wx.EVT_SPINCTRL, self.OnNumEta, self.numEta_spn) # detector section + self.Bind(wx.EVT_TEXT_ENTER, self.OnChangeRows, self.nrows_txt) + self.Bind(wx.EVT_TEXT_ENTER, self.OnChangeCols, self.ncols_txt) + self.Bind(wx.EVT_TEXT_ENTER, self.OnChangePixl, self.pixel_txt) + self.Bind(EVT_FLOAT_CTRL, self.OnFloatXC, self.float_xc) self.Bind(EVT_FLOAT_CTRL, self.OnFloatYC, self.float_yc) self.Bind(EVT_FLOAT_CTRL, self.OnFloatD, self.float_D) @@ -287,10 +296,15 @@ def __makeSizers(self): # # Geometry sizer # - nrow = 13; ncol = 2; padx = 5; pady = 5 + nrow = 15; ncol = 2; padx = 5; pady = 5 self.geoSizer = wx.FlexGridSizer(nrow, ncol, padx, pady) self.geoSizer.AddGrowableCol(0, 1) self.geoSizer.AddGrowableCol(1, 1) + # * row/col hack + self.geoSizer.Add(self.nrows_txt, 1, wx.ALIGN_RIGHT) + self.geoSizer.Add(self.ncols_txt, 1, wx.ALIGN_LEFT) + self.geoSizer.Add(self.pixel_txt, 1, wx.ALIGN_RIGHT) + self.geoSizer.Add(self.pixel_txt_s, 1, wx.ALIGN_LEFT) # * x-center self.geoSizer.Add(self.cbox_xc, 1, wx.EXPAND) self.geoSizer.Add(self.float_xc, 1, wx.EXPAND) @@ -516,6 +530,51 @@ def OnFitBin(self, e): # # Detector Parameters # + def OnChangeRows(self, evt): + """Callback for float_xc choice""" + try: + a = wx.GetApp() + nrows = int(self.nrows_txt.GetValue()) + a.ws.detector.nrows = nrows + a.getCanvas().update() + + except Exception as e: + msg = 'Failed to set nrows: \n%s' % str(e) + wx.MessageBox(msg) + pass + + return + + def OnChangeCols(self, evt): + """Callback for float_xc choice""" + try: + a = wx.GetApp() + ncols = int(self.ncols_txt.GetValue()) + a.ws.detector.ncols = ncols + a.getCanvas().update() + + except Exception as e: + msg = 'Failed to set ncols: \n%s' % str(e) + wx.MessageBox(msg) + pass + + return + + def OnChangePixl(self, evt): + """Callback for float_xc choice""" + try: + a = wx.GetApp() + pixelPitch = float(self.pixel_txt.GetValue()) + a.ws.detector.pixelPitch = pixelPitch + a.getCanvas().update() + + except Exception as e: + msg = 'Failed to set pixel pitch: \n%s' % str(e) + wx.MessageBox(msg) + pass + + return + def OnFloatXC(self, evt): """Callback for float_xc choice""" try: diff --git a/hexrd/wx/gereader.py b/hexrd/wx/gereader.py index 1ca137a4..7d02e537 100644 --- a/hexrd/wx/gereader.py +++ b/hexrd/wx/gereader.py @@ -32,7 +32,7 @@ import wx import wx.lib.mixins.listctrl as listMixins -from hexrd.xrd import detector +from hexrd.xrd.image_io import ReadGE from hexrd.xrd.experiment import ImageModes, ReaderInput from hexrd.wx.guiconfig import WindowParameters as WP @@ -322,7 +322,9 @@ def update(self): # add total number of frames available try: d = rdr.imageDir - r = detector.ReadGE((os.path.join(d, n), 0)) + r = ReadGE( + os.path.join(d, n), + fmt=exp.activeReader.imageFmt) nframe = r.getNFrames() lctrl.SetStringItem(index, 2, str(nframe)) except: diff --git a/hexrd/wx/mainapp.py b/hexrd/wx/mainapp.py index 1c33a681..413ba4e5 100644 --- a/hexrd/wx/mainapp.py +++ b/hexrd/wx/mainapp.py @@ -11,9 +11,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the Free +# Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -28,37 +28,30 @@ # """Main application file """ -import os, sys +import os +import sys import wx -from hexrd.wx import guiconfig from hexrd.wx.mainframe import MainFrame -# -# mdef Modules -# -from hexrd.xrd import detector as detectorModule - from hexrd.xrd.experiment import loadExp, ImageModes -# + + # ---------------------------------------------------CLASS: xrdApp # -class xrdApp(wx.PySimpleApp): +class xrdApp(wx.App): """xrdApp""" def __init__(self, *args): """Constructor for xrdApp""" - # - wx.PySimpleApp.__init__(self) - # - # No command args for now, due to mac build issue (64bit, argv emulation) - # + wx.App.__init__(self) + # No command args for now, due to mac build issue + # (64bit, argv emulation) f = '' - #if len(args) == 0: - # f = '' - #else: - # f = args[0] - # pass - + # if len(args) == 0: + # f = '' + # else: + # f = args[0] + # pass self.__makeData(f) self.mframe = None @@ -76,8 +69,8 @@ def __makeData(self, inpFile): # # * Image Information # - self.imgMode = ImageModes.SINGLE_FRAME - self.imgCal = None + self.imgMode = ImageModes.SINGLE_FRAME + self.imgCal = None self.imgSweep = None return @@ -90,6 +83,7 @@ def __getNotebook(self): # # ============================== API # + @property def imgFrame(self): """Image frame according to image mode""" @@ -109,10 +103,11 @@ def updateFromExp(self): return - pass # end class + pass # end class # # -----------------------------------------------END CLASS: xrdApp + def execute(*args): # # Run program stand-alone. @@ -121,11 +116,11 @@ def execute(*args): app.mframe = MainFrame(None, wx.NewId()) app.SetTopWindow(app.mframe) - #if len(sys.argv) == 1: - # app = xrdApp() - #else: - # app = xrdApp(*sys.argv[1:]) - # pass + # if len(sys.argv) == 1: + # app = xrdApp() + # else: + # app = xrdApp(*sys.argv[1:]) + # pass # # The main window cannot be imported until after the app # is instantiated due to the wx.ColourDatabase() call. @@ -137,7 +132,8 @@ def execute(*args): splashDir = os.path.dirname(__file__) splashImage = wx.Bitmap(os.path.join(splashDir, splashFile)) # - wx.SplashScreen(splashImage, wx.SPLASH_CENTRE_ON_PARENT|wx.SPLASH_TIMEOUT, + wx.SplashScreen(splashImage, + wx.SPLASH_CENTRE_ON_PARENT | wx.SPLASH_TIMEOUT, 1000, app.mframe) # # Main frame diff --git a/hexrd/wx/readerinfo_dlg.py b/hexrd/wx/readerinfo_dlg.py index f16eba6c..456e8cf9 100644 --- a/hexrd/wx/readerinfo_dlg.py +++ b/hexrd/wx/readerinfo_dlg.py @@ -53,6 +53,13 @@ def __make_objects(self): self.format_cho = wx.Choice(self, wx.NewId(), choices=['hdf5', 'frame-cache'] ) + self.pixel_lab = wx.StaticText(self, wx.NewId(), + 'Pixel Pitch', style=wx.ALIGN_RIGHT + ) + self.pixel_txt = wx.TextCtrl(self, wx.NewId(), + value='0.2', + style=wx.RAISED_BORDER + ) self.option_lab = wx.StaticText(self, wx.NewId(), 'Option', style=wx.ALIGN_RIGHT ) @@ -60,24 +67,24 @@ def __make_objects(self): 'Value', style=wx.ALIGN_LEFT ) self.option_cho = wx.Choice(self, wx.NewId(), - choices=['path'] + choices=['path', 'pixel pitch'] ) self.value_txt = wx.TextCtrl(self, wx.NewId(), - value="/imageseries", - style=wx.RAISED_BORDER - ) + value="/imageseries", + style=wx.RAISED_BORDER + ) def __make_bindings(self): """Bind interactors""" self.Bind(wx.EVT_BUTTON, self.OnFileBut, self.file_but) - + def __make_sizers(self): """Lay out the interactors""" self.sizer = wx.BoxSizer(wx.VERTICAL) self.sizer.Add(self.tbarSizer, 0, wx.EXPAND|wx.ALIGN_CENTER) - nrow = 4; ncol = 2; padx = 5; pady = 5 + nrow = 5; ncol = 2; padx = 5; pady = 5 self.info_sz = wx.FlexGridSizer(nrow, ncol, padx, pady) self.info_sz.AddGrowableCol(0, 0) self.info_sz.AddGrowableCol(1, 1) @@ -85,6 +92,8 @@ def __make_sizers(self): self.info_sz.Add(self.file_txt, 0, wx.ALIGN_LEFT|wx.EXPAND) self.info_sz.Add(self.format_lab, 0, wx.ALIGN_RIGHT) self.info_sz.Add(self.format_cho, 0, wx.ALIGN_LEFT|wx.EXPAND) + self.info_sz.Add(self.pixel_lab, 0, wx.ALIGN_RIGHT) + self.info_sz.Add(self.pixel_txt, 0, wx.ALIGN_LEFT|wx.EXPAND) self.info_sz.Add(self.option_lab, 0, wx.ALIGN_RIGHT) self.info_sz.Add(self.value_lab, 0, wx.ALIGN_LEFT) self.info_sz.Add(self.option_cho, 0, wx.ALIGN_RIGHT) @@ -105,7 +114,7 @@ def OnFileBut(self, e): """Load image file name with file dialogue NOTE: converts filenames to str from unicode -""" + """ dlg = wx.FileDialog(self, 'Select Imageseries File', style=wx.FD_FILE_MUST_EXIST) if dlg.ShowModal() == wx.ID_OK: @@ -157,7 +166,6 @@ def __init__(self, parent, id, **kwargs): # def _makeBindings(self): """Bind interactors to functions""" - return def _makeSizers(self): """Lay out windows""" @@ -176,8 +184,8 @@ def GetInfo(self): directory = p.image_dir, file = p.image_fname, format = p.format_cho.GetStringSelection(), - path = p.value_txt.GetValue() - ) + path = p.value_txt.GetValue(), + pixel_size = p.pixel_txt.GetValue()) return d pass # end class diff --git a/hexrd/xrd/detector.py b/hexrd/xrd/detector.py index e8890f74..2ab37b1f 100644 --- a/hexrd/xrd/detector.py +++ b/hexrd/xrd/detector.py @@ -84,8 +84,8 @@ #PIXEL = 0.2 # dexela, horizontal -NROWS = 3072 -NCOLS = 3888 +NROWS = 3888 +NCOLS = 3072 PIXEL = 0.0748 # MAR345 @@ -2057,13 +2057,13 @@ def __init__(self, return def set_ncols(self, ncols): - raise RuntimeError, 'set of ncols not allowed' + self.__ncols = ncols def get_ncols(self): return self.__ncols ncols = property(get_ncols, set_ncols, None) def set_pixelPitch(self, pixelPitch): - raise RuntimeError, 'set of pixelPitch not allowed' + self.__pixelPitch = pixelPitch def get_pixelPitch(self): return self.__pixelPitch pixelPitch = property(get_pixelPitch, set_pixelPitch, None) @@ -2075,7 +2075,7 @@ def get_pixelPitchUnit(self): pixelPitchUnit = property(get_pixelPitchUnit, set_pixelPitchUnit, None) def set_nrows(self, nrows): - raise RuntimeError, 'set of nrows not allowed' + self.__nrows = nrows def get_nrows(self): return self.__nrows nrows = property(get_nrows, set_nrows, None) @@ -3774,11 +3774,12 @@ def __init__(self, *args, **kwargs): else: self.__nrows = self.__idim = reader.nrows self.__ncols = reader.ncols + self.__pixelPitch = reader.pixelPitch # self.__pixelPitch = kwargs.pop('pixelPitch', 0.2) # self.__nrows = kwargs.pop('nrows', 2048) # self.__ncols = kwargs.pop('ncols', 2048) # self.__idim = max(self.__nrows, self.__ncols) - + print self.__pixelPitch Detector2DRC.__init__(self, self.__ncols, self.__nrows, self.__pixelPitch, self.__vfu, self.__vdk, diff --git a/hexrd/xrd/experiment.py b/hexrd/xrd/experiment.py index 23a340f2..9f98e04e 100644 --- a/hexrd/xrd/experiment.py +++ b/hexrd/xrd/experiment.py @@ -1,5 +1,5 @@ #! /usr/bin/env python -# ============================================================ +# ============================================================================= # Copyright (c) 2012, Lawrence Livermore National Security, LLC. # Produced at the Lawrence Livermore National Laboratory. # Written by Joel Bernier and others. @@ -11,9 +11,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the Free +# Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -24,51 +24,52 @@ # License along with this program (see file LICENSE); if not, write to # the Free Software Foundation, Inc., 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA or visit . -# ============================================================ +# ============================================================================= # -###################################################################### -## TOP-LEVEL MODULES AND SOME GLOBALS -## -"""Module for wrapping the main functionality of the xrd package. +# ============================================================================= +# Module for wrapping the main functionality of the xrd package. +# +# The Experiment class is the primary interface. Other classes are helpers. +# ============================================================================= -The Experiment class is the primary interface. Other classes -are helpers. -""" -import sys, os, copy import cPickle -import numpy +import os +import sys -numpy.seterr(invalid='ignore') +import numpy -from scipy.linalg import inv -from scipy.linalg.matfuncs import logm -from scipy import optimize +from scipy import optimize from hexrd import matrixutil from hexrd import valunits from hexrd import data from hexrd.xrd import detector -from hexrd.xrd import grain as G +from hexrd.xrd import grain as G from hexrd.xrd import indexer -from hexrd.xrd import rotations as ROT +from hexrd.xrd import rotations as ROT from hexrd.xrd import spotfinder as SPT -from hexrd.xrd import xrdutil -from hexrd.xrd.hydra import Hydra +from hexrd.xrd.hydra import Hydra from hexrd.xrd.material import Material, loadMaterialList from math import pi + +numpy.seterr(invalid='ignore') r2d = 180. / pi d2r = pi / 180. +OMEGA_PERIOD = (-pi, pi) +coarse_ang_tol = valunits.valWUnit('coarse tol', 'angle', 1.0, 'degrees') +fine_ang_tol = valunits.valWUnit('fine tol', 'angle', 0.5, 'degrees') + +# ============================================================================= +# Defaults (will eventually make to a config file) +# ============================================================================= -# -# Defaults (will eventually make to a config file) -# HERE = os.path.dirname(__file__) -toMatFile = os.path.join(HERE, '..', 'data', 'materials.cfg') -DFLT_MATFILE = os.path.normpath(toMatFile) # check whether it exists -matfileOK = os.access(DFLT_MATFILE, os.F_OK) +toMatFile = os.path.join(HERE, '..', 'data', 'materials.cfg') +DFLT_MATFILE = os.path.normpath(toMatFile) # check whether it exists +matfileOK = os.access(DFLT_MATFILE, os.F_OK) if not matfileOK: # use relative path DFLT_MATFILE = os.path.join('data', 'materials.cfg') pass @@ -76,37 +77,40 @@ if not matfileOK: # set to null DFLT_MATFILE = '' pass -# -# __all__ = ['Experiment', 'FitModes', 'ImageModes', 'ReaderInput', 'CalibrationInput', 'PolarRebinOpts', 'saveExp', 'loadExp'] -# + + # ---------------------------------------------------CLASS: FitModes # class FitModes(object): """Indicators for single-frame or multiframe data files""" # - DIRECT = 0 + DIRECT = 0 MULTIRING = 1 # DEFAULT = MULTIRING # pass # end class # -# -----------------------------------------------END CLASS:FitModes +# -----------------------------------------------END CLASS: FitModes + + # ---------------------------------------------------CLASS: ImageModes # class ImageModes(object): """Indicators for single-frame or multiframe data files""" # SINGLE_FRAME = 0 - MULTI_FRAME = 1 + MULTI_FRAME = 1 # pass # end class # # -----------------------------------------------END CLASS: ImageModes + + # ---------------------------------------------------CLASS: Experiment # class Experiment(object): @@ -125,7 +129,7 @@ def __init__(self, cfgFile=data.materials, matFile=data.all_materials): # # Reader inputs and info # - self.__active_rdr = ReaderInput() + self.__active_rdr = ReaderInput() self.__savedReaders = [self.__active_rdr] # self.__active_img = None @@ -139,7 +143,7 @@ def __init__(self, cfgFile=data.materials, matFile=data.all_materials): # # Detector and calibration information. # - self._detInfo = DetectorInfo() + self._detInfo = DetectorInfo() self._calInput = CalibrationInput(self.matList[0]) # # Spots information. @@ -253,37 +257,40 @@ def refine_grains(self, minCompl, nSubIter=3, doFit=False, - etaTol=valunits.valWUnit('etaTol', 'angle', 1.0, 'degrees'), - omeTol=valunits.valWUnit('etaTol', 'angle', 1.0, 'degrees'), + etaTol=coarse_ang_tol, + omeTol=coarse_ang_tol, fineDspTol=5.0e-3, - fineEtaTol=valunits.valWUnit('etaTol', 'angle', 0.5, 'degrees'), - fineOmeTol=valunits.valWUnit('etaTol', 'angle', 0.5, 'degrees')): + fineEtaTol=fine_ang_tol, + fineOmeTol=fine_ang_tol): """ refine a grain list """ # refine grains formally using a multi-pass refinement - nGrains = self.rMats.shape[0] + nGrains = self.rMats.shape[0] grainList = [] for iG in range(nGrains): - #indexer.progress_bar(float(iG) / nGrains) + # indexer.progress_bar(float(iG) / nGrains) grain = G.Grain(self.spots_for_indexing, - rMat=self.rMats[iG, :, :], - etaTol=etaTol, - omeTol=omeTol, - claimingSpots=False) + rMat=self.rMats[iG, :, :], + etaTol=etaTol, + omeTol=omeTol, + claimingSpots=False) if grain.completeness > minCompl: for i in range(nSubIter): grain.fit() - s1, s2, s3 = grain.findMatches(etaTol=etaTol, omeTol=omeTol, strainMag=fineDspTol, - updateSelf=True, claimingSpots=False, doFit=doFit, - testClaims=True) + s1, s2, s3 = grain.findMatches( + etaTol=etaTol, omeTol=omeTol, strainMag=fineDspTol, + updateSelf=True, claimingSpots=False, doFit=doFit, + testClaims=True) if grain.completeness > minCompl: grainList.append(grain) pass pass pass self.grainList = grainList - self._fitRMats = numpy.array([self.grainList[i].rMat for i in range(len(grainList))]) + self._fitRMats = numpy.array( + [self.grainList[i].rMat for i in range(len(grainList))] + ) return def saveRMats(self, f): @@ -324,8 +331,9 @@ def export_grainList(self, f, omeTol = self.index_opts.omeTol * d2r if sort: - loop_idx = numpy.argsort([self.grainList[i].completeness - for i in range(len(self.grainList))])[::-1] + loop_idx = numpy.argsort( + [grain.completeness for grain in self.grainList] + )[::-1] else: loop_idx = range(len(self.grainList)) pass @@ -336,10 +344,11 @@ def export_grainList(self, f, # grain = self.grainList[iG] print >> fid, '#####################\n# grain %d\n#' % (iG) - s1, s2, s3 = grain.findMatches(etaTol=etaTol, omeTol=omeTol, strainMag=dspTol, - updateSelf=True, claimingSpots=True, doFit=doFit, filename=fid) - print >> fid, '#\n# final completeness for grain %d: %g%%\n' % (iG, grain.completeness*100) + \ - '#####################\n' + s1, s2, s3 = grain.findMatches( + etaTol=etaTol, omeTol=omeTol, strainMag=dspTol, + updateSelf=True, claimingSpots=True, doFit=doFit, filename=fid) + print >> fid, '#\n# final completeness for grain %d: %g%%\n'\ + % (iG, grain.completeness*100) + '#####################\n' pass fid.close() @@ -351,7 +360,7 @@ def simulateGrain(self, vMat=numpy.r_[1., 1., 1., 0., 0., 0.], planeData=None, detector=None, - omegaRanges=[(-pi, pi),], + omegaRanges=[OMEGA_PERIOD], output=None): """ Simulate a grain with choice of active material @@ -368,8 +377,8 @@ def simulateGrain(self, elif isinstance(output, str): fid = open(output, 'w') else: - raise RuntimeError, "output must be a file object or string" - sg.findMatches(filename=output) + raise RuntimeError("output must be a file object or string") + sg.findMatches(filename=fid) return sg def _run_grainspotter(self): @@ -395,7 +404,7 @@ def _run_fiber_search(self): nCPUs=iopts.nCPUs, quitAfter=iopts.quitAfter, outputGrainList=True) - iopts._fitRMats = retval[0] # HUH?! + iopts._fitRMats = retval[0] # WTF!!! self.rMats = retval[0] self.grainList = retval[1] return @@ -410,6 +419,7 @@ def run_indexer(self): self._run_grainspotter() return + # # ==================== Spots # @@ -467,7 +477,10 @@ def spots_for_indexing(self): @property def raw_spots(self): - """(get-only) spots from image before culling and association with rings""" + """ + (get-only) spots from image before culling + and association with rings + """ if not hasattr(self, '_spots'): self._spots = [] return self._spots @@ -539,7 +552,7 @@ def newDetector(self, gp, dp): *dp* - initial distortion parameters """ - self._detInfo = DetectorInfo(gParms=gp, dParms=dp) + self._detInfo = DetectorInfo(gParms=gp, dParms=dp) return @@ -581,13 +594,14 @@ def loadDetector(self, fname): det_class_str = lines[i] f.seek(0) if det_class_str is None: - raise RuntimeError, "detector class label not recongized in file!" + raise RuntimeError("detector class label not recongized in file!") else: plist_rflags = numpy.loadtxt(f) plist = plist_rflags[:, 0] rflag = numpy.array(plist_rflags[:, 1], dtype=bool) - exec_str = "DC = detector." + det_class_str.split('.')[-1].split("'")[0] + exec_str = "DC = detector." + \ + det_class_str.split('.')[-1].split("'")[0] exec(exec_str) gp = plist[:6].tolist() @@ -595,19 +609,17 @@ def loadDetector(self, fname): dp = None else: dp = plist[6:].tolist() - self._detInfo = DetectorInfo(gParms=gp, dParms=dp) + self._detInfo = DetectorInfo(gParms=gp, dParms=dp) self.detector.setupRefinement(rflag) self._detInfo.refineFlags = rflag f.close() return - # # ==================== Calibration Input # # property: calInput - @property def calInput(self): """(get only) Calibration input instance""" @@ -616,7 +628,6 @@ def calInput(self): # ==================== Hydra # # property: hydra - @property def hydra(self): """(read only) hydra image class""" @@ -668,8 +679,7 @@ def _set_matList(self, v): return - matList = property(_get_matList, _set_matList, None, - "List of materials") + matList = property(_get_matList, _set_matList, None, "List of materials") @property def matNames(self): @@ -686,7 +696,7 @@ def newMaterial(self): self._active_mat = Material() # find name not already in list - n = self._active_mat.name + n = self._active_mat.name self._active_mat.name = newName(n, self.matNames) # self._matList.append(self.activeMaterial) @@ -774,9 +784,9 @@ def newReader(self): Changes name if necessary. """ - self.__active_rdr = ReaderInput() + self.__active_rdr = ReaderInput() # find name not already in list - n = self.__active_rdr.name + n = self.__active_rdr.name nl = [r.name for r in self.__savedReaders] self.__active_rdr.name = newName(n, nl) # @@ -787,7 +797,7 @@ def newReader(self): def getSavedReader(self, which): """Get a specified reader""" if isinstance(which, int): - return self.__savedReaders[v] + return self.__savedReaders[which] else: # which is a string for r in self.__savedReaders: @@ -812,6 +822,7 @@ def savedReaders(self): def readerNames(self): """Return list of saved readers""" return [r.name for r in self.__savedReaders] + # # ==================== Image Info # @@ -826,24 +837,15 @@ def numFramesTotal(self): return self.__numFrame @property - def activeImage(self): # to be removed (use active_img instead) + def activeImage(self): # to be removed (use active_img instead) """Active image""" return self.active_img - # - # ==================== Calibration - # - # property: calInput - @property - def calInput(self): - """(read only) Calibration input data""" - return self._calInput # # ========== Public Methods # def readerListAddCurrent(self): """Add current list to list of saved readers""" - return def readImage(self, frameNum=1): @@ -858,7 +860,7 @@ def readImage(self, frameNum=1): # Now read the current frame # aggMode = self.activeReader.aggModeOp - nrFrame = self.activeReader.getNumberOfFrames() # number of reader frames + nrFrame = self.activeReader.getNumberOfFrames() if aggMode: rdFrames = nrFrame self.__numFrame = 1 @@ -878,22 +880,22 @@ def readImage(self, frameNum=1): % (frameNum, nrFrame) raise ValueError(msg) - #if (frameNum == self.__curFrame): return + # if (frameNum == self.__curFrame): return # NOTE: instantiate new reader even when requested frame is current # frame because reader properties may have changed if haveReader and (frameNum > self.__curFrame): nskip = frameNum - self.__curFrame - 1 - self.__active_img = self.__active_reader.read(nframes= rdFrames, - nskip = nskip, - sumImg = aggMode) + self.__active_img = self.__active_reader.read(nframes=rdFrames, + nskip=nskip, + sumImg=aggMode) else: # instantiate new reader self.__active_reader = self.activeReader.makeReader() nskip = frameNum - 1 - self.__active_img = self.__active_reader.read(nframes= rdFrames, - nskip = nskip, - sumImg = aggMode) + self.__active_img = self.__active_reader.read(nframes=rdFrames, + nskip=nskip, + sumImg=aggMode) pass @@ -904,7 +906,8 @@ def readImage(self, frameNum=1): return def calibrate(self, log=None): - """Calibrate the detector + """ + Calibrate the detector Currently, uses polar rebin only. """ @@ -924,22 +927,24 @@ def calibrate(self, log=None): log.write('done') return + # # ==================== Polar Rebinning (Caking) # def polarRebin(self, opts): - """Rebin the image according to certain parameters + """ + Rebin the image according to certain parameters opts -- an instance of PolarRebinOpts """ - - img_info = det.polarRebin(self.activeImage, opts.kwArgs) - + img_info = self.detector.polarRebin(self.activeImage, opts.kwArgs) return img_info # pass # end class # # -----------------------------------------------END CLASS: Experiment + + # ---------------------------------------------------CLASS: geReaderInput # class ReaderInput(object): @@ -993,6 +998,7 @@ def __init__(self, name='reader', desc='no description'): self.imageNameD = dict() self.imageFmt = None self.imageOpts = {} + self.pixelPitch = 0.2 # Dark file self.darkMode = ReaderInput.DARK_MODE_NONE self.darkDir = '' @@ -1097,7 +1103,7 @@ def makeReader(self): self._check() imagePath = os.path.join(self.imageDir, self.imageNames[0]) - r = self.RC(imagePath, fmt=self.imageFmt, **self.imageOpts) + r = self.RC(imagePath, fmt=self.imageFmt, pixelPitch=self.pixelPitch, **self.imageOpts) return r # diff --git a/hexrd/xrd/image_io.py b/hexrd/xrd/image_io.py index 1debe25b..f68b255d 100644 --- a/hexrd/xrd/image_io.py +++ b/hexrd/xrd/image_io.py @@ -95,10 +95,14 @@ def omega(self): class Framer2DRC(object): """Base class for readers. """ - def __init__(self, ncols, nrows, - dtypeDefault='int16', dtypeRead='uint16', dtypeFloat='float64'): + def __init__(self, + ncols, nrows, pixelPitch=0.2, + dtypeDefault='int16', + dtypeRead='uint16', + dtypeFloat='float64'): self._nrows = nrows self._ncols = ncols + self._pixelPitch = pixelPitch self.__frame_dtype_dflt = dtypeDefault self.__frame_dtype_read = dtypeRead self.__frame_dtype_float = dtypeFloat @@ -115,6 +119,10 @@ def get_ncols(self): return self._ncols ncols = property(get_ncols, None, None) + def get_pixelPitch(self): + return self._pixelPitch + pixelPitch = property(get_pixelPitch, None, None) + def get_nbytesFrame(self): return self.__nbytes_frame nbytesFrame = property(get_nbytesFrame, None, None) @@ -227,6 +235,10 @@ def __init__(self, file_info, *args, **kwargs): self._format = kwargs.pop('fmt', None) self._nrows = detector.NROWS self._ncols = detector.NCOLS + self._pixelPitch = detector.PIXEL + pp_key = 'pixelPitch' + if kwargs.has_key(pp_key): + self._pixelPitch = kwargs[pp_key] try: self._omis = _OmegaImageSeries(file_info, fmt=self._format, **kwargs) Framer2DRC.__init__(self, self._omis.nrows, self._omis.ncols) From 5e553e222f18b6b7ded080d48fb05d2123418b03 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 19 Apr 2017 17:25:13 -0700 Subject: [PATCH 098/253] fixed omega windows in pull_spots --- hexrd/instrument.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 71f70b79..8854d63d 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -438,7 +438,6 @@ def extract_polar_maps(self, plane_data, imgser_dict, except(KeyError): msg = "imageseries for '%s' has no omega info" % det_key raise RuntimeError(msg) - # ome_edges = np.r_[omegas[:, 0], omegas[-1, 1]] nrows_ome = len(omegas) ncols_eta = len(full_etas) this_map = np.nan*np.ones((nrows_ome, ncols_eta)) @@ -642,9 +641,10 @@ def pull_spots(self, plane_data, grain_params, ndiv_ome, ome_del = make_tolerance_grid( delta_ome, ome_tol, 1, adjust_window=True, ) - + ome_del_c = np.average(np.vstack([ome_del[:-1], ome_del[1:]]), axis=0) + # generate structuring element for connected component labeling - if len(ome_del) == 1: + if ndiv_ome == 1: label_struct = ndimage.generate_binary_structure(2, lrank) else: label_struct = ndimage.generate_binary_structure(3, lrank) @@ -733,7 +733,7 @@ def pull_spots(self, plane_data, grain_params, for i_pt, angs in enumerate(ang_centers): # the evaluation omegas; # expand about the central value using tol vector - ome_eval = np.degrees(angs[2]) + ome_del + ome_eval = np.degrees(angs[2]) + ome_del_c # ...vectorize the omega_to_frame function to avoid loop? frame_indices = [ @@ -792,7 +792,7 @@ def pull_spots(self, plane_data, grain_params, # the evaluation omegas; # expand about the central value using tol vector - ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del + ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del_c # ???: vectorize the omega_to_frame function to avoid loop? frame_indices = [ @@ -1627,7 +1627,7 @@ class GrainDataWriter(object): """ def __init__(self, filename): sp3_str = '{:12}\t{:12}\t{:12}' - dp3_str = '{:19}\t{:19}\t{:19}' + dp3_str = '{:20}\t{:20}\t{:20}' self._header = \ sp3_str.format( '# grain ID', 'completeness', 'chi^2') + '\t' + \ From 04e34b11f83a65632e238b4309d8617294376552 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 21 Apr 2017 18:10:15 -0700 Subject: [PATCH 099/253] more changes to frame-cache I/O --- hexrd/imageseries/load/framecache.py | 72 ++++++++++++++++++++-------- hexrd/imageseries/save.py | 17 ++++--- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/hexrd/imageseries/load/framecache.py b/hexrd/imageseries/load/framecache.py index 35de0e06..a5227d76 100644 --- a/hexrd/imageseries/load/framecache.py +++ b/hexrd/imageseries/load/framecache.py @@ -15,15 +15,18 @@ class FrameCacheImageSeriesAdapter(ImageSeriesAdapter): format = 'frame-cache' - def __init__(self, fname, **kwargs): + def __init__(self, fname, format='npz', **kwargs): """Constructor for frame cache image series *fname* - filename of the yml file *kwargs* - keyword arguments (none required) """ self._fname = fname - self._load_yml() - self._load_cache() + if format.lower() in ('yml', 'yaml', 'test'): + self._load_yml() + self._load_cache(from_yml=True) + else: + self._load_cache() def _load_yml(self): with open(self._fname, "r") as f: @@ -35,23 +38,53 @@ def _load_yml(self): self._dtype = np.dtype(datad['dtype']) self._meta = yamlmeta(d['meta'], path=self._cache) - def _load_cache(self): + def _load_cache(self, from_yml=False): """load into list of csr sparse matrices""" - bpath = os.path.dirname(self._fname) - if os.path.isabs(self._cache): - cachepath = self._cache - else: - cachepath = os.path.join(bpath, self._cache) - arrs = np.load(cachepath) - self._framelist = [] - for i in range(self._nframes): - row = arrs["%d_row" % i] - col = arrs["%d_col" % i] - data = arrs["%d_data" % i] - frame = csr_matrix((data, (row, col)), - shape=self._shape, dtype=self._dtype) - self._framelist.append(frame) + if from_yml: + bpath = os.path.dirname(self._fname) + if os.path.isabs(self._cache): + cachepath = self._cache + else: + cachepath = os.path.join(bpath, self._cache) + arrs = np.load(cachepath) + + for i in range(self._nframes): + row = arrs["%d_row" % i] + col = arrs["%d_col" % i] + data = arrs["%d_data" % i] + frame = csr_matrix((data, (row, col)), + shape=self._shape, dtype=self._dtype) + self._framelist.append(frame) + else: + arrs = np.load(self._fname) + # HACK: while the loaded npz file has a getitem method + # that mimicks a dict, it doesn't have a "pop" method. + # must make an empty dict to pop after assignment of + # class attributes so we can get to the metadata + keysd = dict.fromkeys(arrs.keys()) + self._nframes = int(arrs['nframes']) + self._shape = tuple(arrs['shape']) + self._dtype = np.dtype(str(arrs['dtype'])) + keysd.pop('nframes') + keysd.pop('shape') + keysd.pop('dtype') + for i in range(self._nframes): + row = arrs["%d_row" % i] + col = arrs["%d_col" % i] + data = arrs["%d_data" % i] + keysd.pop("%d_row" % i) + keysd.pop("%d_col" % i) + keysd.pop("%d_data" % i) + frame = csr_matrix((data, (row, col)), + shape=self._shape, + dtype=self._dtype) + self._framelist.append(frame) + # all rmaining keys should be metadata + for key in keysd: + keysd[key] = arrs[key] + self._meta = keysd + @property def metadata(self): @@ -64,7 +97,8 @@ def load_metadata(self, indict): Currently returns none """ - #### Currently not used: saved temporarily for np.array trigger + # TODO: Remove this. Currently not used; + # saved temporarily for np.array trigger metad = {} for k, v in indict.items(): if v == '++np.array': diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 76e0554b..ed0de6fa 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -153,10 +153,10 @@ def __init__(self, ims, fname, **kwargs): self._cache = os.path.join(cdir, cf) self._cachename = cf - def _process_meta(self): + def _process_meta(self, save_omegas=False): d = {} for k, v in self._meta.items(): - if isinstance(v, np.ndarray): + if isinstance(v, np.ndarray) and save_omegas: # Save as a numpy array file # if file does not exist (careful about directory) # create new file @@ -177,14 +177,16 @@ def _process_meta(self): def _write_yml(self): datad = {'file': self._cachename, 'dtype': str(self._ims.dtype), 'nframes': len(self._ims), 'shape': list(self._ims.shape)} - info = {'data': datad, 'meta': self._process_meta()} + info = {'data': datad, 'meta': self._process_meta(save_omegas=True)} with open(self._fname, "w") as f: yaml.dump(info, f) def _write_frames(self): """also save shape array as originally done (before yaml)""" arrd = dict() - for i, frame in enumerate(self._ims): + for i in range(len(self._ims)): + # RFE: make it so we can use emumerate on self._ims??? + frame = self._ims[i] mask = frame > self._thresh # FIXME: formalize this a little better??? if np.sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.05: @@ -195,14 +197,15 @@ def _write_frames(self): arrd['%d_col' % i] = col arrd['shape'] = self._ims.shape arrd['nframes'] = len(self._ims) - arrd['dtype'] = self._ims.dtype + arrd['dtype'] = str(self._ims.dtype) arrd.update(self._process_meta()) np.savez_compressed(self._cache, **arrd) - def write(self): + def write(self, output_yaml=False): """writes frame cache for imageseries presumes sparse forms are small enough to contain all frames """ self._write_frames() - self._write_yml() + if output_yaml: + self._write_yml() From 0f823e2aefb4bd1bd2888e293b87bca9b06c1e3f Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 28 Apr 2017 21:12:41 -0500 Subject: [PATCH 100/253] fixes to get multipanel paintGrid working (non-numba) --- conda.recipe/meta.yaml | 4 +- hexrd/instrument.py | 9 ++- hexrd/transforms/transforms_CFUNC.c | 8 +-- hexrd/xrd/indexer.py | 108 +++++++++++++++++----------- 4 files changed, 80 insertions(+), 49 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 76f29d69..57faf2f0 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -19,16 +19,15 @@ app: requirements: build: - # - nomkl # in case MKL is broken on Linux - h5py - numpy - python - setuptools run: - dill + - fabio - h5py - matplotlib - # - nomkl # in case MKL is broken on Linux - numba - numpy - progressbar >=2.3 @@ -37,6 +36,7 @@ requirements: - pyyaml - qtconsole - scikit-learn + - scikit-image - scipy - wxpython diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 8854d63d..6e5b1730 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1785,7 +1785,8 @@ class GenerateEtaOmeMaps(object): """ def __init__(self, image_series_dict, instrument, plane_data, - active_hkls=None, eta_step=0.25, threshold=None): + active_hkls=None, eta_step=0.25, threshold=None, + ome_period=(0, 360)): """ image_series must be OmegaImageSeries class instrument_params must be a dict (loaded from yaml spec) @@ -1833,10 +1834,12 @@ def __init__(self, image_series_dict, instrument, plane_data, # handle omegas omegas_array = image_series_dict[det_key].metadata['omega'] self._omegas = mapAngle( - np.radians(np.average(omegas_array, axis=1)) + np.radians(np.average(omegas_array, axis=1)), + np.radians(ome_period) ) self._omeEdges = mapAngle( - np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]) + np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]), + np.radians(ome_period) ) # handle etas diff --git a/hexrd/transforms/transforms_CFUNC.c b/hexrd/transforms/transforms_CFUNC.c index e6c75a18..8cb9f7d8 100644 --- a/hexrd/transforms/transforms_CFUNC.c +++ b/hexrd/transforms/transforms_CFUNC.c @@ -75,9 +75,9 @@ void anglesToDvec_cfunc(long int nvecs, double * angs, { /* * takes an angle spec (2*theta, eta, omega) for nvecs g-vectors and - * returns the unit g-vector components in the crystal frame + * returns the unit d-vector components in the crystal frame * - * For unit g-vector in the lab frame, spec rMat_c = Identity and + * For unit d-vector in the lab frame, spec rMat_c = Identity and * overwrite the omega values with zeros */ int i, j, k, l; @@ -94,7 +94,7 @@ void anglesToDvec_cfunc(long int nvecs, double * angs, gVec_e[1] = sin(angs[3*i]) * sin(angs[3*i+1]); gVec_e[2] = -cos(angs[3*i]); - /* take from beam frame to lab frame */ + /* take from BEAM frame to LAB frame */ for (j=0; j<3; j++) { gVec_l[j] = 0.0; for (k=0; k<3; k++) { @@ -105,7 +105,7 @@ void anglesToDvec_cfunc(long int nvecs, double * angs, /* need pointwise rMat_s according to omega */ makeOscillRotMat_cfunc(chi, angs[3*i+2], rMat_s); - /* Compute dot(rMat_c.T, rMat_s.T) and hit against gVec_l */ + /* compute dot(rMat_c.T, rMat_s.T) and hit against gVec_l */ for (j=0; j<3; j++) { for (k=0; k<3; k++) { rMat_ctst[3*j+k] = 0.0; diff --git a/hexrd/xrd/indexer.py b/hexrd/xrd/indexer.py index a7214666..693c7c25 100644 --- a/hexrd/xrd/indexer.py +++ b/hexrd/xrd/indexer.py @@ -1,12 +1,12 @@ #! /usr/bin/env python -# ============================================================ +# ============================================================================= # Copyright (c) 2012, Lawrence Livermore National Security, LLC. # Produced at the Lawrence Livermore National Laboratory. # Written by Joel Bernier and others. # LLNL-CODE-529294. # All rights reserved. # -# This file is part of HExrd. For details on dowloading the source, +# This file is part of HEXRD. For details on dowloading the source, # see the file COPYING. # # Please also see the file LICENSE. @@ -24,7 +24,7 @@ # License along with this program (see file LICENSE); if not, write to # the Free Software Foundation, Inc., 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA or visit . -# ============================================================ +# ============================================================================= import sys import os import copy @@ -52,6 +52,8 @@ from hexrd.xrd import transforms_CAPI as xfcapi from hexrd import USE_NUMBA +# FIXME: numba implementation of paintGridThis is broken +USE_NUMBA = 0 # OVERRIDE NUMBA if USE_NUMBA: import numba @@ -139,7 +141,7 @@ def __call__(self, spotsArray, **kwargs): gffData = num.loadtxt(gffFile) if gffData.ndim == 1: gffData = gffData.reshape(1, len(gffData)) - gffData_U = gffData[:,6:6+9] + gffData_U = gffData[:, 6:6+9] # process for output retval = convertUToRotMat(gffData_U, U0, symTag=symTag) @@ -153,11 +155,13 @@ def __call__(self, spotsArray, **kwargs): def __del__(self): self.cleanup() return + def cleanup(self): for fname in self.__tempFNameList: os.remove(fname) return + def convertUToRotMat(Urows, U0, symTag='Oh', display=False): """ Takes GrainSpotter gff ouput in rows @@ -176,7 +180,7 @@ def convertUToRotMat(Urows, U0, symTag='Oh', display=False): "input must have 9 columns; received %d" % (testDim) ) - qin = quatOfRotMat(Urows.reshape(numU, 3, 3)) + qin = quatOfRotMat(Urows.reshape(numU, 3, 3)) # what the hell is happening here?: qout = num.dot( quatProductMatrix(quatOfRotMat(fableSampCOB), mult='left'), @@ -199,6 +203,7 @@ def convertUToRotMat(Urows, U0, symTag='Oh', display=False): Uout = rotMatOfQuat(qout) return Uout + def convertRotMatToFableU(rMats, U0=num.eye(3), symTag='Oh', display=False): """ Makes GrainSpotter gff ouput @@ -210,9 +215,7 @@ def convertRotMatToFableU(rMats, U0=num.eye(3), symTag='Oh', display=False): Urows comes from grainspotter's gff output U0 comes from xrd.crystallography.latticeVectors.U0 """ - numU = num.shape(num.atleast_3d(rMats))[0] - - qin = quatOfRotMat(num.atleast_3d(rMats)) + qin = quatOfRotMat(num.atleast_3d(rMats)) # what the hell is this?: qout = num.dot( quatProductMatrix(quatOfRotMat(fableSampCOB.T), mult='left'), @@ -927,18 +930,18 @@ def paintgrid_init(params): paramMP['valid_ome_spans'] = _normalize_ranges(paramMP['omeMin'], paramMP['omeMax'], min(paramMP['omePeriod'])) + return - -################################################################################ - +############################################################################### +# # paintGridThis contains the bulk of the process to perform for paintGrid for a # given quaternion. This is also used as the basis for multiprocessing, as the # work is split in a per-quaternion basis among different processes. # The remainding arguments are marshalled into the module variable "paramMP". - -# There is a version of PaintGridThis using numba, and another version used when -# numba is not available. The numba version should be noticeably faster. +# +# There is a version of PaintGridThis using numba, and another version used +# when numba is not available. The numba version should be noticeably faster. def _check_dilated(eta, ome, dpix_eta, dpix_ome, etaOmeMap, threshold): """This is part of paintGridThis: @@ -948,15 +951,29 @@ def _check_dilated(eta, ome, dpix_eta, dpix_ome, etaOmeMap, threshold): Note this function is "numba friendly" and will be jitted when using numba. + TODO: currently behaves like "num.any" call for values above threshold. + There is some ambigutiy if there are NaNs in the dilation range, but it + hits a value above threshold first. Is that ok??? + + FIXME: works in non-numba implementation of paintGridThis only + """ i_max, j_max = etaOmeMap.shape - ome_start, ome_stop = max(ome - dpix_ome, 0), min(ome + dpix_ome + 1, i_max) - eta_start, eta_stop = max(eta - dpix_eta, 0), min(eta + dpix_eta + 1, j_max) + ome_start, ome_stop = ( + max(ome - dpix_ome, 0), + min(ome + dpix_ome + 1, i_max) + ) + eta_start, eta_stop = ( + max(eta - dpix_eta, 0), + min(eta + dpix_eta + 1, j_max) + ) for i in range(ome_start, ome_stop): for j in range(eta_start, eta_stop): - if etaOmeMap[i,j] > threshold: + if etaOmeMap[i, j] > threshold: return 1 + if num.isnan(etaOmeMap[i, j]): + return -1 return 0 @@ -1003,7 +1020,7 @@ def paintGridThis(quat): # Compute the oscillation angles of all the symHKLs at once oangs_pair = xfcapi.oscillAnglesOfHKLs(symHKLs, 0., rMat, bMat, wavelength) - #pdb.set_trace() + # pdb.set_trace() return _filter_and_count_hits(oangs_pair[0], oangs_pair[1], symHKLs_ix, etaEdges, valid_eta_spans, valid_ome_spans, omeEdges, omePeriod, @@ -1063,6 +1080,9 @@ def _angle_is_hit(ang, eta_offset, ome_offset, hkl, valid_eta_spans, Note the function returns both, if it was a hit and if it passed the the filtering, as we'll want to discard the filtered values when computing the hit percentage. + + CAVEAT: added map-based nan filtering to _check_dilated; this may not + be the best option. Perhaps filter here? """ tth, eta, ome = ang @@ -1095,9 +1115,10 @@ def _angle_is_hit(ang, eta_offset, ome_offset, hkl, valid_eta_spans, ome = omeIndices[ome_idx] isHit = _check_dilated(eta, ome, dpix_eta, dpix_ome, etaOmeMaps[hkl], threshold[hkl]) - - return isHit, 1 - + if isHit == -1: + return 0, 0 + else: + return isHit, 1 @numba.njit def _filter_and_count_hits(angs_0, angs_1, symHKLs_ix, etaEdges, @@ -1126,20 +1147,26 @@ def _filter_and_count_hits(angs_0, angs_1, symHKLs_ix, etaEdges, if i >= end_curr: curr_hkl_idx += 1 end_curr = symHKLs_ix[curr_hkl_idx+1] - hit, not_filtered = _angle_is_hit(angs_0[i], eta_offset, ome_offset, - curr_hkl_idx, valid_eta_spans, - valid_ome_spans, etaEdges, - omeEdges, etaOmeMaps, etaIndices, - omeIndices, dpix_eta, dpix_ome, - threshold) + + # first solution + hit, not_filtered = _angle_is_hit( + angs_0[i], eta_offset, ome_offset, + curr_hkl_idx, valid_eta_spans, + valid_ome_spans, etaEdges, + omeEdges, etaOmeMaps, etaIndices, + omeIndices, dpix_eta, dpix_ome, + threshold) hits += hit total += not_filtered - hit, not_filtered = _angle_is_hit(angs_1[i], eta_offset, ome_offset, - curr_hkl_idx, valid_eta_spans, - valid_ome_spans, etaEdges, - omeEdges, etaOmeMaps, etaIndices, - omeIndices, dpix_eta, dpix_ome, - threshold) + + # second solution + hit, not_filtered = _angle_is_hit( + angs_1[i], eta_offset, ome_offset, + curr_hkl_idx, valid_eta_spans, + valid_ome_spans, etaEdges, + omeEdges, etaOmeMaps, etaIndices, + omeIndices, dpix_eta, dpix_ome, + threshold) hits += hit total += not_filtered @@ -1201,13 +1228,12 @@ def paintGridThis(quat): valid_ome_spans, omePeriod) if len(hkl_idx > 0): - hits = _count_hits(eta_idx, ome_idx, hkl_idx, etaOmeMaps, + hits, predicted = _count_hits(eta_idx, ome_idx, hkl_idx, etaOmeMaps, etaIndices, omeIndices, dpix_eta, dpix_ome, threshold) - retval = float(hits) / float(len(hkl_idx)) - else: - retval = 0 - + retval = float(hits) / float(predicted) + if retval > 1: + import pdb; pdb.set_trace() return retval def _normalize_angs_hkls(angs_0, angs_1, omePeriod, symHKLs_ix): @@ -1307,10 +1333,12 @@ def _count_hits(eta_idx, ome_idx, hkl_idx, etaOmeMaps, isHit = _check_dilated(eta, ome, dpix_eta, dpix_ome, etaOmeMaps[iHKL], threshold[iHKL]) - if isHit: + if isHit > 0: hits += 1 + if isHit == -1: + predicted -= 1 - return hits + return hits, predicted def writeGVE(spotsArray, fileroot, **kwargs): From ce3f14680d1c54951e2d403126d5233c708c95a2 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 18 May 2017 16:08:31 -0700 Subject: [PATCH 101/253] fix to image looping in line position extractor method --- hexrd/instrument.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 6e5b1730..c487730e 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -484,8 +484,15 @@ def extract_line_positions(self, plane_data, imgser_dict, panel = self.detectors[detector_id] instr_cfg = panel.config_dict(self.chi, self.tvec) native_area = panel.pixel_area # pixel ref area - n_images = len(imgser_dict[detector_id]) - + images = imgser_dict[detector_id] + if images.ndim == 2: + n_images = 1 + images = np.dstack(images) + elif images.ndim == 3: + n_images = len(images) + else: + raise RuntimeError("images must be 2- or 3-d") + # make rings pow_angs, pow_xys = panel.make_powder_rings( plane_data, merge_hkls=True, delta_eta=eta_tol) @@ -553,7 +560,7 @@ def extract_line_positions(self, plane_data, imgser_dict, # interpolate if not collapse_tth: ims_data = [] - for j_p, image in enumerate(imgser_dict[detector_id]): + for j_p, image in enumerate(images): # catch interpolation type if do_interpolation: tmp = panel.interpolate_bilinear( From 8173b62afcb9c5218b80fe1439e29001853dcc50 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 18 May 2017 16:20:37 -0700 Subject: [PATCH 102/253] replaced dstack with tile --- hexrd/instrument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index c487730e..51e46b3d 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -487,7 +487,7 @@ def extract_line_positions(self, plane_data, imgser_dict, images = imgser_dict[detector_id] if images.ndim == 2: n_images = 1 - images = np.dstack(images) + images = np.tile(images, (1, 1, 1)) elif images.ndim == 3: n_images = len(images) else: From 00ffb215b10bb45182781de6355b3801f6e25d5b Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 8 Jun 2017 14:29:17 -0700 Subject: [PATCH 103/253] framecache load munging --- hexrd/imageseries/load/framecache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/load/framecache.py b/hexrd/imageseries/load/framecache.py index a5227d76..bc525646 100644 --- a/hexrd/imageseries/load/framecache.py +++ b/hexrd/imageseries/load/framecache.py @@ -15,14 +15,14 @@ class FrameCacheImageSeriesAdapter(ImageSeriesAdapter): format = 'frame-cache' - def __init__(self, fname, format='npz', **kwargs): + def __init__(self, fname, style='npz', **kwargs): """Constructor for frame cache image series *fname* - filename of the yml file *kwargs* - keyword arguments (none required) """ self._fname = fname - if format.lower() in ('yml', 'yaml', 'test'): + if style.lower() in ('yml', 'yaml', 'test'): self._load_yml() self._load_cache(from_yml=True) else: From b7680788dda5d6d48e343992d724c3e08ff9e033 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 9 Jun 2017 15:46:51 -0700 Subject: [PATCH 104/253] added numba as build req --- conda.recipe/meta.yaml | 7 ++++--- hexrd/xrd/fitting.py | 23 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 57faf2f0..43d6a0f9 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -3,12 +3,12 @@ package: version: master source: - #git_url: https://github.com/joelvbernier/hexrd.git - #git_tag: instrument # edit to point to specific branch or tag + # git_url: https://github.com/joelvbernier/hexrd.git + # git_tag: instrument # edit to point to specific branch or tag path: ../ build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} - # detect_binary_files_with_prefix: true + # detect_binary_files_with_prefix: true osx_is_app: yes entry_points: - hexrd = hexrd.cli:main @@ -20,6 +20,7 @@ app: requirements: build: - h5py + - numba - numpy - python - setuptools diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index e297bbab..a2daba42 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -11,9 +11,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the Free +# Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -269,7 +269,7 @@ def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, refineFlag = np.array(np.hstack([pFlag, dFlag]), dtype=bool) print refineFlag - + # pFull[refineFlag] = pFit/scl[refineFlag] pFull[refineFlag] = pFit @@ -278,7 +278,7 @@ def objFuncSX(pFit, pFull, pFlag, dFunc, dFlag, xys = dFunc(xyo_det[:, :2], dParams) else: xys = xyo_det[:, :2] - + # detector quantities wavelength = pFull[0] @@ -411,10 +411,10 @@ def objFuncFitGrain(gFit, gFull, gFlag, omePeriod, simOnly=False, return_value_flag=return_value_flag) """ - + bVec = instrument.beam_vector eVec = instrument.eta_vector - + # fill out parameters gFull[gFlag] = gFit @@ -431,18 +431,18 @@ def objFuncFitGrain(gFit, gFull, gFlag, for det_key, panel in instrument.detectors.iteritems(): rMat_d, tVec_d, chi, tVec_s = extract_detector_transformation( instrument.detector_parameters[det_key]) - + results = reflections_dict[det_key] if len(results) == 0: continue - + """ extract data from results list fields: refl_id, gvec_id, hkl, sum_int, max_int, pred_ang, meas_ang, meas_xy """ - # WARNING: hkls and derived vectors below must be columnwise; + # WARNING: hkls and derived vectors below must be columnwise; # strictly necessary??? change affected APIs instead? # hkls = np.atleast_2d( @@ -458,8 +458,7 @@ def objFuncFitGrain(gFit, gFull, gFlag, xy_unwarped = panel.distortion[0]( meas_xyo[:, :2], panel.distortion[1]) meas_xyo = np.vstack([xy_unwarped.T, meas_omes]).T - - + # g-vectors: # 1. calculate full g-vector components in CRYSTAL frame from B # 2. rotate into SAMPLE frame and apply stretch From 73e22f03f646477a3ec69766c63d51f3c1018e64 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 15 Jun 2017 12:07:35 -0500 Subject: [PATCH 105/253] Added storage of raw patch data in pull_spots This is desired for marking saturated peaks; may also add these data to the hdf5 output. Max frame calculation still uses segmentation of interpolated data. --- hexrd/instrument.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 51e46b3d..d1ccafe3 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -822,10 +822,15 @@ def pull_spots(self, plane_data, grain_params, # quick check for intensity contains_signal = False + patch_data_raw = [] for i_frame in frame_indices: + tmp = ome_imgser[i_frame][ijs[0], ijs[1]] contains_signal = contains_signal or np.any( - ome_imgser[i_frame][ijs[0], ijs[1]] > threshold + tmp > threshold ) + patch_data_raw.append(tmp) + pass + patch_data_raw = np.stack(patch_data_raw, axis=0) compl.append(contains_signal) if contains_signal: @@ -891,16 +896,13 @@ def pull_spots(self, plane_data, grain_params, ] ) max_int = np.max( - patch_data[ + patch_data_raw[ labels == slabels[closest_peak_idx] ] ) # ???: Should this only use labeled pixels? - # max_int = np.max( - # patch_data[ - # labels == slabels[closest_peak_idx] - # ] - # ) + # Those are segmented from interpolated data, + # not raw; likely ok in most cases. # need xy coords gvec_c = anglesToGVec( From 4e762049d6650f331ee863549f53e570d494c0ce Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 14 Jul 2017 12:43:20 -0700 Subject: [PATCH 106/253] versioning fix in conda recipe --- conda.recipe/meta.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 43d6a0f9..bd45893b 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,11 +1,7 @@ package: name: hexrd - version: master - -source: - # git_url: https://github.com/joelvbernier/hexrd.git - # git_tag: instrument # edit to point to specific branch or tag - path: ../ + version: {{ environ.get('GIT_DESCRIBE_TAG', '') }} + build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} # detect_binary_files_with_prefix: true @@ -13,6 +9,11 @@ build: entry_points: - hexrd = hexrd.cli:main +source: + # git_url: https://github.com/joelvbernier/hexrd.git + # git_tag: instrument # edit to point to specific branch or tag + path: ../ + app: entry: hexrd gui summary: High-energy x-ray diffraction analysis From 15025afad27089deb73c295a80b12059f545faf1 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 20 Jul 2017 17:57:15 -0700 Subject: [PATCH 107/253] conda-build 3.0 compatibility --- conda.recipe/meta.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index bd45893b..45f9a45e 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,10 +1,14 @@ package: name: hexrd version: {{ environ.get('GIT_DESCRIBE_TAG', '') }} - + build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} - # detect_binary_files_with_prefix: true + + # Note that this will override the default build string with the Python + # and NumPy versions + string: {{ environ.get('GIT_BUILD_STR', '') }} + osx_is_app: yes entry_points: - hexrd = hexrd.cli:main @@ -12,7 +16,7 @@ build: source: # git_url: https://github.com/joelvbernier/hexrd.git # git_tag: instrument # edit to point to specific branch or tag - path: ../ + git_url: ../ app: entry: hexrd gui From 907ff67543b5724f228074830a0ef6b7f25bfd35 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 3 Aug 2017 12:26:32 -0700 Subject: [PATCH 108/253] another fix for conda-build 3; minor instrument tweak --- conda.recipe/meta.yaml | 10 +++++----- hexrd/instrument.py | 38 ++++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index bd45893b..8c20af35 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,9 +1,9 @@ package: name: hexrd - version: {{ environ.get('GIT_DESCRIBE_TAG', '') }} - + # version: {{ environ.get('GIT_DESCRIBE_TAG', '') }} + version: {{ GIT_DESCRIBE_TAG }} build: - number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + number: {{ GIT_DESCRIBE_NUMBER|int }} # detect_binary_files_with_prefix: true osx_is_app: yes entry_points: @@ -22,7 +22,7 @@ requirements: build: - h5py - numba - - numpy + - numpy ==1.12 - python - setuptools run: @@ -31,7 +31,7 @@ requirements: - h5py - matplotlib - numba - - numpy + - numpy ==1.12 - progressbar >=2.3 - python - python.app # [osx] diff --git a/hexrd/instrument.py b/hexrd/instrument.py index d1ccafe3..8e66c92f 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -648,7 +648,8 @@ def pull_spots(self, plane_data, grain_params, ndiv_ome, ome_del = make_tolerance_grid( delta_ome, ome_tol, 1, adjust_window=True, ) - ome_del_c = np.average(np.vstack([ome_del[:-1], ome_del[1:]]), axis=0) + # ??? + # ome_del_c = np.average(np.vstack([ome_del[:-1], ome_del[1:]]), axis=0) # generate structuring element for connected component labeling if ndiv_ome == 1: @@ -740,7 +741,7 @@ def pull_spots(self, plane_data, grain_params, for i_pt, angs in enumerate(ang_centers): # the evaluation omegas; # expand about the central value using tol vector - ome_eval = np.degrees(angs[2]) + ome_del_c + ome_eval = np.degrees(angs[2]) + ome_del # ...vectorize the omega_to_frame function to avoid loop? frame_indices = [ @@ -799,7 +800,7 @@ def pull_spots(self, plane_data, grain_params, # the evaluation omegas; # expand about the central value using tol vector - ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del_c + ome_eval = np.degrees(ang_centers[i_pt, 2]) + ome_del # ???: vectorize the omega_to_frame function to avoid loop? frame_indices = [ @@ -1386,6 +1387,22 @@ def make_powder_rings( tvec_c=ct.zeros_3, output_ranges=False, output_etas=False): """ """ + # in case you want to give it tth angles directly + if hasattr(pd, '__len__'): + tth = np.array(pd).flatten() + else: + if merge_hkls: + tth_idx, tth_ranges = pd.getMergedRanges() + if output_ranges: + tth = np.r_[tth_ranges].flatten() + else: + tth = np.array([0.5*sum(i) for i in tth_ranges]) + else: + if output_ranges: + tth = pd.getTThRanges().flatten() + else: + tth = pd.getTTh() + if delta_tth is not None: pd.tThWidth = np.radians(delta_tth) else: @@ -1407,21 +1424,6 @@ def make_powder_rings( eta_period[0], eta_period ) - # in case you want to give it tth angles directly - if hasattr(pd, '__len__'): - tth = np.array(pd).flatten() - else: - if merge_hkls: - tth_idx, tth_ranges = pd.getMergedRanges() - if output_ranges: - tth = np.r_[tth_ranges].flatten() - else: - tth = np.array([0.5*sum(i) for i in tth_ranges]) - else: - if output_ranges: - tth = pd.getTThRanges().flatten() - else: - tth = pd.getTTh() angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] # need xy coords and pixel sizes From efe490364b34d2b5ba947c908d3c8c69eeb89dc5 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 3 Aug 2017 12:34:37 -0700 Subject: [PATCH 109/253] another fix for conda-build 3 --- conda.recipe/meta.yaml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 8c20af35..48e15b58 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,17 +1,20 @@ package: name: hexrd - # version: {{ environ.get('GIT_DESCRIBE_TAG', '') }} - version: {{ GIT_DESCRIBE_TAG }} + version: {{ environ.get('GIT_DESCRIBE_TAG', '') }} + build: - number: {{ GIT_DESCRIBE_NUMBER|int }} - # detect_binary_files_with_prefix: true + number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} + + # Note that this will override the default build string with the Python + # and NumPy versions + string: {{ environ.get('GIT_BUILD_STR', '') }} + + detect_binary_files_with_prefix: true osx_is_app: yes entry_points: - hexrd = hexrd.cli:main source: - # git_url: https://github.com/joelvbernier/hexrd.git - # git_tag: instrument # edit to point to specific branch or tag path: ../ app: @@ -22,7 +25,7 @@ requirements: build: - h5py - numba - - numpy ==1.12 + - numpy - python - setuptools run: @@ -31,7 +34,7 @@ requirements: - h5py - matplotlib - numba - - numpy ==1.12 + - numpy - progressbar >=2.3 - python - python.app # [osx] From 942f92ec87eaa308097521291f5334eac128c980 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 3 Aug 2017 12:38:48 -0700 Subject: [PATCH 110/253] another fix for conda-build, reverting to v2 for now --- conda.recipe/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 48e15b58..44108437 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -7,7 +7,7 @@ build: # Note that this will override the default build string with the Python # and NumPy versions - string: {{ environ.get('GIT_BUILD_STR', '') }} + #string: {{ environ.get('GIT_BUILD_STR', '') }} detect_binary_files_with_prefix: true osx_is_app: yes From 430c333c4689f209cc319bcd4416559e1feebbbe Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 14 Sep 2017 10:53:04 -0400 Subject: [PATCH 111/253] added 'nearest' interpolation mode --- hexrd/instrument.py | 78 +++++++++++++++++++++++++++++++++----------- hexrd/xrd/indexer.py | 4 +-- 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 8e66c92f..16b65fe1 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -492,7 +492,7 @@ def extract_line_positions(self, plane_data, imgser_dict, n_images = len(images) else: raise RuntimeError("images must be 2- or 3-d") - + # make rings pow_angs, pow_xys = panel.make_powder_rings( plane_data, merge_hkls=True, delta_eta=eta_tol) @@ -609,7 +609,8 @@ def pull_spots(self, plane_data, grain_params, eta_ranges=None, ome_period=(-np.pi, np.pi), dirname='results', filename=None, output_format='text', save_spot_list=False, - quiet=True, lrank=1, check_only=False): + quiet=True, lrank=1, check_only=False, + interp='nearest'): if eta_ranges is None: eta_ranges = [(-np.pi, np.pi), ] @@ -650,7 +651,7 @@ def pull_spots(self, plane_data, grain_params, ) # ??? # ome_del_c = np.average(np.vstack([ome_del[:-1], ome_del[1:]]), axis=0) - + # generate structuring element for connected component labeling if ndiv_ome == 1: label_struct = ndimage.generate_binary_structure(2, lrank) @@ -844,11 +845,19 @@ def pull_spots(self, plane_data, grain_params, ome_imgser.omega[frame_indices][-1, 1]] ) for i, i_frame in enumerate(frame_indices): - patch_data[i] = \ - panel.interpolate_bilinear( - xy_eval, - ome_imgser[i_frame], - ).reshape(prows, pcols)*nrm_fac + if interp.lower() is 'nearest': + patch_data[i] = \ + panel.interpolate_nearest( + xy_eval, + ome_imgser[i_frame], + ).reshape(prows, pcols)*nrm_fac + elif interp.lower() is 'bilinear': + patch_data[i] = \ + panel.interpolate_bilinear( + xy_eval, + ome_imgser[i_frame], + ).reshape(prows, pcols)*nrm_fac + pass pass # now have interpolated patch data... @@ -1338,8 +1347,39 @@ def cart_to_angles(self, xy_data, tth_eta = np.vstack([angs[0], angs[1]]).T return tth_eta, g_vec + def interpolate_nearest(self, xy, img, pad_with_nans=True): + """ + TODO: revisit normalization in here? + + """ + is_2d = img.ndim == 2 + right_shape = img.shape[0] == self.rows and img.shape[1] == self.cols + assert is_2d and right_shape,\ + "input image must be 2-d with shape (%d, %d)"\ + % (self.rows, self.cols) + + # initialize output with nans + if pad_with_nans: + int_xy = np.nan*np.ones(len(xy)) + else: + int_xy = np.zeros(len(xy)) + + # clip away points too close to or off the edges of the detector + xy_clip, on_panel = self.clip_to_panel(xy, buffer_edges=True) + + # get pixel indices of clipped points + i_src = cellIndices(self.row_pixel_vec, xy_clip[:, 1]) + j_src = cellIndices(self.col_pixel_vec, xy_clip[:, 0]) + + # next interpolate across cols + int_vals = img[i_src, j_src] + int_xy[on_panel] = int_vals + return int_xy + + def interpolate_bilinear(self, xy, img, pad_with_nans=True): """ + TODO: revisit normalization in here? """ is_2d = img.ndim == 2 right_shape = img.shape[0] == self.rows and img.shape[1] == self.cols @@ -1556,7 +1596,7 @@ def simulate_rotation_series(self, plane_data, grain_param_list, ) # filter by eta and omega ranges - # get eta range from detector? + # ??? get eta range from detector? allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( full_hkls, angList, [(-np.pi, np.pi), ], ome_ranges ) @@ -1728,7 +1768,7 @@ def dump_patch(self, panel_id, pangs, mangs, mxy, gzip=9): """ to be called inside loop over patches - + default GZIP level for data arrays is 9 """ panel_grp = self.data_grp[panel_id] @@ -1750,17 +1790,17 @@ def dump_patch(self, panel_id, centers_of_edge_vec(eta_edges), centers_of_edge_vec(tth_edges), indexing='ij') - spot_grp.create_dataset('tth_crd', data=tth_crd, + spot_grp.create_dataset('tth_crd', data=tth_crd, compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('eta_crd', data=eta_crd, + spot_grp.create_dataset('eta_crd', data=eta_crd, compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('ome_crd', data=ome_crd, + spot_grp.create_dataset('ome_crd', data=ome_crd, compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('xy_centers', data=xy_centers, + spot_grp.create_dataset('xy_centers', data=xy_centers, compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('ij_centers', data=ijs, + spot_grp.create_dataset('ij_centers', data=ijs, compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('intensities', data=spot_data, + spot_grp.create_dataset('intensities', data=spot_data, compression="gzip", compression_opts=gzip) return @@ -1796,7 +1836,7 @@ class GenerateEtaOmeMaps(object): """ def __init__(self, image_series_dict, instrument, plane_data, - active_hkls=None, eta_step=0.25, threshold=None, + active_hkls=None, eta_step=0.25, threshold=None, ome_period=(0, 360)): """ image_series must be OmegaImageSeries class @@ -1845,11 +1885,11 @@ def __init__(self, image_series_dict, instrument, plane_data, # handle omegas omegas_array = image_series_dict[det_key].metadata['omega'] self._omegas = mapAngle( - np.radians(np.average(omegas_array, axis=1)), + np.radians(np.average(omegas_array, axis=1)), np.radians(ome_period) ) self._omeEdges = mapAngle( - np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]), + np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]), np.radians(ome_period) ) diff --git a/hexrd/xrd/indexer.py b/hexrd/xrd/indexer.py index 693c7c25..dcd5ace9 100644 --- a/hexrd/xrd/indexer.py +++ b/hexrd/xrd/indexer.py @@ -1080,7 +1080,7 @@ def _angle_is_hit(ang, eta_offset, ome_offset, hkl, valid_eta_spans, Note the function returns both, if it was a hit and if it passed the the filtering, as we'll want to discard the filtered values when computing the hit percentage. - + CAVEAT: added map-based nan filtering to _check_dilated; this may not be the best option. Perhaps filter here? @@ -1158,7 +1158,7 @@ def _filter_and_count_hits(angs_0, angs_1, symHKLs_ix, etaEdges, threshold) hits += hit total += not_filtered - + # second solution hit, not_filtered = _angle_is_hit( angs_1[i], eta_offset, ome_offset, From 9f858fdc187b65a4ebdfca3fa3534e784e82120f Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 14 Sep 2017 15:13:34 -0400 Subject: [PATCH 112/253] changed default tth width --- hexrd/instrument.py | 6 +++--- hexrd/xrd/material.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 16b65fe1..59595e89 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -462,6 +462,8 @@ def extract_line_positions(self, plane_data, imgser_dict, collapse_tth=False, do_interpolation=True): """ TODO: handle wedge boundaries + + FIXME: must handle merged ranges!!! """ if tth_tol is None: tth_tol = np.degrees(plane_data.tThWidth) @@ -649,8 +651,6 @@ def pull_spots(self, plane_data, grain_params, ndiv_ome, ome_del = make_tolerance_grid( delta_ome, ome_tol, 1, adjust_window=True, ) - # ??? - # ome_del_c = np.average(np.vstack([ome_del[:-1], ome_del[1:]]), axis=0) # generate structuring element for connected component labeling if ndiv_ome == 1: @@ -737,6 +737,7 @@ def pull_spots(self, plane_data, grain_params, ang_pixel_size = panel.angularPixelSize(patch_xys[:, 0, :]) # TODO: add polygon testing right here! + # done if check_only: patch_output = [] for i_pt, angs in enumerate(ang_centers): @@ -1376,7 +1377,6 @@ def interpolate_nearest(self, xy, img, pad_with_nans=True): int_xy[on_panel] = int_vals return int_xy - def interpolate_bilinear(self, xy, img, pad_with_nans=True): """ TODO: revisit normalization in here? diff --git a/hexrd/xrd/material.py b/hexrd/xrd/material.py index 0fe77399..741e0ef7 100644 --- a/hexrd/xrd/material.py +++ b/hexrd/xrd/material.py @@ -61,8 +61,8 @@ class Material(object): DFLT_SSMAX = 50 DFLT_KEV = valWUnit('wavelength', 'energy', 80.725e0, 'keV') - DFLT_STR = 0.002 - DFLT_TTH = 0.002 + DFLT_STR = 0.0025 + DFLT_TTH = numpy.radians(0.25) DFLT_ATOMINFO = numpy.array([[0,0,0,1]]) """Fractional Atom Position of an atom in the unit cell followed by the number of electrons within that atom. The max number of electrons is 96. From f7caf0446e79575c25c8cd68e9848d84fe09c1ea Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 14 Sep 2017 15:37:07 -0400 Subject: [PATCH 113/253] added method for converting angles to cartesian (x,y) --- hexrd/instrument.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 59595e89..429ec75a 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1336,11 +1336,13 @@ def clip_to_panel(self, xy, buffer_edges=True): on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel - def cart_to_angles(self, xy_data, - rmat_s=ct.identity_3x3, - tvec_s=ct.zeros_3, tvec_c=ct.zeros_3): + def cart_to_angles(self, xy_data): """ + TODO: distortion """ + rmat_s = ct.identity_3x3 + tvec_s = ct.zeros_3 + tvec_c = ct.zeros_3 angs, g_vec = detectorXYToGvec( xy_data, self.rmat, rmat_s, self.tvec, tvec_s, tvec_c, @@ -1348,6 +1350,22 @@ def cart_to_angles(self, xy_data, tth_eta = np.vstack([angs[0], angs[1]]).T return tth_eta, g_vec + def angles_to_cart(self, tth_eta): + """ + TODO: distortion + """ + rmat_s = rmat_c = ct.identity_3x3 + tvec_s = tvec_c = ct.zeros_3 + + angs = np.hstack([tth_eta, np.zeros((len(tth_eta), 1))]) + + xy_det = gvecToDetectorXY( + anglesToGVec(angs, bHat_l=self.bvec, eHat_l=self.evec), + self.rmat, rmat_s, rmat_c, + self.tvec, tvec_s, tvec_c, + beamVec=self.bvec) + return xy_det + def interpolate_nearest(self, xy, img, pad_with_nans=True): """ TODO: revisit normalization in here? From dc7a29af6bdcd5381de91474f42c169f0b12ab91 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sat, 16 Sep 2017 11:40:40 -0400 Subject: [PATCH 114/253] fixed array contiguity in CAPI function --- hexrd/xrd/transforms_CAPI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hexrd/xrd/transforms_CAPI.py b/hexrd/xrd/transforms_CAPI.py index 6b9334da..7f51159c 100644 --- a/hexrd/xrd/transforms_CAPI.py +++ b/hexrd/xrd/transforms_CAPI.py @@ -416,6 +416,7 @@ def rowNorm(a): return cnrma def unitRowVector(vecIn): + vecIn = np.ascontiguousarray(vecIn) if vecIn.ndim == 1: return _transforms_CAPI.unitRowVector(vecIn) elif vecIn.ndim == 2: From 92c25938b817f03e5bb4705d53e81d93612574ee Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sat, 16 Sep 2017 13:24:28 -0400 Subject: [PATCH 115/253] fixed misuse of 'is' in pull_spots --- hexrd/instrument.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 429ec75a..af48c1d0 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -846,13 +846,13 @@ def pull_spots(self, plane_data, grain_params, ome_imgser.omega[frame_indices][-1, 1]] ) for i, i_frame in enumerate(frame_indices): - if interp.lower() is 'nearest': + if interp.lower() == 'nearest': patch_data[i] = \ panel.interpolate_nearest( xy_eval, ome_imgser[i_frame], ).reshape(prows, pcols)*nrm_fac - elif interp.lower() is 'bilinear': + elif interp.lower() == 'bilinear': patch_data[i] = \ panel.interpolate_bilinear( xy_eval, From 216456613e28c98f8fa8eff9cc054f11ad8e7a90 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 13 Oct 2017 17:59:30 -0500 Subject: [PATCH 116/253] Update instrument.py running changes --- hexrd/instrument.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index af48c1d0..a02517fa 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -517,8 +517,7 @@ def extract_line_positions(self, plane_data, imgser_dict, np.hstack([patch_vertices, np.zeros((4*npts, 1))]), panel.rmat, ct.identity_3x3, self.chi, panel.tvec, ct.zeros_3, self.tvec, - panel.distortion - ) + panel.distortion) tmp_xy, on_panel = panel.clip_to_panel(det_xy) # all vertices must be on... @@ -611,7 +610,7 @@ def pull_spots(self, plane_data, grain_params, eta_ranges=None, ome_period=(-np.pi, np.pi), dirname='results', filename=None, output_format='text', save_spot_list=False, - quiet=True, lrank=1, check_only=False, + quiet=True, check_only=False, interp='nearest'): if eta_ranges is None: @@ -654,9 +653,9 @@ def pull_spots(self, plane_data, grain_params, # generate structuring element for connected component labeling if ndiv_ome == 1: - label_struct = ndimage.generate_binary_structure(2, lrank) + label_struct = ndimage.generate_binary_structure(2, 2) else: - label_struct = ndimage.generate_binary_structure(3, lrank) + label_struct = ndimage.generate_binary_structure(3, 3) # filter by eta and omega ranges allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( @@ -717,8 +716,7 @@ def pull_spots(self, plane_data, grain_params, np.hstack([patch_vertices, ome_dupl]), panel.rmat, rMat_c, self.chi, panel.tvec, tVec_c, self.tvec, - panel.distortion - ) + panel.distortion) scrap, on_panel = panel.clip_to_panel(det_xy) # all vertices must be on... @@ -785,7 +783,7 @@ def pull_spots(self, plane_data, grain_params, vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch prows, pcols = areas.shape nrm_fac = areas/float(native_area) - + import pdb;pdb.set_trace() # grab hkl info hkl = hkls_p[i_pt, :] hkl_id = hkl_ids[i_pt] @@ -886,8 +884,10 @@ def pull_spots(self, plane_data, grain_params, closest_peak_idx = 0 pass # end multipeak conditional coms = coms[closest_peak_idx] + # meas_omes = \ + # ome_edges[0] + (0.5 + coms[0])*delta_ome meas_omes = \ - ome_edges[0] + (0.5 + coms[0])*delta_ome + ome_eval[0] + coms[0]*delta_ome meas_angs = np.hstack( [tth_edges[0] + (0.5 + coms[2])*delta_tth, eta_edges[0] + (0.5 + coms[1])*delta_eta, @@ -952,8 +952,8 @@ def pull_spots(self, plane_data, grain_params, ).transpose(2, 0, 1) writer.dump_patch( detector_id, iRefl, peak_id, hkl_id, hkl, - tth_edges, eta_edges, ome_edges, - xyc_arr, ijs, patch_data, + tth_edges, eta_edges, np.radians(ome_eval), + xyc_arr, ijs, frame_indices, patch_data, ang_centers[i_pt], meas_angs, meas_xy) pass # end conditional on write output pass # end conditional on check only @@ -1624,8 +1624,7 @@ def simulate_rotation_series(self, plane_data, grain_param_list, allAngs, self.rmat, rMat_c, chi, self.tvec, tVec_c, tVec_s, - self.distortion - ) + self.distortion) xys_p, on_panel = self.clip_to_panel(det_xy) valid_xys.append(xys_p) @@ -1781,14 +1780,16 @@ def close(self): def dump_patch(self, panel_id, i_refl, peak_id, hkl_id, hkl, - tth_edges, eta_edges, ome_edges, - xy_centers, ijs, spot_data, - pangs, mangs, mxy, gzip=9): + tth_edges, eta_edges, ome_centers, + xy_centers, ijs, frame_indices, + spot_data, pangs, mangs, mxy, gzip=9): """ to be called inside loop over patches default GZIP level for data arrays is 9 """ + fi = np.array(frame_indices, dtype=int) + panel_grp = self.data_grp[panel_id] spot_grp = panel_grp.create_group("spot_%05d" % i_refl) spot_grp.attrs.create('peak_id', peak_id) @@ -1804,7 +1805,7 @@ def dump_patch(self, panel_id, # get centers crds from edge arrays ome_crd, eta_crd, tth_crd = np.meshgrid( - centers_of_edge_vec(ome_edges), + ome_centers, centers_of_edge_vec(eta_edges), centers_of_edge_vec(tth_edges), indexing='ij') @@ -1818,6 +1819,8 @@ def dump_patch(self, panel_id, compression="gzip", compression_opts=gzip) spot_grp.create_dataset('ij_centers', data=ijs, compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('frame_indices', data=fi, + compression="gzip", compression_opts=gzip) spot_grp.create_dataset('intensities', data=spot_data, compression="gzip", compression_opts=gzip) return From 4b853ac38a13114750abe64b3923ebe81e967ffc Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 13 Oct 2017 18:00:44 -0500 Subject: [PATCH 117/253] Update xrdutil.py running changes --- hexrd/xrd/xrdutil.py | 56 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 39e70524..c725d518 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -45,6 +45,7 @@ from matplotlib import cm, colors from matplotlib import collections +from hexrd import constants from hexrd import plotwrap from hexrd import tens from hexrd import matrixutil as mutil @@ -104,6 +105,10 @@ ten_epsf = 10 * epsf # ~2.2e-15 sqrt_epsf = num.sqrt(epsf) # ~1.5e-8 +bHat_l_DFLT = constants.beam_vec.flatten() +eHat_l_DFLT = constants.eta_vec.flatten() + + class FormatEtaOme: 'for plotting data as a matrix, with ijAsXY=True' def __init__(self, etas, omes, A, T=False, debug=False): @@ -3595,17 +3600,11 @@ def _project_on_detector_plane(allAngs, rMat_d, rMat_c, chi, tVec_d, tVec_c, tVec_s, distortion): # hkls not needed # gVec_cs = num.dot(bMat, allHKLs.T) - gVec_cs = xfcapi.anglesToGVec( - allAngs, chi=chi, rMat_c=rMat_c - ) - rMat_ss = xfcapi.makeOscillRotMatArray( - chi, num.ascontiguousarray(allAngs[:,2]) - ) - tmp_xys = xfcapi.gvecToDetectorXYArray( - gVec_cs, rMat_d, rMat_ss, rMat_c, - tVec_d, tVec_s, tVec_c - ) - valid_mask = ~(num.isnan(tmp_xys[:,0]) | num.isnan(tmp_xys[:,1])) + gVec_cs = xfcapi.anglesToGVec(allAngs, chi=chi, rMat_c=rMat_c) + rMat_ss = xfcapi.makeOscillRotMatArray(chi, allAngs[:, 2]) + tmp_xys = xfcapi.gvecToDetectorXYArray(gVec_cs, rMat_d, rMat_ss, rMat_c, + tVec_d, tVec_s, tVec_c) + valid_mask = ~(num.isnan(tmp_xys[:, 0]) | num.isnan(tmp_xys[:, 1])) if distortion is None or len(distortion) == 0: det_xy = tmp_xys[valid_mask] @@ -3686,7 +3685,7 @@ def simulateGVecs(pd, detector_params, grain_params, ang_ps = [] else: #...preallocate for speed...? - det_xy, rMat_s = _project_on_detector_plane( + det_xy, rMat_s = _project_on_detector_plane( allAngs, rMat_d, rMat_c, chi, tVec_d, tVec_c, tVec_s, @@ -4606,3 +4605,36 @@ def extract_detector_transformation(detector_params): chi = detector_params[6] tVec_s = num.ascontiguousarray(detector_params[7:10]) return rMat_d, tVec_d, chi, tVec_s + + +def _angles_to_xy(angs, + rMat_d, tVec_d, + chi, tVec_s, + rMat_c, tVec_c, + bHat_l=bHat_l_DFLT, + eHat_l=eHat_l_DFLT, + distortion=None): + """ + """ + # make G-vectors + gVec_c = xfcapi.anglesToGVec( + angs, + chi=chi, + rMat_c=rMat_c, + bHat_l=bHat_l, + eHat_l=eHat_l) + + rMat_s = xfcapi.makeOscillRotMatArray(chi, angs[:, 2]) + + # map to xy + xy_eval = xfcapi.gvecToDetectorXYArray( + gVec_c, + rMat_d, rMat_s, rMat_c, + tVec_d, tVec_s, tVec_c, + beamVec=bHat_l) + + # apply distortion (if applicable) + if distortion is not None and len(distortion) == 2: + xy_eval = distortion[0](xy_eval, distortion[1], invert=True) + + return xy_eval From 3c3e93f37967068a2ec1c804c5ee302d037ed849 Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Fri, 13 Oct 2017 19:17:37 -0500 Subject: [PATCH 118/253] cleanup of pull_spots --- hexrd/instrument.py | 103 +++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 55 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index a02517fa..0d14901e 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -589,7 +589,9 @@ def extract_line_positions(self, plane_data, imgser_dict, return panel_data def simulate_rotation_series(self, plane_data, grain_param_list, + eta_ranges=[(-np.pi, np.pi), ], ome_ranges=[(-np.pi, np.pi), ], + ome_period=(-np.pi, np.pi), wavelength=None): """ TODO: revisit output; dict, or concatenated list? @@ -598,7 +600,9 @@ def simulate_rotation_series(self, plane_data, grain_param_list, for det_key, panel in self.detectors.iteritems(): results[det_key] = panel.simulate_rotation_series( plane_data, grain_param_list, - ome_ranges, + eta_ranges=eta_ranges, + ome_ranges=ome_ranges, + ome_period=ome_period, chi=self.chi, tVec_s=self.tvec, wavelength=wavelength) return results @@ -607,34 +611,13 @@ def pull_spots(self, plane_data, grain_params, imgser_dict, tth_tol=0.25, eta_tol=1., ome_tol=1., npdiv=2, threshold=10, - eta_ranges=None, ome_period=(-np.pi, np.pi), + eta_ranges=[(-np.pi, np.pi), ], + ome_period=(-np.pi, np.pi), dirname='results', filename=None, output_format='text', save_spot_list=False, quiet=True, check_only=False, interp='nearest'): - if eta_ranges is None: - eta_ranges = [(-np.pi, np.pi), ] - - bMat = plane_data.latVecOps['B'] - - rMat_c = makeRotMatOfExpMap(grain_params[:3]) - tVec_c = grain_params[3:6] - vInv_s = grain_params[6:] - - # vstacked G-vector id, h, k, l - full_hkls = xrdutil._fetch_hkls_from_planedata(plane_data) - - # All possible bragg conditions as vstacked [tth, eta, ome] for - # each omega solution - angList = np.vstack( - oscillAnglesOfHKLs( - full_hkls[:, 1:], self.chi, - rMat_c, bMat, self.beam_wavelength, - vInv=vInv_s, - ) - ) - # grab omega ranges from first imageseries # # WARNING: all imageseries AND all wedges within are assumed to have @@ -647,6 +630,7 @@ def pull_spots(self, plane_data, grain_params, delta_ome = oims0.omega[0, 1] - oims0.omega[0, 0] # make omega grid for frame expansion around reference frame + # in DEGREES ndiv_ome, ome_del = make_tolerance_grid( delta_ome, ome_tol, 1, adjust_window=True, ) @@ -657,27 +641,23 @@ def pull_spots(self, plane_data, grain_params, else: label_struct = ndimage.generate_binary_structure(3, 3) - # filter by eta and omega ranges - allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( - full_hkls, angList, eta_ranges, ome_ranges - ) - allAngs[:, 2] = mapAngle(allAngs[:, 2], ome_period) - - # dilate angles tth and eta to patch corners - nangs = len(allAngs) + # + # simulate rotation series + # + sim_results = self.simulate_rotation_series( + plane_data, [grain_params, ], + eta_ranges=eta_ranges, + ome_ranges=ome_ranges, + ome_period=ome_period) + + # patch vertex generator (global for instrument) tol_vec = 0.5*np.radians( [-tth_tol, -eta_tol, -tth_tol, eta_tol, tth_tol, eta_tol, tth_tol, -eta_tol]) - patch_vertices = ( - np.tile(allAngs[:, :2], (1, 4)) + np.tile(tol_vec, (nangs, 1)) - ).reshape(4*nangs, 2) - ome_dupl = np.tile( - allAngs[:, 2], (4, 1) - ).T.reshape(len(patch_vertices), 1) - + # prepare output if requested if filename is not None and output_format.lower() == 'hdf5': this_filename = os.path.join(dirname, filename) writer = GrainDataWriter_h5( @@ -711,7 +691,27 @@ def pull_spots(self, plane_data, grain_params, # pull out the OmegaImageSeries for this panel from input dict ome_imgser = imgser_dict[detector_id] - # find points that fall on the panel + # extract simulation results + sim_results_p = sim_results[detector_id] + hkl_ids = sim_results_p[0][0] + hkls_p = sim_results_p[1][0] + ang_centers = sim_results_p[2][0] + xy_centers = sim_results_p[3][0] + ang_pixel_size = sim_results_p[4][0] + + # now verify that full patch falls on detector... + # ???: strictly necessary? + # + # patch vertex array from sim + nangs = len(ang_centers) + patch_vertices = ( + np.tile(ang_centers[:, :2], (1, 4)) + np.tile(tol_vec, (nangs, 1)) + ).reshape(4*nangs, 2) + ome_dupl = np.tile( + ang_centers[:, 2], (4, 1) + ).T.reshape(len(patch_vertices), 1) + + # find vertices that all fall on the panel det_xy, rMat_s = xrdutil._project_on_detector_plane( np.hstack([patch_vertices, ome_dupl]), panel.rmat, rMat_c, self.chi, @@ -721,19 +721,8 @@ def pull_spots(self, plane_data, grain_params, # all vertices must be on... patch_is_on = np.all(on_panel.reshape(nangs, 4), axis=1) - patch_xys = det_xy.reshape(nangs, 4, 2)[patch_is_on] - - # grab hkls and gvec ids for this panel - hkls_p = np.array(allHKLs[patch_is_on, 1:], dtype=int) - hkl_ids = np.array(allHKLs[patch_is_on, 0], dtype=int) - - # reflection angles (voxel centers) - ang_centers = allAngs[patch_is_on, :] - - # calculate angular (tth, eta) pixel size using - # first vertex of each - ang_pixel_size = panel.angularPixelSize(patch_xys[:, 0, :]) - + patch_xys = det_xy.reshape(nangs, 4, 2)[patch_is_on] + # TODO: add polygon testing right here! # done if check_only: @@ -1568,7 +1557,10 @@ def map_to_plane(self, pts, rmat, tvec): return np.dot(rmat.T, pts_map_lab - tvec_map_lab)[:2, :].T def simulate_rotation_series(self, plane_data, grain_param_list, - ome_ranges, chi=0., tVec_s=ct.zeros_3, + eta_ranges=[(-np.pi, np.pi), ], + ome_ranges=[(-np.pi, np.pi), ], + ome_period=(-np.pi, np.pi), + chi=0., tVec_s=ct.zeros_3, wavelength=None): """ """ @@ -1616,8 +1608,9 @@ def simulate_rotation_series(self, plane_data, grain_param_list, # filter by eta and omega ranges # ??? get eta range from detector? allAngs, allHKLs = xrdutil._filter_hkls_eta_ome( - full_hkls, angList, [(-np.pi, np.pi), ], ome_ranges + full_hkls, angList, eta_ranges, ome_ranges ) + allAngs[:, 2] = mapAngle(allAngs[:, 2], ome_period) # find points that fall on the panel det_xy, rMat_s = xrdutil._project_on_detector_plane( From d7fae3b125394b8cc3c761a5660ae83c93063fa7 Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Fri, 13 Oct 2017 21:36:28 -0500 Subject: [PATCH 119/253] pull_spots bugs --- hexrd/imageseries/save.py | 4 ++-- hexrd/instrument.py | 36 ++++++++++++++++++++++++++---------- hexrd/xrd/transforms_CAPI.py | 2 +- hexrd/xrd/xrdutil.py | 12 +++++++++--- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index ed0de6fa..7c4ff7ba 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -189,8 +189,8 @@ def _write_frames(self): frame = self._ims[i] mask = frame > self._thresh # FIXME: formalize this a little better??? - if np.sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.05: - raise Warning("frame %d is less than 95%% sparse" % i) + if np.sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.25: + raise Warning("frame %d is less than 75%% sparse" % i) row, col = mask.nonzero() arrd['%d_data' % i] = frame[mask] arrd['%d_row' % i] = row diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 0d14901e..22f114f7 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -617,6 +617,14 @@ def pull_spots(self, plane_data, grain_params, save_spot_list=False, quiet=True, check_only=False, interp='nearest'): + """ + Exctract reflection info from a rotation series encoded as an + OmegaImageseries object + """ + + # grain parameters + rMat_c = makeRotMatOfExpMap(grain_params[:3]) + tVec_c = grain_params[3:6] # grab omega ranges from first imageseries # @@ -649,7 +657,7 @@ def pull_spots(self, plane_data, grain_params, eta_ranges=eta_ranges, ome_ranges=ome_ranges, ome_period=ome_period) - + # patch vertex generator (global for instrument) tol_vec = 0.5*np.radians( [-tth_tol, -eta_tol, @@ -698,14 +706,15 @@ def pull_spots(self, plane_data, grain_params, ang_centers = sim_results_p[2][0] xy_centers = sim_results_p[3][0] ang_pixel_size = sim_results_p[4][0] - + # now verify that full patch falls on detector... # ???: strictly necessary? # # patch vertex array from sim nangs = len(ang_centers) patch_vertices = ( - np.tile(ang_centers[:, :2], (1, 4)) + np.tile(tol_vec, (nangs, 1)) + np.tile(ang_centers[:, :2], (1, 4)) + + np.tile(tol_vec, (nangs, 1)) ).reshape(4*nangs, 2) ome_dupl = np.tile( ang_centers[:, 2], (4, 1) @@ -721,8 +730,15 @@ def pull_spots(self, plane_data, grain_params, # all vertices must be on... patch_is_on = np.all(on_panel.reshape(nangs, 4), axis=1) - patch_xys = det_xy.reshape(nangs, 4, 2)[patch_is_on] - + patch_xys = det_xy.reshape(nangs, 4, 2)[patch_is_on] + + # re-filter... + hkl_ids = hkl_ids[patch_is_on] + hkls_p = hkls_p[patch_is_on, :] + ang_centers = ang_centers[patch_is_on, :] + xy_centers = xy_centers[patch_is_on, :] + ang_pixel_size = ang_pixel_size[patch_is_on, :] + # TODO: add polygon testing right here! # done if check_only: @@ -772,7 +788,7 @@ def pull_spots(self, plane_data, grain_params, vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch prows, pcols = areas.shape nrm_fac = areas/float(native_area) - import pdb;pdb.set_trace() + # grab hkl info hkl = hkls_p[i_pt, :] hkl_id = hkl_ids[i_pt] @@ -828,10 +844,10 @@ def pull_spots(self, plane_data, grain_params, patch_data = np.zeros( (len(frame_indices), prows, pcols) ) - ome_edges = np.hstack( - [ome_imgser.omega[frame_indices][:, 0], - ome_imgser.omega[frame_indices][-1, 1]] - ) + # ome_edges = np.hstack( + # [ome_imgser.omega[frame_indices][:, 0], + # ome_imgser.omega[frame_indices][-1, 1]] + # ) for i, i_frame in enumerate(frame_indices): if interp.lower() == 'nearest': patch_data[i] = \ diff --git a/hexrd/xrd/transforms_CAPI.py b/hexrd/xrd/transforms_CAPI.py index 7f51159c..1eb71f13 100644 --- a/hexrd/xrd/transforms_CAPI.py +++ b/hexrd/xrd/transforms_CAPI.py @@ -446,7 +446,7 @@ def makeOscillRotMatArray(chi, omeArray): chi value and an array of omega values. """ arg = np.ascontiguousarray(omeArray) - return _transforms_CAPI.makeOscillRotMatArray(chi, omeArray) + return _transforms_CAPI.makeOscillRotMatArray(chi, arg) def makeRotMatOfExpMap(expMap): """ diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index c725d518..e3ee5c4e 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -3599,11 +3599,17 @@ def _filter_hkls_eta_ome(hkls, angles, eta_range, ome_range): def _project_on_detector_plane(allAngs, rMat_d, rMat_c, chi, tVec_d, tVec_c, tVec_s, distortion): - # hkls not needed # gVec_cs = num.dot(bMat, allHKLs.T) + """ + utility routine for projecting a list of (tth, eta, ome) onto the + detector plane parameterized by the args + """ + gVec_cs = xfcapi.anglesToGVec(allAngs, chi=chi, rMat_c=rMat_c) rMat_ss = xfcapi.makeOscillRotMatArray(chi, allAngs[:, 2]) - tmp_xys = xfcapi.gvecToDetectorXYArray(gVec_cs, rMat_d, rMat_ss, rMat_c, - tVec_d, tVec_s, tVec_c) + tmp_xys = xfcapi.gvecToDetectorXYArray( + gVec_cs, rMat_d, rMat_ss, rMat_c, + tVec_d, tVec_s, tVec_c + ) valid_mask = ~(num.isnan(tmp_xys[:, 0]) | num.isnan(tmp_xys[:, 1])) if distortion is None or len(distortion) == 0: From aa78aa5656cb600bc3e511c811e14cc186c700c0 Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Sat, 14 Oct 2017 12:59:50 -0500 Subject: [PATCH 120/253] fix to pull_spots interpolation --- hexrd/instrument.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 22f114f7..48b99929 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -788,6 +788,7 @@ def pull_spots(self, plane_data, grain_params, vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch prows, pcols = areas.shape nrm_fac = areas/float(native_area) + nrm_fac = nrm_fac / np.min(nrm_fac) # grab hkl info hkl = hkls_p[i_pt, :] @@ -811,6 +812,7 @@ def pull_spots(self, plane_data, grain_params, frame_indices = [ ome_imgser.omega_to_frame(ome)[0] for ome in ome_eval ] + if -1 in frame_indices: if not quiet: msg = """ @@ -840,6 +842,7 @@ def pull_spots(self, plane_data, grain_params, compl.append(contains_signal) if contains_signal: + """ # initialize patch data array for intensities patch_data = np.zeros( (len(frame_indices), prows, pcols) @@ -854,7 +857,7 @@ def pull_spots(self, plane_data, grain_params, panel.interpolate_nearest( xy_eval, ome_imgser[i_frame], - ).reshape(prows, pcols)*nrm_fac + ).reshape(prows, pcols) elif interp.lower() == 'bilinear': patch_data[i] = \ panel.interpolate_bilinear( @@ -863,12 +866,15 @@ def pull_spots(self, plane_data, grain_params, ).reshape(prows, pcols)*nrm_fac pass pass + """ + patch_data = patch_data_raw # * nrm_fac # now have interpolated patch data... labels, num_peaks = ndimage.label( patch_data > threshold, structure=label_struct ) slabels = np.arange(1, num_peaks + 1) + if num_peaks > 0: peak_id = iRefl coms = np.array( From 63733d0ae5e6c7aa8e3cfe32f9ebdaac94507686 Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Mon, 16 Oct 2017 12:55:15 -0500 Subject: [PATCH 121/253] fix to interpolation --- hexrd/instrument.py | 33 ++++++++++----------------- hexrd/xrd/xrdutil.py | 53 ++++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 48b99929..252ccbc3 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -842,32 +842,23 @@ def pull_spots(self, plane_data, grain_params, compl.append(contains_signal) if contains_signal: - """ # initialize patch data array for intensities - patch_data = np.zeros( - (len(frame_indices), prows, pcols) - ) - # ome_edges = np.hstack( - # [ome_imgser.omega[frame_indices][:, 0], - # ome_imgser.omega[frame_indices][-1, 1]] - # ) - for i, i_frame in enumerate(frame_indices): - if interp.lower() == 'nearest': - patch_data[i] = \ - panel.interpolate_nearest( - xy_eval, - ome_imgser[i_frame], - ).reshape(prows, pcols) - elif interp.lower() == 'bilinear': + if interp.lower() == 'bilinear': + patch_data = np.zeros( + (len(frame_indices), prows, pcols)) + for i, i_frame in enumerate(frame_indices): patch_data[i] = \ panel.interpolate_bilinear( xy_eval, ome_imgser[i_frame], - ).reshape(prows, pcols)*nrm_fac - pass - pass - """ - patch_data = patch_data_raw # * nrm_fac + ).reshape(prows, pcols) # * nrm_fac + elif interp.lower() == 'nearest': + patch_data = patch_data_raw # * nrm_fac + else: + raise(RuntimeError, + "interpolation option '%s' not understood" + % interp + ) # now have interpolated patch data... labels, num_peaks = ndimage.label( diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index e3ee5c4e..c801a0eb 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -3691,7 +3691,7 @@ def simulateGVecs(pd, detector_params, grain_params, ang_ps = [] else: #...preallocate for speed...? - det_xy, rMat_s = _project_on_detector_plane( + det_xy, rMat_s = _project_on_detector_plane( allAngs, rMat_d, rMat_c, chi, tVec_d, tVec_c, tVec_s, @@ -4145,35 +4145,26 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, if distortion is not None and len(distortion) == 2: xy_eval = distortion[0](xy_eval, distortion[1], invert=True) pass - row_indices = gutil.cellIndices(row_edges, xy_eval[:, 1]) - col_indices = gutil.cellIndices(col_edges, xy_eval[:, 0]) + row_indices = gutil.cellIndices(row_edges, xy_eval[:, 1]) + col_indices = gutil.cellIndices(col_edges, xy_eval[:, 0]) # append patch data to list patches.append( - ( - ( - gVec_angs_vtx[:, 0].reshape(m_tth.shape), - gVec_angs_vtx[:, 1].reshape(m_tth.shape), - ), - ( - xy_eval_vtx[:, 0].reshape(m_tth.shape), - xy_eval_vtx[:, 1].reshape(m_tth.shape), - ), - conn, - areas.reshape(sdims[0], sdims[1]), - ( - xy_eval[:, 0].reshape(sdims[0], sdims[1]), - xy_eval[:, 1].reshape(sdims[0], sdims[1]), - ), - ( - row_indices.reshape(sdims[0], sdims[1]), - col_indices.reshape(sdims[0], sdims[1]), - ), - ) + ((gVec_angs_vtx[:, 0].reshape(m_tth.shape), + gVec_angs_vtx[:, 1].reshape(m_tth.shape)), + (xy_eval_vtx[:, 0].reshape(m_tth.shape), + xy_eval_vtx[:, 1].reshape(m_tth.shape)), + conn, + areas.reshape(sdims[0], sdims[1]), + (xy_eval[:, 0].reshape(sdims[0], sdims[1]), + xy_eval[:, 1].reshape(sdims[0], sdims[1])), + (row_indices.reshape(sdims[0], sdims[1]), + col_indices.reshape(sdims[0], sdims[1]))) ) pass # close loop over angles return patches + def pullSpots(pd, detector_params, grain_params, reader, ome_period=(-num.pi, num.pi), eta_range=[(-num.pi, num.pi), ], @@ -4613,12 +4604,12 @@ def extract_detector_transformation(detector_params): return rMat_d, tVec_d, chi, tVec_s -def _angles_to_xy(angs, - rMat_d, tVec_d, - chi, tVec_s, +def _angles_to_xy(angs, + rMat_d, tVec_d, + chi, tVec_s, rMat_c, tVec_c, - bHat_l=bHat_l_DFLT, - eHat_l=eHat_l_DFLT, + bHat_l=bHat_l_DFLT, + eHat_l=eHat_l_DFLT, distortion=None): """ """ @@ -4627,7 +4618,7 @@ def _angles_to_xy(angs, angs, chi=chi, rMat_c=rMat_c, - bHat_l=bHat_l, + bHat_l=bHat_l, eHat_l=eHat_l) rMat_s = xfcapi.makeOscillRotMatArray(chi, angs[:, 2]) @@ -4638,9 +4629,9 @@ def _angles_to_xy(angs, rMat_d, rMat_s, rMat_c, tVec_d, tVec_s, tVec_c, beamVec=bHat_l) - + # apply distortion (if applicable) if distortion is not None and len(distortion) == 2: xy_eval = distortion[0](xy_eval, distortion[1], invert=True) - + return xy_eval From e6a845a31f00229d6650e981eddefc382777999d Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 30 Oct 2017 23:00:02 -0700 Subject: [PATCH 122/253] Update meta.yaml Fix to versioning tag template --- conda.recipe/meta.yaml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 5d9347c3..d4110624 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,23 +1,19 @@ package: name: hexrd - version: {{ environ.get('GIT_DESCRIBE_TAG', '') }} + version: {{ environ.get('GIT_DESCRIBE_TAG', '')[1:] }} + +source: + #git_url: https://github.com/joelvbernier/hexrd.git + #git_tag: v0.3.x + git_url: ../ build: number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} - - # Note that this will override the default build string with the Python - # and NumPy versions - string: {{ environ.get('GIT_BUILD_STR', '') }} - + detect_binary_files_with_prefix: true osx_is_app: yes entry_points: - hexrd = hexrd.cli:main -source: - # git_url: https://github.com/joelvbernier/hexrd.git - # git_tag: instrument # edit to point to specific branch or tag - path: ../ - app: entry: hexrd gui summary: High-energy x-ray diffraction analysis @@ -41,8 +37,8 @@ requirements: - python.app # [osx] - pyyaml - qtconsole - - scikit-learn - scikit-image + - scikit-learn - scipy - wxpython From 56deda4b09a57d3babcba6a49b8008d0b45c35a4 Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Wed, 1 Nov 2017 10:23:32 -0700 Subject: [PATCH 123/253] changes enabling refit --- hexrd/instrument.py | 18 ++++++++++++++++ hexrd/xrd/fitting.py | 50 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 252ccbc3..8c80a0e6 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -994,6 +994,7 @@ def __init__(self, name='default', bvec=ct.beam_vec, evec=ct.eta_vec, + saturation_level=None, panel_buffer=None, distortion=None): """ @@ -1008,6 +1009,8 @@ def __init__(self, self._pixel_size_row = pixel_size[0] self._pixel_size_col = pixel_size[1] + self._saturation_level = saturation_level + if panel_buffer is None: self._panel_buffer = 25*np.r_[self._pixel_size_col, self._pixel_size_row] @@ -1071,6 +1074,16 @@ def pixel_size_col(self, x): def pixel_area(self): return self.pixel_size_row * self.pixel_size_col + @property + def saturation_level(self): + return self._saturation_level + + @saturation_level.setter + def saturation_level(self, x): + if x is not None: + assert np.isreal(x) + self._saturation_level = x + @property def panel_buffer(self): return self._panel_buffer @@ -1232,6 +1245,9 @@ def pixel_angles(self): def config_dict(self, chi, t_vec_s, sat_level=None): """ """ + if sat_level is None: + sat_level = self.saturation_level + t_vec_s = np.atleast_1d(t_vec_s) d = dict( @@ -1251,8 +1267,10 @@ def config_dict(self, chi, t_vec_s, sat_level=None): t_vec_s=t_vec_s.tolist(), ), ) + if sat_level is not None: d['detector']['saturation_level'] = sat_level + if self.distortion is not None: """...HARD CODED DISTORTION! FIX THIS!!!""" dist_d = dict( diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index a2daba42..0d643c5b 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -114,7 +114,10 @@ def matchOmegas(xyo_det, hkls_idx, chi, rMat_c, bMat, wavelength, beamVec=beamVec, etaVec=etaVec) if np.any(np.isnan(oangs0)): - import pdb; pdb.set_trace() + # debugging + # TODO: remove this + import pdb + pdb.set_trace() nanIdx = np.where(np.isnan(oangs0[:, 0]))[0] errorString = "Infeasible parameters for hkls:\n" for i in range(len(nanIdx)): @@ -387,7 +390,8 @@ def objFuncFitGrain(gFit, gFull, gFlag, reflections_dict, bMat, wavelength, omePeriod, - simOnly=False, return_value_flag=return_value_flag): + simOnly=False, + return_value_flag=return_value_flag): """ gFull[0] = expMap_c[0] gFull[1] = expMap_c[1] @@ -425,10 +429,17 @@ def objFuncFitGrain(gFit, gFull, gFlag, vMat_s = mutil.vecMVToSymm(vInv_s) # NOTE: Inverse of V from F = V * R # loop over instrument panels - calc_omes_all = [] - calc_xy_all = [] + # CAVEAT: keeping track of key ordering in the "detectors" attribute of + # instrument here because I am not sure if instatiating them using + # dict.fromkeys() preserves the same order if using iteration... + # + calc_omes_dict = dict.fromkeys(instrument.detectors) + calc_xy_dict = dict.fromkeys(instrument.detectors) meas_xyo_all = [] + det_keys_ordered = [] for det_key, panel in instrument.detectors.iteritems(): + det_keys_ordered.append(det_key) + rMat_d, tVec_d, chi, tVec_s = extract_detector_transformation( instrument.detector_parameters[det_key]) @@ -448,6 +459,7 @@ def objFuncFitGrain(gFit, gFull, gFlag, hkls = np.atleast_2d( np.vstack([x[2] for x in results]) ).T + meas_xyo = np.atleast_2d( np.vstack([np.r_[x[7], x[6][-1]] for x in results]) ) @@ -458,8 +470,12 @@ def objFuncFitGrain(gFit, gFull, gFlag, xy_unwarped = panel.distortion[0]( meas_xyo[:, :2], panel.distortion[1]) meas_xyo = np.vstack([xy_unwarped.T, meas_omes]).T + pass + + # append to meas_omes + meas_xyo_all.append(meas_xyo) - # g-vectors: + # G-vectors: # 1. calculate full g-vector components in CRYSTAL frame from B # 2. rotate into SAMPLE frame and apply stretch # 3. rotate back into CRYSTAL frame and normalize to unit magnitude @@ -475,18 +491,23 @@ def objFuncFitGrain(gFit, gFull, gFlag, vInv=vInv_s, beamVec=bVec, etaVec=eVec, omePeriod=omePeriod) + # append to omes dict + calc_omes_dict[det_key] = calc_omes + # TODO: try Numba implementations rMat_s = xfcapi.makeOscillRotMatArray(chi, calc_omes) calc_xy = xfcapi.gvecToDetectorXYArray(gHat_c.T, rMat_d, rMat_s, rMat_c, tVec_d, tVec_s, tVec_c, beamVec=bVec) - calc_omes_all.append(calc_omes) - calc_xy_all.append(calc_xy) - meas_xyo_all.append(meas_xyo) + + # append to xy dict + calc_xy_dict[det_key] = calc_xy pass - calc_omes_all = np.hstack(calc_omes_all) - calc_xy_all = np.vstack(calc_xy_all) + + # stack results to concatenated arrays + calc_omes_all = np.hstack([calc_omes_dict[k] for k in det_keys_ordered]) + calc_xy_all = np.vstack([calc_xy_dict[k] for k in det_keys_ordered]) meas_xyo_all = np.vstack(meas_xyo_all) npts = len(meas_xyo_all) @@ -498,7 +519,14 @@ def objFuncFitGrain(gFit, gFull, gFlag, # return values if simOnly: # return simulated values - retval = np.hstack([calc_xy_all, calc_omes_all.reshape(npts, 1)]) + if return_value_flag in [None, 1]: + retval = np.hstack([calc_xy_all, calc_omes_all.reshape(npts, 1)]) + else: + rd = dict.fromkeys(det_keys_ordered) + for det_key in det_keys_ordered: + rd[det_key] = {'calc_xy': calc_xy_dict[det_key], + 'calc_omes': calc_omes_dict[det_key]} + retval = rd else: # return residual vector # IDEA: try angles instead of xys? From f668c75c21a8462b5233ad623f0775552f9c59e4 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 13 Dec 2017 19:36:49 -0600 Subject: [PATCH 124/253] fix transposing of cw90 and ccw90 flip options. --- hexrd/imageseries/process.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index b196660e..d0fe8eb2 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -71,9 +71,9 @@ def _flip(self, img, flip): elif flip in ('t', 'T'): # transpose (possible shape change) pimg = img.T elif flip in ('ccw90', 'r90'): # rotate 90 (possible shape change) - pimg = img.T[:, ::-1] - elif flip in ('cw90', 'r270'): # rotate 270 (possible shape change) pimg = img.T[::-1, :] + elif flip in ('cw90', 'r270'): # rotate 270 (possible shape change) + pimg = img.T[:, ::-1] else: pimg = img From ab99111781b62a3609b817e199a82b72d21e6ef9 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 5 Jan 2018 18:04:11 -0800 Subject: [PATCH 125/253] add masking, fix fitting --- hexrd/fitting/fitpeak.py | 217 +++++++++++---------- hexrd/fitting/peakfunctions.py | 332 ++++++++++++++++----------------- hexrd/instrument.py | 41 +++- 3 files changed, 304 insertions(+), 286 deletions(-) diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index 74302fe1..dcc124b9 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -1,24 +1,24 @@ # ============================================================ -# Copyright (c) 2012, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. -# Written by Joel Bernier and others. -# LLNL-CODE-529294. +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. # All rights reserved. -# +# # This file is part of HEXRD. For details on dowloading the source, # see the file COPYING. -# +# # Please also see the file LICENSE. -# +# # This program is free software; you can redistribute it and/or modify it under the # terms of the GNU Lesser General Public License (as published by the Free Software # Foundation) version 2.1 dated February 1999. -# +# # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this program (see file LICENSE); if not, write to # the Free Software Foundation, Inc., 59 Temple Place, Suite 330, @@ -47,57 +47,57 @@ def estimate_pk_parms_1d(x,f,pktype='pvoigt'): pktype -- string, type of analytic function that will be used to fit the data, current options are "gaussian","lorentzian","pvoigt" (psuedo voigt), and "split_pvoigt" (split psuedo voigt) - + Outputs: - p -- (m) ndarray containing initial guesses for parameters for the input peaktype + p -- (m) ndarray containing initial guesses for parameters for the input peaktype (see peak function help for what each parameters corresponds to) """ - + data_max=np.max(f) # lbg=np.mean(f[:2]) -# rbg=np.mean(f[:2]) - if((f[0]> (0.25*data_max)) and (f[-1]> (0.25*data_max))):#heuristic for wide peaks +# rbg=np.mean(f[:2]) + if((f[0]> (0.25*data_max)) and (f[-1]> (0.25*data_max))):#heuristic for wide peaks bg0=0. - elif (f[0]> (0.25*data_max)): #peak cut off on the left + elif (f[0]> (0.25*data_max)): #peak cut off on the left bg0=f[-1] - elif (f[-1]> (0.25*data_max)): #peak cut off on the right + elif (f[-1]> (0.25*data_max)): #peak cut off on the right bg0=f[0] else: - bg0=(f[0]+f[-1])/2. + bg0=(f[0]+f[-1])/2. #bg1=(rbg-lbg)/(x[-1]-x[0]) - - cen_index=np.argmax(f) - x0=x[cen_index] - A=data_max-bg0#-(bg0+bg1*x0) - + + cen_index=np.argmax(f) + x0=x[cen_index] + A=data_max-bg0#-(bg0+bg1*x0) + num_pts=len(f) - + #checks for peaks that are cut off if cen_index == (num_pts-1): - FWHM=x[cen_index]-x[np.argmin(np.abs(f[:cen_index]-A/2.))]#peak cut off on the left + FWHM=x[cen_index]-x[np.argmin(np.abs(f[:cen_index]-A/2.))]#peak cut off on the left elif cen_index == 0: - FWHM=x[cen_index+np.argmin(np.abs(f[cen_index+1:]-A/2.))]-x[0] #peak cut off on the right - else: + FWHM=x[cen_index+np.argmin(np.abs(f[cen_index+1:]-A/2.))]-x[0] #peak cut off on the right + else: FWHM=x[cen_index+np.argmin(np.abs(f[cen_index+1:]-A/2.))]-x[np.argmin(np.abs(f[:cen_index]-A/2.))] - + if FWHM <=0:##uh,oh something went bad FWHM=(x[-1]-x[0])/4. #completely arbitrary, set peak width to 1/4 window size - - + + if pktype=='gaussian' or pktype=='lorentzian': p=[A,x0,FWHM,bg0,0.] elif pktype=='pvoigt': p=[A,x0,FWHM,0.5,bg0,0.] elif pktype=='split_pvoigt': p=[A,x0,FWHM,FWHM,0.5,0.5,bg0,0.] - + p=np.array(p) return p - - + + def fit_pk_parms_1d(p0,x,f,pktype='pvoigt'): """ Performs least squares fit to find parameters for 1d analytic functions fit @@ -110,66 +110,66 @@ def fit_pk_parms_1d(p0,x,f,pktype='pvoigt'): pktype -- string, type of analytic function that will be used to fit the data, current options are "gaussian","lorentzian","pvoigt" (psuedo voigt), and "split_pvoigt" (split psuedo voigt) - + Outputs: p -- (m) ndarray containing fit parameters for the input peaktype (see peak function help for what each parameters corresponds to) - - + + Notes: 1. Currently no checks are in place to make sure that the guess of parameters has a consistent number of parameters with the requested peak type - """ - - + """ + + fitArgs=(x,f,pktype) - + ftol=1e-6 - xtol=1e-6 - + xtol=1e-6 + weight=np.max(f)*10.#hard coded should be changed - + if pktype == 'gaussian': - p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,Dfun=eval_pk_deriv_1d,ftol=ftol,xtol=xtol) + p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,Dfun=eval_pk_deriv_1d,ftol=ftol,xtol=xtol) elif pktype == 'lorentzian': - p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,Dfun=eval_pk_deriv_1d,ftol=ftol,xtol=xtol) + p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,Dfun=eval_pk_deriv_1d,ftol=ftol,xtol=xtol) elif pktype == 'pvoigt': lb=[p0[0]*0.5,np.min(x),0., 0., 0.,None] - ub=[p0[0]*2.0,np.max(x),4.*p0[2],1., 2.*p0[4],None] - + ub=[p0[0]*2.0,np.max(x),4.*p0[2],1., 2.*p0[4],None] + fitArgs=(x,f,pktype,weight,lb,ub) - p, outflag = optimize.leastsq(fit_pk_obj_1d_bnded, p0, args=fitArgs,ftol=ftol,xtol=xtol) + p, outflag = optimize.leastsq(fit_pk_obj_1d_bnded, p0, args=fitArgs,ftol=ftol,xtol=xtol) elif pktype == 'split_pvoigt': lb=[p0[0]*0.5,np.min(x),0., 0., 0., 0., 0.,None] - ub=[p0[0]*2.0,np.max(x),4.*p0[2],4.*p0[2],1., 1., 2.*p0[4],None] - - p, outflag = optimize.leastsq(fit_pk_obj_1d_bnded, p0, args=fitArgs,ftol=ftol,xtol=xtol) - + ub=[p0[0]*2.0,np.max(x),4.*p0[2],4.*p0[2],1., 1., 2.*p0[4],None] + fitArgs=(x,f,pktype,weight,lb,ub) + p, outflag = optimize.leastsq(fit_pk_obj_1d_bnded, p0, args=fitArgs,ftol=ftol,xtol=xtol) + elif pktype == 'tanh_stepdown': p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,ftol=ftol,xtol=xtol) else: - p=p0 + p=p0 print('non-valid option, returning guess') - - + + if np.any(np.isnan(p)): p=p0 print('failed fitting, returning guess') - + return p - -def eval_pk_deriv_1d(p,x,y0,pktype): + +def eval_pk_deriv_1d(p,x,y0,pktype): if pktype == 'gaussian': d_mat=pkfuncs.gaussian1d_deriv(p,x) elif pktype == 'lorentzian': d_mat=pkfuncs.lorentzian1d_deriv(p,x) - + return d_mat.T - -def fit_pk_obj_1d(p,x,f0,pktype): + +def fit_pk_obj_1d(p,x,f0,pktype): if pktype == 'gaussian': f=pkfuncs.gaussian1d(p,x) elif pktype == 'lorentzian': @@ -180,12 +180,12 @@ def fit_pk_obj_1d(p,x,f0,pktype): f=pkfuncs.split_pvoigt1d(p,x) elif pktype == 'tanh_stepdown': f=pkfuncs.tanh_stepdown_nobg(p,x) - + resd = f-f0 return resd -def fit_pk_obj_1d_bnded(p,x,f0,pktype,weight,lb,ub): +def fit_pk_obj_1d_bnded(p,x,f0,pktype,weight,lb,ub): if pktype == 'gaussian': f=pkfuncs.gaussian1d(p,x) elif pktype == 'lorentzian': @@ -194,18 +194,18 @@ def fit_pk_obj_1d_bnded(p,x,f0,pktype,weight,lb,ub): f=pkfuncs.pvoigt1d(p,x) elif pktype == 'split_pvoigt': f=pkfuncs.split_pvoigt1d(p,x) - + num_data=len(f) num_parm=len(p) resd=np.zeros(num_data+num_parm) - #tub bnds implementation - + #tub bnds implementation + resd[:num_data] = f-f0 for ii in range(num_parm): - if lb[ii] is not None: + if lb[ii] is not None: resd[num_data+ii]=weight*np.max([-(p[ii]-lb[ii]),0.,(p[ii]-ub[ii])]) - - + + return resd #### 2-D Peak Fitting @@ -220,9 +220,9 @@ def estimate_pk_parms_2d(x,y,f,pktype): y -- (n x 0) ndarray of coordinate positions for dimension 2 (numpy.meshgrid formatting) f -- (n x 0) ndarray of intensity measurements at coordinate positions x and y pktype -- string, type of analytic function that will be used to fit the data, - current options are "gaussian", "gaussian_rot" (gaussian with arbitrary axes) and + current options are "gaussian", "gaussian_rot" (gaussian with arbitrary axes) and "split_pvoigt_rot" (split psuedo voigt with arbitrary axes) - + Outputs: p -- (m) ndarray containing initial guesses for parameters for the input peaktype @@ -230,45 +230,45 @@ def estimate_pk_parms_2d(x,y,f,pktype): """ - + bg0=np.mean([f[0,0],f[-1,0],f[-1,-1],f[0,-1]]) bg1x=(np.mean([f[-1,-1],f[0,-1]])-np.mean([f[0,0],f[-1,0]]))/(x[0,-1]-x[0,0]) bg1y=(np.mean([f[-1,-1],f[-1,0]])-np.mean([f[0,0],f[0,-1]]))/(y[-1,0]-y[0,0]) - - fnobg=f-(bg0+bg1x*x+bg1y*y) - + + fnobg=f-(bg0+bg1x*x+bg1y*y) + labels,numlabels=imgproc.label(fnobg>np.max(fnobg)/2.) - + #looks for the largest peak areas=np.zeros(numlabels) for ii in np.arange(1,numlabels+1,1): areas[ii-1]= np.sum(labels==ii) - - peakIndex=np.argmax(areas)+1 - - + + peakIndex=np.argmax(areas)+1 + + # #currently looks for peak closest to center # dist=np.zeros(numlabels) # for ii in np.arange(1,numlabels+1,1): # dist[ii-1]= ###### -# +# # peakIndex=np.argmin(dist)+1 - + FWHMx=np.max(x[labels==peakIndex])-np.min(x[labels==peakIndex]) FWHMy=np.max(y[labels==peakIndex])-np.min(y[labels==peakIndex]) - + coords=imgproc.maximum_position(fnobg, labels=labels, index=peakIndex) A=imgproc.maximum(fnobg, labels=labels, index=peakIndex) x0=x[coords] y0=y[coords] - + if pktype=='gaussian': p=[A,x0,y0,FWHMx,FWHMy,bg0,bg1x,bg1y] elif pktype=='gaussian_rot': p=[A,x0,y0,FWHMx,FWHMy,0.,bg0,bg1x,bg1y] elif pktype=='split_pvoigt_rot': p=[A,x0,y0,FWHMx,FWHMx,FWHMy,FWHMy,0.5,0.5,0.5,0.5,0.,bg0,bg1x,bg1y] - + p=np.array(p) return p @@ -284,49 +284,49 @@ def fit_pk_parms_2d(p0,x,y,f,pktype): y -- (n x 0) ndarray of coordinate positions for dimension 2 (numpy.meshgrid formatting) f -- (n x 0) ndarray of intensity measurements at coordinate positions x and y pktype -- string, type of analytic function that will be used to fit the data, - current options are "gaussian", "gaussian_rot" (gaussian with arbitrary axes) and + current options are "gaussian", "gaussian_rot" (gaussian with arbitrary axes) and "split_pvoigt_rot" (split psuedo voigt with arbitrary axes) - + Outputs: p -- (m) ndarray containing fit parameters for the input peaktype (see peak function help for what each parameters corresponds to) - - + + Notes: 1. Currently no checks are in place to make sure that the guess of parameters has a consisten number of parameters with the requested peak type - """ + """ fitArgs=(x,y,f,pktype) ftol=1e-9 xtol=1e-9 - + if pktype == 'gaussian': - p, outflag = optimize.leastsq(fit_pk_obj_2d, p0, args=fitArgs,ftol=ftol, xtol=xtol) + p, outflag = optimize.leastsq(fit_pk_obj_2d, p0, args=fitArgs,ftol=ftol, xtol=xtol) elif pktype == 'gaussian_rot': p, outflag = optimize.leastsq(fit_pk_obj_2d, p0, args=fitArgs,ftol=ftol, xtol=xtol) elif pktype == 'split_pvoigt_rot': - p, outflag = optimize.leastsq(fit_pk_obj_2d, p0, args=fitArgs,ftol=ftol, xtol=xtol) - - + p, outflag = optimize.leastsq(fit_pk_obj_2d, p0, args=fitArgs,ftol=ftol, xtol=xtol) + + if np.any(np.isnan(p)): p=p0 - + return p -def fit_pk_obj_2d(p,x,y,f0,pktype): +def fit_pk_obj_2d(p,x,y,f0,pktype): if pktype == 'gaussian': f=pkfuncs.gaussian2d(p,x,y) - elif pktype == 'gaussian_rot': + elif pktype == 'gaussian_rot': f=pkfuncs.gaussian2d_rot(p,x,y) - elif pktype == 'split_pvoigt_rot': + elif pktype == 'split_pvoigt_rot': f=pkfuncs.split_pvoigt2d_rot(p,x,y) - + resd = f-f0 return resd.flatten() - + #### Extra Utilities @@ -342,15 +342,12 @@ def goodness_of_fit(f,f0): Outputs: R -- (1) goodness of fit measure which is sum(error^2)/sum(meas^2) Rw -- (1) goodness of fit measure weighted by intensity sum(meas*error^2)/sum(meas^3) - - - """ + + + """ R=np.sum((f-f0)**2)/np.sum(f0**2) Rw=np.sum(np.abs(f0*(f-f0)**2))/np.sum(np.abs(f0**3)) - - return R, Rw - - - + + return R, Rw diff --git a/hexrd/fitting/peakfunctions.py b/hexrd/fitting/peakfunctions.py index 85747147..5a753052 100644 --- a/hexrd/fitting/peakfunctions.py +++ b/hexrd/fitting/peakfunctions.py @@ -1,24 +1,24 @@ # ============================================================ -# Copyright (c) 2012, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. -# Written by Joel Bernier and others. -# LLNL-CODE-529294. +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. # All rights reserved. -# +# # This file is part of HEXRD. For details on dowloading the source, # see the file COPYING. -# +# # Please also see the file LICENSE. -# +# # This program is free software; you can redistribute it and/or modify it under the # terms of the GNU Lesser General Public License (as published by the Free Software # Foundation) version 2.1 dated February 1999. -# +# # This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU Lesser General Public # License along with this program (see file LICENSE); if not, write to # the Free Software Foundation, Inc., 59 Temple Place, Suite 330, @@ -37,18 +37,18 @@ def _unit_gaussian(p,x):#Split the unit gaussian so this can be called for 2d an """ Required Arguments: p -- (m) [x0,FWHM] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ x0=p[0] FWHM=p[1] - sigma=FWHM/gauss_width_fact - + sigma=FWHM/gauss_width_fact + f=np.exp(-(x-x0)**2/(2.*sigma**2.)) return f @@ -56,14 +56,14 @@ def _gaussian1d_no_bg(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ A=p[0] - f=A*_unit_gaussian(p[[1,2]],x) + f=A*_unit_gaussian(p[[1,2]],x) return f @@ -71,64 +71,64 @@ def gaussian1d(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM,c0,c1] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ bg0=p[3] - bg1=p[4] - - f=_gaussian1d_no_bg(p[:3],x)+bg0+bg1*x - + bg1=p[4] + + f=_gaussian1d_no_bg(p[:3],x)+bg0+bg1*x + return f - - + + def _gaussian1d_no_bg_deriv(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: d_mat -- (3 x n) ndarray of derivative values at positions x - """ + """ x0=p[1] FWHM=p[2] - - sigma=FWHM/gauss_width_fact + + sigma=FWHM/gauss_width_fact dydx0=_gaussian1d_no_bg(p,x)*((x-x0)/(sigma**2.)) - dydA=_unit_gaussian(p[[1,2]],x) + dydA=_unit_gaussian(p[[1,2]],x) dydFWHM=_gaussian1d_no_bg(p,x)*((x-x0)**2./(sigma**3.))/gauss_width_fact - + d_mat=np.zeros((len(p),len(x))) - + d_mat[0,:]=dydA d_mat[1,:]=dydx0 d_mat[2,:]=dydFWHM - + return d_mat - -def gaussian1d_deriv(p,x): + +def gaussian1d_deriv(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM,c0,c1] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: d_mat -- (5 x n) ndarray of derivative values at positions x - """ + """ d_mat=np.zeros((len(p),len(x))) d_mat[0:3,:]=_gaussian1d_no_bg_deriv(p[0:3],x) - d_mat[3,:]=1. + d_mat[3,:]=1. d_mat[4,:]=x - + return d_mat @@ -138,16 +138,16 @@ def _unit_lorentzian(p,x):#Split the unit function so this can be called for 2d """ Required Arguments: p -- (m) [x0,FWHM] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ x0=p[0] FWHM=p[1] - gamma=FWHM/lorentz_width_fact - + gamma=FWHM/lorentz_width_fact + f= gamma**2 / ((x-x0)**2 + gamma**2) return f @@ -155,32 +155,32 @@ def _lorentzian1d_no_bg(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ A=p[0] - f= A*_unit_lorentzian(p[[1,2]],x) - + f= A*_unit_lorentzian(p[[1,2]],x) + return f - + def lorentzian1d(p,x): """ Required Arguments: p -- (m) [x0,FWHM,c0,c1] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ - + """ + bg0=p[3] bg1=p[4] - f=_lorentzian1d_no_bg(p[:3],x)+bg0+bg1*x - + f=_lorentzian1d_no_bg(p[:3],x)+bg0+bg1*x + return f @@ -188,43 +188,43 @@ def _lorentzian1d_no_bg_deriv(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: d_mat -- (3 x n) ndarray of derivative values at positions x - """ - + """ + x0=p[1] FWHM=p[2] - - gamma=FWHM/lorentz_width_fact + + gamma=FWHM/lorentz_width_fact dydx0=_lorentzian1d_no_bg(p,x)*((2.*(x-x0))/((x-x0)**2 + gamma**2)) - dydA=_unit_lorentzian(p[[1,2]],x) + dydA=_unit_lorentzian(p[[1,2]],x) dydFWHM=_lorentzian1d_no_bg(p,x)*((2.*(x-x0)**2.)/(gamma*((x-x0)**2 + gamma**2)))/lorentz_width_fact - + d_mat=np.zeros((len(p),len(x))) - d_mat[0,:]=dydA + d_mat[0,:]=dydA d_mat[1,:]=dydx0 d_mat[2,:]=dydFWHM - + return d_mat - -def lorentzian1d_deriv(p,x): + +def lorentzian1d_deriv(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM,c0,c1] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: d_mat -- (5 x n) ndarray of derivative values at positions x - """ + """ d_mat=np.zeros((len(p),len(x))) d_mat[0:3,:]=_lorentzian1d_no_bg_deriv(p[0:3],x) - d_mat[3,:]=1. + d_mat[3,:]=1. d_mat[4,:]=x - + return d_mat @@ -233,15 +233,15 @@ def _unit_pvoigt1d(p,x):#Split the unit function so this can be called for 2d an """ Required Arguments: p -- (m) [x0,FWHM,n] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ + + n=p[2] - n=p[2] - f=(n*_unit_gaussian(p[:2],x)+(1.-n)*_unit_lorentzian(p[:2],x)) return f @@ -249,31 +249,31 @@ def _pvoigt1d_no_bg(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM,n] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ - A=p[0] + A=p[0] f=A*_unit_pvoigt1d(p[[1,2,3]],x) return f -def pvoigt1d(p,x): +def pvoigt1d(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM,n,c0,c1] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x - """ + """ bg0=p[4] bg1=p[5] - f=_pvoigt1d_no_bg(p[:4],x)+bg0+bg1*x - + f=_pvoigt1d_no_bg(p[:4],x)+bg0+bg1*x + return f #### 1-D Split Psuedo Voigt Functions @@ -281,7 +281,7 @@ def _split_pvoigt1d_no_bg(p,x): """ Required Arguments: p -- (m) [A,x0,FWHM-,FWHM+,n-,n+] - x -- (n) ndarray of coordinate positions + x -- (n) ndarray of coordinate positions Outputs: f -- (n) ndarray of function values at positions x @@ -290,27 +290,27 @@ def _split_pvoigt1d_no_bg(p,x): A=p[0] x0=p[1] - f=np.zeros(x.shape[0]) - + f=np.zeros(x.shape[0]) + #Define halves, using gthanorequal and lthan, choice is arbitrary xr=x>=x0 - xl=x=x0 - xl=x=y0 - yl=y= -xlim, xy[:, 0] <= xlim) - on_panel_y = np.logical_and(xy[:, 1] >= -ylim, xy[:, 1] <= ylim) - on_panel = np.logical_and(on_panel_x, on_panel_y) + if self.panel_buffer.ndim == 2: + pix = self.cartToPixel(xy, pixels=True) + + roff = np.logical_or(pix[:, 0] < 0, pix[:, 0] >= self.rows) + coff = np.logical_or(pix[:, 1] < 0, pix[:, 1] >= self.cols) + + idx = np.logical_or(roff, coff) + + pix[idx, :] = 0 + + on_panel = self.panel_buffer[pix[:, 0], pix[:, 1]] + on_panel[idx] = False + else: + xlim -= self.panel_buffer[0] + ylim -= self.panel_buffer[1] + on_panel_x = np.logical_and( + xy[:, 0] >= -xlim, xy[:, 0] <= xlim + ) + on_panel_y = np.logical_and( + xy[:, 1] >= -ylim, xy[:, 1] <= ylim + ) + on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel def cart_to_angles(self, xy_data): From 2ac922dbe99957f74aa4bbe98957553197d654cf Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Mon, 26 Mar 2018 14:50:52 -0400 Subject: [PATCH 126/253] fixed processed imageseries dtype --- hexrd/imageseries/process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index d0fe8eb2..d58067c1 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -83,7 +83,7 @@ def _flip(self, img, flip): # @property def dtype(self): - return self._imser.dtype + return self[0].dtype @property def shape(self): From 8c87af7c04b83017982c687d5d1bcb8a851095c7 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Tue, 27 Mar 2018 17:28:02 -0400 Subject: [PATCH 127/253] added unittests for shape & dtype in processed imageseries; disabled (skipped) unittests for frame-cache because they need to be updated --- hexrd/imageseries/tests/test_formats.py | 8 ++++++-- hexrd/imageseries/tests/test_process.py | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/hexrd/imageseries/tests/test_formats.py b/hexrd/imageseries/tests/test_formats.py index b1138c9f..9d9b027d 100644 --- a/hexrd/imageseries/tests/test_formats.py +++ b/hexrd/imageseries/tests/test_formats.py @@ -1,5 +1,6 @@ import os import tempfile +import unittest import numpy as np @@ -8,6 +9,7 @@ from hexrd import imageseries + class ImageSeriesFormatTest(ImageSeriesTest): @classmethod def setUpClass(cls): @@ -17,6 +19,7 @@ def setUpClass(cls): def tearDownClass(cls): os.rmdir(cls.tmpdir) + class TestFormatH5(ImageSeriesFormatTest): def setUp(self): @@ -89,16 +92,17 @@ def tearDown(self): os.remove(self.fcfile) os.remove(os.path.join(self.tmpdir, self.cache_file)) - + @unittest.skip("need to fix unit tests for framecache") def test_fmtfc(self): """save/load frame-cache format""" imageseries.write(self.is_a, self.fcfile, self.fmt, threshold=self.thresh, cache_file=self.cache_file) - is_fc = imageseries.open(self.fcfile, self.fmt) + is_fc = imageseries.open(self.fcfile, self.fmt, style='yml') diff = compare(self.is_a, is_fc) self.assertAlmostEqual(diff, 0., "frame-cache reconstruction failed") self.assertTrue(compare_meta(self.is_a, is_fc)) + @unittest.skip("need to fix unit tests for framecache") def test_fmtfc_nparray(self): """frame-cache format with numpy array metadata""" key = 'np-array' diff --git a/hexrd/imageseries/tests/test_process.py b/hexrd/imageseries/tests/test_process.py index 3acef9db..9fbb2103 100644 --- a/hexrd/imageseries/tests/test_process.py +++ b/hexrd/imageseries/tests/test_process.py @@ -78,7 +78,6 @@ def test_process_dark(self): diff = compare(is_a1, is_p) self.assertAlmostEqual(diff, 0., msg="dark image failed") - def test_process_framelist(self): a = make_array() is_a = imageseries.open(None, 'array', data=a) @@ -88,3 +87,20 @@ def test_process_framelist(self): is_a2 = imageseries.open(None, 'array', data=a[tuple(frames), ...]) diff = compare(is_a2, is_p) self.assertAlmostEqual(diff, 0., msg="frame list failed") + + def test_process_shape(self): + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ops = [] + is_p = process.ProcessedImageSeries(is_a, ops) + pshape = is_p.shape + fshape = is_p[0].shape + for i in range(2): + self.assertEqual(fshape[i], pshape[i]) + + def test_process_dtype(self): + a = make_array() + is_a = imageseries.open(None, 'array', data=a) + ops = [] + is_p = process.ProcessedImageSeries(is_a, ops) + self.assertEqual(is_p.dtype, is_p[0].dtype) From 565d7af2186ee39c5e4ca48fd5f8a411dd319cb3 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 28 Mar 2018 18:58:47 -0400 Subject: [PATCH 128/253] fixed unit tests to match recent bugfix on flips --- hexrd/imageseries/tests/test_process.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hexrd/imageseries/tests/test_process.py b/hexrd/imageseries/tests/test_process.py index 9fbb2103..f0ccede7 100644 --- a/hexrd/imageseries/tests/test_process.py +++ b/hexrd/imageseries/tests/test_process.py @@ -46,24 +46,24 @@ def test_process_flip_h(self): self._runfliptest(a, flip, aflip) def test_process_flip_vh(self): - """Processed image series: flip horizontal""" + """Processed image series: flip vertical + horizontal""" flip = 'vh' a = make_array() aflip = a[:, ::-1, ::-1] self._runfliptest(a, flip, aflip) def test_process_flip_r90(self): - """Processed image series: flip horizontal""" + """Processed image series: flip counterclockwise 90""" flip = 'ccw90' a = make_array() - aflip = np.transpose(a, (0, 2, 1))[:, :, ::-1] + aflip = np.transpose(a, (0, 2, 1))[:, ::-1, :] self._runfliptest(a, flip, aflip) def test_process_flip_r270(self): - """Processed image series: flip horizontal""" + """Processed image series: flip clockwise 90 """ flip = 'cw90' a = make_array() - aflip = np.transpose(a, (0, 2, 1))[:, ::-1, :] + aflip = np.transpose(a, (0, 2, 1))[:, :, ::-1] self._runfliptest(a, flip, aflip) def test_process_dark(self): From a976359ae59d2d61e289dbd463c40529f7da391a Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 28 Mar 2018 16:22:33 -0700 Subject: [PATCH 129/253] added laue simulation --- hexrd/instrument.py | 148 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index b33711ce..3b17128c 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -55,6 +55,7 @@ makeRotMatOfExpMap, \ mapAngle, \ oscillAnglesOfHKLs, \ + rowNorm, \ validateAngleRanges from hexrd.xrd import xrdutil from hexrd import constants as ct @@ -592,6 +593,22 @@ def extract_line_positions(self, plane_data, imgser_dict, # pbar.finish() return panel_data + def simulate_laue_pattern(self, plane_data, + minEnergy=5., maxEnergy=35., + rmat_s=None, grain_params=None): + """ + TODO: revisit output; dict, or concatenated list? + """ + results = dict.fromkeys(self.detectors) + for det_key, panel in self.detectors.iteritems(): + results[det_key] = panel.simulate_laue_pattern( + plane_data, + minEnergy=minEnergy, maxEnergy=maxEnergy, + rmat_s=rmat_s, tvec_s=self.tvec, + grain_params=grain_params, + beam_vec=self.beam_vector) + return results + def simulate_rotation_series(self, plane_data, grain_param_list, eta_ranges=[(-np.pi, np.pi), ], ome_ranges=[(-np.pi, np.pi), ], @@ -1682,6 +1699,137 @@ def simulate_rotation_series(self, plane_data, grain_param_list, ang_pixel_size.append(self.angularPixelSize(xys_p)) return valid_ids, valid_hkls, valid_angs, valid_xys, ang_pixel_size + def simulate_laue_pattern(self, plane_data, + minEnergy=5., maxEnergy=35., + rmat_s=None, tvec_s=None, + grain_params=None, + beam_vec=None): + """ + """ + # grab the expanded list of hkls from plane_data + hkls = np.hstack(plane_data.getSymHKLs()) + nhkls_tot = hkls.shape[1] + + # and the unit plane normals (G-vectors) in CRYSTAL FRAME + gvec_c = np.dot(plane_data.latVecOps['B'], hkls) + + # parse energy ranges + # TODO: allow for spectrum parsing + multipleEnergyRanges = False + if hasattr(maxEnergy, '__len__'): + assert len(maxEnergy) == len(minEnergy), \ + 'energy cutoff ranges must have the same length' + multipleEnergyRanges = True + lmin = [] + lmax = [] + for i in range(len(maxEnergy)): + lmin.append(ct.keVToAngstrom(maxEnergy[i])) + lmax.append(ct.keVToAngstrom(minEnergy[i])) + else: + lmin = ct.keVToAngstrom(maxEnergy) + lmax = ct.keVToAngstrom(minEnergy) + + # parse grain parameters kwarg + if grain_params is None: + grain_params = np.atleast_2d( + np.hstack([np.zeros(6), ct.identity_6x1]) + ) + n_grains = len(grain_params) + + # sample rotation + if rmat_s is None: + rmat_s = ct.identity_3x3 + + # dummy translation vector... make input + if tvec_s is None: + tvec_s = ct.zeros_3 + + # beam vector + if beam_vec is None: + beam_vec = ct.beam_vec + + # ========================================================================= + # LOOP OVER GRAINS + # ========================================================================= + + # pre-allocate output arrays + xy_det = np.nan*np.ones((n_grains, nhkls_tot, 2)) + hkls_in = np.nan*np.ones((n_grains, 3, nhkls_tot)) + angles = np.nan*np.ones((n_grains, nhkls_tot, 2)) + dspacing = np.nan*np.ones((n_grains, nhkls_tot)) + energy = np.nan*np.ones((n_grains, nhkls_tot)) + for iG, gp in enumerate(grain_params): + rmat_c = makeRotMatOfExpMap(gp[:3]) + tvec_c = gp[3:6].reshape(3, 1) + vInv_s = mutil.vecMVToSymm(gp[6:].reshape(6, 1)) + + # stretch them: V^(-1) * R * Gc + gvec_s_str = np.dot(vInv_s, np.dot(rmat_c, gvec_c)) + ghat_c_str = mutil.unitVector(np.dot(rmat_c.T, gvec_s_str)) + + # project + dpts = gvecToDetectorXY(ghat_c_str.T, + self.rmat, rmat_s, rmat_c, + self.tvec, tvec_s, tvec_c, + beamVec=beam_vec) + + # check intersections with detector plane + canIntersect = ~np.isnan(dpts[:, 0]) + npts_in = sum(canIntersect) + + if np.any(canIntersect): + dpts = dpts[canIntersect, :].reshape(npts_in, 2) + dhkl = hkls[:, canIntersect].reshape(3, npts_in) + + # back to angles + tth_eta, gvec_l = detectorXYToGvec( + dpts, + self.rmat, rmat_s, + self.tvec, tvec_s, tvec_c, + beamVec=beam_vec) + tth_eta = np.vstack(tth_eta).T + + # warp measured points + if self.distortion is not None: + if len(self.distortion) == 2: + dpts = self.distortion[0]( + dpts, self.distortion[1], + invert=True) + else: + raise(RuntimeError, + "something is wrong with the distortion") + + # plane spacings and energies + dsp = 1. / rowNorm(gvec_s_str[:, canIntersect].T) + wlen = 2*dsp*np.sin(0.5*tth_eta[:, 0]) + + # clip to detector panel + _, on_panel = self.clip_to_panel(dpts, buffer_edges=True) + + if multipleEnergyRanges: + validEnergy = np.zeros(len(wlen), dtype=bool) + for i in range(len(lmin)): + in_energy_range = np.logical_and( + wlen >= lmin[i], + wlen <= lmax[i]) + validEnergy = validEnergy | in_energy_range + pass + else: + validEnergy = np.logical_and(wlen >= lmin, wlen <= lmax) + pass + + # index for valid reflections + keepers = np.where(np.logical_and(on_panel, validEnergy))[0] + + # assign output arrays + xy_det[iG][keepers, :] = dpts[keepers, :] + hkls_in[iG][:, keepers] = dhkl[:, keepers] + angles[iG][keepers, :] = tth_eta[keepers, :] + dspacing[iG, keepers] = dsp[keepers] + energy[iG, keepers] = ct.keVToAngstrom(wlen[keepers]) + pass # close conditional on valids + pass # close loop on grains + return xy_det, hkls_in, angles, dspacing, energy # ============================================================================= # UTILITIES From 69867838427c12f885f978b87698be7c97078911 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 28 Mar 2018 16:27:07 -0700 Subject: [PATCH 130/253] removed lambda function --- hexrd/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hexrd/constants.py b/hexrd/constants.py index 582eba29..c9f36aba 100644 --- a/hexrd/constants.py +++ b/hexrd/constants.py @@ -69,5 +69,7 @@ beam_vec = -lab_z eta_vec = lab_x + # for energy/wavelength conversions -keVToAngstrom = lambda x: (1e7*sc.c*sc.h/sc.e) / float(x) +def keVToAngstrom(x): + return (1e7*sc.c*sc.h/sc.e) / np.array(x, dtype=float) From c7984aad8d8741a17799604bc997eb3f27fae2a4 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 30 Mar 2018 10:58:01 -0700 Subject: [PATCH 131/253] fixes to laue simulation and beam angles calc --- hexrd/instrument.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 3b17128c..4036325d 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -58,6 +58,7 @@ rowNorm, \ validateAngleRanges from hexrd.xrd import xrdutil +from hexrd.xrd.crystallography import PlaneData from hexrd import constants as ct # from hexrd.utils.progressbar import ProgressBar, Bar, ETA, ReverseBar @@ -117,9 +118,7 @@ def calc_angles_from_beam_vec(bvec): bvec = np.atleast_2d(bvec).reshape(3, 1) nvec = mutil.unitVector(-bvec) azim = float( - np.degrees( - 0.5*np.pi + np.arctan2(nvec[0], nvec[2]) - ) + np.degrees(np.arctan2(nvec[2], nvec[0])) ) pola = float(np.degrees(np.arccos(nvec[1]))) return azim, pola @@ -593,7 +592,7 @@ def extract_line_positions(self, plane_data, imgser_dict, # pbar.finish() return panel_data - def simulate_laue_pattern(self, plane_data, + def simulate_laue_pattern(self, crystal_data, minEnergy=5., maxEnergy=35., rmat_s=None, grain_params=None): """ @@ -602,7 +601,7 @@ def simulate_laue_pattern(self, plane_data, results = dict.fromkeys(self.detectors) for det_key, panel in self.detectors.iteritems(): results[det_key] = panel.simulate_laue_pattern( - plane_data, + crystal_data, minEnergy=minEnergy, maxEnergy=maxEnergy, rmat_s=rmat_s, tvec_s=self.tvec, grain_params=grain_params, @@ -1699,19 +1698,30 @@ def simulate_rotation_series(self, plane_data, grain_param_list, ang_pixel_size.append(self.angularPixelSize(xys_p)) return valid_ids, valid_hkls, valid_angs, valid_xys, ang_pixel_size - def simulate_laue_pattern(self, plane_data, + def simulate_laue_pattern(self, crystal_data, minEnergy=5., maxEnergy=35., rmat_s=None, tvec_s=None, grain_params=None, beam_vec=None): """ """ - # grab the expanded list of hkls from plane_data - hkls = np.hstack(plane_data.getSymHKLs()) - nhkls_tot = hkls.shape[1] + if isinstance(crystal_data, PlaneData): + + plane_data = crystal_data - # and the unit plane normals (G-vectors) in CRYSTAL FRAME - gvec_c = np.dot(plane_data.latVecOps['B'], hkls) + # grab the expanded list of hkls from plane_data + hkls = np.hstack(plane_data.getSymHKLs()) + + # and the unit plane normals (G-vectors) in CRYSTAL FRAME + gvec_c = np.dot(plane_data.latVecOps['B'], hkls) + elif len(crystal_data) == 2: + # !!! should clean this up + hkls = np.array(crystal_data[0]) + bmat = crystal_data[1] + gvec_c = np.dot(bmat, hkls) + else: + raise(RuntimeError, 'argument list not understood') + nhkls_tot = hkls.shape[1] # parse energy ranges # TODO: allow for spectrum parsing From d11fc228ef3f38b180fc00ea1693af7e6029a7fe Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 23 Apr 2018 15:39:25 -0700 Subject: [PATCH 132/253] change to HDF5 output --- hexrd/instrument.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 4036325d..a009f026 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -690,7 +690,7 @@ def pull_spots(self, plane_data, grain_params, this_filename = os.path.join(dirname, filename) writer = GrainDataWriter_h5( os.path.join(dirname, filename), - self.write_config()) + self.write_config(), grain_params) # ===================================================================== # LOOP OVER PANELS @@ -1955,8 +1955,8 @@ class GrainDataWriter_h5(object): """ TODO: add material spec """ - def __init__(self, filename, instr_cfg): - use_attr = True + def __init__(self, filename, instr_cfg, grain_params, use_attr=False): + if isinstance(filename, h5py.File): self.fid = filename else: @@ -1968,6 +1968,24 @@ def __init__(self, filename, instr_cfg): self.instr_grp = self.fid.create_group('instrument') unwrap_dict_to_h5(self.instr_grp, icfg, asattr=use_attr) + # add grain group + self.grain_grp = self.fid.create_group('grain') + rmat_c = makeRotMatOfExpMap(grain_params[:3]) + tvec_c = np.array(grain_params[3:6]).flatten() + vinv_s = np.array(grain_params[6:]).flatten() + vmat_s = np.linalg.inv(mutil.vecMVToSymm(vinv_s)) + + if use_attr: # attribute version + self.grain_grp.attrs.create('rmat_c', rmat_c) + self.grain_grp.attrs.create('tvec_c', tvec_c) + self.grain_grp.attrs.create('inv(V)_s', vinv_s) + self.grain_grp.attrs.create('vmat_s', vmat_s) + else: # dataset version + self.grain_grp.create_dataset('rmat_c', data=rmat_c) + self.grain_grp.create_dataset('tvec_c', data=tvec_c) + self.grain_grp.create_dataset('inv(V)_s', data=vinv_s) + self.grain_grp.create_dataset('vmat_s', data=vmat_s) + data_key = 'reflection_data' self.data_grp = self.fid.create_group(data_key) From 1cc10d292b11580f384d645e5dc37e6e2dc34e9c Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 23 Apr 2018 16:40:22 -0700 Subject: [PATCH 133/253] remove numba hack in indexer, cleanup hdf5 output --- hexrd/instrument.py | 25 +++++++++++++++++-------- hexrd/xrd/indexer.py | 1 - 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index a009f026..5f1d8c15 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1956,7 +1956,7 @@ class GrainDataWriter_h5(object): TODO: add material spec """ def __init__(self, filename, instr_cfg, grain_params, use_attr=False): - + if isinstance(filename, h5py.File): self.fid = filename else: @@ -1974,7 +1974,7 @@ def __init__(self, filename, instr_cfg, grain_params, use_attr=False): tvec_c = np.array(grain_params[3:6]).flatten() vinv_s = np.array(grain_params[6:]).flatten() vmat_s = np.linalg.inv(mutil.vecMVToSymm(vinv_s)) - + if use_attr: # attribute version self.grain_grp.attrs.create('rmat_c', rmat_c) self.grain_grp.attrs.create('tvec_c', tvec_c) @@ -2025,16 +2025,25 @@ def dump_patch(self, panel_id, spot_grp.attrs.create('measured_xy', mxy) # get centers crds from edge arrays - ome_crd, eta_crd, tth_crd = np.meshgrid( - ome_centers, - centers_of_edge_vec(eta_edges), - centers_of_edge_vec(tth_edges), - indexing='ij') + # FIXME: export full coordinate arrays, or just center vectors??? + # + # ome_crd, eta_crd, tth_crd = np.meshgrid( + # ome_centers, + # centers_of_edge_vec(eta_edges), + # centers_of_edge_vec(tth_edges), + # indexing='ij') + # + # ome_dim, eta_dim, tth_dim = spot_data.shape + + # !!! for now just exporting center vectors for spot_data + tth_crd = centers_of_edge_vec(tth_edges) + eta_crd = centers_of_edge_vec(eta_edges) + spot_grp.create_dataset('tth_crd', data=tth_crd, compression="gzip", compression_opts=gzip) spot_grp.create_dataset('eta_crd', data=eta_crd, compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('ome_crd', data=ome_crd, + spot_grp.create_dataset('ome_crd', data=ome_centers, compression="gzip", compression_opts=gzip) spot_grp.create_dataset('xy_centers', data=xy_centers, compression="gzip", compression_opts=gzip) diff --git a/hexrd/xrd/indexer.py b/hexrd/xrd/indexer.py index dcd5ace9..da52cdbb 100644 --- a/hexrd/xrd/indexer.py +++ b/hexrd/xrd/indexer.py @@ -53,7 +53,6 @@ from hexrd import USE_NUMBA # FIXME: numba implementation of paintGridThis is broken -USE_NUMBA = 0 # OVERRIDE NUMBA if USE_NUMBA: import numba From 0d52747765b1ca9e6fce26d6764c3daf528b5365 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 24 Apr 2018 14:16:29 -0700 Subject: [PATCH 134/253] roi addition to PlanarDetector --- conda.recipe/meta.yaml | 2 +- hexrd/instrument.py | 86 +++++++++++++++++++++++++++++------------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index d4110624..5f48bb05 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -20,7 +20,6 @@ app: requirements: build: - - h5py - numba - numpy - python @@ -29,6 +28,7 @@ requirements: - dill - fabio - h5py + - joblib - matplotlib - numba - numpy diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 5f1d8c15..fa5f0343 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -875,10 +875,9 @@ def pull_spots(self, plane_data, grain_params, elif interp.lower() == 'nearest': patch_data = patch_data_raw # * nrm_fac else: - raise(RuntimeError, - "interpolation option '%s' not understood" - % interp - ) + msg = "interpolation option " + \ + "'%s' not understood" + raise(RuntimeError, msg % interp) # now have interpolated patch data... labels, num_peaks = ndimage.label( @@ -1014,8 +1013,8 @@ def __init__(self, name='default', bvec=ct.beam_vec, evec=ct.eta_vec, - saturation_level=None, panel_buffer=None, + roi=None, distortion=None): """ panel buffer is in pixels... @@ -1035,6 +1034,8 @@ def __init__(self, self._panel_buffer = 25*np.r_[self._pixel_size_col, self._pixel_size_row] + self._roi = roi + self._tvec = np.array(tvec).flatten() self._tilt = np.array(tilt).flatten() @@ -1115,6 +1116,24 @@ def panel_buffer(self, x): assert len(x) == 2 or x.ndim == 2 self._panel_buffer = x + @property + def roi(self): + return self._roi + + @roi.setter + def roi(self, vertex_array): + """ + vertex array must be + + [[r0, c0], [r1, c1], ..., [rn, cn]] + + and have len >= 3 + + does NOT need to repeat start vertex for closure + """ + assert len(vertex_array) >= 3 + self._roi = vertex_array + @property def row_dim(self): return self.rows * self.pixel_size_row @@ -1364,33 +1383,48 @@ def angularPixelSize(self, xy, rMat_s=None, tVec_s=None, tVec_c=None): def clip_to_panel(self, xy, buffer_edges=True): """ + if self.roi is not None, uses it by default + + TODO: check if need shape kwarg + TODO: optimize ROI search better than list comprehension below + TODO: panel_buffer can be a 2-d boolean mask, but needs testing + """ xy = np.atleast_2d(xy) - xlim = 0.5*self.col_dim - ylim = 0.5*self.row_dim - if buffer_edges and self.panel_buffer is not None: - if self.panel_buffer.ndim == 2: - pix = self.cartToPixel(xy, pixels=True) - roff = np.logical_or(pix[:, 0] < 0, pix[:, 0] >= self.rows) - coff = np.logical_or(pix[:, 1] < 0, pix[:, 1] >= self.cols) + if self.roi is not None: + ij_crds = self.cartToPixel(xy, pixels=True) + ii, jj = polygon(self.roi[:, 0], self.roi[:, 1], + shape=(self.rows, self.cols)) + on_panel_rows = [i in ii for i in ij_crds[:, 0]] + on_panel_cols = [j in jj for j in ij_crds[:, 1]] + on_panel = np.logical_and(on_panel_rows, on_panel_cols) + else: + xlim = 0.5*self.col_dim + ylim = 0.5*self.row_dim + if buffer_edges and self.panel_buffer is not None: + if self.panel_buffer.ndim == 2: + pix = self.cartToPixel(xy, pixels=True) - idx = np.logical_or(roff, coff) + roff = np.logical_or(pix[:, 0] < 0, pix[:, 0] >= self.rows) + coff = np.logical_or(pix[:, 1] < 0, pix[:, 1] >= self.cols) - pix[idx, :] = 0 + idx = np.logical_or(roff, coff) - on_panel = self.panel_buffer[pix[:, 0], pix[:, 1]] - on_panel[idx] = False - else: - xlim -= self.panel_buffer[0] - ylim -= self.panel_buffer[1] - on_panel_x = np.logical_and( - xy[:, 0] >= -xlim, xy[:, 0] <= xlim - ) - on_panel_y = np.logical_and( - xy[:, 1] >= -ylim, xy[:, 1] <= ylim - ) - on_panel = np.logical_and(on_panel_x, on_panel_y) + pix[idx, :] = 0 + + on_panel = self.panel_buffer[pix[:, 0], pix[:, 1]] + on_panel[idx] = False + else: + xlim -= self.panel_buffer[0] + ylim -= self.panel_buffer[1] + on_panel_x = np.logical_and( + xy[:, 0] >= -xlim, xy[:, 0] <= xlim + ) + on_panel_y = np.logical_and( + xy[:, 1] >= -ylim, xy[:, 1] <= ylim + ) + on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel def cart_to_angles(self, xy_data): From e08d2739792cd8fd72cfd136309a5c7e800eff0b Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Wed, 25 Apr 2018 00:02:32 -0700 Subject: [PATCH 135/253] arg typo --- hexrd/instrument.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index fa5f0343..b13890b9 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1013,6 +1013,7 @@ def __init__(self, name='default', bvec=ct.beam_vec, evec=ct.eta_vec, + saturation_level=None, panel_buffer=None, roi=None, distortion=None): From bee37d63dd224e9fb54affea09218ecf839024ca Mon Sep 17 00:00:00 2001 From: joelvbernier Date: Wed, 25 Apr 2018 06:49:55 -0700 Subject: [PATCH 136/253] running fix for versioning badness under conda-build>=3 --- conda.recipe/bld.bat | 5 +++-- conda.recipe/build.sh | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/conda.recipe/bld.bat b/conda.recipe/bld.bat index 2d095a2d..72dfb81d 100644 --- a/conda.recipe/bld.bat +++ b/conda.recipe/bld.bat @@ -1,5 +1,6 @@ -git describe --tags --dirty > %SRC_DIR%/__conda_version__.txt -%PYTHON% %RECIPE_DIR%/format_version.py %SRC_DIR%/__conda_version__.txt +REM !!! need to replace for proper versioning under setuptools??? +REM git describe --tags --dirty > %SRC_DIR%/__conda_version__.txt +REM %PYTHON% %RECIPE_DIR%/format_version.py %SRC_DIR%/__conda_version__.txt rmdir build /s /q diff --git a/conda.recipe/build.sh b/conda.recipe/build.sh index 6942abb9..eacf2510 100755 --- a/conda.recipe/build.sh +++ b/conda.recipe/build.sh @@ -1,5 +1,6 @@ -git describe --tags --dirty > $SRC_DIR/__conda_version__.txt -$PYTHON $RECIPE_DIR/format_version.py $SRC_DIR/__conda_version__.txt +# !!! need to replace for proper versioning under setuptools??? +#git describe --tags --dirty > $SRC_DIR/__conda_version__.txt +#$PYTHON $RECIPE_DIR/format_version.py $SRC_DIR/__conda_version__.txt rm -rf build From dc37c57eebd2841ef091f998a7523cc7f49d4eb7 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 27 Apr 2018 15:56:18 -0700 Subject: [PATCH 137/253] reconcile CAPI transforms to v0.3.x --- hexrd/transforms/debug_helpers.h | 61 ++ hexrd/transforms/transforms_CAPI.c | 157 +++- hexrd/transforms/transforms_CAPI.h | 2 + hexrd/transforms/transforms_CFUNC.c | 1155 +++++++++++++++++++-------- hexrd/transforms/transforms_CFUNC.h | 6 + hexrd/xrd/transforms_CAPI.py | 47 ++ 6 files changed, 1099 insertions(+), 329 deletions(-) create mode 100644 hexrd/transforms/debug_helpers.h diff --git a/hexrd/transforms/debug_helpers.h b/hexrd/transforms/debug_helpers.h new file mode 100644 index 00000000..aacce7d5 --- /dev/null +++ b/hexrd/transforms/debug_helpers.h @@ -0,0 +1,61 @@ +#include + + + +static void debug_dump_val(const char *name, double val) +{ + printf("%s: %10.6f\n", name, val); +} + +static void debug_dump_m33(const char *name, double *array) +{ + printf("%s:\n", name); + printf("\t%10.6f %10.6f %10.6f\n", array[0], array[1], array[2]); + printf("\t%10.6f %10.6f %10.6f\n", array[3], array[4], array[5]); + printf("\t%10.6f %10.6f %10.6f\n", array[6], array[7], array[8]); +} + +static void debug_dump_v3(const char *name, double *vec) +{ + printf("%s: %10.6f %10.6f %10.6f\n", name, vec[0], vec[1], vec[2]); +} + +/* ============================================================================ + * These can be used to initialize and check buffers if we suspect the + * code may leave it uninitialized + * ============================================================================ + */ + +#define SNAN_HI 0x7ff700a0 +#define SNAN_LO 0xbad0feed +void fill_signaling_nans(double *arr, int count) +{ + int i; + npy_uint32 *arr_32 = (npy_uint32 *)arr; + /* Fills an array with signaling nans to detect errors + * Use the 0x7ff700a0bad0feed as the pattern + */ + for (i = 0; i < count; ++i) + { + arr_32[2*i+0] = SNAN_LO; + arr_32[2*i+1] = SNAN_HI; + } +} + +int detect_signaling_nans(double *arr, int count) +{ + int i; + npy_uint32 *arr_32 = (npy_uint32 *) arr; + for (i = 0; i < count; ++i) + { + if (arr_32[2*i+0] == SNAN_LO && + arr_32[2*i+1] == SNAN_HI) + { + return 1; + } + } + + return 0; +} +#undef SNAN_HI +#undef SNAN_LO diff --git a/hexrd/transforms/transforms_CAPI.c b/hexrd/transforms/transforms_CAPI.c index d3ce2934..dc07b65e 100644 --- a/hexrd/transforms/transforms_CAPI.c +++ b/hexrd/transforms/transforms_CAPI.c @@ -14,6 +14,7 @@ static PyMethodDef _transform_methods[] = { {"gvecToDetectorXY",gvecToDetectorXY,METH_VARARGS,""}, {"gvecToDetectorXYArray",gvecToDetectorXYArray,METH_VARARGS,""}, {"detectorXYToGvec",detectorXYToGvec,METH_VARARGS,"take cartesian coordinates to G-vectors"}, + {"detectorXYToGvecArray",detectorXYToGvecArray,METH_VARARGS,"take cartesian coordinates to G-vectors"}, {"oscillAnglesOfHKLs",oscillAnglesOfHKLs,METH_VARARGS,"solve angle specs for G-vectors"}, {"arccosSafe",arccosSafe,METH_VARARGS,""}, {"angularDifference",angularDifference,METH_VARARGS,"difference for cyclical angles"}, @@ -38,7 +39,7 @@ static PyMethodDef _transform_methods[] = { void init_transforms_CAPI(void) { - (void)Py_InitModule("_transforms_CAPI",_transform_methods); + (void)Py_InitModule("_transforms_CAPI", _transform_methods); import_array(); } @@ -197,18 +198,18 @@ static PyObject * makeGVector(PyObject * self, PyObject * args) static PyObject * gvecToDetectorXY(PyObject * self, PyObject * args) { PyArrayObject *gVec_c, - *rMat_d, *rMat_s, *rMat_c, - *tVec_d, *tVec_s, *tVec_c, - *beamVec; + *rMat_d, *rMat_s, *rMat_c, + *tVec_d, *tVec_s, *tVec_c, + *beamVec; PyArrayObject *result; int dgc, drd, drs, drc, dtd, dts, dtc, dbv; npy_intp npts, dims[2]; double *gVec_c_Ptr, - *rMat_d_Ptr, *rMat_s_Ptr, *rMat_c_Ptr, - *tVec_d_Ptr, *tVec_s_Ptr, *tVec_c_Ptr, - *beamVec_Ptr; + *rMat_d_Ptr, *rMat_s_Ptr, *rMat_c_Ptr, + *tVec_d_Ptr, *tVec_s_Ptr, *tVec_c_Ptr, + *beamVec_Ptr; double *result_Ptr; /* Parse arguments */ @@ -301,18 +302,18 @@ static PyObject * gvecToDetectorXY(PyObject * self, PyObject * args) static PyObject * gvecToDetectorXYArray(PyObject * self, PyObject * args) { PyArrayObject *gVec_c, - *rMat_d, *rMat_s, *rMat_c, - *tVec_d, *tVec_s, *tVec_c, - *beamVec; + *rMat_d, *rMat_s, *rMat_c, + *tVec_d, *tVec_s, *tVec_c, + *beamVec; PyArrayObject *result; int dgc, drd, drs, drc, dtd, dts, dtc, dbv; npy_intp npts, dims[2]; double *gVec_c_Ptr, - *rMat_d_Ptr, *rMat_s_Ptr, *rMat_c_Ptr, - *tVec_d_Ptr, *tVec_s_Ptr, *tVec_c_Ptr, - *beamVec_Ptr; + *rMat_d_Ptr, *rMat_s_Ptr, *rMat_c_Ptr, + *tVec_d_Ptr, *tVec_s_Ptr, *tVec_c_Ptr, + *beamVec_Ptr; double *result_Ptr; /* Parse arguments */ @@ -345,7 +346,7 @@ static PyObject * gvecToDetectorXYArray(PyObject * self, PyObject * args) if (npts != PyArray_DIM(rMat_s, 0)) { PyErr_Format(PyExc_ValueError, "gVec_c and rMat_s length mismatch %d vs %d", - (int)PyArray_DIM(gVec_c, 0), (int)PyArray_DIM(rMat_s, 0)); + (int)PyArray_DIM(gVec_c, 0), (int)PyArray_DIM(rMat_s, 0)); return NULL; } assert( PyArray_DIMS(gVec_c)[1] == 3 ); @@ -412,8 +413,8 @@ static PyObject * gvecToDetectorXYArray(PyObject * self, PyObject * args) static PyObject * detectorXYToGvec(PyObject * self, PyObject * args) { PyArrayObject *xy_det, *rMat_d, *rMat_s, - *tVec_d, *tVec_s, *tVec_c, - *beamVec, *etaVec; + *tVec_d, *tVec_s, *tVec_c, + *beamVec, *etaVec; PyArrayObject *tTh, *eta, *gVec_l; PyObject *inner_tuple, *outer_tuple; @@ -421,8 +422,8 @@ static PyObject * detectorXYToGvec(PyObject * self, PyObject * args) npy_intp npts, dims[2]; double *xy_Ptr, *rMat_d_Ptr, *rMat_s_Ptr, - *tVec_d_Ptr, *tVec_s_Ptr, *tVec_c_Ptr, - *beamVec_Ptr, *etaVec_Ptr; + *tVec_d_Ptr, *tVec_s_Ptr, *tVec_c_Ptr, + *beamVec_Ptr, *etaVec_Ptr; double *tTh_Ptr, *eta_Ptr, *gVec_l_Ptr; /* Parse arguments */ @@ -502,6 +503,126 @@ static PyObject * detectorXYToGvec(PyObject * self, PyObject * args) return outer_tuple; } +/* + Takes a list cartesian (x, y) pairs in the detector coordinates and calculates + the associated reciprocal lattice (G) vectors and (bragg angle, azimuth) pairs + with respect to the specified beam and azimth (eta) reference directions + + Required Arguments: + xy_det -- (n, 2) ndarray or list-like input of n detector (x, y) points + rMat_d -- (3, 3) ndarray, the COB taking DETECTOR FRAME components to LAB FRAME + rMat_s -- (n, 3, 3) ndarray, the COB taking SAMPLE FRAME components to LAB FRAME + tVec_d -- (3, 1) ndarray, the translation vector connecting LAB to DETECTOR + tVec_s -- (3, 1) ndarray, the translation vector connecting LAB to SAMPLE + tVec_c -- (3, 1) ndarray, the translation vector connecting SAMPLE to CRYSTAL + + Optional Keyword Arguments: + beamVec -- (1, 3) mdarray containing the incident beam direction components in the LAB FRAME + etaVec -- (1, 3) mdarray containing the reference azimuth direction components in the LAB FRAME + + Outputs: + (n, 2) ndarray containing the (tTh, eta) pairs associated with each (x, y) + (n, 3) ndarray containing the associated G vector directions in the LAB FRAME + associated with gVecs +*/ +static PyObject * detectorXYToGvecArray(PyObject * self, PyObject * args) +{ + PyArrayObject *xy_det, *rMat_d, *rMat_s, + *tVec_d, *tVec_s, *tVec_c, + *beamVec, *etaVec; + PyArrayObject *tTh, *eta, *gVec_l; + PyObject *inner_tuple, *outer_tuple; + + int dxy, drd, drs, dtd, dts, dtc, dbv, dev; + npy_intp npts, dims[2]; + + double *xy_Ptr, *rMat_d_Ptr, *rMat_s_Ptr, + *tVec_d_Ptr, *tVec_s_Ptr, *tVec_c_Ptr, + *beamVec_Ptr, *etaVec_Ptr; + double *tTh_Ptr, *eta_Ptr, *gVec_l_Ptr; + + /* Parse arguments */ + if ( !PyArg_ParseTuple(args,"OOOOOOOO", + &xy_det, + &rMat_d, &rMat_s, + &tVec_d, &tVec_s, &tVec_c, + &beamVec, &etaVec)) return(NULL); + if ( xy_det == NULL || rMat_d == NULL || rMat_s == NULL || + tVec_d == NULL || tVec_s == NULL || tVec_c == NULL || + beamVec == NULL || etaVec == NULL ) return(NULL); + + /* Verify shape of input arrays */ + dxy = PyArray_NDIM(xy_det); + drd = PyArray_NDIM(rMat_d); + drs = PyArray_NDIM(rMat_s); + dtd = PyArray_NDIM(tVec_d); + dts = PyArray_NDIM(tVec_s); + dtc = PyArray_NDIM(tVec_c); + dbv = PyArray_NDIM(beamVec); + dev = PyArray_NDIM(etaVec); + assert( dxy == 2 && drd == 2 && drs == 2 && + dtd == 1 && dts == 1 && dtc == 1 && + dbv == 1 && dev == 1); + + /* Verify dimensions of input arrays */ + npts = PyArray_DIMS(xy_det)[0]; + if (npts != PyArray_DIM(rMat_s, 0)) { + PyErr_Format(PyExc_ValueError, "xy_det and rMat_s length mismatch %d vs %d", + (int)PyArray_DIM(xy_det, 0), (int)PyArray_DIM(rMat_s, 0)); + return NULL; + } + + assert( PyArray_DIMS(xy_det)[1] == 2 ); + assert( PyArray_DIMS(rMat_d)[0] == 3 && PyArray_DIMS(rMat_d)[1] == 3 ); + assert( PyArray_DIMS(rMat_s)[0] == 3 && PyArray_DIMS(rMat_s)[1] == 3 ); + assert( PyArray_DIMS(tVec_d)[0] == 3 ); + assert( PyArray_DIMS(tVec_s)[0] == 3 ); + assert( PyArray_DIMS(tVec_c)[0] == 3 ); + assert( PyArray_DIMS(beamVec)[0] == 3 ); + assert( PyArray_DIMS(etaVec)[0] == 3 ); + + /* Allocate arrays for return values */ + dims[0] = npts; dims[1] = 3; + gVec_l = (PyArrayObject*)PyArray_EMPTY(2,dims,NPY_DOUBLE,0); + + tTh = (PyArrayObject*)PyArray_EMPTY(1,&npts,NPY_DOUBLE,0); + eta = (PyArrayObject*)PyArray_EMPTY(1,&npts,NPY_DOUBLE,0); + + /* Grab data pointers into various arrays */ + xy_Ptr = (double*)PyArray_DATA(xy_det); + gVec_l_Ptr = (double*)PyArray_DATA(gVec_l); + + tTh_Ptr = (double*)PyArray_DATA(tTh); + eta_Ptr = (double*)PyArray_DATA(eta); + + rMat_d_Ptr = (double*)PyArray_DATA(rMat_d); + rMat_s_Ptr = (double*)PyArray_DATA(rMat_s); + + tVec_d_Ptr = (double*)PyArray_DATA(tVec_d); + tVec_s_Ptr = (double*)PyArray_DATA(tVec_s); + tVec_c_Ptr = (double*)PyArray_DATA(tVec_c); + + beamVec_Ptr = (double*)PyArray_DATA(beamVec); + etaVec_Ptr = (double*)PyArray_DATA(etaVec); + + /* Call the computational routine */ + detectorXYToGvecArray_cfunc(npts, xy_Ptr, + rMat_d_Ptr, rMat_s_Ptr, + tVec_d_Ptr, tVec_s_Ptr, tVec_c_Ptr, + beamVec_Ptr, etaVec_Ptr, + tTh_Ptr, eta_Ptr, gVec_l_Ptr); + + /* Build and return the nested data structure */ + /* Note that Py_BuildValue with 'O' increases reference count */ + inner_tuple = Py_BuildValue("OO",tTh,eta); + outer_tuple = Py_BuildValue("OO", inner_tuple, gVec_l); + Py_DECREF(inner_tuple); + Py_DECREF(tTh); + Py_DECREF(eta); + Py_DECREF(gVec_l); + return outer_tuple; +} + static PyObject * oscillAnglesOfHKLs(PyObject * self, PyObject * args) { PyArrayObject *hkls, *rMat_c, *bMat, diff --git a/hexrd/transforms/transforms_CAPI.h b/hexrd/transforms/transforms_CAPI.h index fa9a53ca..b72d3448 100644 --- a/hexrd/transforms/transforms_CAPI.h +++ b/hexrd/transforms/transforms_CAPI.h @@ -35,6 +35,8 @@ static PyObject * gvecToDetectorXYArray(PyObject * self, PyObject * args); static PyObject * detectorXYToGvec(PyObject * self, PyObject * args); +static PyObject * detectorXYToGvecArray(PyObject * self, PyObject * args); + static PyObject * oscillAnglesOfHKLs(PyObject * self, PyObject * args); /******************************************************************************/ diff --git a/hexrd/transforms/transforms_CFUNC.c b/hexrd/transforms/transforms_CFUNC.c index 8cb9f7d8..f9b233c3 100644 --- a/hexrd/transforms/transforms_CFUNC.c +++ b/hexrd/transforms/transforms_CFUNC.c @@ -6,195 +6,687 @@ #include "transforms_CFUNC.h" +/* + * Microsoft's C compiler, when running in C mode, does not support the inline + * keyword. However it does support an __inline one. + * + * So if compiling with MSC, just use __inline as inline + */ +#if defined(_MSC_VER) +# define inline __inline +#endif + +/* + * For now, disable C99 codepaths + */ +#define USE_C99_CODE 0 +#if ! defined(USE_C99_CODE) +# if defined(__STDC__) +# if (__STD_VERSION__ >= 199901L) +# define USE_C99_CODE 1 +# else +# define USE_C99_CODE 0 +# endif +# endif +#endif + +#if ! USE_C99_CODE +/* + * Just remove any "restrict" keyword that may be present. + */ +#define restrict +#endif + static double epsf = 2.2e-16; static double sqrt_epsf = 1.5e-8; - static double Zl[3] = {0.0,0.0,1.0}; + /******************************************************************************/ /* Functions */ +#if USE_C99_CODE +static inline +double * +v3_v3s_inplace_add(double *dst_src1, + const double *src2, int stride) +{ + dst_src1[0] += src2[0]; + dst_src1[1] += src2[1*stride]; + dst_src1[2] += src2[2*stride]; + return dst_src1; +} -void anglesToGvec_cfunc(long int nvecs, double * angs, - double * bHat_l, double * eHat_l, - double chi, double * rMat_c, - double * gVec_c) +static inline +double * +v3_v3s_add(const double *src1, + const double *src2, int stride, + double * restrict dst) { - /* - * takes an angle spec (2*theta, eta, omega) for nvecs g-vectors and - * returns the unit g-vector components in the crystal frame - * - * For unit g-vector in the lab frame, spec rMat_c = Identity and - * overwrite the omega values with zeros - */ - int i, j, k, l; - double rMat_e[9], rMat_s[9], rMat_ctst[9]; - double gVec_e[3], gVec_l[3], gVec_c_tmp[3]; + dst[0] = src1[0] + src2[0]; + dst[1] = src1[1] + src2[1*stride]; + dst[2] = src1[2] + src2[2*stride]; - /* Need eta frame cob matrix (could omit for standard setting) */ - makeEtaFrameRotMat_cfunc(bHat_l, eHat_l, rMat_e); + return dst; +} - /* make vector array */ - for (i=0; i epsf) { + double normalize_factor = 1./sqrt(sqr_norm); + v[0] *= normalize_factor; + v[1] *= normalize_factor; + v[2] *= normalize_factor; } - /* need pointwise rMat_s according to omega */ - makeOscillRotMat_cfunc(chi, angs[3*i+2], rMat_s); + return v; +} - /* Compute dot(rMat_c.T, rMat_s.T) and hit against gVec_l */ - for (j=0; j<3; j++) { - for (k=0; k<3; k++) { - rMat_ctst[3*j+k] = 0.0; - for (l=0; l<3; l++) { - rMat_ctst[3*j+k] += rMat_c[3*l+j]*rMat_s[3*k+l]; - } - } - gVec_c_tmp[j] = 0.0; - for (k=0; k<3; k++) { - gVec_c_tmp[j] += rMat_ctst[3*j+k]*gVec_l[k]; - } - gVec_c[3*i+j] = gVec_c_tmp[j]; +static inline +double * +v3_normalize(const double *in, + double * restrict out) +{ + double in0 = in[0], in1 = in[1], in2 = in[2]; + double sqr_norm = in0*in0 + in1*in1 + in2*in2; + + if (sqr_norm > epsf) { + double normalize_factor = 1./sqrt(sqr_norm); + out[0] = in0 * normalize_factor; + out[1] = in1 * normalize_factor; + out[2] = in2 * normalize_factor; + } else { + out[0] = in0; + out[1] = in1; + out[2] = in2; } - } + + return out; +} + +static inline +double * +m33_inplace_transpose(double * restrict m) +{ + double e1 = m[1]; + double e2 = m[2]; + double e5 = m[5]; + m[1] = m[3]; + m[2] = m[6]; + m[5] = m[7]; + m[3] = e1; + m[6] = e2; + m[7] = e5; + + return m; +} + +static inline +double * +m33_transpose(const double *m, + double * restrict dst) +{ + dst[0] = m[0]; dst[1] = m[3]; dst[2] = m[6]; + dst[3] = m[1]; dst[4] = m[4]; dst[5] = m[7]; + dst[7] = m[2]; dst[8] = m[5]; dst[9] = m[9]; + + return dst; +} + +static inline +double +v3_v3s_dot(const double *v1, + const double *v2, int stride) +{ + return v1[0]*v2[0] + v1[1]*v2[stride] + v1[2]*v2[2*stride]; +} + + +/* 3x3 matrix by strided 3 vector product ------------------------------------- + hopefully a constant stride will be optimized + */ +static inline +double * +m33_v3s_multiply(const double *m, + const double *v, int stride, + double * restrict dst) +{ + dst[0] = m[0]*v[0] + m[1]*v[stride] + m[2]*v[2*stride]; + dst[1] = m[3]*v[0] + m[4]*v[stride] + m[5]*v[2*stride]; + dst[2] = m[6]*v[0] + m[7]*v[stride] + m[8]*v[2*stride]; + + return dst; +} + +/* transposed 3x3 matrix by strided 3 vector product -------------------------- + */ +static inline +double * +v3s_m33t_multiply(const double *v, int stride, + const double *m, + double * restrict dst) +{ + double v0 = v[0]; double v1 = v[stride]; double v2 = v[2*stride]; + dst[0] = v0*m[0] + v1*m[1] + v2*m[2]; + dst[1] = v0*m[3] + v1*m[4] + v2*m[5]; + dst[2] = v0*m[6] + v1*m[7] + v2*m[8]; + + return dst; +} + +static inline +double * +v3s_m33_multiply(const double *v, int stride, + const double *m, + double * restrict dst) +{ + double v0 = v[0]; double v1 = v[stride]; double v2 = v[2*stride]; + dst[0] = v0*m[0] + v1*m[3] + v2*m[6]; + dst[1] = v0*m[1] + v1*m[4] + v2*m[7]; + dst[2] = v0*m[2] + v1*m[5] + v2*m[8]; + + return dst; +} + +static inline +double * +m33t_v3s_multiply(const double *m, + const double *v, int stride, + double * restrict dst) +{ + dst[0] = m[0]*v[0] + m[3]*v[stride] + m[6]*v[2*stride]; + dst[1] = m[1]*v[0] + m[4]*v[stride] + m[7]*v[2*stride]; + dst[2] = m[2]*v[0] + m[5]*v[stride] + m[8]*v[2*stride]; + + return dst; +} + +static inline +double * +m33_m33_multiply(const double *src1, + const double *src2, + double * restrict dst) +{ + v3s_m33_multiply(src1 + 0, 1, src2, dst+0); + v3s_m33_multiply(src1 + 3, 1, src2, dst+3); + v3s_m33_multiply(src1 + 6, 1, src2, dst+6); + + return dst; +} + +static inline +double * +m33t_m33_multiply(const double *src1, + const double *src2, + double * restrict dst) +{ + v3s_m33_multiply(src1 + 0, 3, src2, dst+0); + v3s_m33_multiply(src1 + 1, 3, src2, dst+3); + v3s_m33_multiply(src1 + 2, 3, src2, dst+6); + + return dst; +} + +static inline +double * +m33_m33t_multiply(const double *src1, + const double *src2, + double * restrict dst) +{ + return m33_inplace_transpose(m33t_m33_multiply(src2, src1, dst)); +} + +static inline +double * +m33t_m33t_multiply(const double *src1, + const double *src2, + double * restrict dst) +{ + return m33_inplace_transpose(m33_m33_multiply(src2, src1, dst)); +} + +#endif + +#if USE_C99_CODE +static inline +void anglesToGvec_single(double *v3_ang, double *m33_e, + double chi, double *m33_c, + double * restrict v3_c) +{ + double v3_g[3], v3_tmp1[3], v3_tmp2[3], m33_s[9], m33_ctst[9]; + + /* build g */ + double cx = cos(0.5*v3_ang[0]); + double sx = sin(0.5*v3_ang[0]); + double cy = cos(v3_ang[1]); + double sy = sin(v3_ang[1]); + v3_g[0] = cx*cy; + v3_g[1] = cx*sy; + v3_g[2] = sx; + + /* build S */ + makeOscillRotMat_cfunc(chi, v3_ang[2], m33_s); + + /* beam frame to lab frame */ + /* eval the chain: + C.T _dot_ S.T _dot_ E _dot_ g + */ + m33_v3s_multiply (m33_e, v3_g, 1, v3_tmp1); /* E _dot_ g */ + m33t_v3s_multiply(m33_s, v3_tmp1, 1, v3_tmp2); /* S.T _dot_ E _dot_ g */ + m33t_v3s_multiply(m33_c, v3_tmp2, 1, v3_c); /* the whole lot */ } +void anglesToGvec_cfunc(long int nvecs, double * angs, + double * bHat_l, double * eHat_l, + double chi, double * rMat_c, + double * gVec_c) +{ + double m33_e[9]; + + makeEtaFrameRotMat_cfunc(bHat_l, eHat_l, m33_e); + + for (int i = 0; i= ztol && bDot <= 1.0-ztol ) { + /* + * If we are here diffraction is possible so increment the number of + * admissable vectors + */ + double brMat[9]; + makeBinaryRotMat_cfunc(gVec_l, brMat); + + double dVec_l[3]; + m33_v3s_multiply(brMat, bHat_l, 1, dVec_l); + double denom = v3_v3s_dot(nVec_l, dVec_l, 1); + + if (denom > ztol) { + double u = num/denom; + double v3_tmp[3]; + + /* v3_tmp = P0_l + u*dVec_l - tVec_d */ + for (int j=0; j<3; j++) + v3_tmp[j] = P0_l[j] + u*dVec_l[j] - tVec_d[j]; + + result[0] = v3_v3s_dot(v3_tmp, rMat_d + 0, 3); + result[1] = v3_v3s_dot(v3_tmp, rMat_d + 1, 3); + + /* result when computation can be finished */ + return; + } } - /* need pointwise rMat_s according to omega */ - makeOscillRotMat_cfunc(chi, angs[3*i+2], rMat_s); + /* default result when computation can't be finished */ + result[0] = NAN; + result[1] = NAN; +} - /* compute dot(rMat_c.T, rMat_s.T) and hit against gVec_l */ - for (j=0; j<3; j++) { - for (k=0; k<3; k++) { - rMat_ctst[3*j+k] = 0.0; - for (l=0; l<3; l++) { - rMat_ctst[3*j+k] += rMat_c[3*l+j]*rMat_s[3*k+l]; - } - } - gVec_c_tmp[j] = 0.0; - for (k=0; k<3; k++) { - gVec_c_tmp[j] += rMat_ctst[3*j+k]*gVec_l[k]; - } - gVec_c[3*i+j] = gVec_c_tmp[j]; +/* + * The only difference between this and the non-Array version + * is that rMat_s is an array of matrices of length npts instead + * of a single matrix. + */ +void gvecToDetectorXYArray_cfunc(long int npts, double * gVec_c_array, + double * rMat_d, double * rMat_s_array, double * rMat_c, + double * tVec_d, double * tVec_s, double * tVec_c, + double * beamVec, double * result_array) +{ + /* Normalize the beam vector */ + double bHat_l[3]; + v3_normalize(beamVec, bHat_l); + double nVec_l[3]; + m33_v3s_multiply(rMat_d, Zl, 1, nVec_l); + + for (size_t i = 0; i < npts; i++) { + double *rMat_s = rMat_s_array + 9*i; + double *gVec_c = gVec_c_array + 3*i; + double * restrict result = result_array + 2*i; + /* Initialize the detector normal and frame origins */ + + double P0_l[3]; + m33_v3s_multiply(rMat_s, tVec_c, 1, P0_l); + v3_v3s_inplace_add(P0_l, tVec_s, 1); + + double P3_l_minus_P0_l[3]; + v3_v3s_sub(tVec_d, P0_l, 1, P3_l_minus_P0_l); + double num = v3_v3s_dot(nVec_l, P3_l_minus_P0_l, 1); + + double gHat_c[3]; + v3_normalize(gVec_c, gHat_c); + /* + double rMat_sc[9]; + m33_m33_multiply(rMat_s, rMat_c, rMat_sc); + double gVec_l[3]; + m33_v3s_multiply(rMat_sc, gHat_c, 1, gVec_l); + */ + double tmp_vec[3], gVec_l[3]; + m33_v3s_multiply(rMat_c, gHat_c, 1, tmp_vec); + m33_v3s_multiply(rMat_s, tmp_vec, 1, gVec_l); + + double bDot = -v3_v3s_dot(bHat_l, gVec_l, 1); + double ztol = epsf; + + if (bDot < ztol || bDot > 1.0-ztol) { + result[0] = NAN; result[1] = NAN; + continue; + } + + double brMat[9]; + makeBinaryRotMat_cfunc(gVec_l, brMat); + + double dVec_l[3]; + m33_v3s_multiply(brMat, bHat_l, 1, dVec_l); + double denom = v3_v3s_dot(nVec_l, dVec_l, 1); + if (denom < ztol) { + result[0] = NAN; result[1] = NAN; + continue; + } + + double u = num/denom; + double v3_tmp[3]; + for (int j=0; j < 3; j++) + v3_tmp[j] = u*dVec_l[j] - P3_l_minus_P0_l[j]; + + result[0] = v3_v3s_dot(v3_tmp, rMat_d + 0, 3); + result[1] = v3_v3s_dot(v3_tmp, rMat_d + 1, 3); } - } } +#else void gvecToDetectorXYOne_cfunc(double * gVec_c, double * rMat_d, - double * rMat_sc, double * tVec_d, - double * bHat_l, - double * nVec_l, double num, double * P0_l, - double * result) + double * rMat_sc, double * tVec_d, + double * bHat_l, + double * nVec_l, double num, double * P0_l, + double * result) { - int j, k; - double bDot, ztol, denom, u; - double gHat_c[3], gVec_l[3], dVec_l[3], P2_l[3], P2_d[3]; - double brMat[9]; + int j, k; + double bDot, ztol, denom, u; + double gHat_c[3], gVec_l[3], dVec_l[3], P2_l[3], P2_d[3]; + double brMat[9]; - ztol = epsf; + ztol = epsf; - /* Compute unit reciprocal lattice vector in crystal frame w/o translation */ - unitRowVector_cfunc(3,gVec_c,gHat_c); + /* Compute unit reciprocal lattice vector in crystal frame w/o + translation */ + unitRowVector_cfunc(3, gVec_c, gHat_c); - /* - * Compute unit reciprocal lattice vector in lab frame - * and dot with beam vector - */ - bDot = 0.0; - for (j=0; j<3; j++) { - gVec_l[j] = 0.0; - for (k=0; k<3; k++) - gVec_l[j] += rMat_sc[3*j+k]*gHat_c[k]; + /* Compute unit reciprocal lattice vector in lab frame and dot with beam + vector */ + bDot = 0.0; + for (j=0; j<3; j++) { + gVec_l[j] = 0.0; + for (k=0; k<3; k++) + gVec_l[j] += rMat_sc[3*j+k]*gHat_c[k]; - bDot -= bHat_l[j]*gVec_l[j]; - } + bDot -= bHat_l[j]*gVec_l[j]; + } - if ( bDot >= ztol && bDot <= 1.0-ztol ) { - /* - * If we are here diffraction is possible so increment - * the number of admissable vectors - */ - makeBinaryRotMat_cfunc(gVec_l,brMat); + if ( bDot >= ztol && bDot <= 1.0-ztol ) { + /* If we are here diffraction is possible so increment the number of + admissable vectors */ + makeBinaryRotMat_cfunc(gVec_l, brMat); - denom = 0.0; - for (j=0; j<3; j++) { - dVec_l[j] = 0.0; - for (k=0; k<3; k++) - dVec_l[j] -= brMat[3*j+k]*bHat_l[k]; + denom = 0.0; + for (j=0; j<3; j++) { + dVec_l[j] = 0.0; + for (k=0; k<3; k++) + dVec_l[j] -= brMat[3*j+k]*bHat_l[k]; - denom += nVec_l[j]*dVec_l[j]; - } + denom += nVec_l[j]*dVec_l[j]; + } - if ( denom < -ztol ) { + if ( denom < -ztol ) { - u = num/denom; + u = num/denom; - for (j=0; j<3; j++) - P2_l[j] = P0_l[j]+u*dVec_l[j]; + for (j=0; j<3; j++) + P2_l[j] = P0_l[j]+u*dVec_l[j]; - for (j=0; j<2; j++) { - P2_d[j] = 0.0; - for (k=0; k<3; k++) - P2_d[j] += rMat_d[3*k+j]*(P2_l[k]-tVec_d[k]); - result[j] = P2_d[j]; - } - } else { - result[0] = NAN; - result[1] = NAN; + for (j=0; j<2; j++) { + P2_d[j] = 0.0; + for (k=0; k<3; k++) + P2_d[j] += rMat_d[3*k+j]*(P2_l[k]-tVec_d[k]); + result[j] = P2_d[j]; + } + /* result when computation can be finished */ + return; + } } - - } else { + /* default result when computation can't be finished */ result[0] = NAN; result[1] = NAN; - } } +/* + * The only difference between this and the non-Array version + * is that rMat_s is an array of matrices of length npts instead + * of a single matrix. + */ +void gvecToDetectorXYArray_cfunc(long int npts, double * gVec_c, + double * rMat_d, double * rMat_s, + double * rMat_c, double * tVec_d, + double * tVec_s, double * tVec_c, + double * beamVec, double * result) +{ + long int i; + int j, k, l; + double num; + double nVec_l[3], bHat_l[3], P0_l[3], P3_l[3]; + double rMat_sc[9]; + + /* Normalize the beam vector */ + unitRowVector_cfunc(3,beamVec,bHat_l); + + for (i=0L; i < npts; i++) { + /* Initialize the detector normal and frame origins */ + num = 0.0; + for (j=0; j<3; j++) { + nVec_l[j] = 0.0; + P0_l[j] = tVec_s[j]; + + for (k=0; k<3; k++) { + nVec_l[j] += rMat_d[3*j+k]*Zl[k]; + P0_l[j] += rMat_s[9*i + 3*j+k]*tVec_c[k]; + } + + P3_l[j] = tVec_d[j]; + + num += nVec_l[j]*(P3_l[j]-P0_l[j]); + } + + /* Compute the matrix product of rMat_s and rMat_c */ + for (j=0; j<3; j++) { + for (k=0; k<3; k++) { + rMat_sc[3*j+k] = 0.0; + for (l=0; l<3; l++) { + rMat_sc[3*j+k] += rMat_s[9*i + 3*j+l]*rMat_c[3*l+k]; + } + } + } + + gvecToDetectorXYOne_cfunc(gVec_c + 3*i, rMat_d, rMat_sc, + tVec_d, bHat_l, nVec_l, num, + P0_l, result + 2*i); + } +} + +#endif void gvecToDetectorXY_cfunc(long int npts, double * gVec_c, - double * rMat_d, double * rMat_s, double * rMat_c, - double * tVec_d, double * tVec_s, double * tVec_c, - double * beamVec, double * result) + double * rMat_d, double * rMat_s, double * rMat_c, + double * tVec_d, double * tVec_s, double * tVec_c, + double * beamVec, double * result) { long int i; int j, k, l; @@ -206,9 +698,9 @@ void gvecToDetectorXY_cfunc(long int npts, double * gVec_c, /* Normalize the beam vector */ unitRowVector_cfunc(3,beamVec,bHat_l); - /* Initialize the detector normal and frame origins */ - num = 0.0; - for (j=0; j<3; j++) { + /* Initialize the detector normal and frame origins */ + num = 0.0; + for (j=0; j<3; j++) { nVec_l[j] = 0.0; P0_l[j] = tVec_s[j]; @@ -220,87 +712,116 @@ void gvecToDetectorXY_cfunc(long int npts, double * gVec_c, P3_l[j] = tVec_d[j]; num += nVec_l[j]*(P3_l[j]-P0_l[j]); - } + } - /* Compute the matrix product of rMat_s and rMat_c */ - for (j=0; j<3; j++) { - for (k=0; k<3; k++) { - rMat_sc[3*j+k] = 0.0; - for (l=0; l<3; l++) { - rMat_sc[3*j+k] += rMat_s[3*j+l]*rMat_c[3*l+k]; + /* Compute the matrix product of rMat_s and rMat_c */ + for (j=0; j<3; j++) { + for (k=0; k<3; k++) { + rMat_sc[3*j+k] = 0.0; + for (l=0; l<3; l++) { + rMat_sc[3*j+k] += rMat_s[3*j+l]*rMat_c[3*l+k]; + } } } - } for (i=0L; i epsf ) { + double nrm_factor = 1.0/sqrt(nrm); + for (j=0; j<3; j++) { + dHat_l[j] *= nrm_factor; + } + } - P3_l[j] = tVec_d[j]; + /* Compute tTh */ + b_dot_dHat_l = 0.0; + for (j=0; j<3; j++) { + b_dot_dHat_l += bVec[j]*dHat_l[j]; + } + tTh = acos(b_dot_dHat_l); - num += nVec_l[j]*(P3_l[j]-P0_l[j]); - } + /* Compute eta */ + for (j=0; j<2; j++) { + tVec2[j] = 0.0; + for (k=0; k<3; k++) { + tVec2[j] += rMat_e[3*k+j]*dHat_l[k]; + } + } + eta = atan2(tVec2[1], tVec2[0]); - /* Compute the matrix product of rMat_s and rMat_c */ + /* Compute n_g vector */ + nrm = 0.0; for (j=0; j<3; j++) { - for (k=0; k<3; k++) { - rMat_sc[3*j+k] = 0.0; - for (l=0; l<3; l++) { - rMat_sc[3*j+k] += rMat_s[9*i + 3*j+l]*rMat_c[3*l+k]; - } - } + double val; + int j1 = j < 2 ? j+1 : 0; + int j2 = j > 0 ? j-1 : 2; + val = bVec[j1] * dHat_l[j2] - bVec[j2] * dHat_l[j1]; + nrm += val*val; + n_g[j] = val; + } + if ( nrm > epsf ) { + double nrm_factor = 1.0/sqrt(nrm); + for (j=0; j<3; j++) { + n_g[j] *= nrm_factor; + } } - gvecToDetectorXYOne_cfunc(&gVec_c[3*i], rMat_d, rMat_sc, tVec_d, - bHat_l, nVec_l, num, - P0_l, &result[2*i]); - } + /* Rotate dHat_l vector */ + phi = 0.5*(M_PI-tTh); + *tTh_out = tTh; + *eta_out = eta; + rotate_vecs_about_axis_cfunc(1, &phi, 1, n_g, 1, dHat_l, gVec_l_out); } void detectorXYToGvec_cfunc(long int npts, double * xy, - double * rMat_d, double * rMat_s, - double * tVec_d, double * tVec_s, double * tVec_c, - double * beamVec, double * etaVec, - double * tTh, double * eta, double * gVec_l) + double * rMat_d, double * rMat_s, + double * tVec_d, double * tVec_s, double * tVec_c, + double * beamVec, double * etaVec, + double * tTh, double * eta, double * gVec_l) { long int i; int j, k; - double nrm, phi, bVec[3], tVec1[3], tVec2[3], dHat_l[3], n_g[3]; + double nrm, bVec[3], tVec1[3]; double rMat_e[9]; /* Fill rMat_e */ @@ -311,10 +832,11 @@ void detectorXYToGvec_cfunc(long int npts, double * xy, for (j=0; j<3; j++) { nrm += beamVec[j]*beamVec[j]; } - nrm = sqrt(nrm); + if ( nrm > epsf ) { + double nrm_factor = 1.0/sqrt(nrm); for (j=0; j<3; j++) - bVec[j] = beamVec[j]/nrm; + bVec[j] = beamVec[j]*nrm_factor; } else { for (j=0; j<3; j++) bVec[j] = beamVec[j]; @@ -329,59 +851,66 @@ void detectorXYToGvec_cfunc(long int npts, double * xy, } for (i=0; i epsf ) { - for (j=0; j<3; j++) { - dHat_l[j] /= sqrt(nrm); + double nrm_factor = 1.0/sqrt(nrm); + for (j=0; j<3; j++) + bVec[j] = beamVec[j]*nrm_factor; + } else { + for (j=0; j<3; j++) + bVec[j] = beamVec[j]; } - } - /* Compute tTh */ - nrm = 0.0; for (j=0; j<3; j++) { - nrm += bVec[j]*dHat_l[j]; - } - tTh[i] = acos(nrm); - - /* Compute eta */ - for (j=0; j<2; j++) { - tVec2[j] = 0.0; + tVec1[j] = tVec_d[j]-tVec_s[j]; for (k=0; k<3; k++) { - tVec2[j] += rMat_e[3*k+j]*dHat_l[k]; + tVec1[j] -= rMat_s[3*j+k]*tVec_c[k]; } } - eta[i] = atan2(tVec2[1],tVec2[0]); - /* Compute n_g vector */ - nrm = 0.0; + for (i=0; i epsf ) { for (j=0; j<3; j++) { - gHat_c[j] /= nrm0; - gHat_s[j] = tmpVec[j]/nrm0; + gHat_c[j] /= nrm0; + gHat_s[j] = tmpVec[j]/nrm0; } } @@ -490,9 +1019,9 @@ void oscillAnglesOfHKLs_cfunc(long int npts, double * hkls, double chi, if ( fabs(rhs) > 1.0 ) { for (j=0; j<3; j++) - oangs0[3L*i+j] = NAN; + oangs0[3L*i+j] = NAN; for (j=0; j<3; j++) - oangs1[3L*i+j] = NAN; + oangs1[3L*i+j] = NAN; continue; } @@ -511,16 +1040,16 @@ void oscillAnglesOfHKLs_cfunc(long int npts, double * hkls, double chi, makeOscillRotMat_cfunc(oVec[0], oVec[1], rMat_s); for (j=0; j<3; j++) { - tVec0[j] = 0.0; - for (k=0; k<3; k++) { - tVec0[j] += rMat_s[3*j+k]*gHat_s[k]; - } + tVec0[j] = 0.0; + for (k=0; k<3; k++) { + tVec0[j] += rMat_s[3*j+k]*gHat_s[k]; + } } for (j=0; j<2; j++) { - gVec_e[j] = 0.0; - for (k=0; k<3; k++) { - gVec_e[j] += rMat_e[3*k+j]*tVec0[k]; - } + gVec_e[j] = 0.0; + for (k=0; k<3; k++) { + gVec_e[j] += rMat_e[3*k+j]*tVec0[k]; + } } oangs0[3L*i+1] = atan2(gVec_e[1],gVec_e[0]); @@ -528,16 +1057,16 @@ void oscillAnglesOfHKLs_cfunc(long int npts, double * hkls, double chi, makeOscillRotMat_cfunc(oVec[0], oVec[1], rMat_s); for (j=0; j<3; j++) { - tVec0[j] = 0.0; - for (k=0; k<3; k++) { - tVec0[j] += rMat_s[3*j+k]*gHat_s[k]; - } + tVec0[j] = 0.0; + for (k=0; k<3; k++) { + tVec0[j] += rMat_s[3*j+k]*gHat_s[k]; + } } for (j=0; j<2; j++) { - gVec_e[j] = 0.0; - for (k=0; k<3; k++) { - gVec_e[j] += rMat_e[3*k+j]*tVec0[k]; - } + gVec_e[j] = 0.0; + for (k=0; k<3; k++) { + gVec_e[j] += rMat_e[3*k+j]*tVec0[k]; + } } oangs1[3L*i+1] = atan2(gVec_e[1],gVec_e[0]); @@ -584,11 +1113,11 @@ void unitRowVectors_cfunc(int m, int n, double * cIn, double * cOut) nrm = sqrt(nrm); if ( nrm > epsf ) { for (j=0; j 2.0*M_PI ) - thetaMax -= 2.0*M_PI; + thetaMax -= 2.0*M_PI; while ( theta < 0.0 ) - theta += 2.0*M_PI; + theta += 2.0*M_PI; while ( theta > 2.0*M_PI ) - theta -= 2.0*M_PI; + theta -= 2.0*M_PI; if ( theta > -sqrt_epsf && theta < thetaMax + sqrt_epsf ) { - rPtr[i] = true; + rPtr[i] = true; - /* No need to check other ranges */ - break; + /* No need to check other ranges */ + break; } } } } void validateAngleRanges_cfunc(int na, double * aPtr, int nr, - double * minPtr, double * maxPtr, - bool * rPtr, int ccw) + double * minPtr, double * maxPtr, + bool * rPtr, int ccw) { int i, j; double thetaMax, theta; @@ -822,58 +1354,59 @@ void validateAngleRanges_cfunc(int na, double * aPtr, int nr, for (j=0; j 2.0*M_PI ) - thetaMax -= 2.0*M_PI; + thetaMax -= 2.0*M_PI; /* Check for an empty range */ if ( fabs(thetaMax) < sqrt_epsf ) { - rPtr[i] = true; + rPtr[i] = true; - /* No need to check other ranges */ - break; + /* No need to check other ranges */ + break; } /* Check for a range which spans a full circle */ if ( fabs(thetaMax-2.0*M_PI) < sqrt_epsf ) { - /* Double check the initial range */ - if ( (ccw && maxPtr[j] > minPtr[j]) || ((!ccw) && maxPtr[j] < minPtr[j]) ) { - rPtr[i] = true; + /* Double check the initial range */ + if ( (ccw && maxPtr[j] > minPtr[j]) || ((!ccw) && maxPtr[j] < minPtr[j]) ) { + rPtr[i] = true; - /* No need to check other ranges */ - break; - } + /* No need to check other ranges */ + break; + } } while ( theta < 0.0 ) - theta += 2.0*M_PI; + theta += 2.0*M_PI; while ( theta > 2.0*M_PI ) - theta -= 2.0*M_PI; + theta -= 2.0*M_PI; if ( theta >= -sqrt_epsf && theta <= thetaMax+sqrt_epsf ) { - rPtr[i] = true; + rPtr[i] = true; - /* No need to check other ranges */ - break; + /* No need to check other ranges */ + break; } } } } + void rotate_vecs_about_axis_cfunc(long int na, double * angles, - long int nax, double * axes, - long int nv, double * vecs, - double * rVecs) + long int nax, double * axes, + long int nv, double * vecs, + double * rVecs) { int i, j, sa, sax; double c, s, nrm, proj, aCrossV[3]; @@ -898,7 +1431,7 @@ void rotate_vecs_about_axis_cfunc(long int na, double * angles, if ( nax > 1 || i == 0 ) { nrm = 0.0; for (j=0; j<3; j++) - nrm += axes[sax*i+j]*axes[sax*i+j]; + nrm += axes[sax*i+j]*axes[sax*i+j]; nrm = sqrt(nrm); } @@ -969,9 +1502,9 @@ void homochoricOfQuat_cfunc(int nq, double * qPtr, double * hPtr) if (phi > epsf) { arg = 0.75*(phi - sin(phi)); if (arg < 0.) { - f = -pow(-arg, 1./3.); + f = -pow(-arg, 1./3.); } else { - f = pow(arg, 1./3.); + f = pow(arg, 1./3.); } s = 1. / sin(0.5*phi); diff --git a/hexrd/transforms/transforms_CFUNC.h b/hexrd/transforms/transforms_CFUNC.h index 7460e8d1..628883f7 100644 --- a/hexrd/transforms/transforms_CFUNC.h +++ b/hexrd/transforms/transforms_CFUNC.h @@ -53,6 +53,12 @@ void detectorXYToGvec_cfunc(long int npts, double * xy, double * beamVec, double * etaVec, double * tTh, double * eta, double * gVec_l); +void detectorXYToGvecArray_cfunc(long int npts, double * xy, + double * rMat_d, double * rMat_s, + double * tVec_d, double * tVec_s, double * tVec_c, + double * beamVec, double * etaVec, + double * tTh, double * eta, double * gVec_l); + void oscillAnglesOfHKLs_cfunc(long int npts, double * hkls, double chi, double * rMat_c, double * bMat, double wavelength, double * vInv_s, double * beamVec, double * etaVec, diff --git a/hexrd/xrd/transforms_CAPI.py b/hexrd/xrd/transforms_CAPI.py index 1eb71f13..0b4d9fac 100644 --- a/hexrd/xrd/transforms_CAPI.py +++ b/hexrd/xrd/transforms_CAPI.py @@ -137,6 +137,9 @@ def gvecToDetectorXY(gVec_c, (m, 2) ndarray containing the intersections of m <= n diffracted beams associated with gVecs """ + rMat_d = np.ascontiguousarray( rMat_d ) + rMat_s = np.ascontiguousarray( rMat_s ) + rMat_c = np.ascontiguousarray( rMat_c ) gVec_c = np.ascontiguousarray( np.atleast_2d( gVec_c ) ) tVec_d = np.ascontiguousarray( tVec_d.flatten() ) tVec_s = np.ascontiguousarray( tVec_s.flatten() ) @@ -172,7 +175,9 @@ def gvecToDetectorXYArray(gVec_c, associated with gVecs """ gVec_c = np.ascontiguousarray( gVec_c ) + rMat_d = np.ascontiguousarray( rMat_d ) rMat_s = np.ascontiguousarray( rMat_s ) + rMat_c = np.ascontiguousarray( rMat_c ) tVec_d = np.ascontiguousarray( tVec_d.flatten() ) tVec_s = np.ascontiguousarray( tVec_s.flatten() ) tVec_c = np.ascontiguousarray( tVec_c.flatten() ) @@ -209,6 +214,8 @@ def detectorXYToGvec(xy_det, associated with gVecs """ xy_det = np.ascontiguousarray( np.atleast_2d(xy_det) ) + rMat_d = np.ascontiguousarray( rMat_d ) + rMat_s = np.ascontiguousarray( rMat_s ) tVec_d = np.ascontiguousarray( tVec_d.flatten() ) tVec_s = np.ascontiguousarray( tVec_s.flatten() ) tVec_c = np.ascontiguousarray( tVec_c.flatten() ) @@ -219,6 +226,46 @@ def detectorXYToGvec(xy_det, tVec_d, tVec_s, tVec_c, beamVec, etaVec) +def detectorXYToGvecArray(xy_det, + rMat_d, rMat_s, + tVec_d, tVec_s, tVec_c, + beamVec=bVec_ref, etaVec=eta_ref): + """ + Takes a list cartesian (x, y) pairs in the detector coordinates and calculates + the associated reciprocal lattice (G) vectors and (bragg angle, azimuth) pairs + with respect to the specified beam and azimth (eta) reference directions + + Required Arguments: + xy_det -- (n, 2) ndarray or list-like input of n detector (x, y) points + rMat_d -- (3, 3) ndarray, the COB taking DETECTOR FRAME components to LAB FRAME + rMat_s -- (n, 3, 3) ndarray, the COB taking SAMPLE FRAME components to LAB FRAME + tVec_d -- (3, 1) ndarray, the translation vector connecting LAB to DETECTOR in LAB + tVec_s -- (3, 1) ndarray, the translation vector connecting LAB to SAMPLE in LAB + tVec_c -- (3, 1) ndarray, the translation vector connecting SAMPLE to CRYSTAL in SAMPLE + + Optional Keyword Arguments: + beamVec -- (3, 1) mdarray containing the incident beam direction components in the LAB FRAME + etaVec -- (3, 1) mdarray containing the reference azimuth direction components in the LAB FRAME + + Outputs: + (n, 2) ndarray containing the (tTh, eta) pairs associated with each (x, y) + (n, 3) ndarray containing the associated G vector directions in the LAB FRAME + associated with gVecs + """ + xy_det = np.ascontiguousarray( np.atleast_2d(xy_det) ) + rMat_d = np.ascontiguousarray( rMat_d ) + rMat_s = np.ascontiguousarray( rMat_s ) + tVec_d = np.ascontiguousarray( tVec_d.flatten() ) + tVec_s = np.ascontiguousarray( tVec_s.flatten() ) + tVec_c = np.ascontiguousarray( tVec_c.flatten() ) + beamVec = np.ascontiguousarray( beamVec.flatten() ) + etaVec = np.ascontiguousarray( etaVec.flatten() ) + return _transforms_CAPI.detectorXYToGvec(xy_det, + rMat_d, rMat_s, + tVec_d, tVec_s, tVec_c, + beamVec, etaVec) + + def oscillAnglesOfHKLs(hkls, chi, rMat_c, bMat, wavelength, vInv=None, beamVec=bVec_ref, etaVec=eta_ref): """ From 7ebd8e8df7484ec2ef4af8126b6b480759a86342 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 1 May 2018 13:43:31 -0700 Subject: [PATCH 138/253] Create build.rst build instructions for master --- docs/build.rst | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/build.rst diff --git a/docs/build.rst b/docs/build.rst new file mode 100644 index 00000000..c249e20b --- /dev/null +++ b/docs/build.rst @@ -0,0 +1,78 @@ +HEXRD Build Instructions +------------------------ + +The preferred method for building the HEXRD package is via the conda +recipe located in ``/conda.recipe`` + +Requirements +------------ +The following tools are needed to build the package:: + + conda + conda-build + +With `Anaconda `_-based Python +environments, you should be able to run:: + + conda build conda.recipe/ + +Building +-------- + +The procedure for building/installing is as follows + +First, update conda and conda-build:: + + conda update conda + conda update conda-build + +Second, using ``conda-build``, purge previous builds (recommended, +not strictly required):: + + conda build purge + +In the event that you have previously run either +``python setup.py develop`` OR ``python setup.py install``, then first run +either:: + + python setup.py develop --uninstall + +or:: + + python setup.py install --record files.txt + cat files.txt | xargs rm -rf + +depending on how it was installed using ``distutils``. This will +remove any old builds/links. + +Note that the "nuclear option" for removing hexrd is as follows:: + + rm -rf /lib/python2.7/site-packages/hexrd* + rm /bin/hexrd* + +If you have installed ``hexrd`` in a specific conda environment, then +be sure to use the proper path to ``lib/`` under the root anaconda directory. + +Next, run ``conda-build``:: + + conda build conda.recipe/ --no-test + +Note that the ``--no-test`` flag supresses running the internal tests +until they are fixed (stay tuned...) + +Installation +------------ + +Findally, run ``conda install`` using the local package:: + + conda install hexrd=0.5 --use-local + +Conda should echo the proper version number package in the package +install list, which includes all dependencies. + +At this point, a check in a fresh terminal (outside the root hexrd +directory) and run:: + + hexrd --verison + +It should currently read ``hexrd 0.5.14`` From 8fa78657c4c7d69e6c0d528bb96e4ce99505e13c Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 1 May 2018 14:46:16 -0700 Subject: [PATCH 139/253] change default buffer value --- hexrd/instrument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index b13890b9..614b28d4 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1032,7 +1032,7 @@ def __init__(self, self._saturation_level = saturation_level if panel_buffer is None: - self._panel_buffer = 25*np.r_[self._pixel_size_col, + self._panel_buffer = 20*np.r_[self._pixel_size_col, self._pixel_size_row] self._roi = roi From b95d506ce9cc9e60be5b9f71956073e82b029149 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 3 May 2018 16:35:20 -0700 Subject: [PATCH 140/253] changes for debugging vs v0.3.x --- hexrd/instrument.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 614b28d4..d1705895 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -746,7 +746,7 @@ def pull_spots(self, plane_data, grain_params, panel.rmat, rMat_c, self.chi, panel.tvec, tVec_c, self.tvec, panel.distortion) - scrap, on_panel = panel.clip_to_panel(det_xy) + _, on_panel = panel.clip_to_panel(det_xy, buffer_edges=False) # all vertices must be on... patch_is_on = np.all(on_panel.reshape(nangs, 4), axis=1) @@ -806,6 +806,7 @@ def pull_spots(self, plane_data, grain_params, # strip relevant objects out of current patch vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch + prows, pcols = areas.shape nrm_fac = areas/float(native_area) nrm_fac = nrm_fac / np.min(nrm_fac) @@ -936,7 +937,7 @@ def pull_spots(self, plane_data, grain_params, # Those are segmented from interpolated data, # not raw; likely ok in most cases. - # need xy coords + # need MEASURED xy coords gvec_c = anglesToGVec( meas_angs, chi=self.chi, @@ -959,6 +960,30 @@ def pull_spots(self, plane_data, grain_params, pass # FIXME: why is this suddenly necessary??? meas_xy = meas_xy.squeeze() + + # need PREDICTED xy coords + gvec_c = anglesToGVec( + ang_centers[i_pt], + chi=self.chi, + rMat_c=rMat_c, + bHat_l=self.beam_vector) + rMat_s = makeOscillRotMat( + [self.chi, ang_centers[i_pt][2]] + ) + pred_xy = gvecToDetectorXY( + gvec_c, + panel.rmat, rMat_s, rMat_c, + panel.tvec, self.tvec, tVec_c, + beamVec=self.beam_vector) + if panel.distortion is not None: + # FIXME: distortion handling + pred_xy = panel.distortion[0]( + np.atleast_2d(pred_xy), + panel.distortion[1], + invert=True).flatten() + pass + # FIXME: why is this suddenly necessary??? + pred_xy = pred_xy.squeeze() pass # end num_peaks > 0 pass # end contains_signal # write output @@ -975,7 +1000,7 @@ def pull_spots(self, plane_data, grain_params, detector_id, iRefl, peak_id, hkl_id, hkl, tth_edges, eta_edges, np.radians(ome_eval), xyc_arr, ijs, frame_indices, patch_data, - ang_centers[i_pt], meas_angs, meas_xy) + ang_centers[i_pt], pred_xy, meas_angs, meas_xy) pass # end conditional on write output pass # end conditional on check only patch_output.append([ @@ -1426,6 +1451,8 @@ def clip_to_panel(self, xy, buffer_edges=True): xy[:, 1] >= -ylim, xy[:, 1] <= ylim ) on_panel = np.logical_and(on_panel_x, on_panel_y) + elif not buffer_edges: + on_panel = np.ones(len(xy), dtype=bool) return xy[on_panel, :], on_panel def cart_to_angles(self, xy_data): @@ -2038,7 +2065,7 @@ def dump_patch(self, panel_id, i_refl, peak_id, hkl_id, hkl, tth_edges, eta_edges, ome_centers, xy_centers, ijs, frame_indices, - spot_data, pangs, mangs, mxy, gzip=9): + spot_data, pangs, pxy, mangs, mxy, gzip=9): """ to be called inside loop over patches @@ -2052,6 +2079,7 @@ def dump_patch(self, panel_id, spot_grp.attrs.create('hkl_id', hkl_id) spot_grp.attrs.create('hkl', hkl) spot_grp.attrs.create('predicted_angles', pangs) + spot_grp.attrs.create('predicted_xy', pxy) if mangs is None: mangs = np.nan*np.ones(3) spot_grp.attrs.create('measured_angles', mangs) From e07aedc54dca30b8b030d86cfef3ec6abff39c87 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 3 May 2018 17:04:11 -0700 Subject: [PATCH 141/253] reconcile xrdutil with v0.3.x updates --- hexrd/xrd/xrdutil.py | 226 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 204 insertions(+), 22 deletions(-) diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index c801a0eb..3bf535ea 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -32,7 +32,7 @@ from math import pi import shelve - +import h5py import numpy as num from scipy import sparse @@ -4176,7 +4176,8 @@ def pullSpots(pd, detector_params, grain_params, reader, npdiv=1, threshold=10, doClipping=False, filename=None, save_spot_list=False, use_closest=True, - quiet=True): + discard_at_bounds=True, + quiet=True, output_hdf5=True): """ Function for pulling spots from a reader object for specific detector panel and crystal specifications @@ -4242,6 +4243,8 @@ def pullSpots(pd, detector_params, grain_params, reader, iframe = num.arange(0, nframes) + # If this is 0, then scan covers a full 360 + # !!! perhaps need to catch roundoff better? full_range = xf.angularDifference(ome_range[0], ome_range[1]) if ome_tol <= 0.5*r2d*abs(del_ome): @@ -4279,6 +4282,11 @@ def pullSpots(pd, detector_params, grain_params, reader, "pred tth \tpred eta \t pred ome \t" + \ "meas tth \tmeas eta \t meas ome \t" + \ "meas X \tmeas Y \t meas ome\n#" + if output_hdf5: + # !!! this is a bit kludgey as it puts constraints on the filename... + h5fname = fid.name.split('.')[0] + gw = GrainDataWriter_h5(h5fname, detector_params, grain_params) + iRefl = 0 spot_list = [] for hklid, hkl, angs, xy, pix in zip(*sim_g): @@ -4361,12 +4369,22 @@ def pullSpots(pd, detector_params, grain_params, reader, patch_i = patch_i.flatten(); patch_j = patch_j.flatten() # read frame in, splitting reader if necessary + # !!! in cases without full ranges, perhaps skip spot completely if ome_tol falls outside ranges? split_reader = False if min(frame_indices) < 0: if full_range > 0: - reidx = num.where(frame_indices >= 0)[0] - sdims[0] = len(reidx) - frame_indices = frame_indices[reidx] + if discard_at_bounds: + # our tol box has run outside scan range + # !!! in this case we will DISCARD the spot + if not quiet: + print "(%d, %d, %d): window falls below omega range; skipping..." \ + % tuple(hkl) + continue + else: + reidx = num.where(frame_indices >= 0)[0] + sdims[0] = len(reidx) + ome_centers = ome_centers[reidx] + frame_indices = frame_indices[reidx] elif full_range == 0: split_reader = True reidx1 = num.where(frame_indices < 0)[0] @@ -4375,9 +4393,18 @@ def pullSpots(pd, detector_params, grain_params, reader, oidx2 = frame_indices[reidx2] if max(frame_indices) >= nframes: if full_range > 0: - reidx = num.where(frame_indices < nframes)[0] - sdims[0] = len(reidx) - frame_indices = frame_indices[reidx] + if discard_at_bounds: + # our tol box has run outside scan range + # !!! in this case we will DISCARD the spot + if not quiet: + print "(%d, %d, %d): window falls above omega range; skipping..." \ + % tuple(hkl) + continue + else: + reidx = num.where(frame_indices < nframes)[0] + sdims[0] = len(reidx) + ome_centers = ome_centers[reidx] + frame_indices = frame_indices[reidx] elif full_range == 0: split_reader = True reidx1 = num.where(frame_indices < nframes)[0] @@ -4392,7 +4419,6 @@ def pullSpots(pd, detector_params, grain_params, reader, frames = num.hstack([f1, f2]) else: frames = reader[0][frame_indices[0]:sdims[0]+frame_indices[0]] - else: rdr = reader.makeNew() if split_reader: @@ -4510,6 +4536,7 @@ def pullSpots(pd, detector_params, grain_params, reader, spot_intensity = num.sum(spot_data[labels == 1]) max_intensity = num.max(spot_data[labels == 1]) pass + if coms is not None: com_angs = num.array([tth_edges[0] + (0.5 + coms[2])*delta_tth, eta_edges[0] + (0.5 + coms[1])*delta_eta, @@ -4531,9 +4558,32 @@ def pullSpots(pd, detector_params, grain_params, reader, spot_intensity = num.nan max_intensity = num.nan pass + + # ################################# + # generate PREDICTED xy coords + rMat_s = xfcapi.makeOscillRotMat([chi, angs[2]]) + gVec_c = xf.anglesToGVec(num.atleast_2d(angs), bVec, eVec, + rMat_s=rMat_s, rMat_c=rMat_c) + # these are on ``ideal'' detector + pxy = xfcapi.gvecToDetectorXY( + gVec_c.T, + rMat_d, rMat_s, rMat_c, + tVec_d, tVec_s, tVec_c + ).flatten() + # apply inverser distortion (if provided) + if distortion is not None and len(distortion) == 2: + pxy = distortion[0]( + num.atleast_2d(new_xy), + distortion[1], + invert=True + ).flatten() # + # ################################# + + # ===================================================================== # OUTPUT - # + # ===================================================================== + # output dictionary if save_spot_list: w_dict = {} @@ -4559,26 +4609,58 @@ def pullSpots(pd, detector_params, grain_params, reader, spot_list.append(w_dict) pass if filename is not None: + nans_tabbed_12_2 = '{:^12}\t{:^12}\t' + nans_tabbed_18_6 = '{:^18}\t{:^18}\t{:^18}\t{:^18}\t{:^18}\t{:^18}' + output_str = \ + '{:<6d}\t{:<6d}\t'.format(int(peakId), int(hklid)) + \ + '{:<3d}\t{:<3d}\t{:<3d}\t'.format(*num.array(hkl, dtype=int)) if peakId >= 0: - print >> fid, "%d\t%d\t" % (peakId, hklid) + \ - "%d\t%d\t%d\t" % tuple(hkl) + \ - "%1.6e\t%1.6e\t" % (spot_intensity, max_intensity) + \ - "%1.12e\t%1.12e\t%1.12e\t" % tuple(angs) + \ - "%1.12e\t%1.12e\t%1.12e\t" % tuple(com_angs) + \ - "%1.12e\t%1.12e\t%1.12e" % (new_xy[0], new_xy[1], com_angs[2]) + output_str += \ + '{:<1.6e}\t{:<1.6e}\t'.format(spot_intensity, max_intensity) + \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*angs) + \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*com_angs) + \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}'.format(new_xy[0], new_xy[1], com_angs[2]) + #print >> fid, "%d\t%d\t" % (peakId, hklid) + \ + # "%d\t%d\t%d\t" % tuple(hkl) + \ + # "%1.6e\t%1.6e\t" % (spot_intensity, max_intensity) + \ + # "%1.12e\t%1.12e\t%1.12e\t" % tuple(angs) + \ + # "%1.12e\t%1.12e\t%1.12e\t" % tuple(com_angs) + \ + # "%1.12e\t%1.12e\t%1.12e" % (new_xy[0], new_xy[1], com_angs[2]) else: - print >> fid, "%d\t%d\t" % (peakId, hklid) + \ - "%d\t%d\t%d\t" % tuple(hkl) + \ - "%f \t%f \t" % tuple(num.nan*num.ones(2)) + \ - "%1.12e\t%1.12e\t%1.12e\t" % tuple(angs) + \ - "%f \t%f \t%f" % tuple(num.nan*num.ones(3)) + \ - " \t%f \t%f \t%f" % tuple(num.nan*num.ones(3)) + output_str += \ + nans_tabbed_12_2.format(*nans_2) + \ + '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*angs) + \ + nans_tabbed_18_6.format(*nans_6) + #print >> fid, "%d\t%d\t" % (peakId, hklid) + \ + # "%d\t%d\t%d\t" % tuple(hkl) + \ + # "%f \t%f \t" % tuple(nans_2) + \ + # "%1.12e\t%1.12e\t%1.12e\t" % tuple(angs) + \ + # "%f \t%f \t%f" % tuple(nans_3) + \ + # " \t%f \t%f \t%f" % tuple(nans_3) + pass + print >> fid, output_str + + if output_hdf5: + if peakId < 0: + mangs = nans_3 + mxy = nans_2 + else: + mangs = com_angs + mxy = new_xy + ijs = num.stack([row_indices, col_indices]) + gw.dump_patch( + iRefl, peakId, hklid, hkl, + tth_eta_cen[:, 0], tth_eta_cen[:, 1], ome_centers, + xy_eval, ijs, frame_indices, + spot_data, angs, pxy, mangs, mxy, gzip=9) pass pass iRefl += 1 pass if filename is not None: fid.close() + if output_hdf5: + gw.close() return spot_list @@ -4604,6 +4686,106 @@ def extract_detector_transformation(detector_params): return rMat_d, tVec_d, chi, tVec_s +class GrainDataWriter_h5(object): + """ + TODO: add material spec + """ + def __init__(self, filename, detector_params, grain_params): + use_attr = True + if isinstance(filename, h5py.File): + self.fid = filename + else: + self.fid = h5py.File(filename + '.hdf5', 'w') + + # add instrument groups and attributes + self.instr_grp = self.fid.create_group('instrument') + rMat_d, tVec_d, chi, tVec_s = extract_detector_transformation(detector_params) + #self.instr_grp.attrs.create('rmat_d', rMat_d) + #self.instr_grp.attrs.create('tvec_d', tVec_d.flatten()) + #self.instr_grp.attrs.create('chi', chi) + #self.instr_grp.attrs.create('tvec_s', tVec_s.flatten()) + self.instr_grp.create_dataset('rmat_d', data=rMat_d) + self.instr_grp.create_dataset('tvec_d', data=tVec_d.flatten()) + self.instr_grp.create_dataset('chi', data=chi) + self.instr_grp.create_dataset('tvec_s', data=tVec_s.flatten()) + + + + self.grain_grp = self.fid.create_group('grain') + rMat_c = xfcapi.makeRotMatOfExpMap(grain_params[:3]) + tVec_c = num.array(grain_params[3:6]).flatten() + vInv_s = num.array(grain_params[6:]).flatten() + vMat_s = num.linalg.inv(mutil.vecMVToSymm(vInv_s)) + #self.grain_grp.attrs.create('rmat_c', rMat_c) + #self.grain_grp.attrs.create('tvec_c', tVec_c.flatten()) + #self.grain_grp.attrs.create('inv(V)_s', vInv_s) + #self.grain_grp.attrs.create('vmat_s', vMat_s) + self.grain_grp.create_dataset('rmat_c', data=rMat_c) + self.grain_grp.create_dataset('tvec_c', data=tVec_c.flatten()) + self.grain_grp.create_dataset('inv(V)_s', data=vInv_s) + self.grain_grp.create_dataset('vmat_s', data=vMat_s) + + # add grain parameter + data_key = 'reflection_data' + self.data_grp = self.fid.create_group(data_key) + + # FIXME: throws exception when called after close method + # def __del__(self): + # self.close() + + def close(self): + self.fid.close() + + def dump_patch(self, + i_refl, peak_id, hkl_id, hkl, + tth_centers, eta_centers, ome_centers, + xy_centers, ijs, frame_indices, + spot_data, pangs, pxy, mangs, mxy, gzip=4): + """ + to be called inside loop over patches + + default GZIP level for data arrays is 4 + """ + + # create spot group + spot_grp = self.data_grp.create_group("spot_%05d" % i_refl) + spot_grp.attrs.create('peak_id', peak_id) + spot_grp.attrs.create('hkl_id', hkl_id) + spot_grp.attrs.create('hkl', hkl) + spot_grp.attrs.create('predicted_angles', pangs) + spot_grp.attrs.create('predicted_xy', pxy) + if mangs is None: + mangs = num.nan*num.ones(3) + spot_grp.attrs.create('measured_angles', mangs) + if mxy is None: + mxy = num.nan*num.ones(3) + spot_grp.attrs.create('measured_xy', mxy) + + # get centers crds from edge arrays + ome_dim, eta_dim, tth_dim = spot_data.shape + + tth_crd = tth_centers.reshape(eta_dim, tth_dim) + eta_crd = eta_centers.reshape(eta_dim, tth_dim) + ome_crd = num.tile(ome_centers, (eta_dim*tth_dim, 1)).T.reshape(ome_dim, eta_dim, tth_dim) + + # make datasets + spot_grp.create_dataset('tth_crd', data=tth_crd, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('eta_crd', data=eta_crd, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('ome_crd', data=ome_crd, + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('xy_centers', data=xy_centers.T.reshape(2, eta_dim, tth_dim), + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('ij_centers', data=ijs.reshape(2, eta_dim, tth_dim), + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('frame_indices', data=num.array(frame_indices, dtype=int), + compression="gzip", compression_opts=gzip) + spot_grp.create_dataset('intensities', data=spot_data, + compression="gzip", compression_opts=gzip) + return + + def _angles_to_xy(angs, rMat_d, tVec_d, chi, tVec_s, From 0803afe72c9cc04162d8e6c5d364b4ca34970463 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 7 May 2018 10:23:23 -0700 Subject: [PATCH 142/253] Pin wxpython to version 3 Necessary until issues with the old GUI and wxpython v4 are resolved. --- conda.recipe/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 5f48bb05..8584f30d 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -40,7 +40,7 @@ requirements: - scikit-image - scikit-learn - scipy - - wxpython + - wxpython ==3 test: imports: From 2ae093299591519394191ed454d64d747b05103d Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 7 May 2018 12:54:14 -0700 Subject: [PATCH 143/253] updated transforms doc --- docs/source/_static/transforms.pdf | Bin 283612 -> 393987 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/source/_static/transforms.pdf b/docs/source/_static/transforms.pdf index 00372eebdcd55643b402127d5e6d61e76d74f729..c734905593ce6358c3f4b075bdb0509efee8390c 100644 GIT binary patch literal 393987 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

7a%tWwWXN2bVVjF=0bow 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

3ZwYCH+`XRB&;U>Gbqr!gS$cp&F8u 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�R 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

a7UJ9XbPs^_ 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 literal 283612 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 z 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 From b111f90d72ec436c9b2266e25ae92b6e3969bd5a Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 11 May 2018 16:46:20 -0700 Subject: [PATCH 144/253] cleanup --- hexrd/instrument.py | 33 +++++---------------------------- hexrd/xrd/xrdutil.py | 3 ++- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index d1705895..7a60fa3c 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -870,9 +870,9 @@ def pull_spots(self, plane_data, grain_params, for i, i_frame in enumerate(frame_indices): patch_data[i] = \ panel.interpolate_bilinear( - xy_eval, - ome_imgser[i_frame], - ).reshape(prows, pcols) # * nrm_fac + xy_eval, + ome_imgser[i_frame], + ).reshape(prows, pcols) # * nrm_fac elif interp.lower() == 'nearest': patch_data = patch_data_raw # * nrm_fac else: @@ -960,30 +960,6 @@ def pull_spots(self, plane_data, grain_params, pass # FIXME: why is this suddenly necessary??? meas_xy = meas_xy.squeeze() - - # need PREDICTED xy coords - gvec_c = anglesToGVec( - ang_centers[i_pt], - chi=self.chi, - rMat_c=rMat_c, - bHat_l=self.beam_vector) - rMat_s = makeOscillRotMat( - [self.chi, ang_centers[i_pt][2]] - ) - pred_xy = gvecToDetectorXY( - gvec_c, - panel.rmat, rMat_s, rMat_c, - panel.tvec, self.tvec, tVec_c, - beamVec=self.beam_vector) - if panel.distortion is not None: - # FIXME: distortion handling - pred_xy = panel.distortion[0]( - np.atleast_2d(pred_xy), - panel.distortion[1], - invert=True).flatten() - pass - # FIXME: why is this suddenly necessary??? - pred_xy = pred_xy.squeeze() pass # end num_peaks > 0 pass # end contains_signal # write output @@ -1000,7 +976,8 @@ def pull_spots(self, plane_data, grain_params, detector_id, iRefl, peak_id, hkl_id, hkl, tth_edges, eta_edges, np.radians(ome_eval), xyc_arr, ijs, frame_indices, patch_data, - ang_centers[i_pt], pred_xy, meas_angs, meas_xy) + ang_centers[i_pt], xy_centers[i_pt], + meas_angs, meas_xy) pass # end conditional on write output pass # end conditional on check only patch_output.append([ diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 3bf535ea..7690c344 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -3598,7 +3598,8 @@ def _filter_hkls_eta_ome(hkls, angles, eta_range, ome_range): def _project_on_detector_plane(allAngs, rMat_d, rMat_c, chi, - tVec_d, tVec_c, tVec_s, distortion): + tVec_d, tVec_c, tVec_s, + distortion): """ utility routine for projecting a list of (tth, eta, ome) onto the detector plane parameterized by the args From 97fdc705429fa9818aed08b5ed6ded6cb21b8143 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 16 May 2018 14:02:37 +0200 Subject: [PATCH 145/253] fix patch building bug edit author --- hexrd/instrument.py | 5 +- hexrd/xrd/xrdutil.py | 179 +++++++++++++++++++------------------------ 2 files changed, 82 insertions(+), 102 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 7a60fa3c..5c2e98f0 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -741,7 +741,7 @@ def pull_spots(self, plane_data, grain_params, ).T.reshape(len(patch_vertices), 1) # find vertices that all fall on the panel - det_xy, rMat_s = xrdutil._project_on_detector_plane( + det_xy, _ = xrdutil._project_on_detector_plane( np.hstack([patch_vertices, ome_dupl]), panel.rmat, rMat_c, self.chi, panel.tvec, tVec_c, self.tvec, @@ -794,6 +794,7 @@ def pull_spots(self, plane_data, grain_params, # make the tth,eta patches for interpolation patches = xrdutil.make_reflection_patches( instr_cfg, ang_centers[:, :2], ang_pixel_size, + omega=ang_centers[:, 2], tth_tol=tth_tol, eta_tol=eta_tol, rMat_c=rMat_c, tVec_c=tVec_c, distortion=panel.distortion, @@ -2042,7 +2043,7 @@ def dump_patch(self, panel_id, i_refl, peak_id, hkl_id, hkl, tth_edges, eta_edges, ome_centers, xy_centers, ijs, frame_indices, - spot_data, pangs, pxy, mangs, mxy, gzip=9): + spot_data, pangs, pxy, mangs, mxy, gzip=2): """ to be called inside loop over patches diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 7690c344..69adb30d 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -3599,27 +3599,34 @@ def _filter_hkls_eta_ome(hkls, angles, eta_range, ome_range): def _project_on_detector_plane(allAngs, rMat_d, rMat_c, chi, tVec_d, tVec_c, tVec_s, - distortion): + distortion, + beamVec=constants.beam_vec): """ utility routine for projecting a list of (tth, eta, ome) onto the detector plane parameterized by the args """ + gVec_cs = xfcapi.anglesToGVec(allAngs, + chi=chi, + rMat_c=rMat_c, + bHat_l=beamVec) - gVec_cs = xfcapi.anglesToGVec(allAngs, chi=chi, rMat_c=rMat_c) rMat_ss = xfcapi.makeOscillRotMatArray(chi, allAngs[:, 2]) + tmp_xys = xfcapi.gvecToDetectorXYArray( gVec_cs, rMat_d, rMat_ss, rMat_c, - tVec_d, tVec_s, tVec_c - ) + tVec_d, tVec_s, tVec_c, + beamVec=beamVec) + valid_mask = ~(num.isnan(tmp_xys[:, 0]) | num.isnan(tmp_xys[:, 1])) - if distortion is None or len(distortion) == 0: - det_xy = tmp_xys[valid_mask] - else: - det_xy = distortion[0](tmp_xys[valid_mask], + det_xy = num.atleast_2d(tmp_xys[valid_mask, :]) + + # FIXME: distortion kludge + if distortion is not None and len(distortion) == 2: + det_xy = distortion[0](det_xy, distortion[1], invert=True) - return det_xy, rMat_ss[-1] + return det_xy, rMat_ss def simulateGVecs(pd, detector_params, grain_params, @@ -4013,7 +4020,8 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, tth_tol=0.2, eta_tol=1.0, rMat_c=num.eye(3), tVec_c=num.zeros((3, 1)), distortion=None, - npdiv=1, quiet=False, compute_areas_func=gutil.compute_areas, + npdiv=1, quiet=False, + compute_areas_func=gutil.compute_areas, beamVec=None): """ prototype function for making angular patches on a detector @@ -4059,10 +4067,12 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, panel_dims = ( -0.5*num.r_[frame_ncols*pixel_size[1], frame_nrows*pixel_size[0]], - 0.5*num.r_[frame_ncols*pixel_size[1], frame_nrows*pixel_size[0]] + 0.5*num.r_[frame_ncols*pixel_size[1], frame_nrows*pixel_size[0]] ) - row_edges = num.arange(frame_nrows + 1)[::-1]*pixel_size[1] + panel_dims[0][1] - col_edges = num.arange(frame_ncols + 1)*pixel_size[0] + panel_dims[0][0] + row_edges = num.arange(frame_nrows + 1)[::-1]*pixel_size[1] \ + + panel_dims[0][1] + col_edges = num.arange(frame_ncols + 1)*pixel_size[0] \ + + panel_dims[0][0] # sample frame chi = instr_cfg['oscillation_stage']['chi'] @@ -4081,71 +4091,68 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, patches = [] for angs, pix in zip(full_angs, ang_pixel_size): - ndiv_tth = npdiv*num.ceil( tth_tol/num.degrees(pix[0]) ) - ndiv_eta = npdiv*num.ceil( eta_tol/num.degrees(pix[1]) ) + ndiv_tth = npdiv*num.ceil(tth_tol/num.degrees(pix[0])) + ndiv_eta = npdiv*num.ceil(eta_tol/num.degrees(pix[1])) - tth_del = num.arange(0, ndiv_tth+1)*tth_tol/float(ndiv_tth) - 0.5*tth_tol - eta_del = num.arange(0, ndiv_eta+1)*eta_tol/float(ndiv_eta) - 0.5*eta_tol + tth_del = num.arange(0, ndiv_tth + 1)*tth_tol/float(ndiv_tth) \ + - 0.5*tth_tol + eta_del = num.arange(0, ndiv_eta + 1)*eta_tol/float(ndiv_eta) \ + - 0.5*eta_tol # store dimensions for convenience # * etas and tths are bin vertices, ome is already centers - sdims = [ len(eta_del)-1, len(tth_del)-1 ] + sdims = [len(eta_del) - 1, len(tth_del) - 1] + + # FOR ANGULAR MESH + conn = gutil.cellConnectivity( + sdims[0], + sdims[1], + origin='ll' + ) # meshgrid args are (cols, rows), a.k.a (fast, slow) m_tth, m_eta = num.meshgrid(tth_del, eta_del) - npts_patch = m_tth.size + npts_patch = m_tth.size # calculate the patch XY coords from the (tth, eta) angles # * will CHEAT and ignore the small perturbation the different # omega angle values causes and simply use the central value gVec_angs_vtx = num.tile(angs, (npts_patch, 1)) \ - + num.radians( - num.vstack([m_tth.flatten(), - m_eta.flatten(), - num.zeros(npts_patch) - ]).T - ) - - # FOR ANGULAR MESH - conn = gutil.cellConnectivity( sdims[0], sdims[1], origin='ll') - - rMat_s = xfcapi.makeOscillRotMat([chi, angs[2]]) + + num.radians( + num.vstack([m_tth.flatten(), + m_eta.flatten(), + num.zeros(npts_patch) + ]).T + ) - # make G-vectors - gVec_c = xfcapi.anglesToGVec( - gVec_angs_vtx, - chi=chi, - rMat_c=rMat_c, - bHat_l=beamVec) - xy_eval_vtx = xfcapi.gvecToDetectorXY( - gVec_c, - rMat_d, rMat_s, rMat_c, - tVec_d, tVec_s, tVec_c, - beamVec=beamVec) - if distortion is not None and len(distortion) == 2: - xy_eval_vtx = distortion[0](xy_eval_vtx, distortion[1], invert=True) - pass + xy_eval_vtx, _ = _project_on_detector_plane( + gVec_angs_vtx, + rMat_d, rMat_c, + chi, + tVec_d, tVec_c, tVec_s, + distortion, + beamVec=beamVec) areas = compute_areas_func(xy_eval_vtx, conn) # EVALUATION POINTS # * for lack of a better option will use centroids - tth_eta_cen = gutil.cellCentroids( num.atleast_2d(gVec_angs_vtx[:, :2]), conn ) - gVec_angs = num.hstack([tth_eta_cen, - num.tile(angs[2], (len(tth_eta_cen), 1))]) - gVec_c = xfcapi.anglesToGVec( - gVec_angs, - chi=chi, - rMat_c=rMat_c, - bHat_l=beamVec) - xy_eval = xfcapi.gvecToDetectorXY( - gVec_c, - rMat_d, rMat_s, rMat_c, - tVec_d, tVec_s, tVec_c, - beamVec=beamVec) - if distortion is not None and len(distortion) == 2: - xy_eval = distortion[0](xy_eval, distortion[1], invert=True) - pass + tth_eta_cen = gutil.cellCentroids( + num.atleast_2d(gVec_angs_vtx[:, :2]), + conn + ) + + gVec_angs = num.hstack([tth_eta_cen, + num.tile(angs[2], (len(tth_eta_cen), 1))]) + + xy_eval, _ = _project_on_detector_plane( + gVec_angs, + rMat_d, rMat_c, + chi, + tVec_d, tVec_c, tVec_s, + distortion, + beamVec=beamVec) + row_indices = gutil.cellIndices(row_edges, xy_eval[:, 1]) col_indices = gutil.cellIndices(col_edges, xy_eval[:, 0]) @@ -4162,7 +4169,7 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, (row_indices.reshape(sdims[0], sdims[1]), col_indices.reshape(sdims[0], sdims[1]))) ) - pass # close loop over angles + pass # close loop over angles return patches @@ -4741,11 +4748,11 @@ def dump_patch(self, i_refl, peak_id, hkl_id, hkl, tth_centers, eta_centers, ome_centers, xy_centers, ijs, frame_indices, - spot_data, pangs, pxy, mangs, mxy, gzip=4): + spot_data, pangs, pxy, mangs, mxy, gzip=2): """ to be called inside loop over patches - default GZIP level for data arrays is 4 + default GZIP level for data arrays is 2 """ # create spot group @@ -4767,7 +4774,9 @@ def dump_patch(self, tth_crd = tth_centers.reshape(eta_dim, tth_dim) eta_crd = eta_centers.reshape(eta_dim, tth_dim) - ome_crd = num.tile(ome_centers, (eta_dim*tth_dim, 1)).T.reshape(ome_dim, eta_dim, tth_dim) + ome_crd = num.tile( + ome_centers, (eta_dim*tth_dim, 1) + ).T.reshape(ome_dim, eta_dim, tth_dim) # make datasets spot_grp.create_dataset('tth_crd', data=tth_crd, @@ -4776,45 +4785,15 @@ def dump_patch(self, compression="gzip", compression_opts=gzip) spot_grp.create_dataset('ome_crd', data=ome_crd, compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('xy_centers', data=xy_centers.T.reshape(2, eta_dim, tth_dim), + spot_grp.create_dataset('xy_centers', + data=xy_centers.T.reshape(2, eta_dim, tth_dim), compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('ij_centers', data=ijs.reshape(2, eta_dim, tth_dim), + spot_grp.create_dataset('ij_centers', + data=ijs.reshape(2, eta_dim, tth_dim), compression="gzip", compression_opts=gzip) - spot_grp.create_dataset('frame_indices', data=num.array(frame_indices, dtype=int), + spot_grp.create_dataset('frame_indices', + data=num.array(frame_indices, dtype=int), compression="gzip", compression_opts=gzip) spot_grp.create_dataset('intensities', data=spot_data, compression="gzip", compression_opts=gzip) return - - -def _angles_to_xy(angs, - rMat_d, tVec_d, - chi, tVec_s, - rMat_c, tVec_c, - bHat_l=bHat_l_DFLT, - eHat_l=eHat_l_DFLT, - distortion=None): - """ - """ - # make G-vectors - gVec_c = xfcapi.anglesToGVec( - angs, - chi=chi, - rMat_c=rMat_c, - bHat_l=bHat_l, - eHat_l=eHat_l) - - rMat_s = xfcapi.makeOscillRotMatArray(chi, angs[:, 2]) - - # map to xy - xy_eval = xfcapi.gvecToDetectorXYArray( - gVec_c, - rMat_d, rMat_s, rMat_c, - tVec_d, tVec_s, tVec_c, - beamVec=bHat_l) - - # apply distortion (if applicable) - if distortion is not None and len(distortion) == 2: - xy_eval = distortion[0](xy_eval, distortion[1], invert=True) - - return xy_eval From 03304a505f80fdb96a2ecf5b9cf57fec1fe113e7 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 16 May 2018 16:07:04 +0200 Subject: [PATCH 146/253] indexer input casting, grain output formatting --- hexrd/instrument.py | 60 +++++++++++++++++++++++--------------------- hexrd/xrd/indexer.py | 14 +++++------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 5c2e98f0..120a9639 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1937,25 +1937,26 @@ class GrainDataWriter(object): """ """ def __init__(self, filename): - sp3_str = '{:12}\t{:12}\t{:12}' - dp3_str = '{:20}\t{:20}\t{:20}' - self._header = \ - sp3_str.format( - '# grain ID', 'completeness', 'chi^2') + '\t' + \ - dp3_str.format( - 'exp_map_c[0]', 'exp_map_c[1]', 'exp_map_c[2]') + '\t' + \ - dp3_str.format( - 't_vec_c[0]', 't_vec_c[1]', 't_vec_c[2]') + '\t' + \ - dp3_str.format( - 'inv(V_s)[0,0]', - 'inv(V_s)[1,1]', - 'inv(V_s)[2,2]') + '\t' + \ - dp3_str.format( - 'inv(V_s)[1,2]*√2', 'inv(V_s)[0,2]*√2', 'inv(V_s)[0,2]*√2' - ) + '\t' + dp3_str.format( - 'ln(V_s)[0,0]', 'ln(V_s)[1,1]', 'ln(V_s)[2,2]') + '\t' + \ - dp3_str.format( - 'ln(V_s)[1,2]', 'ln(V_s)[0,2]', 'ln(V_s)[0,1]') + self._delim = ' ' + header_items = ( + '# grain ID', 'completeness', 'chi^2', + 'exp_map_c[0]', 'exp_map_c[1]', 'exp_map_c[2]', + 't_vec_c[0]', 't_vec_c[1]', 't_vec_c[2]', + 'inv(V_s)[0,0]', 'inv(V_s)[1,1]', 'inv(V_s)[2,2]', + 'inv(V_s)[1,2]*sqrt(2)', + 'inv(V_s)[0,2]*sqrt(2)', + 'inv(V_s)[0,2]*sqrt(2)', + 'ln(V_s)[0,0]', 'ln(V_s)[1,1]', 'ln(V_s)[2,2]', + 'ln(V_s)[1,2]', 'ln(V_s)[0,2]', 'ln(V_s)[0,1]' + ) + self._header = self._delim.join( + [self._delim.join( + np.tile('{:<12}', 3) + ).format(*header_items[:3]), + self._delim.join( + np.tile('{:<23}', len(header_items) - 3) + ).format(*header_items[3:])] + ) if isinstance(filename, file): self.fid = filename else: @@ -1977,16 +1978,17 @@ def dump_grain(self, grain_id, completeness, chisq, emat = logm(np.linalg.inv(mutil.vecMVToSymm(grain_params[6:]))) evec = mutil.symmToVecMV(emat, scale=False) - dp3_e_str = '{:<1.12e}\t{:<1.12e}\t{:<1.12e}' - dp6_e_str = \ - '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t{:<1.12e}' - output_str = \ - '{:<12d}\t{:<12f}\t{:<12f}\t'.format( - grain_id, completeness, chisq) + \ - dp3_e_str.format(*grain_params[:3]) + '\t' + \ - dp3_e_str.format(*grain_params[3:6]) + '\t' + \ - dp6_e_str.format(*grain_params[6:]) + '\t' + \ - dp6_e_str.format(*evec) + res = [int(grain_id), completeness, chisq] \ + + grain_params.tolist() \ + + evec.tolist() + output_str = self._delim.join( + [self._delim.join( + ['{:<12d}', '{:<12f}', '{:<12e}'] + ).format(*res[:3]), + self._delim.join( + np.tile('{:<23.16e}', len(res) - 3) + ).format(*res[3:])] + ) print(output_str, file=self.fid) return output_str diff --git a/hexrd/xrd/indexer.py b/hexrd/xrd/indexer.py index da52cdbb..5835bfee 100644 --- a/hexrd/xrd/indexer.py +++ b/hexrd/xrd/indexer.py @@ -826,12 +826,11 @@ def paintGrid(quats, etaOmeMaps, 'etaTol': etaTol, 'etaIndices': etaIndices, 'etaEdges': etaOmeMaps.etaEdges, - 'etaOmeMaps': etaOmeMaps.dataStore, + 'etaOmeMaps': num.stack(etaOmeMaps.dataStore), 'bMat': bMat, 'threshold': threshold } - # do the mapping start = time.time() retval = None @@ -843,14 +842,15 @@ def paintGrid(quats, etaOmeMaps, else: # single process version. global paramMP - paintgrid_init(params) # sets paramMP + paintgrid_init(params) # sets paramMP retval = map(paintGridThis, quats.T) - paramMP = None # clear paramMP + paramMP = None # clear paramMP elapsed = (time.time() - start) logger.info("paintGrid took %.3f seconds", elapsed) return retval + def _meshgrid2d(x, y): """ A special-cased implementation of num.meshgrid, for just @@ -1058,7 +1058,6 @@ def _find_in_range(value, spans): return li - @numba.njit def _angle_is_hit(ang, eta_offset, ome_offset, hkl, valid_eta_spans, valid_ome_spans, etaEdges, omeEdges, etaOmeMaps, @@ -1076,7 +1075,7 @@ def _angle_is_hit(ang, eta_offset, ome_offset, hkl, valid_eta_spans, - actual check for a hit, using dilation for the tolerance. - Note the function returns both, if it was a hit and if it passed the the + Note the function returns both, if it was a hit and if it passed the filtering, as we'll want to discard the filtered values when computing the hit percentage. @@ -1147,7 +1146,7 @@ def _filter_and_count_hits(angs_0, angs_1, symHKLs_ix, etaEdges, curr_hkl_idx += 1 end_curr = symHKLs_ix[curr_hkl_idx+1] - # first solution + # first solution hit, not_filtered = _angle_is_hit( angs_0[i], eta_offset, ome_offset, curr_hkl_idx, valid_eta_spans, @@ -1171,7 +1170,6 @@ def _filter_and_count_hits(angs_0, angs_1, symHKLs_ix, etaEdges, return float(hits)/float(total) if total != 0 else 0.0 - @numba.njit def _map_angle(angle, offset): """Equivalent to xf.mapAngle in this context, and 'numba friendly' From 3510973118f5b3c438a8677117f0c2d71a93851c Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 16 May 2018 17:21:44 +0200 Subject: [PATCH 147/253] added predicted xy output, formatting cleanup --- hexrd/instrument.py | 71 +++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 120a9639..694f2f5a 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -873,6 +873,7 @@ def pull_spots(self, plane_data, grain_params, panel.interpolate_bilinear( xy_eval, ome_imgser[i_frame], + pad_with_nans=False ).reshape(prows, pcols) # * nrm_fac elif interp.lower() == 'nearest': patch_data = patch_data_raw # * nrm_fac @@ -968,7 +969,8 @@ def pull_spots(self, plane_data, grain_params, if output_format.lower() == 'text': writer.dump_patch( peak_id, hkl_id, hkl, sum_int, max_int, - ang_centers[i_pt], meas_angs, meas_xy) + ang_centers[i_pt], meas_angs, + xy_centers[i_pt], meas_xy) elif output_format.lower() == 'hdf5': xyc_arr = xy_eval.reshape( prows, pcols, 2 @@ -1890,14 +1892,21 @@ class PatchDataWriter(object): """ """ def __init__(self, filename): - dp3_str = '{:18}\t{:18}\t{:18}' - self._header = \ - '{:6}\t{:6}\t'.format('# ID', 'PID') + \ - '{:3}\t{:3}\t{:3}\t'.format('H', 'K', 'L') + \ - '{:12}\t{:12}\t'.format('sum(int)', 'max(int)') + \ - dp3_str.format('pred tth', 'pred eta', 'pred ome') + '\t' + \ - dp3_str.format('meas tth', 'meas eta', 'meas ome') + '\t' + \ - '{:18}\t{:18}'.format('meas X', 'meas Y') + self._delim = ' ' + header_items = ( + '# ID', 'PID', + 'H', 'K', 'L', + 'sum(int)', 'max(int)', + 'pred tth', 'pred eta', 'pred ome', + 'meas tth', 'meas eta', 'meas ome', + 'pred X', 'pred Y', + 'meas X', 'meas Y' + ) + self._header = self._delim.join([ + self._delim.join(np.tile('{:<6}', 5)).format(*header_items[:5]), + self._delim.join(np.tile('{:<12}', 2)).format(*header_items[5:7]), + self._delim.join(np.tile('{:<23}', 10)).format(*header_items[7:17]) + ]) if isinstance(filename, file): self.fid = filename else: @@ -1912,23 +1921,27 @@ def close(self): def dump_patch(self, peak_id, hkl_id, hkl, spot_int, max_int, - pangs, mangs, xy): - nans_tabbed_12 = '{:^12}\t{:^12}\t' - nans_tabbed_18 = '{:^18}\t{:^18}\t{:^18}\t{:^18}\t{:^18}' - output_str = \ - '{:<6d}\t{:<6d}\t'.format(int(peak_id), int(hkl_id)) + \ - '{:<3d}\t{:<3d}\t{:<3d}\t'.format(*np.array(hkl, dtype=int)) - if peak_id >= 0: - output_str += \ - '{:<1.6e}\t{:<1.6e}\t'.format(spot_int, max_int) + \ - '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*pangs) + \ - '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*mangs) + \ - '{:<1.12e}\t{:<1.12e}'.format(xy[0], xy[1]) - else: - output_str += \ - nans_tabbed_12.format(*np.ones(2)*np.nan) + \ - '{:<1.12e}\t{:<1.12e}\t{:<1.12e}\t'.format(*pangs) + \ - nans_tabbed_18.format(*np.ones(5)*np.nan) + pangs, mangs, pxy, mxy): + """ + !!! maybe need to check that last four inputs are arrays + """ + if mangs is None: + mangs = np.ones(3)*np.nan + if mxy is None: + mxy = np.ones(2)*np.nan + res = [int(peak_id), int(hkl_id)] \ + + np.array(hkl, dtype=int).tolist() \ + + [spot_int, max_int] \ + + pangs.tolist() \ + + mangs.tolist() \ + + pxy.tolist() \ + + mxy.tolist() + + output_str = self._delim.join( + [self._delim.join(np.tile('{:<6d}', 5)).format(*res[:5]), + self._delim.join(np.tile('{:<12e}', 2)).format(*res[5:7]), + self._delim.join(np.tile('{:<23.16e}', 10)).format(*res[7:])] + ) print(output_str, file=self.fid) return output_str @@ -2055,9 +2068,9 @@ def dump_patch(self, panel_id, panel_grp = self.data_grp[panel_id] spot_grp = panel_grp.create_group("spot_%05d" % i_refl) - spot_grp.attrs.create('peak_id', peak_id) - spot_grp.attrs.create('hkl_id', hkl_id) - spot_grp.attrs.create('hkl', hkl) + spot_grp.attrs.create('peak_id', int(peak_id)) + spot_grp.attrs.create('hkl_id', int(hkl_id)) + spot_grp.attrs.create('hkl', np.array(hkl, dtype=int)) spot_grp.attrs.create('predicted_angles', pangs) spot_grp.attrs.create('predicted_xy', pxy) if mangs is None: From a65a4d509ae2a9dd55a22e92bb572f542c2ab6ac Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 16 May 2018 18:53:04 +0200 Subject: [PATCH 148/253] spots output formatting error None --- hexrd/instrument.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 694f2f5a..5f575931 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1926,9 +1926,11 @@ def dump_patch(self, peak_id, hkl_id, !!! maybe need to check that last four inputs are arrays """ if mangs is None: + spot_int = np.nan + max_int = np.nan mangs = np.ones(3)*np.nan - if mxy is None: mxy = np.ones(2)*np.nan + res = [int(peak_id), int(hkl_id)] \ + np.array(hkl, dtype=int).tolist() \ + [spot_int, max_int] \ From 0bf20591ea5f309ffc007ea5aceff82562871abf Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 21 May 2018 10:37:57 -0700 Subject: [PATCH 149/253] added calibration params to instrument --- hexrd/gridutil.py | 38 +++++++++++++++--------------- hexrd/instrument.py | 56 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/hexrd/gridutil.py b/hexrd/gridutil.py index 5b3ae184..ac47c57e 100644 --- a/hexrd/gridutil.py +++ b/hexrd/gridutil.py @@ -58,7 +58,7 @@ def cellIndices(edges, points_1d): idx = ceil( (points_1d - edges[0]) / delta ) - 1 else: raise RuntimeError, "edges array gives delta of 0" - # ...will catch exceptions elsewhere... + # ...will catch exceptions elsewhere... # if np.any(np.logical_or(idx < 0, idx > len(edges) - 1)): # raise RuntimeWarning, "some input points are outside the grid" return array(idx, dtype=int) @@ -134,7 +134,7 @@ def compute_areas(xy_eval_vtx, conn): v1x = vtx_x - vtx0x v1y = vtx_y - vtx0y acc += v0x*v1y - v1x*v0y - + areas[i] = 0.5 * acc return areas else: @@ -159,7 +159,7 @@ def compute_areas(xy_eval_vtx, conn): for i in range(len(conn)): polygon = [[xy_eval_vtx[conn[i, j], 0], xy_eval_vtx[conn[i, j], 1]] for j in range(4)] - areas[i] = gutil.computeArea(polygon) + areas[i] = computeArea(polygon) return areas def computeArea(polygon): @@ -168,12 +168,12 @@ def computeArea(polygon): """ n_vertices = len(polygon) polygon = array(polygon) - + triv = array([ [ [0, i-1], [0, i] ] for i in range(2, n_vertices) ]) - + area = 0 for i in range(len(triv)): - tvp = diff( hstack([ polygon[triv[i][0], :], + tvp = diff( hstack([ polygon[triv[i][0], :], polygon[triv[i][1], :] ]), axis=0).flatten() area += 0.5 * cross(tvp[:2], tvp[2:]) return area @@ -202,14 +202,14 @@ def computeIntersection(line1, line2): line1 = [ [x0, y0], [x1, y1] ] line1 = [ [x3, y3], [x4, y4] ] - + """ intersection = zeros(2) l1 = array(line1) l2 = array(line2) - + det_l1 = det(l1) det_l2 = det(l2) @@ -234,7 +234,7 @@ def isinside(point, boundary, ccw=True): """ pointPositionVector = hstack([ point - boundary[0, :], 0.]) boundaryVector = hstack([boundary[1, :] - boundary[0, :], 0.]) - + crossVector = cross(pointPositionVector, boundaryVector) inside = False @@ -246,7 +246,7 @@ def isinside(point, boundary, ccw=True): inside = True else: inside = True - + return inside def sutherlandHodgman(subjectPolygon, clipPolygon): @@ -254,30 +254,30 @@ def sutherlandHodgman(subjectPolygon, clipPolygon): """ subjectPolygon = array(subjectPolygon) clipPolygon = array(clipPolygon) - + numClipEdges = len(clipPolygon) prev_clipVertex = clipPolygon[-1, :] - + # loop over clipping edges outputList = array(subjectPolygon) for iClip in range(numClipEdges): - + curr_clipVertex = clipPolygon[iClip, :] - clipBoundary = vstack([ curr_clipVertex, + clipBoundary = vstack([ curr_clipVertex, prev_clipVertex ]) - + inputList = array(outputList) if len(inputList) > 0: - prev_subjectVertex = inputList[-1, :] + prev_subjectVertex = inputList[-1, :] outputList = [] - + for iInput in range(len(inputList)): curr_subjectVertex = inputList[iInput, :] - + if isinside(curr_subjectVertex, clipBoundary): if not isinside(prev_subjectVertex, clipBoundary): subjectLineSegment = vstack([ curr_subjectVertex, @@ -292,7 +292,7 @@ def sutherlandHodgman(subjectPolygon, clipPolygon): pass prev_subjectVertex = curr_subjectVertex prev_clipVertex = curr_clipVertex - pass + pass pass return outputList diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 5f575931..56d5a4f2 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -#! /usr/bin/env python -# ============================================================ +# ============================================================================= # Copyright (c) 2012, Lawrence Livermore National Security, LLC. # Produced at the Lawrence Livermore National Laboratory. # Written by Joel Bernier and others. @@ -25,7 +24,7 @@ # License along with this program (see file LICENSE); if not, write to # the Free Software Foundation, Inc., 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA or visit . -# ============================================================ +# ============================================================================= """ Created on Fri Dec 9 13:05:27 2016 @@ -88,6 +87,13 @@ chi_DFLT = 0. t_vec_s_DFLT = np.zeros(3) +# [wavelength, chi, tvec_s, expmap_c, tec_c], len is 11 +instr_param_flags_DFLT = np.array( + [0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1], + dtype=bool) +panel_param_flags_DFLT = np.array( + [1, 1, 1, 1, 1, 1], + dtype=bool) # ============================================================================= # UTILITY METHODS @@ -248,6 +254,10 @@ def __init__(self, instrument_config=None, ] self._chi = instrument_config['oscillation_stage']['chi'] + self._param_flags = np.hstack( + [instr_param_flags_DFLT, + np.tile(panel_param_flags_DFLT, self._num_panels)] + ) return # properties for physical size of rectangular detector @@ -330,10 +340,47 @@ def eta_vector(self, x): panel = self.detectors[detector_id] panel.evec = self._eta_vector + @property + def param_flags(self): + return self._param_flags + + @param_flags.setter + def param_flags(self, x): + x = np.array(x, dtype=bool).flatten() + assert len(x) == 11 + 6*self.num_panels, \ + "length of parameter list must be %d; you gave %d" \ + % (len(self._param_flags), len(x)) + self._param_flags = x + # ========================================================================= # METHODS # ========================================================================= + def calibration_params(self, expmap_c, tvec_c): + plist = np.zeros(11 + 6*self.num_panels) + + plist[0] = self.beam_wavelength + plist[1] = self.chi + plist[2], plist[3], plist[4] = self.tvec + plist[5], plist[6], plist[7] = expmap_c + plist[8], plist[9], plist[10] = tvec_c + + ii = 11 + for panel in self.detectors.itervalues(): + plist[ii:ii + 6] = np.hstack([ + panel.tilt.flatten(), + panel.tvec.flatten(), + ]) + ii += 6 + + # FIXME: FML!!! + # this assumes old style distiortion = (func, params) + retval = plist + for panel in self.detectors.itervalues(): + if panel.distortion is not None: + retval = np.hstack([retval, panel.distortion[1]]) + return retval + def write_config(self, filename=None, calibration_dict={}): """ WRITE OUT YAML FILE """ # initialize output dictionary @@ -844,6 +891,7 @@ def pull_spots(self, plane_data, grain_params, continue else: # initialize spot data parameters + # !!! maybe change these to nan to not fuck up writer peak_id = -999 sum_int = None max_int = None @@ -1930,7 +1978,7 @@ def dump_patch(self, peak_id, hkl_id, max_int = np.nan mangs = np.ones(3)*np.nan mxy = np.ones(2)*np.nan - + res = [int(peak_id), int(hkl_id)] \ + np.array(hkl, dtype=int).tolist() \ + [spot_int, max_int] \ From fd1c2fd52a64ec5a83dc8c6c476fac9467ee900a Mon Sep 17 00:00:00 2001 From: Stan Seibert Date: Tue, 22 May 2018 15:26:09 -0500 Subject: [PATCH 150/253] Drop default compression level to 1 and use byte shuffling for better compression ratios --- hexrd/instrument.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 56d5a4f2..345fd4bf 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -2108,11 +2108,11 @@ def dump_patch(self, panel_id, i_refl, peak_id, hkl_id, hkl, tth_edges, eta_edges, ome_centers, xy_centers, ijs, frame_indices, - spot_data, pangs, pxy, mangs, mxy, gzip=2): + spot_data, pangs, pxy, mangs, mxy, gzip=1): """ to be called inside loop over patches - default GZIP level for data arrays is 9 + default GZIP level for data arrays is 1 """ fi = np.array(frame_indices, dtype=int) @@ -2145,20 +2145,28 @@ def dump_patch(self, panel_id, tth_crd = centers_of_edge_vec(tth_edges) eta_crd = centers_of_edge_vec(eta_edges) + shuffle_data = True # reduces size by 20% spot_grp.create_dataset('tth_crd', data=tth_crd, - compression="gzip", compression_opts=gzip) + compression="gzip", compression_opts=gzip, + shuffle=shuffle_data) spot_grp.create_dataset('eta_crd', data=eta_crd, - compression="gzip", compression_opts=gzip) + compression="gzip", compression_opts=gzip, + shuffle=shuffle_data) spot_grp.create_dataset('ome_crd', data=ome_centers, - compression="gzip", compression_opts=gzip) + compression="gzip", compression_opts=gzip, + shuffle=shuffle_data) spot_grp.create_dataset('xy_centers', data=xy_centers, - compression="gzip", compression_opts=gzip) + compression="gzip", compression_opts=gzip, + shuffle=shuffle_data) spot_grp.create_dataset('ij_centers', data=ijs, - compression="gzip", compression_opts=gzip) + compression="gzip", compression_opts=gzip, + shuffle=shuffle_data) spot_grp.create_dataset('frame_indices', data=fi, - compression="gzip", compression_opts=gzip) + compression="gzip", compression_opts=gzip, + shuffle=shuffle_data) spot_grp.create_dataset('intensities', data=spot_data, - compression="gzip", compression_opts=gzip) + compression="gzip", compression_opts=gzip, + shuffle=shuffle_data) return From b2cc6689ad6055e450928715086b91643597ffed Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 23 May 2018 09:50:05 -0700 Subject: [PATCH 151/253] compression level change in HDF5 output --- hexrd/instrument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 56d5a4f2..d5d5c1c0 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -2108,7 +2108,7 @@ def dump_patch(self, panel_id, i_refl, peak_id, hkl_id, hkl, tth_edges, eta_edges, ome_centers, xy_centers, ijs, frame_indices, - spot_data, pangs, pxy, mangs, mxy, gzip=2): + spot_data, pangs, pxy, mangs, mxy, gzip=1): """ to be called inside loop over patches From b669ba1b49fad202c3e1a4d4489a98dd9c6662e9 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 23 May 2018 12:14:40 -0700 Subject: [PATCH 152/253] fix to clip_to_panel, distortion bug uncovered --- hexrd/instrument.py | 16 +++++++++++----- hexrd/xrd/distortion.py | 15 ++++++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index d5d5c1c0..0c4a9662 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -232,9 +232,9 @@ def __init__(self, instrument_config=None, det_list = [] for pix, xform, dist in zip(pixel_info, affine_info, distortion): # HARD CODED GE DISTORTION !!! FIX - dist_tuple = None + dist_list = None if dist is not None: - dist_tuple = (GE_41RT, dist['parameters']) + dist_list = [GE_41RT, dist['parameters']] det_list.append( PlanarDetector( @@ -244,7 +244,7 @@ def __init__(self, instrument_config=None, tilt=xform['tilt_angles'], bvec=self._beam_vector, evec=ct.eta_vec, - distortion=dist_tuple) + distortion=dist_list) ) pass self._detectors = dict(zip(detector_ids, det_list)) @@ -793,7 +793,7 @@ def pull_spots(self, plane_data, grain_params, panel.rmat, rMat_c, self.chi, panel.tvec, tVec_c, self.tvec, panel.distortion) - _, on_panel = panel.clip_to_panel(det_xy, buffer_edges=False) + _, on_panel = panel.clip_to_panel(det_xy, buffer_edges=True) # all vertices must be on... patch_is_on = np.all(on_panel.reshape(nangs, 4), axis=1) @@ -1480,7 +1480,13 @@ def clip_to_panel(self, xy, buffer_edges=True): ) on_panel = np.logical_and(on_panel_x, on_panel_y) elif not buffer_edges: - on_panel = np.ones(len(xy), dtype=bool) + on_panel_x = np.logical_and( + xy[:, 0] >= -xlim, xy[:, 0] <= xlim + ) + on_panel_y = np.logical_and( + xy[:, 1] >= -ylim, xy[:, 1] <= ylim + ) + on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel def cart_to_angles(self, xy_data): diff --git a/hexrd/xrd/distortion.py b/hexrd/xrd/distortion.py index 4e79d5fd..b1b16dcd 100644 --- a/hexrd/xrd/distortion.py +++ b/hexrd/xrd/distortion.py @@ -100,17 +100,22 @@ def _ge_41rt_inverse_distortion(out, in_, rhoMax, params): xi, yi = in_[:, 0], in_[:,1] ri = np.sqrt(xi*xi + yi*yi) - if ri < sqrt_epsf: - ri_inv = 0.0 - else: - ri_inv = 1.0/ri + # !!! adding fix TypeError when processings list of coords + zfix = [] + if np.any(ri) < sqrt_epsf: + zfix = ri < sqrt_epsf + ri[zfix] = 1.0 + ri_inv = 1.0/ri + ri_inv[zfix] = 0. + sinni = yi*ri_inv cosni = xi*ri_inv ro = ri cos2ni = cosni*cosni - sinni*sinni sin2ni = 2*sinni*cosni cos4ni = cos2ni*cos2ni - sin2ni*sin2ni - + + # FIXME: looks like we hae a problem here, should iterate over single coord pairs? for i in range(maxiter): # newton solver iteration ratio = ri*rxi fx = (p0*ratio**p3*cos2ni + From b1379256d4003eff94033be4b31ab881df362b75 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 24 May 2018 13:35:02 -0400 Subject: [PATCH 153/253] added shuffle option for hdf5 imageseries and reset default compression to 1 --- hexrd/imageseries/save.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 7c4ff7ba..9d47dc0b 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -73,8 +73,9 @@ def __init__(self, ims, fname, **kwargs): class WriteH5(Writer): fmt = 'hdf5' - dflt_gzip = 4 + dflt_gzip = 1 dflt_chrows = 0 + dflt_shuffle = True def __init__(self, ims, fname, **kwargs): """Write imageseries in HDF5 file @@ -111,6 +112,11 @@ def write(self): @property def h5opts(self): d = {} + + # shuffle + shuffle = self._opts.pop('shuffle', self.dflt_shuffle) + d['shuffle'] = shuffle + # compression compress = self._opts.pop('gzip', self.dflt_gzip) if compress > 9: From 1af99811da6fc038d094165e422379349c3bbe67 Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Thu, 24 May 2018 13:51:29 -0400 Subject: [PATCH 154/253] cleaned up prints in imageseries.stats.percentile --- hexrd/imageseries/stats.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index c7a0df70..b8ae1aee 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -1,4 +1,6 @@ """Stats for imageseries""" +from __future__ import print_function + import numpy as np import logging @@ -38,13 +40,12 @@ def percentile(ims, pct, nframes=0): dt = ims.dtype (nr, nc) = ims.shape nrpb = _rows_in_buffer(nframes, nf*nc*dt.itemsize) - print 'rows per buffer: ', nrpb + print('Buffering percentile calculation with', nrpb, 'rows per buffer.') # now build the result a rectangle at a time img = np.zeros_like(ims[0]) for rr in _row_ranges(nr, nrpb): rect = np.array([[rr[0], rr[1]], [0, nc]]) pims = PIS(ims, [('rectangle', rect)]) - print 'pims: ', len(pims), pims.shape img[rr[0]:rr[1], :] = np.percentile(_toarray(pims, nf), pct, axis=0) return img From c87a7e4b88d7e385b149dfe0e379b4672c482e7e Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 24 May 2018 14:48:39 -0700 Subject: [PATCH 155/253] exports raw data on patches containing no signal' --- hexrd/instrument.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 0c4a9662..8da08b96 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -716,9 +716,7 @@ def pull_spots(self, plane_data, grain_params, else: label_struct = ndimage.generate_binary_structure(3, 3) - # # simulate rotation series - # sim_results = self.simulate_rotation_series( plane_data, [grain_params, ], eta_ranges=eta_ranges, @@ -910,8 +908,8 @@ def pull_spots(self, plane_data, grain_params, pass patch_data_raw = np.stack(patch_data_raw, axis=0) compl.append(contains_signal) - if contains_signal: + if contains_signal: # initialize patch data array for intensities if interp.lower() == 'bilinear': patch_data = np.zeros( @@ -1011,6 +1009,8 @@ def pull_spots(self, plane_data, grain_params, # FIXME: why is this suddenly necessary??? meas_xy = meas_xy.squeeze() pass # end num_peaks > 0 + else: + patch_data = patch_data_raw pass # end contains_signal # write output if filename is not None: From 0e16a190d815295cf6976527af053675b83c6398 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 25 May 2018 17:37:47 -0700 Subject: [PATCH 156/253] list casting in instrument parameter output --- hexrd/instrument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 0c4a9662..cc84923f 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1368,7 +1368,7 @@ def config_dict(self, chi, t_vec_s, sat_level=None): """...HARD CODED DISTORTION! FIX THIS!!!""" dist_d = dict( function_name='GE_41RT', - parameters=self.distortion[1] + parameters=self.distortion[1].tolist() ) d['detector']['distortion'] = dist_d return d From 8d3f72cf8ea02ca426c321608d4c260da06d1f24 Mon Sep 17 00:00:00 2001 From: Bernier Date: Fri, 8 Jun 2018 16:52:11 -0700 Subject: [PATCH 157/253] added euler functions to rotations; recipe cleanup --- conda.recipe/meta.yaml | 25 +++++------ hexrd/xrd/rotations.py | 94 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 8584f30d..90c28246 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -25,10 +25,8 @@ requirements: - python - setuptools run: - - dill - fabio - h5py - - joblib - matplotlib - numba - numpy @@ -36,7 +34,6 @@ requirements: - python - python.app # [osx] - pyyaml - - qtconsole - scikit-image - scikit-learn - scipy @@ -45,17 +42,17 @@ requirements: test: imports: - hexrd - commands: - - hexrd -V - - hexrd -h - - hexrd help - - hexrd find-orientations -h - - hexrd help find-orientations - - hexrd fit-grains -h - - hexrd help fit-grains - - hexrd gui -h - - hexrd help gui - - hexrd test + #commands: + # - hexrd -V + # - hexrd -h + # - hexrd help + # - hexrd find-orientations -h + # - hexrd help find-orientations + # - hexrd fit-grains -h + # - hexrd help fit-grains + # - hexrd gui -h + # - hexrd help gui + # - hexrd test about: license: LGPL diff --git a/hexrd/xrd/rotations.py b/hexrd/xrd/rotations.py index 5ad8da84..d155441b 100644 --- a/hexrd/xrd/rotations.py +++ b/hexrd/xrd/rotations.py @@ -28,13 +28,14 @@ # # Module containing functions relevant to rotations # -import sys, os, time +import sys +import time import numpy from numpy import \ - arange, array, asarray, atleast_1d, average, \ - ndarray, diag, empty, ones, zeros, \ + arange, arctan2, array, asarray, atleast_1d, average, \ + ndarray, diag, zeros, \ cross, dot, pi, arccos, arcsin, cos, sin, sqrt, \ - sort, squeeze, tile, vstack, hstack, r_, c_, ix_, \ + sort, tile, vstack, hstack, c_, ix_, \ abs, mod, sign, \ finfo, isscalar from numpy import float_ as nFloat @@ -46,19 +47,22 @@ multMatArray, nullSpace # # Module Data -tinyRotAng = finfo(float).eps # ~2e-16 -angularUnits = 'radians' # module-level angle units -I3 = array([[1., 0., 0.], # (3, 3) identity - [0., 1., 0.], - [0., 0., 1.]]) +tinyRotAng = finfo(float).eps # ~2e-16 +angularUnits = 'radians' # module-level angle units +I3 = array([[1., 0., 0.], # (3, 3) identity + [0., 1., 0.], + [0., 0., 1.]]) # periodDict = {'degrees': 360.0, 'radians': 2*numpy.pi} + + # # ================================================== Functions # def arccosSafe(temp): """ - Protect against numbers slightly larger than 1 in magnitude due to round-off + Protect against numbers slightly larger than 1 in magnitude + due to round-off """ temp = atleast_1d(temp) if (abs(temp) > 1.00001).any(): @@ -635,6 +639,76 @@ def angleAxisOfRotMat(R): raxis[:, special] = saxis return angle, unitVector(raxis) + + +def make_rmat_euler(tilt_angles, axes_order, extrinsic=True): + """ + extrinsic or intrinsic by kw + """ + axes = numpy.eye(3) + + axes_dict = dict(x=0, y=1, z=2) + + # orders = [] + # for l in [''.join(i) for i in itertools.product(['x', 'y', 'z'], repeat=3)]: + # if numpy.sum(numpy.array([j for j in l]) == l[0]) < 3: + # if l[1] != l[0] and l[2] != l[1]: + # orders.append(l) + + orders = [ + 'xyz', 'zyx', + 'zxy', 'yxz', + 'yzx', 'xzy', + 'xyx', 'xzx', + 'yxy', 'yzy', + 'zxz', 'zyz', + ] + + axo = axes_order.lower() + assert axo in orders and len(axes_order) == 3, \ + '%s is not a valid choice' % axes_order + + if extrinsic: + rmats = numpy.zeros((3, 3, 3)) + for i, ax in enumerate(axo): + rmats[i] = rotMatOfExpMap( + tilt_angles[i]*axes[axes_dict[ax]] + ) + return numpy.dot(rmats[2], numpy.dot(rmats[1], rmats[0])) + else: + rm0 = rotMatOfExpMap( + tilt_angles[0]*axes[axes_dict[axo[0]]] + ) + rm1 = rotMatOfExpMap( + tilt_angles[1]*rm0[:, axes_dict[axo[1]]] + ) + rm2 = rotMatOfExpMap( + tilt_angles[2]*numpy.dot(rm1, rm0[:, axes_dict[axo[2]]]) + ) + return numpy.dot(rm2, numpy.dot(rm1, rm0)) + + +def angles_from_rmat_xyz(rmat): + """ + calculate x-y-z euler angles from a rotation matrix in + the PASSIVE convention + """ + eps = sqrt(finfo('float').eps) + ry = -arcsin(rmat[2, 0]) + sgny = sign(ry) + if abs(ry) < 0.5*pi - eps: + cosy = cos(ry) + rz = arctan2(rmat[1, 0]/cosy, rmat[0, 0]/cosy) + rx = arctan2(rmat[2, 1]/cosy, rmat[2, 2]/cosy) + else: + rz = 0.5*arctan2(sgny*rmat[1, 2], sgny*rmat[0, 2]) + if sgny > 0: + rx = -rz + else: + rx = rz + return rx, ry, rz + + # # ==================== Fiber # From 71c2ff97839972412072883f4cc3060c62e68b22 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 14 Jun 2018 10:01:56 -0700 Subject: [PATCH 158/253] fix on omega edges in maps --- hexrd/instrument.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 22259c94..b2c0402b 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -47,6 +47,7 @@ from hexrd import matrixutil as mutil from hexrd.valunits import valWUnit from hexrd.xrd.transforms_CAPI import anglesToGVec, \ + angularDifference, \ detectorXYToGvec, \ gvecToDetectorXY, \ makeDetectorRotMat, \ @@ -1368,7 +1369,7 @@ def config_dict(self, chi, t_vec_s, sat_level=None): """...HARD CODED DISTORTION! FIX THIS!!!""" dist_d = dict( function_name='GE_41RT', - parameters=self.distortion[1].tolist() + parameters=np.r_[self.distortion[1]].tolist() ) d['detector']['distortion'] = dist_d return d @@ -2263,7 +2264,13 @@ def __init__(self, image_series_dict, instrument, plane_data, np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]), np.radians(ome_period) ) - + + # !!! must avoid the case where omeEdges[0] = omeEdges[-1] for the + # indexer to work properly + if abs(self._omeEdges[0] - self._omeEdges[-1]) <= ct.sqrt_epsf: + del_ome = np.radians(omegas_array[0, 1] - omegas_array[0, 0]) # signed + self._omeEdges[-1] = self._omeEdges[-2] + del_ome + # handle etas # WARNING: unlinke the omegas in imageseries metadata, # these are in RADIANS and represent bin centers From 926d222840f92a535899c4e165fa90bdec7189b8 Mon Sep 17 00:00:00 2001 From: Jun-Sang Park Date: Thu, 21 Jun 2018 00:44:15 -0500 Subject: [PATCH 159/253] parallelize overlaps, None bug in fitting --- hexrd/xrd/fitting.py | 8 +- scripts/makeOverlapTable.py | 392 ++++++++++++++++++++++-------------- 2 files changed, 245 insertions(+), 155 deletions(-) diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index 0d643c5b..791f0313 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -433,7 +433,7 @@ def objFuncFitGrain(gFit, gFull, gFlag, # instrument here because I am not sure if instatiating them using # dict.fromkeys() preserves the same order if using iteration... # - calc_omes_dict = dict.fromkeys(instrument.detectors) + calc_omes_dict = dict.fromkeys(instrument.detectors, []) calc_xy_dict = dict.fromkeys(instrument.detectors) meas_xyo_all = [] det_keys_ordered = [] @@ -507,7 +507,11 @@ def objFuncFitGrain(gFit, gFull, gFlag, # stack results to concatenated arrays calc_omes_all = np.hstack([calc_omes_dict[k] for k in det_keys_ordered]) - calc_xy_all = np.vstack([calc_xy_dict[k] for k in det_keys_ordered]) + tmp = [] + for k in det_keys_ordered: + if calc_xy_dict[k] is not None: + tmp.append(calc_xy_dict[k]) + calc_xy_all = np.vstack(tmp) meas_xyo_all = np.vstack(meas_xyo_all) npts = len(meas_xyo_all) diff --git a/scripts/makeOverlapTable.py b/scripts/makeOverlapTable.py index 30d80748..2cd459a8 100755 --- a/scripts/makeOverlapTable.py +++ b/scripts/makeOverlapTable.py @@ -4,54 +4,97 @@ This is a temporary script file. """ +from __future__ import print_function -import argparse, os, sys +import argparse +import os +import sys +import time +import yaml -import cPickle +try: + import dill as cpl +except(ImportError): + import cPickle as cpl import numpy as np -from mpl_toolkits.mplot3d import Axes3D import matplotlib.pyplot as plt from matplotlib.colors import BoundaryNorm from hexrd import config +from hexrd import instrument from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.coreutil import get_instrument_parameters from sklearn.cluster import dbscan -from scipy import cluster + +# ============================================================================= +# PARAMETERS +# ============================================================================= + +do_plots = False +save_fig = False + + +# ============================================================================= +# LOCAL FUNCTIONS +# ============================================================================= + +# plane data +def load_pdata(cpkl, key): + with file(cpkl, "r") as matf: + mat_list = cpl.load(matf) + return dict(zip([i.name for i in mat_list], mat_list))[key].planeData + + +# instrument +def load_instrument(yml): + with file(yml, 'r') as f: + icfg = yaml.load(f) + return instrument.HEDMInstrument(instrument_config=icfg) + def adist(ang0, ang1): - resd = xfcapi.angularDifference(ang0 ,ang1) + resd = xfcapi.angularDifference(ang0, ang1) return np.sqrt(sum(resd**2)) + +def postprocess_dbscan(labels): + cl = np.array(labels, dtype=int) # convert to array + noise_points = cl == -1 # index for marking noise + cl += 1 # move index to 1-based instead of 0 + cl[noise_points] = -1 # re-mark noise as -1 + + # extract number of clusters + if np.any(cl == -1): + nblobs = len(np.unique(cl)) - 1 + else: + nblobs = len(np.unique(cl)) + return cl, nblobs + + def build_overlap_table(cfg, tol_mult=0.5): - - icfg = get_instrument_parameters(cfg) - + + # get instrument object + instr = load_instrument(cfg.instrument.parameters) + + # load grain table gt = np.loadtxt( os.path.join(cfg.analysis_dir, 'grains.out') ) - ngrains = len(gt) - - mat_list = cPickle.load(open(cfg.material.definitions, 'r')) - mat_names = [mat_list[i].name for i in range(len(mat_list))] - mat_dict = dict(zip(mat_names, mat_list)) - - matl = mat_dict[cfg.material.active] - - pd = matl.planeData + + # get plane data + pd = load_pdata(cfg.material.definitions, cfg.material.active) pd.exclusions = np.zeros(len(pd.exclusions), dtype=bool) pd.tThMax = np.radians(cfg.fit_grains.tth_max) pd.tThWidth = np.radians(cfg.fit_grains.tolerance.tth[-1]) - + # for clustering... eps = tol_mult*np.radians( min( - min(cfg.fit_grains.tolerance.eta), + min(cfg.fit_grains.tolerance.eta), 2*min(cfg.fit_grains.tolerance.omega) ) ) @@ -63,167 +106,210 @@ def build_overlap_table(cfg, tol_mult=0.5): pids.append( [pd.hklDataList[hklids[i]]['hklID'] for i in range(len(hklids))] ) - + # Make table of unit diffraction vectors - st = [] - for i in range(ngrains): - this_st = np.loadtxt( - os.path.join(cfg.analysis_dir, 'spots_%05d.out' %i) - ) - #... do all predicted? - valid_spt = this_st[:, 0] >= 0 - #valid_spt = np.ones(len(this_st), dtype=bool) + overlap_table_dict = {} + for det_key, panel in instr.detectors.iteritems(): + st = [] + for i in range(ngrains): + this_st = np.loadtxt( + os.path.join(cfg.analysis_dir, + os.path.join(det_key, 'spots_%05d.out' % i) + ) + ) - angs = this_st[valid_spt, 7:10] + # ??? do all predicted? + valid_spt = this_st[:, 0] >= 0 + # valid_spt = np.ones(len(this_st), dtype=bool) - dvec = xfcapi.anglesToDVec( - angs, - chi=icfg['oscillation_stage']['chi'] - ) + # !!! double check this is still correct + angs = this_st[valid_spt, 7:10] - # [ grainID, reflID, hklID, D_s[0], D_s[1], D_s[2], tth, eta, ome ] - st.append( - np.hstack([ - i*np.ones((sum(valid_spt), 1)), - this_st[valid_spt, :2], - dvec, + # get unit diffraction vectors + dvec = xfcapi.anglesToDVec( angs, - ]) - ) + chi=instr.chi + ) - # make overlap table - # [[range_0], [range_1], ..., [range_n]] - # range_0 = [grainIDs, reflIDs, hklIDs] that are within tol - overlap_table = [] - ii = 0 - for pid in pids: - tmp = []; a = []; b = []; c = [] - for j in range(len(pid)): - a.append( - np.vstack( - [st[i][st[i][:, 2] == pid[j], 3:6] for i in range(len(st))] - ) + # [ grainID, reflID, hklID, D_s[0], D_s[1], D_s[2], tth, eta, ome ] + st.append( + np.hstack([ + i*np.ones((sum(valid_spt), 1)), + this_st[valid_spt, :2], + dvec, + angs, + ]) ) - b.append( - np.vstack( - [st[i][st[i][:, 2] == pid[j], 0:3] for i in range(len(st))] + + # ========================================================================= + # make overlap table + # ========================================================================= + # [[range_0], [range_1], ..., [range_n]] + # range_0 = [grainIDs, reflIDs, hklIDs] that are within tol + overlap_table = [] + ii = 0 + for pid in pids: + print("processing ring set %d" % ii) + start0 = time.clock() + tmp = [] + a = [] # unit diffraction vectors in sample frame + b = [] # [grainID, reflID, hklID] + c = [] # predicted angles [tth, eta, ome] + for j in range(len(pid)): + a.append( + np.vstack( + [st[i][st[i][:, 2] == pid[j], 3:6] + for i in range(len(st))] + ) ) - ) - c.append( - np.vstack( - [st[i][st[i][:, 2] == pid[j], 6:9] for i in range(len(st))] + b.append( + np.vstack( + [st[i][st[i][:, 2] == pid[j], 0:3] + for i in range(len(st))] + ) ) - ) - pass - a = np.vstack(a) - b = np.vstack(b) - c = np.vstack(c) - if len(a) > 0: - # run dbscan - core_samples, labels = dbscan( - a, - eps=eps, - min_samples=2, - metric='minkowski', p=2, - ) - - cl = np.array(labels, dtype=int) # convert to array - noise_points = cl == -1 # index for marking noise - cl += 1 # move index to 1-based instead of 0 - cl[noise_points] = -1 # re-mark noise as -1 - - # extract number of clusters - if np.any(cl == -1): - nblobs = len(np.unique(cl)) - 1 - else: - nblobs = len(np.unique(cl)) - - for i in range(1, nblobs+1): - # put in check on omega here - these_angs = c[np.where(cl == i)[0], :] - local_cl = cluster.hierarchy.fclusterdata( - these_angs[:, 1:], - eps, - criterion='distance', - metric=adist + c.append( + np.vstack( + [st[i][st[i][:, 2] == pid[j], 6:9] + for i in range(len(st))] ) - local_nblobs = len(np.unique(local_cl)) - if local_nblobs < len(these_angs): - for j in range(1, local_nblobs + 1): - npts = sum(local_cl == j) - if npts >= 2: - cl_idx = np.where(local_cl == j)[0] - #import pdb; pdb.set_trace() - tmp.append( - b[np.where(cl == i)[0][cl_idx], :] - ) - print "processing ring set %d" %ii - ii += 1 - overlap_table.append(tmp) - return overlap_table - + ) + pass + a = np.vstack(a) # unit diffraction vectors in sample frame + b = np.vstack(b) # [grainID, reflID, hklID] + c = np.vstack(c) # predicted angles [tth, eta, ome] + if len(a) > 0: + # run dbscan + core_samples, labels = dbscan( + a, + eps=eps, + min_samples=2, + metric='minkowski', p=2, + ) + cl, nblobs = postprocess_dbscan(labels) + elapsed0 = time.clock() - start0 + print("\tdbscan took %.2f seconds" % elapsed0) + # import pdb; pdb.set_trace() + print("\tcollapsing incidentals for %d candidates..." % nblobs) + start1 = time.clock() # time this + for i in range(1, nblobs + 1): + # put in check on omega here + these_angs = c[np.where(cl == i)[0], :] + # local_cl = cluster.hierarchy.fclusterdata( + # these_angs[:, 1:], + # eps, + # criterion='distance', + # metric=adist + # ) + # local_nblobs = len(np.unique(local_cl)) + _, local_labels = dbscan( + these_angs[:, 1:], + eps=eps, + min_samples=2, + metric=adist, + n_jobs=-1, + ) + local_cl, local_nblobs = postprocess_dbscan(local_labels) + + if local_nblobs < len(these_angs): + for j in range(1, local_nblobs + 1): + npts = sum(local_cl == j) + if npts >= 2: + cl_idx = np.where(local_cl == j)[0] + # import pdb; pdb.set_trace() + tmp.append( + b[np.where(cl == i)[0][cl_idx], :] + ) + elapsed1 = time.clock() - start1 + print("\tomega filtering took %.2f seconds" % elapsed1) + ii += 1 + overlap_table.append(tmp) + overlap_table_dict[det_key] = overlap_table + return overlap_table_dict + + def build_discrete_cmap(ngrains): - + # define the colormap - cmap = plt.cm.jet - + cmap = plt.cm.inferno + # extract all colors from the .jet map cmaplist = [cmap(i) for i in range(cmap.N)] - + # create the new map cmap = cmap.from_list('Custom cmap', cmaplist, cmap.N) - + # define the bins and normalize bounds = np.linspace(0, ngrains, ngrains+1) norm = BoundaryNorm(bounds, cmap.N) return cmap, norm - -#%% + + +# ============================================================================= +# %% CLI +# ============================================================================= + if __name__ == '__main__': parser = argparse.ArgumentParser( description='Make overlap table from cfg file') parser.add_argument( - 'cfg', metavar='cfg_filename', + 'cfg', metavar='cfg_filename', type=str, help='a YAML config filename') parser.add_argument( - '-m', '--multiplier', - help='multiplier on angular tolerance', + '-m', '--multiplier', + help='multiplier on angular tolerance', type=float, default=0.5) + parser.add_argument( + '-b', '--block-id', + help='block-id in config', + type=int, default=0) args = vars(parser.parse_args(sys.argv[1:])) - - cfg = config.open(args['cfg'])[0] - print "loaded config file %s" %args['cfg'] - overlap_table = build_overlap_table(cfg) - np.savez(os.path.join(cfg.analysis_dir, 'overlap_table.npz'), + + print("loaded config file %s" % args['cfg']) + print("will use block %d" % args['block_id']) + cfg = config.open(args['cfg'])[args['block_id']] + overlap_table = build_overlap_table(cfg, tol_mult=args['multiplier']) + np.savez(os.path.join(cfg.analysis_dir, 'overlap_table.npz'), *overlap_table) -#%% -#fig = plt.figure() -#ax = fig.add_subplot(111, projection='3d') -# -#etas = np.radians(np.linspace(0, 359, num=360)) -#cx = np.cos(etas) -#cy = np.sin(etas) -#cz = np.zeros_like(etas) -# -#ax.plot(cx, cy, cz, c='b') -#ax.plot(cx, cz, cy, c='g') -#ax.plot(cz, cx, cy, c='r') -#ax.scatter3D(a[:, 0], a[:, 1], a[:, 2], c=b[:, 0], cmap=cmap, norm=norm, marker='o', s=20) -# -#ax.set_xlabel(r'$\mathbf{\mathrm{X}}_s$') -#ax.set_ylabel(r'$\mathbf{\mathrm{Y}}_s$') -#ax.set_zlabel(r'$\mathbf{\mathrm{Z}}_s$') -# -#ax.elev = 124 -#ax.azim = -90 -# -#ax.axis('equal') -# -##fname = "overlaps_%03d.png" -##for i in range(360): -## ax.azim += i -## fig.savefig( -## fname %i, dpi=200, facecolor='w', edgecolor='w', -## orientation='landcape') + + +# ============================================================================= +# %% OPTIONAL PLOTTING +# ============================================================================= + +if do_plots: + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + + etas = np.radians(np.linspace(0, 359, num=360)) + cx = np.cos(etas) + cy = np.sin(etas) + cz = np.zeros_like(etas) + + ax.plot(cx, cy, cz, c='b') + ax.plot(cx, cz, cy, c='g') + ax.plot(cz, cx, cy, c='r') + + # !!! need a and b + cmap, norm = build_discrete_cmap(len(b)) + ax.scatter3D(a[:, 0], a[:, 1], a[:, 2], c=b[:, 0], + cmap=cmap, norm=norm, marker='o', s=20) + + ax.set_xlabel(r'$\mathbf{\mathrm{X}}_s$') + ax.set_ylabel(r'$\mathbf{\mathrm{Y}}_s$') + ax.set_zlabel(r'$\mathbf{\mathrm{Z}}_s$') + + ax.elev = 124 + ax.azim = -90 + + ax.axis('equal') + + if save_fig: + fname = "overlaps_%03d.png" + for i in range(360): + ax.azim += i + fig.savefig( + fname % i, dpi=200, facecolor='w', edgecolor='w', + orientation='landcape') From d5b2fbff9ec771d9d5b098653acb58eae0e7f7c8 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 22 Jun 2018 17:52:57 -0500 Subject: [PATCH 160/253] changed HDF write behavior --- hexrd/imageseries/save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 9d47dc0b..ee6cbebb 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -95,7 +95,7 @@ def __init__(self, ims, fname, **kwargs): # def write(self): """Write imageseries to HDF5 file""" - f = h5py.File(self._fname, "a") + f = h5py.File(self._fname, "w") g = f.create_group(self._path) s0, s1 = self._shape From 4a8c4e4f59be5a5cbba4798b772546de6992ac49 Mon Sep 17 00:00:00 2001 From: Jun-Sang Park Date: Fri, 22 Jun 2018 19:00:42 -0500 Subject: [PATCH 161/253] first port of NF utils --- hexrd/utils/profiler.py | 2 +- scripts/new_simulate_nf.py | 1087 +++++++++++++++++++++++++++++++ scripts/process_nf_grain_map.py | 275 ++++++++ scripts/stitch_grains.py | 286 ++++++++ 4 files changed, 1649 insertions(+), 1 deletion(-) create mode 100644 scripts/new_simulate_nf.py create mode 100644 scripts/process_nf_grain_map.py create mode 100644 scripts/stitch_grains.py diff --git a/hexrd/utils/profiler.py b/hexrd/utils/profiler.py index d4591f25..aeb9076a 100644 --- a/hexrd/utils/profiler.py +++ b/hexrd/utils/profiler.py @@ -16,7 +16,7 @@ pass try: - from numbapro import nvtx + import nvtxpy as nvtx except ImportError: pass diff --git a/scripts/new_simulate_nf.py b/scripts/new_simulate_nf.py new file mode 100644 index 00000000..d9704af1 --- /dev/null +++ b/scripts/new_simulate_nf.py @@ -0,0 +1,1087 @@ +""" +Refactor of simulate_nf so that an experiment is mocked up. + +Also trying to minimize imports +""" +from __future__ import print_function + +import os +import logging + +import numpy as np +import numba +import yaml +import argparse +import time +import contextlib +import multiprocessing +import tempfile +import shutil + +# import of hexrd modules +import hexrd +from hexrd.xrd import transforms as xf +from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd import xrdutil +import hexrd.xrd.material + +from skimage.morphology import dilation as ski_dilation + + +# ============================================================================== +# %% SOME SCAFFOLDING +# ============================================================================== + +class ProcessController(object): + """This is a 'controller' that provides the necessary hooks to + track the results of the process as well as to provide clues of + the progress of the process""" + + def __init__(self, result_handler=None, progress_observer=None, ncpus = 1, + chunk_size = 100): + self.rh = result_handler + self.po = progress_observer + self.ncpus = ncpus + self.chunk_size = chunk_size + self.limits = {} + self.timing = [] + + + # progress handling -------------------------------------------------------- + + def start(self, name, count): + self.po.start(name, count) + t = time.time() + self.timing.append((name, count, t)) + + + def finish(self, name): + t = time.time() + self.po.finish() + entry = self.timing.pop() + assert name==entry[0] + total = t - entry[2] + logging.info("%s took %8.3fs (%8.6fs per item).", entry[0], total, total/entry[1]) + + + def update(self, value): + self.po.update(value) + + # result handler ----------------------------------------------------------- + + def handle_result(self, key, value): + logging.debug("handle_result (%(key)s)", locals()) + self.rh.handle_result(key, value) + + # value limitting ---------------------------------------------------------- + def set_limit(self, key, limit_function): + if key in self.limits: + logging.warn("Overwritting limit funtion for '%(key)s'", locals()) + + self.limits[key] = limit_function + + def limit(self, key, value): + try: + value = self.limits[key](value) + except KeyError: + pass + except Exception: + logging.warn("Could not apply limit to '%(key)s'", locals()) + + return value + + # configuration ----------------------------------------------------------- + def get_process_count(self): + return self.ncpus + + def get_chunk_size(self): + return self.chunk_size + + +def null_progress_observer(): + class NullProgressObserver(object): + def start(self, name, count): + pass + + def update(self, value): + pass + + def finish(self): + pass + + return NullProgressObserver() + + +def progressbar_progress_observer(): + from progressbar import ProgressBar, Percentage, Bar + + class ProgressBarProgressObserver(object): + def start(self, name, count): + self.pbar = ProgressBar(widgets=[name, Percentage(), Bar()], + maxval=count) + self.pbar.start() + + def update(self, value): + self.pbar.update(value) + + def finish(self): + self.pbar.finish() + + return ProgressBarProgressObserver() + + +def forgetful_result_handler(): + class ForgetfulResultHandler(object): + def handle_result(self, key, value): + pass # do nothing + + return ForgetfulResultHandler() + + +def saving_result_handler(filename): + """returns a result handler that saves the resulting arrays into a file + with name filename""" + class SavingResultHandler(object): + def __init__(self, file_name): + self.filename = file_name + self.arrays = {} + + def handle_result(self, key, value): + self.arrays[key] = value + + def __del__(self): + logging.debug("Writing arrays in %(filename)s", self.__dict__) + try: + np.savez_compressed(open(self.filename, "wb"), **self.arrays) + except IOError: + logging.error("Failed to write %(filename)s", self.__dict__) + + return SavingResultHandler(filename) + + +def checking_result_handler(filename): + """returns a return handler that checks the results against a + reference file. + + The Check will consider a FAIL either a result not present in the + reference file (saved as a numpy savez or savez_compressed) or a + result that differs. It will consider a PARTIAL PASS if the + reference file has a shorter result, but the existing results + match. A FULL PASS will happen when all existing results match + + """ + class CheckingResultHandler(object): + def __init__(self, reference_file): + """Checks the result against those save in 'reference_file'""" + logging.info("Loading reference results from '%s'", reference_file) + self.reference_results = np.load(open(reference_file, 'rb')) + + def handle_result(self, key, value): + if key in ['experiment', 'image_stack']: + return #ignore these + + try: + reference = self.reference_results[key] + except KeyError as e: + logging.warning("%(key)s: %(e)s", locals()) + reference = None + + if reference is None: + msg = "'{0}': No reference result." + logging.warn(msg.format(key)) + + try: + if key=="confidence": + reference = reference.T + value = value.T + + check_len = min(len(reference), len(value)) + test_passed = np.allclose(value[:check_len], reference[:check_len]) + + if not test_passed: + msg = "'{0}': FAIL" + logging.warn(msg.format(key)) + lvl = logging.WARN + elif len(value) > check_len: + msg = "'{0}': PARTIAL PASS" + lvl = logging.WARN + else: + msg = "'{0}': FULL PASS" + lvl = logging.INFO + logging.log(lvl, msg.format(key)) + except Exception as e: + msg = "%(key)s: Failure trying to check the results.\n%(e)s" + logging.error(msg, locals()) + + return CheckingResultHandler(filename) + + +# ============================================================================== +# %% SETUP FUNCTION +# ============================================================================== +def mockup_experiment(): + # user options + # each grain is provided in the form of a quaternion. + + # The following array contains the quaternions for the array. Note that the + # quaternions are in the columns, with the first row (row 0) being the real + # part w. We assume that we are dealing with unit quaternions + + quats = np.array([[ 0.91836393, 0.90869942], + [ 0.33952917, 0.1834835 ], + [ 0.17216207, 0.10095837], + [ 0.10811041, 0.36111851]]) + + n_grains = quats.shape[-1] # last dimension provides the number of grains + phis = 2.*np.arccos(quats[0, :]) # phis are the angles for the quaternion + # ns contains the rotation axis as an unit vector + ns = hexrd.matrixutil.unitVector(quats[1:, :]) + exp_maps = np.array([phis[i]*ns[:, i] for i in range(n_grains)]) + rMat_c = hexrd.xrd.rotations.rotMatOfQuat(quats) + + cvec = np.arange(-25, 26) + X, Y, Z = np.meshgrid(cvec, cvec, cvec) + + crd0 = 1e-3*np.vstack([X.flatten(), Y.flatten(), Z.flatten()]).T + crd1 = crd0 + np.r_[0.100, 0.100, 0] + crds = np.array([crd0, crd1]) + + # make grain parameters + grain_params = [] + for i in range(n_grains): + for j in range(len(crd0)): + grain_params.append( + np.hstack([exp_maps[i, :], crds[i][j, :], xf.vInv_ref.flatten()]) + ) + + # scan range and period + ome_period = (0, 2*np.pi) + ome_range = [ome_period,] + ome_step = np.radians(1.) + nframes = 0 + for i in range(len(ome_range)): + del_ome = ome_range[i][1]-ome_range[i][0] + nframes += int((ome_range[i][1]-ome_range[i][0])/ome_step) + + ome_edges = np.arange(nframes+1)*ome_step + + # instrument + with open('./retiga.yml', 'r') as fildes: + instr_cfg = yaml.load(fildes) + + tiltAngles = instr_cfg['detector']['transform']['tilt_angles'] + tVec_d = np.array(instr_cfg['detector']['transform']['t_vec_d']).reshape(3,1) + chi = instr_cfg['oscillation_stage']['chi'] + tVec_s = np.array(instr_cfg['oscillation_stage']['t_vec_s']).reshape(3,1) + rMat_d = xfcapi.makeDetectorRotMat(tiltAngles) + rMat_s = xfcapi.makeOscillRotMat([chi, 0.]) + + pixel_size = instr_cfg['detector']['pixels']['size'] + nrows = instr_cfg['detector']['pixels']['rows'] + ncols = instr_cfg['detector']['pixels']['columns'] + + col_ps = pixel_size[1] + row_ps = pixel_size[0] + + row_dim = row_ps*nrows # in mm + col_dim = col_ps*ncols # in mm + panel_dims = [(-0.5*ncols*col_ps, -0.5*nrows*row_ps), + ( 0.5*ncols*col_ps, 0.5*nrows*row_ps)] + + x_col_edges = col_ps * (np.arange(ncols + 1) - 0.5*ncols) + y_row_edges = row_ps * (np.arange(nrows, -1, -1) - 0.5*nrows) + rx, ry = np.meshgrid(x_col_edges, y_row_edges) + + gcrds = xfcapi.detectorXYToGvec(np.vstack([rx.flatten(), ry.flatten()]).T, + rMat_d, rMat_s, + tVec_d, tVec_s, np.zeros(3)) + + max_pixel_tth = np.amax(gcrds[0][0]) + detector_params = np.hstack([tiltAngles, tVec_d.flatten(), chi, + tVec_s.flatten()]) + distortion = None + + # a different parametrization for the sensor (makes for faster quantization) + base = np.array([x_col_edges[0], + y_row_edges[0], + ome_edges[0]]) + deltas = np.array([x_col_edges[1] - x_col_edges[0], + y_row_edges[1] - y_row_edges[0], + ome_edges[1] - ome_edges[0]]) + inv_deltas = 1.0/deltas + clip_vals = np.array([ncols, nrows]) + + # dilation + max_diameter = np.sqrt(3)*0.005 + row_dilation = int(np.ceil(0.5 * max_diameter/row_ps)) + col_dilation = int(np.ceil(0.5 * max_diameter/col_ps)) + + # crystallography data + from hexrd import valunits + gold = hexrd.xrd.material.Material('gold') + gold.sgnum = 225 + gold.latticeParameters = [4.0782, ] + gold.hklMax = 200 + gold.beamEnergy = valunits.valWUnit("wavelength", "ENERGY", 52, "keV") + gold.planeData.exclusions = None + gold.planeData.tThMax = max_pixel_tth #note this comes from info in the detector + + ns = argparse.Namespace() + # grains related information + ns.n_grains = n_grains # this can be derived from other values... + ns.rMat_c = rMat_c # n_grains rotation matrices (one per grain) + ns.exp_maps = exp_maps # n_grains exp_maps -angle * rotation axis- (one per grain) + + ns.plane_data = gold.planeData + ns.detector_params = detector_params + ns.pixel_size = pixel_size + ns.ome_range = ome_range + ns.ome_period = ome_period + ns.x_col_edges = x_col_edges + ns.y_row_edges = y_row_edges + ns.ome_edges = ome_edges + ns.ncols = ncols + ns.nrows = nrows + ns.nframes = nframes # used only in simulate... + ns.rMat_d = rMat_d + ns.tVec_d = tVec_d + ns.chi = chi # note this is used to compute S... why is it needed? + ns.tVec_s = tVec_s + ns.rMat_c = rMat_c + ns.row_dilation = row_dilation + ns.col_dilation = col_dilation + ns.distortion = distortion + ns.panel_dims = panel_dims # used only in simulate... + ns.base = base + ns.inv_deltas = inv_deltas + ns.clip_vals = clip_vals + + return grain_params, ns + + +# ============================================================================== +# %% OPTIMIZED BITS +# ============================================================================== + +# Some basic 3d algebra ======================================================== +@numba.njit +def _v3_dot(a, b): + return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + + +@numba.njit +def _m33_v3_multiply(m, v, dst): + v0 = v[0]; v1 = v[1]; v2 = v[2] + dst[0] = m[0, 0]*v0 + m[0, 1]*v1 + m[0, 2]*v2 + dst[1] = m[1, 0]*v0 + m[1, 1]*v1 + m[1, 2]*v2 + dst[2] = m[2, 0]*v0 + m[2, 1]*v1 + m[2, 2]*v2 + + return dst + + +@numba.njit +def _v3_normalized(src, dst): + v0 = src[0] + v1 = src[1] + v2 = src[2] + sqr_norm = v0*v0 + v1*v1 + v2*v2 + inv_norm = 1.0 if sqr_norm == 0.0 else 1./np.sqrt(sqr_norm) + + dst[0] = v0 * inv_norm + dst[1] = v1 * inv_norm + dst[2] = v2 * inv_norm + + return dst + + +@numba.njit +def _make_binary_rot_mat(src, dst): + v0 = src[0]; v1 = src[1]; v2 = src[2] + + dst[0,0] = 2.0*v0*v0 - 1.0 + dst[0,1] = 2.0*v0*v1 + dst[0,2] = 2.0*v0*v2 + dst[1,0] = 2.0*v1*v0 + dst[1,1] = 2.0*v1*v1 - 1.0 + dst[1,2] = 2.0*v1*v2 + dst[2,0] = 2.0*v2*v0 + dst[2,1] = 2.0*v2*v1 + dst[2,2] = 2.0*v2*v2 - 1.0 + + return dst + + +# code transcribed in numba from transforms module ============================= + +# This is equivalent to the transform module anglesToGVec, but written in +# numba. This should end in a module to share with other scripts +@numba.njit +def _anglesToGVec(angs, rMat_ss, rMat_c): + """From a set of angles return them in crystal space""" + result = np.empty_like(angs) + for i in range(len(angs)): + cx = np.cos(0.5*angs[i, 0]) + sx = np.sin(0.5*angs[i, 0]) + cy = np.cos(angs[i,1]) + sy = np.sin(angs[i,1]) + g0 = cx*cy + g1 = cx*sy + g2 = sx + + # with g being [cx*xy, cx*sy, sx] + # result = dot(rMat_c, dot(rMat_ss[i], g)) + t0_0 = rMat_ss[ i, 0, 0]*g0 + rMat_ss[ i, 1, 0]*g1 + rMat_ss[ i, 2, 0]*g2 + t0_1 = rMat_ss[ i, 0, 1]*g0 + rMat_ss[ i, 1, 1]*g1 + rMat_ss[ i, 2, 1]*g2 + t0_2 = rMat_ss[ i, 0, 2]*g0 + rMat_ss[ i, 1, 2]*g1 + rMat_ss[ i, 2, 2]*g2 + + result[i, 0] = rMat_c[0, 0]*t0_0 + rMat_c[ 1, 0]*t0_1 + rMat_c[ 2, 0]*t0_2 + result[i, 1] = rMat_c[0, 1]*t0_0 + rMat_c[ 1, 1]*t0_1 + rMat_c[ 2, 1]*t0_2 + result[i, 2] = rMat_c[0, 2]*t0_0 + rMat_c[ 1, 2]*t0_1 + rMat_c[ 2, 2]*t0_2 + + return result + + +# This is equivalent to the transform's module gvecToDetectorXYArray, but written in +# numba. +# As of now, it is not a good replacement as efficient allocation of the temporary +# arrays is not competitive with the stack allocation using in the C version of the +# code (WiP) + +# tC varies per coord +# gvec_cs, rSm varies per grain +# +# gvec_cs +beam = xf.bVec_ref[:, 0] +Z_l = xf.Zl[:,0] +@numba.jit() +def _gvec_to_detector_array(vG_sn, rD, rSn, rC, tD, tS, tC): + """ beamVec is the beam vector: (0, 0, -1) in this case """ + ztol = xrdutil.epsf + p3_l = np.empty((3,)) + tmp_vec = np.empty((3,)) + vG_l = np.empty((3,)) + tD_l = np.empty((3,)) + norm_vG_s = np.empty((3,)) + norm_beam = np.empty((3,)) + tZ_l = np.empty((3,)) + brMat = np.empty((3,3)) + result = np.empty((len(rSn), 2)) + + _v3_normalized(beam, norm_beam) + _m33_v3_multiply(rD, Z_l, tZ_l) + + for i in xrange(len(rSn)): + _m33_v3_multiply(rSn[i], tC, p3_l) + p3_l += tS + p3_minus_p1_l = tD - p3_l + + num = _v3_dot(tZ_l, p3_minus_p1_l) + _v3_normalized(vG_sn[i], norm_vG_s) + + _m33_v3_multiply(rC, norm_vG_s, tmp_vec) + _m33_v3_multiply(rSn[i], tmp_vec, vG_l) + + bDot = -_v3_dot(norm_beam, vG_l) + + if bDot < ztol or bDot > 1.0 - ztol: + result[i, 0] = np.nan + result[i, 1] = np.nan + continue + + _make_binary_rot_mat(vG_l, brMat) + _m33_v3_multiply(brMat, norm_beam, tD_l) + denom = _v3_dot(tZ_l, tD_l) + + if denom < ztol: + result[i, 0] = np.nan + result[i, 1] = np.nan + continue + + u = num/denom + tmp_res = u*tD_l - p3_minus_p1_l + result[i,0] = _v3_dot(tmp_res, rD[:,0]) + result[i,1] = _v3_dot(tmp_res, rD[:,1]) + + return result + + +@numba.njit +def _quant_and_clip_confidence(coords, angles, image, base, inv_deltas, clip_vals): + """quantize and clip the parametric coordinates in coords + angles + + coords - (..., 2) array: input 2d parametric coordinates + angles - (...) array: additional dimension for coordinates + base - (3,) array: base value for quantization (for each dimension) + inv_deltas - (3,) array: inverse of the quantum size (for each dimension) + clip_vals - (2,) array: clip size (only applied to coords dimensions) + + clipping is performed on ranges [0, clip_vals[0]] for x and + [0, clip_vals[1]] for y + + returns an array with the quantized coordinates, with coordinates + falling outside the clip zone filtered out. + + """ + count = len(coords) + + in_sensor = 0 + matches = 0 + for i in range(count): + xf = coords[i, 0] + yf = coords[i, 1] + + xf = np.floor((xf - base[0]) * inv_deltas[0]) + if not xf >= 0.0: + continue + if not xf < clip_vals[0]: + continue + + yf = np.floor((yf - base[1]) * inv_deltas[1]) + + if not yf >= 0.0: + continue + if not yf < clip_vals[1]: + continue + + zf = np.floor((angles[i] - base[2]) * inv_deltas[2]) + + in_sensor += 1 + + x, y, z = int(xf), int(yf), int(zf) + + x_byte = x // 8 + x_off = 7 - (x % 8) + if image[z, y, x_byte] & (1<= clip_vals[0]: + continue + + y = int(np.floor((coords[i, 1] - base[1]) * inv_deltas[1])) + + if y < 0 or y >= clip_vals[1]: + continue + + z = int(np.floor((angles[i] - base[2]) * inv_deltas[2])) + + x_byte = x // 8 + x_off = 7 - (x % 8) + image[z, y, x_byte] |= (1 << x_off) + + +# ============================================================================== +# %% ORIENTATION TESTING +# ============================================================================== +def test_orientations(image_stack, experiment, controller): + """grand loop precomputing the grown image stack + + image-stack -- is the image stack to be tested against. + + experiment -- A bunch of experiment related parameters. + + controller -- An external object implementing the hooks to notify progress + as well as figuring out what to do with results. + """ + + # extract some information needed ========================================= + # number of grains, number of coords (maybe limited by call), projection + # function to use, chunk size to use if multiprocessing and the number + # of cpus. + n_grains = experiment.n_grains + chunk_size = controller.get_chunk_size() + ncpus = controller.get_process_count() + + # generate angles ========================================================= + # all_angles will be a list containing arrays for the different angles to + # use, one entry per grain. + # + # Note that the angle generation is driven by the exp_maps in the experiment + all_angles = evaluate_diffraction_angles(experiment, controller) + + # generate coords ========================================================= + # The grid of coords to use to test + test_crds = generate_test_grid(-0.25, 0.25, 101) + n_coords = controller.limit('coords', len(test_crds)) + + # first, perform image dilation =========================================== + # perform image dilation (using scikit_image dilation) + subprocess = 'dilate image_stack' + dilation_shape = np.ones((2*experiment.row_dilation + 1, + 2*experiment.col_dilation + 1), + dtype=np.uint8) + image_stack_dilated = np.empty_like(image_stack) + dilated = np.empty((image_stack.shape[-2], image_stack.shape[-1]<<3), + dtype=np.bool) + n_images = len(image_stack) + controller.start(subprocess, n_images) + for i_image in range(n_images): + to_dilate = np.unpackbits(image_stack[i_image], axis=-1) + ski_dilation(to_dilate, dilation_shape, + out=dilated) + image_stack_dilated[i_image] = np.packbits(dilated, axis=-1) + controller.update(i_image+1) + controller.finish(subprocess) + + # precompute per-grain stuff ============================================== + # gVec_cs and rmat_ss can be precomputed, do so. + subprocess = 'precompute gVec_cs' + controller.start(subprocess, len(all_angles)) + precomp = [] + for i, angs in enumerate(all_angles): + rmat_ss = xfcapi.makeOscillRotMatArray(experiment.chi, angs[:,2]) + gvec_cs = _anglesToGVec(angs, rmat_ss, experiment.rMat_c[i]) + precomp.append((gvec_cs, rmat_ss)) + controller.finish(subprocess) + + # grand loop ============================================================== + # The near field simulation 'grand loop'. Where the bulk of computing is + # performed. We are looking for a confidence matrix that has a n_grains + chunks = xrange(0, n_coords, chunk_size) + subprocess = 'grand_loop' + controller.start(subprocess, n_coords) + finished = 0 + ncpus = min(ncpus, len(chunks)) + + logging.info('Checking confidence for %d coords, %d grains.', + n_coords, n_grains) + confidence = np.empty((n_grains, n_coords)) + if ncpus > 1: + global _multiprocessing_start_method + logging.info('Running multiprocess %d processes (%s)', + ncpus, _multiprocessing_start_method) + with grand_loop_pool(ncpus=ncpus, state=(chunk_size, + image_stack_dilated, + all_angles, precomp, test_crds, + experiment)) as pool: + for rslice, rvalues in pool.imap_unordered(multiproc_inner_loop, + chunks): + count = rvalues.shape[1] + confidence[:, rslice] = rvalues + finished += count + controller.update(finished) + else: + logging.info('Running in a single process') + for chunk_start in chunks: + chunk_stop = min(n_coords, chunk_start+chunk_size) + rslice, rvalues = _grand_loop_inner(image_stack_dilated, all_angles, + precomp, test_crds, experiment, + start=chunk_start, + stop=chunk_stop) + count = rvalues.shape[1] + confidence[:, rslice] = rvalues + finished += count + controller.update(finished) + + controller.finish(subprocess) + controller.handle_result("confidence", confidence) + + +def evaluate_diffraction_angles(experiment, controller=None): + """Uses simulateGVecs to generate the angles used per each grain. + returns a list containg one array per grain. + + experiment -- a bag of experiment values, including the grains specs and other + required parameters. + """ + # extract required data from experiment + exp_maps = experiment.exp_maps + plane_data = experiment.plane_data + detector_params = experiment.detector_params + pixel_size = experiment.pixel_size + ome_range = experiment.ome_range + ome_period = experiment.ome_period + + panel_dims_expanded = [(-10, -10), (10, 10)] + subprocess='evaluate diffraction angles' + pbar = controller.start(subprocess, + len(experiment.exp_maps)) + all_angles = [] + ref_gparams = np.array([0., 0., 0., 1., 1., 1., 0., 0., 0.]) + for i, exp_map in enumerate(experiment.exp_maps): + gparams = np.hstack([exp_map, ref_gparams]) + sim_results = xrdutil.simulateGVecs(plane_data, + detector_params, + gparams, + panel_dims=panel_dims_expanded, + pixel_pitch=pixel_size, + ome_range=ome_range, + ome_period=ome_period, + distortion=None) + all_angles.append(sim_results[2]) + controller.update(i+1) + pass + controller.finish(subprocess) + + return all_angles + + +def _grand_loop_inner(image_stack, angles, precomp, + coords, experiment, start=0, stop=None): + """Actual simulation code for a chunk of data. It will be used both, + in single processor and multiprocessor cases. Chunking is performed + on the coords. + + image_stack -- the image stack from the sensors + angles -- the angles (grains) to test + coords -- all the coords to test + precomp -- (gvec_cs, rmat_ss) precomputed for each grain + experiment -- bag with experiment parameters + start -- chunk start offset + stop -- chunk end offset + """ + + t = time.time() + n_coords = len(coords) + n_angles = len(angles) + + # experiment geometric layout parameters + rD = experiment.rMat_d + rCn = experiment.rMat_c + tD = experiment.tVec_d[:,0] + tS = experiment.tVec_s[:,0] + + # experiment panel related configuration + base = experiment.base + inv_deltas = experiment.inv_deltas + clip_vals = experiment.clip_vals + distortion = experiment.distortion + + _to_detector = xfcapi.gvecToDetectorXYArray + #_to_detector = _gvec_to_detector_array + stop = min(stop, n_coords) if stop is not None else n_coords + + distortion_fn = None + if distortion is not None and len(distortion > 0): + distortion_fn, distortion_args = distortion + + acc_detector = 0.0 + acc_distortion = 0.0 + acc_quant_clip = 0.0 + confidence = np.zeros((n_angles, stop-start)) + grains = 0 + crds = 0 + + if distortion_fn is None: + for igrn in xrange(n_angles): + angs = angles[igrn]; rC = rCn[igrn] + gvec_cs, rMat_ss = precomp[igrn] + grains += 1 + for icrd in xrange(start, stop): + t0 = time.time() + det_xy = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) + t1 = time.time() + c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, + base, inv_deltas, clip_vals) + t2 = time.time() + acc_detector += t1 - t0 + acc_quant_clip += t2 - t1 + crds += 1 + confidence[igrn, icrd - start] = c + else: + for igrn in xrange(n_angles): + angs = angles[igrn]; rC = rCn[igrn] + gvec_cs, rMat_ss = precomp[igrn] + grains += 1 + for icrd in xrange(start, stop): + t0 = time.time() + det_xy = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) + t1 = time.time() + det_xy = distortion_fn(tmp_xys, distortion_args, invert=True) + t2 = time.time() + c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, + base, inv_deltas, clip_vals) + t3 = time.time() + acc_detector += t1 - t0 + acc_distortion += t2 - t1 + acc_quant_clip += t3 - t2 + crds += 1 + confidence[igrn, icrd - start] = c + + t = time.time() - t + return slice(start, stop), confidence + + +def generate_test_grid(low, top, samples): + """generates a test grid of coordinates""" + cvec_s = np.linspace(low, top, samples) + Xs, Ys, Zs = np.meshgrid(cvec_s, cvec_s, cvec_s) + return np.vstack([Xs.flatten(), Ys.flatten(), Zs.flatten()]).T + + +# Multiprocessing bits ======================================================== +# +# The parallellized part of test_orientations uses some big arrays as part of +# the state that needs to be communicated to the spawn processes. +# +# On fork platforms, take advantage of process memory inheritance. +# +# On non fork platforms, rely on joblib dumping the state to disk and loading +# back in the target processes, pickling only the minimal information to load +# state back. Pickling the big arrays directly was causing memory errors and +# would be less efficient in memory (as joblib memmaps by default the big +# arrays, meaning they may be shared between processes). + +def multiproc_inner_loop(chunk): + """function to use in multiprocessing that computes the simulation over the + task's alloted chunk of data""" + + chunk_size = _mp_state[0] + n_coords = len(_mp_state[4]) + chunk_stop = min(n_coords, chunk+chunk_size) + return _grand_loop_inner(*_mp_state[1:], start=chunk, stop=chunk_stop) + + +def worker_init(id_state, id_exp): + """process initialization function. This function is only used when the + child processes are spawned (instead of forked). When using the fork model + of multiprocessing the data is just inherited in process memory.""" + import joblib + + global _mp_state + state = joblib.load(id_state) + experiment = joblib.load(id_exp) + _mp_state = state + (experiment,) + +@contextlib.contextmanager +def grand_loop_pool(ncpus, state): + """function that handles the initialization of multiprocessing. It handles + properly the use of spawned vs forked multiprocessing. The multiprocessing + can be either 'fork' or 'spawn', with 'spawn' being required in non-fork + platforms (like Windows) and 'fork' being preferred on fork platforms due + to its efficiency. + """ + # state = ( chunk_size, + # image_stack, + # angles, + # precomp, + # coords, + # experiment ) + global _multiprocessing_start_method + if _multiprocessing_start_method == 'fork': + # Use FORK multiprocessing. + + # All read-only data can be inherited in the process. So we "pass" it as + # a global that the child process will be able to see. At the end of the + # processing the global is removed. + global _mp_state + _mp_state = state + pool = multiprocessing.Pool(ncpus) + yield pool + del (_mp_state) + else: + # Use SPAWN multiprocessing. + + # As we can not inherit process data, all the required data is + # serialized into a temporary directory using joblib. The + # multiprocessing pool will have the "worker_init" as initialization + # function that takes the key for the serialized data, which will be + # used to load the parameter memory into the spawn process (also using + # joblib). In theory, joblib uses memmap for arrays if they are not + # compressed, so no compression is used for the bigger arrays. + import joblib + tmp_dir = tempfile.mkdtemp(suffix='-nf-grand-loop') + try: + # dumb dumping doesn't seem to work very well.. do something ad-hoc + logging.info('Using "%s" as temporary directory.', tmp_dir) + + id_exp = joblib.dump(state[-1], + os.path.join(tmp_dir, + 'grand-loop-experiment.gz'), + compress=True) + id_state = joblib.dump(state[:-1], + os.path.join(tmp_dir, 'grand-loop-data')) + pool = multiprocessing.Pool(ncpus, worker_init, + (id_state[0], id_exp[0])) + yield pool + finally: + logging.info('Deleting "%s".', tmp_dir) + shutil.rmtree(tmp_dir) + + +# ============================================================================== +# %% SCRIPT ENTRY AND PARAMETER HANDLING +# ============================================================================== +def main(args, controller): + grain_params, experiment = mockup_experiment() + controller.handle_result('experiment', experiment) + controller.handle_result('grain_params', grain_params) + image_stack = get_simulate_diffractions(grain_params, experiment, + controller=controller) + + test_orientations(image_stack, experiment, + controller=controller) + + +def parse_args(): + try: + default_ncpus = multiprocessing.cpu_count() + except NotImplementedError: + default_ncpus = 1 + + parser = argparse.ArgumentParser() + parser.add_argument("--inst-profile", action='append', default=[], + help="instrumented profile") + parser.add_argument("--generate", + help="generate file with intermediate results") + parser.add_argument("--check", + help="check against an file with intermediate results") + parser.add_argument("--limit", type=int, + help="limit the size of the run") + parser.add_argument("--ncpus", type=int, default=default_ncpus, + help="number of processes to use") + parser.add_argument("--chunk-size", type=int, default=100, + help="chunk size for use in multiprocessing/reporting") + parser.add_argument("--force-spawn-multiprocessing", action='store_true', + help="force using spawn as the multiprocessing method") + args = parser.parse_args() + + # keys = ['inst_profile', 'generate', 'check', 'limit', 'ncpus', 'chunk_size'] + # print('\n'.join([': '.join([key, str(getattr(args, key))]) for key in keys])) + + return args + + +def build_controller(args): + # builds the controller to use based on the args + + # result handle + progress_handler = progressbar_progress_observer() + + if args.check is not None: + if args.generate is not None: + logging.warn("generating and checking can not happen at the same time, going with checking") + + result_handler = checking_result_handler(args.check) + elif args.generate is not None: + result_handler = saving_result_handler(args.generate) + else: + result_handler = forgetful_result_handler() + + # if args.ncpus > 1 and os.name == 'nt': + # logging.warn("Multiprocessing on Windows is disabled for now") + # args.ncpus = 1 + + controller = ProcessController(result_handler, progress_handler, + ncpus=args.ncpus, chunk_size=args.chunk_size) + if args.limit is not None: + controller.set_limit('coords', lambda x: min(x, args.limit)) + + return controller + + +# assume that if os has fork, it will be used by multiprocessing. +# note that on python > 3.4 we could use multiprocessing get_start_method and +# set_start_method for a cleaner implementation of this functionality. +_multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + +if __name__=='__main__': + FORMAT="%(relativeCreated)12d [%(process)6d/%(thread)6d] %(levelname)8s: %(message)s" + logging.basicConfig(level=logging.NOTSET, + format=FORMAT) + args = parse_args() + + if len(args.inst_profile) > 0: + from hexrd.utils import profiler + + logging.debug("Instrumenting functions") + profiler.instrument_all(args.inst_profile) + + if args.force_spawn_multiprocessing: + _multiprocessing_start_method = 'spawn' + + controller = build_controller(args) + main(args, controller) + del controller + + if len(args.inst_profile) > 0: + logging.debug("Dumping profiler results") + profiler.dump_results(args.inst_profile) diff --git a/scripts/process_nf_grain_map.py b/scripts/process_nf_grain_map.py new file mode 100644 index 00000000..cb5fcd70 --- /dev/null +++ b/scripts/process_nf_grain_map.py @@ -0,0 +1,275 @@ +#%% Necessary Dependencies + + +import numpy as np + +import matplotlib.pyplot as plt + +import multiprocessing as mp + +import os + +from hexrd.grainmap import nfutil +from hexrd.grainmap import tomoutil +from hexrd.grainmap import vtkutil + + + + +#============================================================================== +# %% FILES TO LOAD -CAN BE EDITED +#============================================================================== +#These files are attached, retiga.yml is a detector configuration file +#The near field detector was already calibrated + +#A materials file, is a cPickle file which contains material information like lattice +#parameters necessary for the reconstruction + +det_file='/####/retiga.yml' + +mat_file='/####/materials.cpl' + +#============================================================================== +# %% OUTPUT INFO -CAN BE EDITED +#============================================================================== + + +output_dir='/####/' +output_stem='####' + +#============================================================================== +# %% NEAR FIELD DATA FILES -CAN BE EDITED +#============================================================================== + +#These are the near field data files used for the reconstruction, a grains.out file +#from the far field analaysis is used as orientation guess for the grid that will +#be used for the near field reconstruction +grain_out_file='/####/grains.out' + + +#Locations of near field images +data_folder='/###/' + +img_start=## +num_imgs=1441 + +img_nums=np.arange(img_start,img_start+num_imgs,1) + + +#============================================================================== +# %% TOMOGRAPHY DATA FILES -CAN BE EDITED +#============================================================================== + + +#Locations of tomography bright field images +tbf_data_folder='/####/' + +tbf_img_start=## +tbf_num_imgs=20 + +#Locations of tomography images +tomo_data_folder='/####/' + +tomo_img_start=## +tomo_num_imgs=720 + +#============================================================================== +# %% USER OPTIONS -CAN BE EDITED +#============================================================================== + +x_ray_energy=### #keV + +#name of the material for the reconstruction +mat_name='MAT_NAME' + +#reconstruction with misorientation included, for many grains, this will quickly +#make the reconstruction size unmanagable +misorientation_bnd=0.0 #degrees +misorientation_spacing=0.25 #degress + +beam_stop_width=0.55#mm, assumed to be in the center of the detector + + +ome_range_deg=[(0.,360.)] #degrees + + +max_tth=-1. #degrees, if a negative number is input, all peaks that will hit the detector are calculated + +#image processing +num_for_dark=250#num images to use for median data +threshold=3. +num_erosions=3 #num iterations of images erosion, don't mess with unless you know what you're doing +num_dilations=2 #num iterations of images erosion, don't mess with unless you know what you're doing +ome_dilation_iter=1 #num iterations of 3d image stack dilations, don't mess with unless you know what you're doing + +chunk_size=500#chunksize for multiprocessing, don't mess with unless you know what you're doing + +#thresholds for grains in reconstructions +comp_thresh=0.5 #only use orientations from grains with completnesses ABOVE this threshold +chi2_thresh=0.05 #only use orientations from grains BELOW this chi^2 + + +#tomography options +layer_row=1024 # row of layer to use to find the cross sectional specimen shape + +#Don't change these unless you know what you are doing, this will close small holes +#and remove noise +recon_thresh=0.00006#usually varies between 0.0001 and 0.0005 +noise_obj_size=5000 +min_hole_size=5000 + + +cross_sectional_dim=1.35 #cross sectional to reconstruct (should be at least 20%-30% over sample width) +#voxel spacing for the near field reconstruction +voxel_spacing=0.005#in mm +##vertical (y) reconstruction voxel bounds in mm +v_bnds=[-0.005,0.005] + + + + +#============================================================================== +# %% LOAD GRAIN DATA +#============================================================================== + +experiment, nf_to_ff_id_map = nfutil.gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ + misorientation_spacing,ome_range_deg, num_imgs, beam_stop_width) + +#============================================================================== +# %% TOMO PROCESSING - GENERATE BRIGHT FIELD +#============================================================================== + +tbf=tomoutil.gen_bright_field(tbf_data_folder,tbf_img_start,tbf_num_imgs,experiment.nrows,experiment.ncols) + +#============================================================================== +# %% TOMO PROCESSING - BUILD RADIOGRAPHS +#============================================================================== + + +rad_stack=tomoutil.gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,experiment.nrows,experiment.ncols) + + +#============================================================================== +# %% TOMO PROCESSING - INVERT SINOGRAM +#============================================================================== + +reconstruction_fbp=tomoutil.tomo_reconstruct_layer(rad_stack,cross_sectional_dim,layer_row=layer_row,\ + start_tomo_ang=ome_range_deg[0][0],end_tomo_ang=ome_range_deg[0][1],\ + tomo_num_imgs=tomo_num_imgs, center=experiment.detector_params[3]) + +#============================================================================== +# %% TOMO PROCESSING - VIEW RAW FILTERED BACK PROJECTION +#============================================================================== + +plt.close('all') +plt.imshow(reconstruction_fbp,vmin=0.75e-4,vmax=2e-4) +#Use this image to view the raw reconstruction, estimate threshold levels. and +#figure out if the rotation axis position needs to be corrected + + +#============================================================================== +# %% TOMO PROCESSING - CLEAN TOMO RECONSTRUCTION +#============================================================================== + +binary_recon=tomoutil.threshold_and_clean_tomo_layer(reconstruction_fbp,recon_thresh, noise_obj_size,min_hole_size) + + +#============================================================================== +# %% TOMO PROCESSING - RESAMPLE TOMO RECONSTRUCTION +#============================================================================== + + +tomo_mask=tomoutil.crop_and_rebin_tomo_layer(binary_recon,recon_thresh,voxel_spacing,experiment.pixel_size[0],cross_sectional_dim) + +#============================================================================== +# %% TOMO PROCESSING - VIEW TOMO_MASK FOR SAMPLE BOUNDS +#============================================================================== +plt.close('all') +plt.imshow(tomo_mask,interpolation='none') + +#============================================================================== +# %% TOMO PROCESSING - CONSTRUCT DATA GRID +#============================================================================== + +test_crds, n_crds, Xs, Ys, Zs = nfutil.gen_nf_test_grid_tomo(tomo_mask.shape[1], tomo_mask.shape[0], v_bnds, voxel_spacing) + +#============================================================================== +# %% NEAR FIELD - MAKE MEDIAN DARK +#============================================================================== +dark=nfutil.gen_nf_dark(data_folder,img_nums,num_for_dark,experiment.nrows,experiment.ncols) + + +#============================================================================== +# %% NEAR FIELD - LOAD IMAGE DATA AND PROCESS +#============================================================================== + +image_stack=nfutil.gen_nf_image_stack(data_folder,img_nums,dark,num_erosions,num_dilations,ome_dilation_iter,threshold,experiment.nrows,experiment.ncols) + + +#============================================================================== +# %% VIEW IMAGES FOR DEBUGGING TO LOOK AT IMAGE PROCESSING PARAMETERS +#============================================================================== +plt.close('all') +img_to_view=0 +plt.imshow(image_stack[img_to_view,:,:],interpolation='none') + + +#============================================================================== +# %% INSTANTIATE CONTROLLER - RUN BLOCK NO EDITING +#============================================================================== + + + +progress_handler = nfutil.progressbar_progress_observer() +save_handler=nfutil.forgetful_result_handler() + +controller = nfutil.ProcessController(save_handler, progress_handler, + ncpus=mp.cpu_count(), chunk_size=chunk_size) + +multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + + +#============================================================================== +# %% TEST ORIENTATIONS - RUN BLOCK NO EDITING +#============================================================================== + + +raw_confidence=nfutil.test_orientations(image_stack, experiment, test_crds, + controller,multiprocessing_start_method) + + + +#============================================================================== +# %% POST PROCESS W WHEN TOMOGRAPHY HAS BEEN USED +#============================================================================== + +grain_map, confidence_map = nfutil.process_raw_confidence(raw_confidence,Xs.shape,tomo_mask=tomo_mask,id_remap=nf_to_ff_id_map) + + + +#============================================================================== +# %% SAVE RAW CONFIDENCE FILES +#============================================================================ + +#This will be a very big file, don't save it if you don't need it +nfutil.save_raw_confidence(output_dir,output_stem,raw_confidence,id_remap=nf_to_ff_id_map) + + +#============================================================================== +# %% SAVE PROCESSED GRAIN MAP DATA +#============================================================================== + +nfutil.save_nf_data(output_dir,output_stem,grain_map,confidence_map,Xs,Ys,Zs,experiment.exp_maps,id_remap=nf_to_ff_id_map) + +#============================================================================== +# %% PLOTTING SINGLE LAYERS FOR DEBUGGING +#============================================================================== + +layer_no=0 +nfutil.plot_ori_map(grain_map, confidence_map, experiment.exp_maps, layer_no,id_remap=nf_to_ff_id_map) + +#============================================================================== +# %% SAVE DATA AS VTK +#============================================================================== + +vtkutil.output_grain_map_vtk(output_dir,[output_stem],output_stem,0.1) diff --git a/scripts/stitch_grains.py b/scripts/stitch_grains.py new file mode 100644 index 00000000..8e333053 --- /dev/null +++ b/scripts/stitch_grains.py @@ -0,0 +1,286 @@ +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on downloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +############################################################################### + +#%% #Import Modules +import os + +import numpy as np + +import copy + +import cPickle as cpl + +from hexrd import matrixutil as mutil +from hexrd.xrd import rotations as rot +from hexrd.xrd import symmetry as sym + +import shutil + + +#%% Functions to preload + +def remove_duplicate_grains(grain_data,qsyms,dist_thresh=0.01,misorient_thresh=0.1,comp_diff=0.1): + total_grains=grain_data.shape[0] + + all_comp=grain_data[:,1] + grain_quats=rot.quatOfExpMap(grain_data[:,3:6].T) + dup_list=np.array([]) + + print 'Removing duplicate grains' + for i in np.arange(total_grains-1): + cur_pos=grain_data[i,3:6] + other_pos=grain_data[(i+1):,3:6] + xdist=cur_pos[0]-other_pos[:,0] + ydist=cur_pos[1]-other_pos[:,1] + zdist=cur_pos[2]-other_pos[:,2] + + dist=np.sqrt(xdist**2.+ydist**2.+zdist**2.) + + if np.min(dist)0): + grain_data[i,:]=grain_data[np.argmin(dist)+i+1,:] + + grain_data=np.delete(grain_data,dup_list,axis=0) + + print 'Removed %d Grains' % (len(dup_list)) + + grain_data[:,0]=np.arange(grain_data.shape[0]) + + return grain_data,dup_list + + + +def assemble_grain_data(grain_data_list,pos_offset=None,rotation_offset=None): + num_grain_files=len(grain_data_list) + + num_grains_list=[None]*num_grain_files + + for i in np.arange(num_grain_files): + num_grains_list[i]=grain_data_list[i].shape[0] + + num_grains=np.sum(num_grains_list) + + grain_data=np.zeros([num_grains,grain_data_list[0].shape[1]]) + + for i in np.arange(num_grain_files): + + tmp=copy.copy(grain_data_list[i]) + + if pos_offset is not None: + pos_tile=np.tile(pos_offset[:,i],[num_grains_list[i],1]) + tmp[:,6:9]=tmp[:,6:9]+pos_tile + #Needs Testing + if rotation_offset is not None: + rot_tile=np.tile(np.atleast_2d(rotation_offset[:,i]).T,[1,num_grains_list[i]]) + quat_tile=rot.quatOfExpMap(rot_tile) + grain_quats=rot.quatOfExpMap(tmp[:,3:6].T) + new_quats=rot.quatProduct(grain_quats,quat_tile) + + sinang = mutil.columnNorm(new_quats[1:,:]) + ang=2.*np.arcsin(sinang) + axis = mutil.unitVector(new_quats[1:,:]) + tmp[:,3:6]=np.tile(np.atleast_2d(ang).T,[1,3])*axis.T + + + grain_data[int(np.sum(num_grains_list[:i])):int(np.sum(num_grains_list[:(i+1)])),:]=tmp + + + old_grain_numbers=copy.copy(grain_data[:,0]) + grain_data[:,0]=np.arange(num_grains) + return grain_data,old_grain_numbers + +############################################################################### +#%% User Input +############################################################################### + + + +material_file_loc='/####/materials.cpl' # hexrd material file in cpickle format +mat_name='####' + +#grain_file_locs=['/nfs/chess/aux/cycles/2017-1/f2/hurley-568-1/angquartz-1-reduction-attempt3/aquartz_%s_v0'%(load_name),\ +# '/nfs/chess/aux/cycles/2017-1/f2/hurley-568-1/angquartz-1-reduction-attempt3/aquartz_%s_v1'%(load_name)]#Can be more than 2 + +grain_file_locs=['/####/layer_0',\ + '/####/layer_1']#Can be more than one granis file + +output_data=True +output_dir='/####' + + +#Position and Misorientation differences to merge grains +dist=0.05 #mm +misorientation=1. #degrees +completeness_diff=0.1 #if two grains are matched, the completenesses are checked, +#if the differences in completion are within completeness_diff, the grain values are averaged, +#if not, the data from the grain with higher completion is kept and the other data is discarded + + +#Offsets, these can be input as arguments +#Each dataset can have positional or rotation offsets +#Position offsets are in mm, 3 x n matrix where n is the number of grains.out files being stitched +#Rotation offsets exponential maps + +#3 x 2 examples +pos_offset=np.array([[0.,0.],[0.,0.],[0.,0.]]) +rot_offset=None + + +low_comp_thresh=0.6 +high_chi2_thresh=0.05 + + + +#vertical dispersion correction +vd_lin=0. #vol_strain/mm +vd_const= 0.#vol_strain + + +############################################################################### +#%% Load data +############################################################################### + +mat_list = cpl.load(open(material_file_loc, 'r')) +mat_idx = np.where([mat_list[i].name == mat_name for i in range(len(mat_list))])[0] + +# grab plane data, and useful things hanging off of it +pd = mat_list[mat_idx[0]].planeData +qsyms=sym.quatOfLaueGroup(pd.getLaueGroup()) + + +num_grain_files=len(grain_file_locs) + +grain_data_list=[None]*num_grain_files + + + + + +for i in np.arange(num_grain_files): + + grain_data_list[i]=np.loadtxt(os.path.join(grain_file_locs[i],'grains.out')) + + pos_0=grain_data_list[i][:,6:9] + grain_data_list[i][:,15]=grain_data_list[i][:,15]-(vd_lin*pos_0[:,1]+vd_const) + grain_data_list[i][:,16]=grain_data_list[i][:,16]-(vd_lin*pos_0[:,1]+vd_const) + grain_data_list[i][:,17]=grain_data_list[i][:,17]-(vd_lin*pos_0[:,1]+vd_const) + + good_comp=np.where(grain_data_list[i][:,1]>=low_comp_thresh)[0] + good_chi2=np.where(grain_data_list[i][:,2]<=high_chi2_thresh)[0] + + to_keep=np.intersect1d(good_comp,good_chi2) + + grain_data_list[i]=grain_data_list[i][to_keep,:] + + + +grain_data,old_grain_numbers=assemble_grain_data(grain_data_list,pos_offset,rot_offset) + +grain_data,dup_list=remove_duplicate_grains(grain_data,qsyms,dist,misorientation,completeness_diff) + +old_grain_numbers=np.delete(old_grain_numbers,dup_list) + +# +divisions=np.array(np.where(np.diff(old_grain_numbers)<0)[0]+1) + +num_blocks=len(divisions)+1 +old_grain_blocks=[None]*num_blocks + +for i in np.arange(num_blocks): + if i==0: + old_grain_blocks[i]=old_grain_numbers[:divisions[i]] + elif i==(num_blocks-1): + old_grain_blocks[i]=old_grain_numbers[divisions[i-1]:] + else: + old_grain_blocks[i]=old_grain_numbers[divisions[i-1]:divisions[i]] + + + + +############################################################################### +#%% Write data +############################################################################### + + + +if not os.path.exists(output_dir): + os.makedirs(output_dir) + + +if output_data: + print('Writing out grain data for ' +str(grain_data.shape[0]) + ' grains') + f = open(os.path.join(output_dir, 'grains.out'), 'w') + + header_items = ( + 'grain ID', 'completeness', 'chi2', + 'xi[0]', 'xi[1]', 'xi[2]', 'tVec_c[0]', 'tVec_c[1]', 'tVec_c[2]', + 'vInv_s[0]', 'vInv_s[1]', 'vInv_s[2]', 'vInv_s[4]*sqrt(2)', + 'vInv_s[5]*sqrt(2)', 'vInv_s[6]*sqrt(2)', 'ln(V[0,0])', + 'ln(V[1,1])', 'ln(V[2,2])', 'ln(V[1,2])', 'ln(V[0,2])', 'ln(V[0,1])', + ) + len_items = [] + for i in header_items[1:]: + temp = len(i) + len_items.append(temp if temp > 19 else 19) # for %19.12g + fmtstr = '#%13s ' + ' '.join(['%%%ds' % i for i in len_items]) + '\n' + f.write(fmtstr % header_items) + for i in np.arange(grain_data.shape[0]): + res_items = ( + grain_data[i,0], grain_data[i,1], grain_data[i,2], grain_data[i,3], grain_data[i,4], grain_data[i,5], + grain_data[i,6], grain_data[i,7], grain_data[i,8], grain_data[i,9], + grain_data[i,10], grain_data[i,11], grain_data[i,12], grain_data[i,13], + grain_data[i,14], grain_data[i,15], grain_data[i,16], grain_data[i,17], grain_data[i,18], + grain_data[i,19], grain_data[i,20], + ) + fmtstr = ( + '%14d ' + ' '.join(['%%%d.12g' % i for i in len_items]) + '\n' + ) + f.write(fmtstr % res_items) + + f.close() + + + counter=0 + for i in np.arange(len(old_grain_blocks)): + for j in np.arange(len(old_grain_blocks[i])): + shutil.copy2(os.path.join(grain_file_locs[i],'spots_%05.5d.out' % (old_grain_blocks[i][j])),os.path.join(output_dir,'spots_%05.5d.out' % (counter))) + counter=counter+1 + + + + + From c7cab72211930ddcee2196f3b12eb66444f58859 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 22 Jun 2018 20:10:35 -0500 Subject: [PATCH 162/253] port of grainmap from v0.3.x --- hexrd/grainmap/__init__.py | 35 + hexrd/grainmap/nfutil.py | 1148 +++++++++++++++++++++++++++++++ hexrd/grainmap/tomoutil.py | 134 ++++ hexrd/grainmap/vtkutil.py | 126 ++++ scripts/new_simulate_nf.py | 169 ++--- scripts/process_nf_grain_map.py | 238 ++++--- 6 files changed, 1659 insertions(+), 191 deletions(-) create mode 100644 hexrd/grainmap/__init__.py create mode 100644 hexrd/grainmap/nfutil.py create mode 100644 hexrd/grainmap/tomoutil.py create mode 100644 hexrd/grainmap/vtkutil.py diff --git a/hexrd/grainmap/__init__.py b/hexrd/grainmap/__init__.py new file mode 100644 index 00000000..b4d4270f --- /dev/null +++ b/hexrd/grainmap/__init__.py @@ -0,0 +1,35 @@ +# ============================================================ +# Copyright (c) 2012, Lawrence Livermore National Security, LLC. +# Produced at the Lawrence Livermore National Laboratory. +# Written by Joel Bernier and others. +# LLNL-CODE-529294. +# All rights reserved. +# +# This file is part of HEXRD. For details on dowloading the source, +# see the file COPYING. +# +# Please also see the file LICENSE. +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License (as published by the Free Software +# Foundation) version 2.1 dated February 1999. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the terms and conditions of the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program (see file LICENSE); if not, write to +# the Free Software Foundation, Inc., 59 Temple Place, Suite 330, +# Boston, MA 02111-1307 USA or visit . +# ============================================================ +"""Tools or X-ray diffraction analysis +""" + + + + + + + diff --git a/hexrd/grainmap/nfutil.py b/hexrd/grainmap/nfutil.py new file mode 100644 index 00000000..5aa51e93 --- /dev/null +++ b/hexrd/grainmap/nfutil.py @@ -0,0 +1,1148 @@ + + + + +#%% + +import time +import os +import logging +import numpy as np +import copy + +import numba +import argparse +import contextlib +import multiprocessing +import tempfile +import shutil + +from hexrd.xrd import transforms as xf +from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd import xrdutil + +from hexrd.xrd import rotations as rot +from hexrd import valunits + +from hexrd.xrd.transforms_CAPI import anglesToGVec, \ + makeRotMatOfExpMap, makeDetectorRotMat, makeOscillRotMat, \ + gvecToDetectorXY, detectorXYToGvec + +import yaml +import cPickle as cpl + +import scipy.ndimage as img + +import matplotlib.pyplot as plt + +# ============================================================================== +# %% SOME SCAFFOLDING +# ============================================================================== + +class ProcessController(object): + """This is a 'controller' that provides the necessary hooks to + track the results of the process as well as to provide clues of + the progress of the process""" + + def __init__(self, result_handler=None, progress_observer=None, ncpus = 1, + chunk_size = 100): + self.rh = result_handler + self.po = progress_observer + self.ncpus = ncpus + self.chunk_size = chunk_size + self.limits = {} + self.timing = [] + + + # progress handling -------------------------------------------------------- + + def start(self, name, count): + self.po.start(name, count) + t = time.time() + self.timing.append((name, count, t)) + + + def finish(self, name): + t = time.time() + self.po.finish() + entry = self.timing.pop() + assert name==entry[0] + total = t - entry[2] + logging.info("%s took %8.3fs (%8.6fs per item).", entry[0], total, total/entry[1]) + + + def update(self, value): + self.po.update(value) + + # result handler ----------------------------------------------------------- + + def handle_result(self, key, value): + logging.debug("handle_result (%(key)s)", locals()) + self.rh.handle_result(key, value) + + # value limitting ---------------------------------------------------------- + def set_limit(self, key, limit_function): + if key in self.limits: + logging.warn("Overwritting limit funtion for '%(key)s'", locals()) + + self.limits[key] = limit_function + + def limit(self, key, value): + try: + value = self.limits[key](value) + except KeyError: + pass + except Exception: + logging.warn("Could not apply limit to '%(key)s'", locals()) + + return value + + # configuration ----------------------------------------------------------- + def get_process_count(self): + return self.ncpus + + def get_chunk_size(self): + return self.chunk_size + + +def null_progress_observer(): + class NullProgressObserver(object): + def start(self, name, count): + pass + + def update(self, value): + pass + + def finish(self): + pass + + return NullProgressObserver() + + +def progressbar_progress_observer(): + from progressbar import ProgressBar, Percentage, Bar + + class ProgressBarProgressObserver(object): + def start(self, name, count): + self.pbar = ProgressBar(widgets=[name, Percentage(), Bar()], + maxval=count) + self.pbar.start() + + def update(self, value): + self.pbar.update(value) + + def finish(self): + self.pbar.finish() + + return ProgressBarProgressObserver() + + +def forgetful_result_handler(): + class ForgetfulResultHandler(object): + def handle_result(self, key, value): + pass # do nothing + + return ForgetfulResultHandler() + + +def saving_result_handler(filename): + """returns a result handler that saves the resulting arrays into a file + with name filename""" + class SavingResultHandler(object): + def __init__(self, file_name): + self.filename = file_name + self.arrays = {} + + def handle_result(self, key, value): + self.arrays[key] = value + + def __del__(self): + logging.debug("Writing arrays in %(filename)s", self.__dict__) + try: + np.savez_compressed(open(self.filename, "wb"), **self.arrays) + except IOError: + logging.error("Failed to write %(filename)s", self.__dict__) + + return SavingResultHandler(filename) + + +def checking_result_handler(filename): + """returns a return handler that checks the results against a + reference file. + + The Check will consider a FAIL either a result not present in the + reference file (saved as a numpy savez or savez_compressed) or a + result that differs. It will consider a PARTIAL PASS if the + reference file has a shorter result, but the existing results + match. A FULL PASS will happen when all existing results match + + """ + class CheckingResultHandler(object): + def __init__(self, reference_file): + """Checks the result against those save in 'reference_file'""" + logging.info("Loading reference results from '%s'", reference_file) + self.reference_results = np.load(open(reference_file, 'rb')) + + def handle_result(self, key, value): + if key in ['experiment', 'image_stack']: + return #ignore these + + try: + reference = self.reference_results[key] + except KeyError as e: + logging.warning("%(key)s: %(e)s", locals()) + reference = None + + if reference is None: + msg = "'{0}': No reference result." + logging.warn(msg.format(key)) + + try: + if key=="confidence": + reference = reference.T + value = value.T + + check_len = min(len(reference), len(value)) + test_passed = np.allclose(value[:check_len], reference[:check_len]) + + if not test_passed: + msg = "'{0}': FAIL" + logging.warn(msg.format(key)) + lvl = logging.WARN + elif len(value) > check_len: + msg = "'{0}': PARTIAL PASS" + lvl = logging.WARN + else: + msg = "'{0}': FULL PASS" + lvl = logging.INFO + logging.log(lvl, msg.format(key)) + except Exception as e: + msg = "%(key)s: Failure trying to check the results.\n%(e)s" + logging.error(msg, locals()) + + return CheckingResultHandler(filename) + + +# ============================================================================== +# %% OPTIMIZED BITS +# ============================================================================== + +# Some basic 3d algebra ======================================================== +@numba.njit +def _v3_dot(a, b): + return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + + +@numba.njit +def _m33_v3_multiply(m, v, dst): + v0 = v[0]; v1 = v[1]; v2 = v[2] + dst[0] = m[0, 0]*v0 + m[0, 1]*v1 + m[0, 2]*v2 + dst[1] = m[1, 0]*v0 + m[1, 1]*v1 + m[1, 2]*v2 + dst[2] = m[2, 0]*v0 + m[2, 1]*v1 + m[2, 2]*v2 + + return dst + + +@numba.njit +def _v3_normalized(src, dst): + v0 = src[0] + v1 = src[1] + v2 = src[2] + sqr_norm = v0*v0 + v1*v1 + v2*v2 + inv_norm = 1.0 if sqr_norm == 0.0 else 1./np.sqrt(sqr_norm) + + dst[0] = v0 * inv_norm + dst[1] = v1 * inv_norm + dst[2] = v2 * inv_norm + + return dst + + +@numba.njit +def _make_binary_rot_mat(src, dst): + v0 = src[0]; v1 = src[1]; v2 = src[2] + + dst[0,0] = 2.0*v0*v0 - 1.0 + dst[0,1] = 2.0*v0*v1 + dst[0,2] = 2.0*v0*v2 + dst[1,0] = 2.0*v1*v0 + dst[1,1] = 2.0*v1*v1 - 1.0 + dst[1,2] = 2.0*v1*v2 + dst[2,0] = 2.0*v2*v0 + dst[2,1] = 2.0*v2*v1 + dst[2,2] = 2.0*v2*v2 - 1.0 + + return dst + + +# code transcribed in numba from transforms module ============================= + +# This is equivalent to the transform module anglesToGVec, but written in +# numba. This should end in a module to share with other scripts +@numba.njit +def _anglesToGVec(angs, rMat_ss, rMat_c): + """From a set of angles return them in crystal space""" + result = np.empty_like(angs) + for i in range(len(angs)): + cx = np.cos(0.5*angs[i, 0]) + sx = np.sin(0.5*angs[i, 0]) + cy = np.cos(angs[i,1]) + sy = np.sin(angs[i,1]) + g0 = cx*cy + g1 = cx*sy + g2 = sx + + # with g being [cx*xy, cx*sy, sx] + # result = dot(rMat_c, dot(rMat_ss[i], g)) + t0_0 = rMat_ss[ i, 0, 0]*g0 + rMat_ss[ i, 1, 0]*g1 + rMat_ss[ i, 2, 0]*g2 + t0_1 = rMat_ss[ i, 0, 1]*g0 + rMat_ss[ i, 1, 1]*g1 + rMat_ss[ i, 2, 1]*g2 + t0_2 = rMat_ss[ i, 0, 2]*g0 + rMat_ss[ i, 1, 2]*g1 + rMat_ss[ i, 2, 2]*g2 + + result[i, 0] = rMat_c[0, 0]*t0_0 + rMat_c[ 1, 0]*t0_1 + rMat_c[ 2, 0]*t0_2 + result[i, 1] = rMat_c[0, 1]*t0_0 + rMat_c[ 1, 1]*t0_1 + rMat_c[ 2, 1]*t0_2 + result[i, 2] = rMat_c[0, 2]*t0_0 + rMat_c[ 1, 2]*t0_1 + rMat_c[ 2, 2]*t0_2 + + return result + + +# This is equivalent to the transform's module gvecToDetectorXYArray, but written in +# numba. +# As of now, it is not a good replacement as efficient allocation of the temporary +# arrays is not competitive with the stack allocation using in the C version of the +# code (WiP) + +# tC varies per coord +# gvec_cs, rSm varies per grain +# +# gvec_cs +beam = xf.bVec_ref[:, 0] +Z_l = xf.Zl[:,0] +@numba.jit() +def _gvec_to_detector_array(vG_sn, rD, rSn, rC, tD, tS, tC): + """ beamVec is the beam vector: (0, 0, -1) in this case """ + ztol = xrdutil.epsf + p3_l = np.empty((3,)) + tmp_vec = np.empty((3,)) + vG_l = np.empty((3,)) + tD_l = np.empty((3,)) + norm_vG_s = np.empty((3,)) + norm_beam = np.empty((3,)) + tZ_l = np.empty((3,)) + brMat = np.empty((3,3)) + result = np.empty((len(rSn), 2)) + + _v3_normalized(beam, norm_beam) + _m33_v3_multiply(rD, Z_l, tZ_l) + + for i in xrange(len(rSn)): + _m33_v3_multiply(rSn[i], tC, p3_l) + p3_l += tS + p3_minus_p1_l = tD - p3_l + + num = _v3_dot(tZ_l, p3_minus_p1_l) + _v3_normalized(vG_sn[i], norm_vG_s) + + _m33_v3_multiply(rC, norm_vG_s, tmp_vec) + _m33_v3_multiply(rSn[i], tmp_vec, vG_l) + + bDot = -_v3_dot(norm_beam, vG_l) + + if bDot < ztol or bDot > 1.0 - ztol: + result[i, 0] = np.nan + result[i, 1] = np.nan + continue + + _make_binary_rot_mat(vG_l, brMat) + _m33_v3_multiply(brMat, norm_beam, tD_l) + denom = _v3_dot(tZ_l, tD_l) + + if denom < ztol: + result[i, 0] = np.nan + result[i, 1] = np.nan + continue + + u = num/denom + tmp_res = u*tD_l - p3_minus_p1_l + result[i,0] = _v3_dot(tmp_res, rD[:,0]) + result[i,1] = _v3_dot(tmp_res, rD[:,1]) + + return result + + +@numba.njit +def _quant_and_clip_confidence(coords, angles, image, base, inv_deltas, clip_vals,bshw): + """quantize and clip the parametric coordinates in coords + angles + + coords - (..., 2) array: input 2d parametric coordinates + angles - (...) array: additional dimension for coordinates + base - (3,) array: base value for quantization (for each dimension) + inv_deltas - (3,) array: inverse of the quantum size (for each dimension) + clip_vals - (2,) array: clip size (only applied to coords dimensions) + bshw - (1,) half width of the beam stop in mm + + clipping is performed on ranges [0, clip_vals[0]] for x and + [0, clip_vals[1]] for y + + returns an array with the quantized coordinates, with coordinates + falling outside the clip zone filtered out. + + """ + count = len(coords) + + in_sensor = 0 + matches = 0 + for i in range(count): + xf = coords[i, 0] + yf = coords[i, 1] + + xf = np.floor((xf - base[0]) * inv_deltas[0]) + if not xf >= 0.0: + continue + if not xf < clip_vals[0]: + continue + + if not np.abs(yf)>bshw: + continue + + yf = np.floor((yf - base[1]) * inv_deltas[1]) + + + + if not yf >= 0.0: + continue + if not yf < clip_vals[1]: + continue + + zf = np.floor((angles[i] - base[2]) * inv_deltas[2]) + + in_sensor += 1 + + x, y, z = int(xf), int(yf), int(zf) + + #x_byte = x // 8 + #x_off = 7 - (x % 8) + #if image[z, y, x_byte] (1< 1: + global _multiprocessing_start_method + _multiprocessing_start_method=multiprocessing_start_method + logging.info('Running multiprocess %d processes (%s)', + ncpus, _multiprocessing_start_method) + with grand_loop_pool(ncpus=ncpus, state=(chunk_size, + image_stack, + all_angles, precomp, test_crds, + experiment)) as pool: + for rslice, rvalues in pool.imap_unordered(multiproc_inner_loop, + chunks): + count = rvalues.shape[1] + confidence[:, rslice] = rvalues + finished += count + controller.update(finished) + else: + logging.info('Running in a single process') + for chunk_start in chunks: + chunk_stop = min(n_coords, chunk_start+chunk_size) + rslice, rvalues = _grand_loop_inner(image_stack, all_angles, + precomp, test_crds, experiment, + start=chunk_start, + stop=chunk_stop) + count = rvalues.shape[1] + confidence[:, rslice] = rvalues + finished += count + controller.update(finished) + + controller.finish(subprocess) + controller.handle_result("confidence", confidence) + + del _multiprocessing_start_method + + pool.close() + + return confidence + + +def evaluate_diffraction_angles(experiment, controller=None): + """Uses simulateGVecs to generate the angles used per each grain. + returns a list containg one array per grain. + + experiment -- a bag of experiment values, including the grains specs and other + required parameters. + """ + # extract required data from experiment + exp_maps = experiment.exp_maps + plane_data = experiment.plane_data + detector_params = experiment.detector_params + pixel_size = experiment.pixel_size + ome_range = experiment.ome_range + ome_period = experiment.ome_period + + panel_dims_expanded = [(-10, -10), (10, 10)] + subprocess='evaluate diffraction angles' + pbar = controller.start(subprocess, + len(exp_maps)) + all_angles = [] + ref_gparams = np.array([0., 0., 0., 1., 1., 1., 0., 0., 0.]) + for i, exp_map in enumerate(exp_maps): + gparams = np.hstack([exp_map, ref_gparams]) + sim_results = xrdutil.simulateGVecs(plane_data, + detector_params, + gparams, + panel_dims=panel_dims_expanded, + pixel_pitch=pixel_size, + ome_range=ome_range, + ome_period=ome_period, + distortion=None) + all_angles.append(sim_results[2]) + controller.update(i+1) + pass + controller.finish(subprocess) + + return all_angles + + +def _grand_loop_inner(image_stack, angles, precomp, + coords, experiment, start=0, stop=None): + """Actual simulation code for a chunk of data. It will be used both, + in single processor and multiprocessor cases. Chunking is performed + on the coords. + + image_stack -- the image stack from the sensors + angles -- the angles (grains) to test + coords -- all the coords to test + precomp -- (gvec_cs, rmat_ss) precomputed for each grain + experiment -- bag with experiment parameters + start -- chunk start offset + stop -- chunk end offset + """ + + t = time.time() + n_coords = len(coords) + n_angles = len(angles) + + # experiment geometric layout parameters + rD = experiment.rMat_d + rCn = experiment.rMat_c + tD = experiment.tVec_d[:,0] + tS = experiment.tVec_s[:,0] + + # experiment panel related configuration + base = experiment.base + inv_deltas = experiment.inv_deltas + clip_vals = experiment.clip_vals + distortion = experiment.distortion + bshw=experiment.bsw/2. + + _to_detector = xfcapi.gvecToDetectorXYArray + #_to_detector = _gvec_to_detector_array + stop = min(stop, n_coords) if stop is not None else n_coords + + distortion_fn = None + if distortion is not None and len(distortion > 0): + distortion_fn, distortion_args = distortion + + acc_detector = 0.0 + acc_distortion = 0.0 + acc_quant_clip = 0.0 + confidence = np.zeros((n_angles, stop-start)) + grains = 0 + crds = 0 + + if distortion_fn is None: + for igrn in xrange(n_angles): + angs = angles[igrn]; rC = rCn[igrn] + gvec_cs, rMat_ss = precomp[igrn] + grains += 1 + for icrd in xrange(start, stop): + t0 = time.time() + det_xy = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) + t1 = time.time() + c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, + base, inv_deltas, clip_vals,bshw) + t2 = time.time() + acc_detector += t1 - t0 + acc_quant_clip += t2 - t1 + crds += 1 + confidence[igrn, icrd - start] = c + else: + for igrn in xrange(n_angles): + angs = angles[igrn]; rC = rCn[igrn] + gvec_cs, rMat_ss = precomp[igrn] + grains += 1 + for icrd in xrange(start, stop): + t0 = time.time() + tmp_xys = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) + t1 = time.time() + det_xy = distortion_fn(tmp_xys, distortion_args, invert=True) + t2 = time.time() + c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, + base, inv_deltas, clip_vals,bshw) + t3 = time.time() + acc_detector += t1 - t0 + acc_distortion += t2 - t1 + acc_quant_clip += t3 - t2 + crds += 1 + confidence[igrn, icrd - start] = c + + t = time.time() - t + return slice(start, stop), confidence + + +def multiproc_inner_loop(chunk): + """function to use in multiprocessing that computes the simulation over the + task's alloted chunk of data""" + + chunk_size = _mp_state[0] + n_coords = len(_mp_state[4]) + chunk_stop = min(n_coords, chunk+chunk_size) + return _grand_loop_inner(*_mp_state[1:], start=chunk, stop=chunk_stop) + + +def worker_init(id_state, id_exp): + """process initialization function. This function is only used when the + child processes are spawned (instead of forked). When using the fork model + of multiprocessing the data is just inherited in process memory.""" + import joblib + + global _mp_state + state = joblib.load(id_state) + experiment = joblib.load(id_exp) + _mp_state = state + (experiment,) + +@contextlib.contextmanager +def grand_loop_pool(ncpus, state): + """function that handles the initialization of multiprocessing. It handles + properly the use of spawned vs forked multiprocessing. The multiprocessing + can be either 'fork' or 'spawn', with 'spawn' being required in non-fork + platforms (like Windows) and 'fork' being preferred on fork platforms due + to its efficiency. + """ + # state = ( chunk_size, + # image_stack, + # angles, + # precomp, + # coords, + # experiment ) + global _multiprocessing_start_method + if _multiprocessing_start_method == 'fork': + # Use FORK multiprocessing. + + # All read-only data can be inherited in the process. So we "pass" it as + # a global that the child process will be able to see. At the end of the + # processing the global is removed. + global _mp_state + _mp_state = state + pool = multiprocessing.Pool(ncpus) + yield pool + del (_mp_state) + else: + # Use SPAWN multiprocessing. + + # As we can not inherit process data, all the required data is + # serialized into a temporary directory using joblib. The + # multiprocessing pool will have the "worker_init" as initialization + # function that takes the key for the serialized data, which will be + # used to load the parameter memory into the spawn process (also using + # joblib). In theory, joblib uses memmap for arrays if they are not + # compressed, so no compression is used for the bigger arrays. + import joblib + tmp_dir = tempfile.mkdtemp(suffix='-nf-grand-loop') + try: + # dumb dumping doesn't seem to work very well.. do something ad-hoc + logging.info('Using "%s" as temporary directory.', tmp_dir) + + id_exp = joblib.dump(state[-1], + os.path.join(tmp_dir, + 'grand-loop-experiment.gz'), + compress=True) + id_state = joblib.dump(state[:-1], + os.path.join(tmp_dir, 'grand-loop-data')) + pool = multiprocessing.Pool(ncpus, worker_init, + (id_state[0], id_exp[0])) + yield pool + finally: + logging.info('Deleting "%s".', tmp_dir) + shutil.rmtree(tmp_dir) + + + + + +#%% Loading Utilities + + +def gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ + misorientation_spacing,ome_range_deg, nframes, beam_stop_width): + + print('Loading Grain Data.....') + #gen_grain_data + ff_data=np.loadtxt(grain_out_file) + + #ff_data=np.atleast_2d(ff_data[2,:]) + + exp_maps=ff_data[:,3:6] + t_vec_ds=ff_data[:,6:9] + + + # + completeness=ff_data[:,1] + + chi2=ff_data[:,2] + + n_grains=exp_maps.shape[0] + + rMat_c = rot.rotMatOfExpMap(exp_maps.T) + + + + + cut=np.where(np.logical_and(completeness>comp_thresh,chi20.: + mat_used.planeData.tThMax = np.amax(np.radians(max_tth)) + else: + mat_used.planeData.tThMax = np.amax(pixel_tth) + + pd=mat_used.planeData + + + print('Final Assembly.....') + experiment = argparse.Namespace() + # grains related information + experiment.n_grains = n_grains # this can be derived from other values... + experiment.rMat_c = rMat_c # n_grains rotation matrices (one per grain) + experiment.exp_maps = exp_maps # n_grains exp_maps -angle * rotation axis- (one per grain) + + experiment.plane_data = pd + experiment.detector_params = detector_params + experiment.pixel_size = pixel_size + experiment.ome_range = ome_range + experiment.ome_period = ome_period + experiment.x_col_edges = x_col_edges + experiment.y_row_edges = y_row_edges + experiment.ome_edges = ome_edges + experiment.ncols = ncols + experiment.nrows = nrows + experiment.nframes = nframes# used only in simulate... + experiment.rMat_d = rMat_d + experiment.tVec_d = np.atleast_2d(detector_params[3:6]).T + experiment.chi = detector_params[6] # note this is used to compute S... why is it needed? + experiment.tVec_s = np.atleast_2d(detector_params[7:]).T + experiment.rMat_c = rMat_c + experiment.distortion = None + experiment.panel_dims = panel_dims # used only in simulate... + experiment.base = base + experiment.inv_deltas = inv_deltas + experiment.clip_vals = clip_vals + experiment.bsw = beam_stop_width + + nf_to_ff_id_map=cut + + return experiment, nf_to_ff_id_map + +#%% + + +def gen_nf_test_grid_tomo(x_dim_pnts, z_dim_pnts, v_bnds, voxel_spacing): + + if v_bnds[0]==v_bnds[1]: + Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),v_bnds[0],np.arange(z_dim_pnts)) + else: + Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),np.arange(v_bnds[0]+voxel_spacing/2.,v_bnds[1],voxel_spacing),np.arange(z_dim_pnts)) + #note numpy shaping of arrays is goofy, returns(length(y),length(x),length(z)) + + + Zs=(Zs-(z_dim_pnts/2))*voxel_spacing + Xs=(Xs-(x_dim_pnts/2))*voxel_spacing + + + test_crds = np.vstack([Xs.flatten(), Ys.flatten(), Zs.flatten()]).T + n_crds = len(test_crds) + + return test_crds, n_crds, Xs, Ys, Zs + + +#%% + +def gen_nf_dark(data_folder,img_nums,num_for_dark,nrows,ncols,dark_type='median',stem='nf_',num_digits=5,ext='.tif'): + + dark_stack=np.zeros([num_for_dark,nrows,ncols]) + + print('Loading data for dark generation...') + for ii in np.arange(num_for_dark): + print('Image #: ' + str(ii)) + dark_stack[ii,:,:]=img.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext) + #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) + + if dark_type=='median': + print('making median...') + dark=np.median(dark_stack,axis=0) + elif dark_type=='min': + print('making min...') + dark=np.min(dark_stack,axis=0) + + return dark + + +#%% +def gen_nf_image_stack(data_folder,img_nums,dark,num_erosions,num_dilations,ome_dilation_iter,threshold,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): + + + image_stack=np.zeros([img_nums.shape[0],nrows,ncols],dtype=bool) + + print('Loading and Cleaning Images...') + for ii in np.arange(img_nums.shape[0]): + print('Image #: ' + str(ii)) + tmp_img=img.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext)-dark + #image procesing + image_stack[ii,:,:]=img.morphology.binary_erosion(tmp_img>threshold,iterations=num_erosions) + image_stack[ii,:,:]=img.morphology.binary_dilation(image_stack[ii,:,:],iterations=num_dilations) + + #%A final dilation that includes omega + print('Final Dilation Including Omega....') + image_stack=img.morphology.binary_dilation(image_stack,iterations=ome_dilation_iter) + + return image_stack + + +#%% +def scan_detector_parm(image_stack, experiment,test_crds,controller,parm_to_opt,parm_vector,slice_shape): + #0-distance + #1-x center + #2-xtilt + #3-ytilt + #4-ztilt + + multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + + #current detector parameters, note the value for the actively optimized parameters will be ignored + distance=experiment.detector_params[5]#mm + x_cen=experiment.detector_params[3]#mm + xtilt=experiment.detector_params[0] + ytilt=experiment.detector_params[1] + ztilt=experiment.detector_params[2] + + num_parm_pts=len(parm_vector) + + trial_data=np.zeros([num_parm_pts,slice_shape[0],slice_shape[1]]) + + tmp_td=copy.copy(experiment.tVec_d) + for jj in np.arange(num_parm_pts): + print('cycle %d of %d'%(jj+1,num_parm_pts)) + + + if parm_to_opt==0: + tmp_td[2]=parm_vector[jj] + else: + tmp_td[2]=distance + + if parm_to_opt==1: + tmp_td[0]=parm_vector[jj] + else: + tmp_td[0]=x_cen + + if parm_to_opt==2: + rMat_d_tmp=makeDetectorRotMat([parm_vector[jj],ytilt,ztilt]) + elif parm_to_opt==3: + rMat_d_tmp=makeDetectorRotMat([xtilt,parm_vector[jj],ztilt]) + elif parm_to_opt==4: + rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,parm_vector[jj]]) + else: + rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,ztilt]) + + experiment.rMat_d = rMat_d_tmp + experiment.tVec_d = tmp_td + + + + conf=test_orientations(image_stack, experiment, test_crds, + controller,multiprocessing_start_method) + + + trial_data[jj]=np.max(conf,axis=0).reshape(slice_shape) + + return trial_data + +#%% + +def extract_max_grain_map(confidence,grid_shape,binary_recon_bin=None): + if binary_recon_bin == None: + binary_recon_bin=np.ones([grid_shape[1],grid_shape[2]]) + + + conf_squeeze=np.max(confidence,axis=0).reshape(grid_shape) + grains=np.argmax(confidence,axis=0).reshape(grid_shape) + out_bounds=np.where(binary_recon_bin==0) + conf_squeeze[:,out_bounds[0],out_bounds[1]] =-0.001 + + return conf_squeeze,grains +#%% + +def process_raw_confidence(raw_confidence,vol_shape,tomo_mask=None,id_remap=None): + + print('Compiling Confidence Map...') + confidence_map=np.max(raw_confidence,axis=0).reshape(vol_shape) + grain_map=np.argmax(raw_confidence,axis=0).reshape(vol_shape) + + + if tomo_mask is not None: + print('Applying tomography mask...') + out_bounds=np.where(tomo_mask==0) + confidence_map[:,out_bounds[0],out_bounds[1]] =-0.001 + grain_map[:,out_bounds[0],out_bounds[1]] =-1 + + + if id_remap is not None: + max_grain_no=np.max(grain_map) + grain_map_copy=copy.copy(grain_map) + print('Remapping grain ids to ff...') + for ii in np.arange(max_grain_no): + this_grain=np.where(grain_map==ii) + grain_map_copy[this_grain]=id_remap[ii] + grain_map=grain_map_copy + + return grain_map, confidence_map + +#%% + +def save_raw_confidence(save_dir,save_stem,raw_confidence,id_remap=None): + print('Saving raw confidence, might take a while...') + if id_remap is not None: + np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence,id_remap=id_remap) + else: + np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence) +#%% + +def save_nf_data(save_dir,save_stem,grain_map,confidence_map,Xs,Ys,Zs,ori_list,id_remap=None): + print('Saving grain map data...') + if id_remap is not None: + np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list,id_remap=id_remap) + else: + np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list) + +#%% + +def plot_ori_map(grain_map, confidence_map, exp_maps, layer_no,id_remap=None): + + grains_plot=np.squeeze(grain_map[layer_no,:,:]) + conf_plot=np.squeeze(confidence_map[layer_no,:,:]) + n_grains=len(exp_maps) + + rgb_image=np.zeros([grains_plot.shape[0],grains_plot.shape[1],4], dtype='float32') + rgb_image[:,:,3]=1. + + for ii in np.arange(n_grains): + if id_remap is not None: + this_grain=np.where(np.squeeze(grains_plot)==id_remap[ii]) + else: + this_grain=np.where(np.squeeze(grains_plot)==ii) + if np.sum(this_grain[0])>0: + + ori=exp_maps[ii,:] + + #cubic mapping + rgb_image[this_grain[0],this_grain[1],0]=(ori[0]+(np.pi/4.))/(np.pi/2.) + rgb_image[this_grain[0],this_grain[1],1]=(ori[1]+(np.pi/4.))/(np.pi/2.) + rgb_image[this_grain[0],this_grain[1],2]=(ori[2]+(np.pi/4.))/(np.pi/2.) + + + + plt.imshow(rgb_image,interpolation='none') + plt.hold(True) + plt.imshow(conf_plot,vmin=0.0,vmax=1.,interpolation='none',cmap=plt.cm.gray,alpha=0.5) + + \ No newline at end of file diff --git a/hexrd/grainmap/tomoutil.py b/hexrd/grainmap/tomoutil.py new file mode 100644 index 00000000..7883f987 --- /dev/null +++ b/hexrd/grainmap/tomoutil.py @@ -0,0 +1,134 @@ +#%% + +import numpy as np +import scipy as sp + +import scipy.ndimage as img + +from skimage.transform import iradon, radon, rescale + + + +#%% + + +def gen_bright_field(tbf_data_folder,tbf_img_start,tbf_num_imgs,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): + + + tbf_img_nums=np.arange(tbf_img_start,tbf_img_start+tbf_num_imgs,1) + + + tbf_stack=np.zeros([tbf_num_imgs,nrows,ncols]) + + print('Loading data for median bright field...') + for ii in np.arange(tbf_num_imgs): + print('Image #: ' + str(ii)) + tbf_stack[ii,:,:]=img.imread(tbf_data_folder+'%s'%(stem)+str(tbf_img_nums[ii]).zfill(num_digits)+ext) + #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) + print('making median...') + + tbf=np.median(tbf_stack,axis=0) + + return tbf + + +def gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): + + + + #Reconstructs a single tompgrahy layer to find the extent of the sample + tomo_img_nums=np.arange(tomo_img_start,tomo_img_start+tomo_num_imgs,1) + + + rad_stack=np.zeros([tomo_num_imgs,nrows,ncols]) + + print('Loading and Calculating Absorption Radiographs ...') + for ii in np.arange(tomo_num_imgs): + print('Image #: ' + str(ii)) + tmp_img=img.imread(tomo_data_folder+'%s'%(stem)+str(tomo_img_nums[ii]).zfill(num_digits)+ext) + + rad_stack[ii,:,:]=-np.log(tmp_img.astype(float)/tbf.astype(float)) + + return rad_stack + + +def tomo_reconstruct_layer(rad_stack,cross_sectional_dim,layer_row=1024,start_tomo_ang=0., end_tomo_ang=360.,tomo_num_imgs=360, center=0.,pixel_size=0.00148): + sinogram=np.squeeze(rad_stack[:,layer_row,:]) + + rotation_axis_pos=-int(np.round(center/pixel_size)) + #rotation_axis_pos=13 + + theta = np.linspace(start_tomo_ang, end_tomo_ang, tomo_num_imgs, endpoint=False) + + max_rad=int(cross_sectional_dim/pixel_size/2.*np.sqrt(2.)) + + if rotation_axis_pos>=0: + sinogram_cut=sinogram[:,2*rotation_axis_pos:] + else: + sinogram_cut=sinogram[:,:(2*rotation_axis_pos)] + + dist_from_edge=np.round(sinogram_cut.shape[1]/2.).astype(int)-max_rad + + sinogram_cut=sinogram_cut[:,dist_from_edge:-dist_from_edge] + + print('Inverting Sinogram....') + reconstruction_fbp = iradon(sinogram_cut.T, theta=theta, circle=True) + + reconstruction_fbp=np.rot90(reconstruction_fbp,3)#Rotation to get the result consistent with hexrd, needs to be checked + + return reconstruction_fbp + + +def threshold_and_clean_tomo_layer(reconstruction_fbp,recon_thresh, noise_obj_size,min_hole_size,edge_cleaning_iter=None): + binary_recon=reconstruction_fbp>recon_thresh + + #hard codeed cleaning, grinding sausage... + binary_recon=img.morphology.binary_erosion(binary_recon,iterations=1) + binary_recon=img.morphology.binary_dilation(binary_recon,iterations=4) + + + labeled_img,num_labels=img.label(binary_recon) + + print('Cleaning...') + print('Removing Noise...') + for ii in np.arange(1,num_labels): + obj1=np.where(labeled_img==ii) + if obj1[0].shape[0]=1 and obj1[0].shape[0] Date: Fri, 22 Jun 2018 20:42:34 -0500 Subject: [PATCH 163/253] more nf mods --- scripts/nf_munge.py | 42 +++++++++++ scripts/process_nf_grain_map.py | 124 +++++++++++++++++--------------- 2 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 scripts/nf_munge.py diff --git a/scripts/nf_munge.py b/scripts/nf_munge.py new file mode 100644 index 00000000..269c7d01 --- /dev/null +++ b/scripts/nf_munge.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Tue Jun 19 18:43:52 2018 + +@author: s1iduser +""" +from skimage import io + +# %% +img_stem = 'MapZBeam_5mmTo9mm_%s__%06d.tif' + +im_or = ['m90deg', '0deg', '90deg'] +im_idx = 31 + +img_list = [io.imread(img_stem % (i, im_idx)) for i in im_or] + +# %% +for i, img in enumerate(img_list): + fig, ax = plt.subplots() + + ax.imshow(img, cmap=plt.cm.inferno, vmin=np.percentile(img, 50)) + fig.suptitle("%s" % im_or[i]) + ax.axis('normal') + +# %% +x_range = [0, 2047] # eyeballed +y_range = [1990, 2040] # eyeballed + +beam_img_list = [img[np.ix_(range(*y_range), range(*x_range))] for img in img_list] + +sinogram = np.vstack([np.sum(bimg, axis=0) for bimg in beam_img_list]) + + +# %% +pix_size = 0.00148 + +left = pix_size*np.r_[361, 2015] +right = pix_size*np.r_[1669, 2029] + +diff = right - left +incl = np.degrees(np.arctan2(diff[1], diff[0])) \ No newline at end of file diff --git a/scripts/process_nf_grain_map.py b/scripts/process_nf_grain_map.py index 580459b6..d2f545e4 100644 --- a/scripts/process_nf_grain_map.py +++ b/scripts/process_nf_grain_map.py @@ -73,94 +73,100 @@ # name of the material for the reconstruction mat_name = 'MAT_NAME' -#reconstruction with misorientation included, for many grains, this will quickly -#make the reconstruction size unmanagable -misorientation_bnd=0.0 #degrees -misorientation_spacing=0.25 #degress +# reconstruction with misorientation included, for many grains, +# this will quickly make the reconstruction size unmanagable +misorientation_bnd = 0.0 # degrees +misorientation_spacing = 0.25 # degrees -beam_stop_width=0.55#mm, assumed to be in the center of the detector +beam_stop_width = 0.55 # mm, assumed to be in the center of the detector +ome_range_deg = [(0., 360.), ] # degrees -ome_range_deg=[(0.,360.)] #degrees +# maximu bragg angle 2theta in degrees +# if -1, all peaks that will hit the detector are calculated +max_tth = -1. +# image processing +num_for_dark = 250 # num images to use for median data +threshold = 3. -max_tth=-1. #degrees, if a negative number is input, all peaks that will hit the detector are calculated +# !!! DO NOT CHANGE ANY OF THESE UNLESS YOU KNOW WHAT YOU ARE DOING +num_erosions = 3 # num iterations of images erosion +num_dilations = 2 # num iterations of images erosion +ome_dilation_iter = 1 # num iterations of 3d image stack dilations +chunk_size = 500 # chunksize for multiprocessing -#image processing -num_for_dark=250#num images to use for median data -threshold=3. -num_erosions=3 #num iterations of images erosion, don't mess with unless you know what you're doing -num_dilations=2 #num iterations of images erosion, don't mess with unless you know what you're doing -ome_dilation_iter=1 #num iterations of 3d image stack dilations, don't mess with unless you know what you're doing +# thresholds for accepting FF grains in NF reconstruction +min_completeness = 0.5 +max_chisq = 0.05 -chunk_size=500#chunksize for multiprocessing, don't mess with unless you know what you're doing +# tomography options +layer_row = 1024 # row of layer to use to find the specimen cross section -#thresholds for grains in reconstructions -comp_thresh=0.5 #only use orientations from grains with completnesses ABOVE this threshold -chi2_thresh=0.05 #only use orientations from grains BELOW this chi^2 +# TOMO OPTIONS +# !!! Don't change these unless you know what you are doing +# this will close small holes and remove noise +recon_thresh = 0.00006 # usually varies between 0.0001 and 0.0005 +noise_obj_size = 5000 +min_hole_size = 5000 +# cross sectional to reconstruct (should be at least 20%-30% over sample width) +cross_sectional_dim = 1.00 -#tomography options -layer_row=1024 # row of layer to use to find the cross sectional specimen shape +voxel_spacing = 0.005 # voxel spacing for the near field reconstruction in mm +v_bnds = [-0.005, 0.005] # vertical (y) reconstruction voxel bounds in mm -#Don't change these unless you know what you are doing, this will close small holes -#and remove noise -recon_thresh=0.00006#usually varies between 0.0001 and 0.0005 -noise_obj_size=5000 -min_hole_size=5000 - - -cross_sectional_dim=1.35 #cross sectional to reconstruct (should be at least 20%-30% over sample width) -#voxel spacing for the near field reconstruction -voxel_spacing=0.005#in mm -##vertical (y) reconstruction voxel bounds in mm -v_bnds=[-0.005,0.005] - - - - -#============================================================================== +# ============================================================================= # %% LOAD GRAIN DATA -#============================================================================== +# ============================================================================= -experiment, nf_to_ff_id_map = nfutil.gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ - misorientation_spacing,ome_range_deg, num_imgs, beam_stop_width) +experiment, nf_to_ff_id_map = nfutil.gen_trial_exp_data( + grain_out_file, det_file, mat_file, x_ray_energy, mat_name, + max_tth, min_completeness, max_chisq, misorientation_bnd, + misorientation_spacing, ome_range_deg, num_imgs, beam_stop_width +) -#============================================================================== +# ============================================================================= # %% TOMO PROCESSING - GENERATE BRIGHT FIELD -#============================================================================== +# ============================================================================= -tbf=tomoutil.gen_bright_field(tbf_data_folder,tbf_img_start,tbf_num_imgs,experiment.nrows,experiment.ncols) +tbf = tomoutil.gen_bright_field( + tbf_data_folder, tbf_img_start, tbf_num_imgs, + experiment.nrows, experiment.ncols +) -#============================================================================== +# ============================================================================= # %% TOMO PROCESSING - BUILD RADIOGRAPHS -#============================================================================== - - -rad_stack=tomoutil.gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,experiment.nrows,experiment.ncols) +# ============================================================================= +rad_stack = tomoutil.gen_attenuation_rads( + tomo_data_folder, tbf, tomo_img_start, tomo_num_imgs, + experiment.nrows, experiment.ncols +) -#============================================================================== +# ============================================================================= # %% TOMO PROCESSING - INVERT SINOGRAM -#============================================================================== +# ============================================================================= -reconstruction_fbp=tomoutil.tomo_reconstruct_layer(rad_stack,cross_sectional_dim,layer_row=layer_row,\ - start_tomo_ang=ome_range_deg[0][0],end_tomo_ang=ome_range_deg[0][1],\ - tomo_num_imgs=tomo_num_imgs, center=experiment.detector_params[3]) +reconstruction_fbp = tomoutil.tomo_reconstruct_layer( + rad_stack, cross_sectional_dim, layer_row=layer_row, + start_tomo_ang=ome_range_deg[0][0], end_tomo_ang=ome_range_deg[0][1], + tomo_num_imgs=tomo_num_imgs, center=experiment.detector_params[3] +) -#============================================================================== +# ============================================================================= # %% TOMO PROCESSING - VIEW RAW FILTERED BACK PROJECTION -#============================================================================== +# ============================================================================= plt.close('all') -plt.imshow(reconstruction_fbp,vmin=0.75e-4,vmax=2e-4) -#Use this image to view the raw reconstruction, estimate threshold levels. and -#figure out if the rotation axis position needs to be corrected +plt.imshow(reconstruction_fbp, vmin=0.75e-4, vmax=2e-4) +# Use this image to view the raw reconstruction, estimate threshold levels. and +# figure out if the rotation axis position needs to be corrected -#============================================================================== +# ============================================================================= # %% TOMO PROCESSING - CLEAN TOMO RECONSTRUCTION -#============================================================================== +# ============================================================================= binary_recon = tomoutil.threshold_and_clean_tomo_layer( reconstruction_fbp, From df357d7bf477f49ba2f971b1fd29d2beeb843b5a Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 28 Jun 2018 17:26:28 -0700 Subject: [PATCH 164/253] cleanup of powder ring utils --- hexrd/instrument.py | 173 ++++++++++++++++++++------------------------ 1 file changed, 80 insertions(+), 93 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index b2c0402b..02a9502f 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -47,7 +47,6 @@ from hexrd import matrixutil as mutil from hexrd.valunits import valWUnit from hexrd.xrd.transforms_CAPI import anglesToGVec, \ - angularDifference, \ detectorXYToGvec, \ gvecToDetectorXY, \ makeDetectorRotMat, \ @@ -510,23 +509,20 @@ def extract_line_positions(self, plane_data, imgser_dict, collapse_eta=True, collapse_tth=False, do_interpolation=True): """ - TODO: handle wedge boundaries + export 'caked' sector data over an instrument - FIXME: must handle merged ranges!!! + FIXME: must handle merged ranges (fixed by JVB 2018/06/28) """ - if tth_tol is None: - tth_tol = np.degrees(plane_data.tThWidth) - tol_vec = 0.5*np.radians( - [-tth_tol, -eta_tol, - -tth_tol, eta_tol, - tth_tol, eta_tol, - tth_tol, -eta_tol]) - # - # pbar = ProgressBar( - # widgets=[Bar('>'), ' ', ETA(), ' ', ReverseBar('<')], - # maxval=self.num_panels, - # ).start() - # + + plane_data = plane_data.makeNew() # make local copy to munge + if tth_tol is not None: + plane_data.tThWidth = np.radians(tth_tol) + tth_ranges = np.degrees(plane_data.getMergedRanges()[1]) + tth_tols = np.vstack([i[1] - i[0] for i in tth_ranges]) + + # ===================================================================== + # LOOP OVER DETECTORS + # ===================================================================== panel_data = dict.fromkeys(self.detectors) for i_det, detector_id in enumerate(self.detectors): print("working on detector '%s'..." % detector_id) @@ -547,40 +543,22 @@ def extract_line_positions(self, plane_data, imgser_dict, # make rings pow_angs, pow_xys = panel.make_powder_rings( plane_data, merge_hkls=True, delta_eta=eta_tol) - n_rings = len(pow_angs) + # ================================================================= + # LOOP OVER RING SETS + # ================================================================= ring_data = [] - for i_ring in range(n_rings): + for i_ring, these_data in enumerate(zip(pow_angs, pow_xys)): print("working on ring %d..." % i_ring) - these_angs = pow_angs[i_ring] - - # make sure no one falls off... - npts = len(these_angs) - patch_vertices = (np.tile(these_angs, (1, 4)) + - np.tile(tol_vec, (npts, 1)) - ).reshape(4*npts, 2) - - # find points that fall on the panel - # WARNING: ignoring effect of crystal tvec - det_xy, rMat_s = xrdutil._project_on_detector_plane( - np.hstack([patch_vertices, np.zeros((4*npts, 1))]), - panel.rmat, ct.identity_3x3, self.chi, - panel.tvec, ct.zeros_3, self.tvec, - panel.distortion) - tmp_xy, on_panel = panel.clip_to_panel(det_xy) - - # all vertices must be on... - patch_is_on = np.all(on_panel.reshape(npts, 4), axis=1) - - # reflection angles (voxel centers) and - # pixel size in (tth, eta) - ang_centers = these_angs[patch_is_on] - ang_pixel_size = panel.angularPixelSize(tmp_xy[::4, :]) + + # points are already checked to fall on detector + angs = these_data[0] + xys = these_data[1] # make the tth,eta patches for interpolation patches = xrdutil.make_reflection_patches( - instr_cfg, ang_centers, ang_pixel_size, - tth_tol=tth_tol, eta_tol=eta_tol, + instr_cfg, angs, panel.angularPixelSize(xys), + tth_tol=tth_tols[i_ring], eta_tol=eta_tol, distortion=panel.distortion, npdiv=npdiv, quiet=True, beamVec=self.beam_vector) @@ -588,7 +566,7 @@ def extract_line_positions(self, plane_data, imgser_dict, # loop over patches # FIXME: fix initialization if collapse_tth: - patch_data = np.zeros((len(ang_centers), n_images)) + patch_data = np.zeros((len(angs), n_images)) else: patch_data = [] for i_p, patch in enumerate(patches): @@ -599,7 +577,7 @@ def extract_line_positions(self, plane_data, imgser_dict, vtx_angs[1][[0, -1], 0]) else: ang_data = (vtx_angs[0][0, :], - ang_centers[i_p][-1]) + angs[i_p][-1]) prows, pcols = areas.shape area_fac = areas/float(native_area) # need to reshape eval pts for interpolation @@ -1596,35 +1574,49 @@ def make_powder_rings( self, pd, merge_hkls=False, delta_tth=None, delta_eta=10., eta_period=None, rmat_s=ct.identity_3x3, tvec_s=ct.zeros_3, - tvec_c=ct.zeros_3, output_ranges=False, output_etas=False): + tvec_c=ct.zeros_3, full_output=False): """ """ # in case you want to give it tth angles directly if hasattr(pd, '__len__'): tth = np.array(pd).flatten() + if delta_tth is None: + raise RuntimeError( + "If supplying a 2theta list as first arg, " + + "must supply a delta_tth") + sector_vertices = np.tile( + 0.5*np.radians([-delta_tth, -delta_eta, + -delta_tth, delta_eta, + delta_tth, delta_eta, + delta_tth, -delta_eta, + 0.0, 0.0]), (len(tth), 1) + ) else: - if merge_hkls: - tth_idx, tth_ranges = pd.getMergedRanges() - if output_ranges: - tth = np.r_[tth_ranges].flatten() - else: - tth = np.array([0.5*sum(i) for i in tth_ranges]) + # Okay, we have a PlaneData object + pd = PlaneData.makeNew(pd) # make a copy to munge + if delta_tth is not None: + pd.tThWidth = np.radians(delta_tth) else: - if output_ranges: - tth = pd.getTThRanges().flatten() - else: - tth = pd.getTTh() + delta_tth = np.degrees(pd.tThWidth) - if delta_tth is not None: - pd.tThWidth = np.radians(delta_tth) - else: - delta_tth = np.degrees(pd.tThWidth) - sector_vec = 0.5*np.radians( - [-delta_tth, -delta_eta, - -delta_tth, delta_eta, - delta_tth, delta_eta, - delta_tth, -delta_eta] - ) + # conversions, meh... + del_eta = np.radians(delta_eta) + + # do merging if asked + if merge_hkls: + _, tth_ranges = pd.getMergedRanges() + tth = np.array([0.5*sum(i) for i in tth_ranges]) + else: + tth_ranges = pd.getTThRanges() + tth = pd.getTTh() + tth_pm = tth_ranges - np.tile(tth, (2, 1)).T + sector_vertices = np.vstack( + [[i[0], -del_eta, + i[0], del_eta, + i[1], del_eta, + i[1], -del_eta, + 0.0, 0.0] + for i in tth_pm]) # for generating rings if eta_period is None: @@ -1642,49 +1634,43 @@ def make_powder_rings( valid_ang = [] valid_xy = [] map_indices = [] + npp = 5 # [ll, ul, ur, lr, center] for i_ring in range(len(angs)): + # expand angles to patch vertices these_angs = angs[i_ring].T - gVec_ring_l = anglesToGVec(these_angs, bHat_l=self.bvec) - xydet_ring = gvecToDetectorXY( - gVec_ring_l, - self.rmat, rmat_s, ct.identity_3x3, - self.tvec, tvec_s, tvec_c, - beamVec=self.bvec) - xydet_ring, on_panel = self.clip_to_panel(xydet_ring) - nangs_r = len(xydet_ring) - - # now expand and check to see which sectors (patches) fall off patch_vertices = ( - np.tile(these_angs[on_panel, :2], (1, 4)) + - np.tile(sector_vec, (nangs_r, 1)) - ).reshape(4*nangs_r, 2) + np.tile(these_angs[:, :2], (1, npp)) + + np.tile(sector_vertices[i_ring], (neta, 1)) + ).reshape(npp*neta, 2) # duplicate ome array ome_dupl = np.tile( - these_angs[on_panel, 2], (4, 1) - ).T.reshape(len(patch_vertices), 1) + these_angs[:, 2], (npp, 1) + ).T.reshape(npp*neta, 1) - # find vertices that fall on the panel + # find vertices that all fall on the panel gVec_ring_l = anglesToGVec( np.hstack([patch_vertices, ome_dupl]), bHat_l=self.bvec) - tmp_xy = gvecToDetectorXY( + all_xy = gvecToDetectorXY( gVec_ring_l, self.rmat, rmat_s, ct.identity_3x3, self.tvec, tvec_s, tvec_c, beamVec=self.bvec) - tmp_xy, on_panel_p = self.clip_to_panel(tmp_xy) + _, on_panel = self.clip_to_panel(all_xy) # all vertices must be on... - patch_is_on = np.all(on_panel_p.reshape(nangs_r, 4), axis=1) + patch_is_on = np.all(on_panel.reshape(neta, npp), axis=1) + patch_xys = all_xy.reshape(neta, 5, 2)[patch_is_on] - idx = np.where(on_panel)[0][patch_is_on] + idx = np.where(patch_is_on)[0] - valid_ang.append(these_angs[idx, :2]) - valid_xy.append(xydet_ring[patch_is_on]) + valid_ang.append(these_angs[patch_is_on, :2]) + valid_xy.append(patch_xys[:, -1, :].squeeze()) map_indices.append(idx) pass - if output_etas: + # ??? is this option necessary? + if full_output: return valid_ang, valid_xy, map_indices, eta else: return valid_ang, valid_xy @@ -2264,13 +2250,14 @@ def __init__(self, image_series_dict, instrument, plane_data, np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]), np.radians(ome_period) ) - + # !!! must avoid the case where omeEdges[0] = omeEdges[-1] for the # indexer to work properly if abs(self._omeEdges[0] - self._omeEdges[-1]) <= ct.sqrt_epsf: - del_ome = np.radians(omegas_array[0, 1] - omegas_array[0, 0]) # signed + # !!! SIGNED delta ome + del_ome = np.radians(omegas_array[0, 1] - omegas_array[0, 0]) self._omeEdges[-1] = self._omeEdges[-2] + del_ome - + # handle etas # WARNING: unlinke the omegas in imageseries metadata, # these are in RADIANS and represent bin centers From 595e3881eeae90ef36bcbc39da49c6b4ef8d7113 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 23 Jul 2018 12:35:17 -0700 Subject: [PATCH 165/253] changed tilt spec to exp map --- hexrd/instrument.py | 31 ++++++++++++++++++++++--------- hexrd/xrd/rotations.py | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 02a9502f..2544adb3 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -49,7 +49,6 @@ from hexrd.xrd.transforms_CAPI import anglesToGVec, \ detectorXYToGvec, \ gvecToDetectorXY, \ - makeDetectorRotMat, \ makeOscillRotMat, \ makeRotMatOfExpMap, \ mapAngle, \ @@ -71,6 +70,8 @@ # PARAMETERS # ============================================================================= +instrument_name_DFLT = 'GE' + beam_energy_DFLT = 65.351 beam_vec_DFLT = ct.beam_vec @@ -81,7 +82,7 @@ ncols_DFLT = 2048 pixel_size_DFLT = (0.2, 0.2) -tilt_angles_DFLT = np.zeros(3) +tilt_params_DFLT = np.zeros(3) t_vec_d_DFLT = np.r_[0., 0., -1000.] chi_DFLT = 0. @@ -181,8 +182,8 @@ class HEDMInstrument(object): """ def __init__(self, instrument_config=None, image_series=None, eta_vector=None, - instrument_name="instrument"): - self._id = instrument_name + instrument_name=None): + self._id = instrument_name_DFLT if eta_vector is None: self._eta_vector = eta_vec_DFLT @@ -190,6 +191,8 @@ def __init__(self, instrument_config=None, self._eta_vector = eta_vector if instrument_config is None: + if instrument_name is not None: + self._id = instrument_name self._num_panels = 1 self._beam_energy = beam_energy_DFLT self._beam_vector = beam_vec_DFLT @@ -199,7 +202,7 @@ def __init__(self, instrument_config=None, rows=nrows_DFLT, cols=ncols_DFLT, pixel_size=pixel_size_DFLT, tvec=t_vec_d_DFLT, - tilt=tilt_angles_DFLT, + tilt=tilt_params_DFLT, bvec=self._beam_vector, evec=self._eta_vector, distortion=None), @@ -208,6 +211,11 @@ def __init__(self, instrument_config=None, self._tvec = t_vec_s_DFLT self._chi = chi_DFLT else: + if instrument_name is None: + if 'id' in instrument_config: + self._id = instrument_config['id'] + else: + self._id = instrument_name self._num_panels = len(instrument_config['detectors']) self._beam_energy = instrument_config['beam']['energy'] # keV self._beam_vector = calc_beam_vec( @@ -317,9 +325,14 @@ def beam_vector(self): @beam_vector.setter def beam_vector(self, x): x = np.array(x).flatten() - assert len(x) == 3 and sum(x*x) > 1-ct.sqrt_epsf, \ - 'input must have length = 3 and have unit magnitude' - self._beam_vector = x + if len(x) == 3: + assert sum(x*x) > 1-ct.sqrt_epsf, \ + 'input must have length = 3 and have unit magnitude' + self._beam_vector = x + elif len(x) == 2: + self._beam_vector = calc_beam_vec(*x) + else: + raise RuntimeError("input must be a unit vector or angle pair") # ...maybe change dictionary item behavior for 3.x compatibility? for detector_id in self.detectors: panel = self.detectors[detector_id] @@ -1264,7 +1277,7 @@ def distortion(self, x): @property def rmat(self): - return makeDetectorRotMat(self.tilt) + return makeRotMatOfExpMap(self.tilt) @property def normal(self): diff --git a/hexrd/xrd/rotations.py b/hexrd/xrd/rotations.py index d155441b..63ae23a0 100644 --- a/hexrd/xrd/rotations.py +++ b/hexrd/xrd/rotations.py @@ -10,9 +10,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the Free +# Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -33,8 +33,8 @@ import numpy from numpy import \ arange, arctan2, array, asarray, atleast_1d, average, \ - ndarray, diag, zeros, \ - cross, dot, pi, arccos, arcsin, cos, sin, sqrt, \ + ndarray, diag, eye, zeros, \ + cross, dot, outer, pi, arccos, arcsin, cos, sin, sqrt, \ sort, tile, vstack, hstack, c_, ix_, \ abs, mod, sign, \ finfo, isscalar @@ -643,18 +643,14 @@ def angleAxisOfRotMat(R): def make_rmat_euler(tilt_angles, axes_order, extrinsic=True): """ - extrinsic or intrinsic by kw + extrinsic (PASSIVE) or intrinsic (ACTIVE) by kw + tilt_angles are in RADIANS """ axes = numpy.eye(3) axes_dict = dict(x=0, y=1, z=2) - # orders = [] - # for l in [''.join(i) for i in itertools.product(['x', 'y', 'z'], repeat=3)]: - # if numpy.sum(numpy.array([j for j in l]) == l[0]) < 3: - # if l[1] != l[0] and l[2] != l[1]: - # orders.append(l) - + # axes orders, all permutations orders = [ 'xyz', 'zyx', 'zxy', 'yxz', @@ -709,6 +705,28 @@ def angles_from_rmat_xyz(rmat): return rx, ry, rz +def angles_from_rmat_zxz(rmat): + """ + calculate z-x-z euler angles from a rotation matrix in + the ACTIVE convention + + alpha, beta, gamma + """ + if abs(rmat[2, 2]) > 1. - sqrt(finfo('float').eps): + beta = 0. + alpha = arctan2(rmat[1, 0], rmat[0, 0]) + gamma = 0. + else: + xnew = rmat[:, 0] + znew = rmat[:, 2] + alpha = arctan2(znew[0], -znew[1]) + rma = rotMatOfExpMap(alpha*c_[0., 0., 1.].T) + znew1 = dot(rma.T, znew) + beta = arctan2(-znew1[1], znew1[2]) + rmb = rotMatOfExpMap(beta*c_[cos(alpha), sin(alpha), 0.].T) + xnew2 = dot(rma.T, dot(rmb.T, xnew)) + gamma = arctan2(xnew2[1], xnew2[0]) + return alpha, beta, gamma # # ==================== Fiber # From 5e9e2af2bede18c35b45d278883b1dbc444d2c34 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 31 Jul 2018 12:50:14 -0700 Subject: [PATCH 166/253] broken kwarg bug fix --- hexrd/instrument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 02a9502f..55ba5237 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -454,7 +454,7 @@ def extract_polar_maps(self, plane_data, imgser_dict, pow_angs, pow_xys, eta_idx, full_etas = panel.make_powder_rings( plane_data, merge_hkls=False, delta_eta=eta_tol, - output_etas=True) + full_output=True) ptth, peta = panel.pixel_angles ring_maps = [] From f3d75e39e5cd4c7cc714b518ac192ef9d72b5503 Mon Sep 17 00:00:00 2001 From: "Darren C. Pagan" Date: Thu, 6 Sep 2018 15:30:18 -0400 Subject: [PATCH 167/253] Adding multipeak fitting to the fitting package. New functionality to analyze peaks included direct analysis and calculating the area under the curve for peaks. Fixed problems with split pv functions. Removed IPython call in crystallography.py --- conda.recipe/meta.yaml | 2 +- hexrd/fitting/fitpeak.py | 182 ++++++++++++++++++++++++++++++++- hexrd/fitting/peakfunctions.py | 86 +++++++++++++++- hexrd/xrd/crystallography.py | 2 - 4 files changed, 260 insertions(+), 12 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 90c28246..2b7ae7e8 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -25,7 +25,7 @@ requirements: - python - setuptools run: - - fabio + #- fabio - h5py - matplotlib - numba diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index dcc124b9..48af95f5 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -31,8 +31,9 @@ import scipy.optimize as optimize import hexrd.fitting.peakfunctions as pkfuncs import scipy.ndimage as imgproc -import copy - +import scipy.integrate as integrate +#import copy +import matplotlib.pyplot as plt #### 1-D Peak Fitting @@ -159,6 +160,48 @@ def fit_pk_parms_1d(p0,x,f,pktype='pvoigt'): return p + + +def fit_mpk_parms_1d(p0,x,f0,pktype,num_pks,bgtype=None,bnds=None): + """ + Performs least squares fit to find parameters for MULTIPLE 1d analytic functions fit + to diffraction data + + + Required Arguments: + p0 -- (m x u + v) guess of peak parameters for number of peaks, m is the number of + parameters per peak ("gaussian" and "lorentzian" - 3, "pvoigt" - 4, "split_pvoigt" + - 5), v is the number of parameters for chosen bgtype + x -- (n) ndarray of coordinate positions + f -- (n) ndarray of intensity measurements at coordinate positions x + pktype -- string, type of analytic function that will be used to fit the data, + current options are "gaussian","lorentzian","pvoigt" (psuedo voigt), and + "split_pvoigt" (split psuedo voigt) + num_pks -- integer 'u' indicating the number of pks, must match length of p + pktype -- string, background functions, available options are "constant", + "linear", and "quadratic" + bnds -- tuple containing + + Outputs: + p -- (m x u) fit peak parameters for number of peaks, m is the number of + parameters per peak ("gaussian" and "lorentzian" - 3, "pvoigt" - 4, "split_pvoigt" + - 5) + """ + + fitArgs=(x,f0,pktype,num_pks,bgtype) + + ftol=1e-6 + xtol=1e-6 + + if bnds != None: + p = optimize.least_squares(fit_mpk_obj_1d, p0,bounds=bnds, args=fitArgs,ftol=ftol,xtol=xtol) + else: + p = optimize.least_squares(fit_mpk_obj_1d, p0, args=fitArgs,ftol=ftol,xtol=xtol) + + return p.x + + + def eval_pk_deriv_1d(p,x,y0,pktype): if pktype == 'gaussian': @@ -208,6 +251,17 @@ def fit_pk_obj_1d_bnded(p,x,f0,pktype,weight,lb,ub): return resd + +def fit_mpk_obj_1d(p,x,f0,pktype,num_pks,bgtype): + + f=pkfuncs.mpeak_1d(p,x,pktype,num_pks,bgtype='linear') + resd = f-f0 + return resd + + + + + #### 2-D Peak Fitting def estimate_pk_parms_2d(x,y,f,pktype): @@ -342,8 +396,6 @@ def goodness_of_fit(f,f0): Outputs: R -- (1) goodness of fit measure which is sum(error^2)/sum(meas^2) Rw -- (1) goodness of fit measure weighted by intensity sum(meas*error^2)/sum(meas^3) - - """ @@ -351,3 +403,125 @@ def goodness_of_fit(f,f0): Rw=np.sum(np.abs(f0*(f-f0)**2))/np.sum(np.abs(f0**3)) return R, Rw + + + +def direct_pk_analysis(x,f,remove_bg=True,low_int=1.,edge_pts=3,pts_per_meas=100): + """ + Performs analysis of a single peak that is not well matched to any analytic functions + + + Required Arguments: + x -- (n) ndarray of coordinate positions + f -- (n) ndarray of intensity measurements at coordinate positions x + + Optional Arguments: + remove_bg -- boolean, if selected a linear background will be subtracted from the peak + low_int -- float, value for area under a peak that defines a lower bound + on what is recognized as peak + edge_pts -- int, number of points at the edges of the data to use to calculated background + pts_per_meas -- how many interpolated points to place between measurement values + + Outputs: + p -- array of values containing the integrated intensity, center of mass, and + FWHM of the peak + """ + + + + + plt.plot(x,f) + #subtract background, assumed linear + if remove_bg: + bg_data=np.hstack((f[:(edge_pts+1)],f[-edge_pts:])) + bg_pts=np.hstack((x[:(edge_pts+1)],x[-edge_pts:])) + + bg_parm=np.polyfit(bg_pts,bg_data,1) + + f=f-(bg_parm[0]*x+bg_parm[1])#pull out high background + + f=f-np.min(f)#set the minimum to 0 + + + plt.plot(bg_pts,bg_data,'x') + plt.plot(x,f,'r') + + spacing=np.diff(x)[0]/pts_per_meas + xfine=np.arange(np.min(x),np.max(x)+spacing,spacing)# make a fine grid of points + ffine=np.interp(xfine,x,f) + + data_max=np.max(f)#find max intensity values + + total_int=integrate.simps(ffine,xfine)#numerically integrate the peak using the simpson rule + + cen_index=np.argmax(ffine) + A=data_max + + if(total_int Date: Fri, 14 Sep 2018 11:42:05 -0400 Subject: [PATCH 168/253] fixed image loading issues in nfutil and tomoutil for future scipy updated changed tomography resizing to use skimage to workaround broken scipy function added imageio to necessary installs added new multipeak guess functionality --- conda.recipe/meta.yaml | 1 + hexrd/fitting/fitpeak.py | 104 ++++++++++++++++++++++++++++++++++++- hexrd/grainmap/nfutil.py | 5 +- hexrd/grainmap/tomoutil.py | 13 +++-- 4 files changed, 117 insertions(+), 6 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 2b7ae7e8..befdd265 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -27,6 +27,7 @@ requirements: run: #- fabio - h5py + - imageio - matplotlib - numba - numpy diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index 48af95f5..b7b37070 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -201,6 +201,108 @@ def fit_mpk_parms_1d(p0,x,f0,pktype,num_pks,bgtype=None,bnds=None): return p.x +def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_guess=0.07): + + + num_pks=len(pk_pos_0) + min_val=np.min(f) + + + + if pktype == 'gaussian' or pktype == 'lorentzian': + p0tmp=np.zeros([num_pks,3]) + p0tmp_lb=np.zeros([num_pks,3]) + p0tmp_ub=np.zeros([num_pks,3]) + + #x is just 2theta values + #make guess for the initital parameters + for ii in np.arange(num_pks): + pt=np.argmin(np.abs(x-pk_pos_0[ii])) + p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-0.02,fwhm_guess*0.5] + p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+0.02,fwhm_guess*2.0] + elif pktype == 'pvoigt': + p0tmp=np.zeros([num_pks,4]) + p0tmp_lb=np.zeros([num_pks,4]) + p0tmp_ub=np.zeros([num_pks,4]) + + #x is just 2theta values + #make guess for the initital parameters + for ii in np.arange(num_pks): + pt=np.argmin(np.abs(x-pk_pos_0[ii])) + p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess,0.5] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-0.02,fwhm_guess*0.5,0.0] + p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+0.02,fwhm_guess*2.0,1.0] + elif pktype == 'split_pvoigt': + p0tmp=np.zeros([num_pks,6]) + p0tmp_lb=np.zeros([num_pks,6]) + p0tmp_ub=np.zeros([num_pks,6]) + + #x is just 2theta values + #make guess for the initital parameters + for ii in np.arange(num_pks): + pt=np.argmin(np.abs(x-pk_pos_0[ii])) + p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess,fwhm_guess,0.5,0.5] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-0.02,fwhm_guess*0.5,fwhm_guess*0.5,0.0,0.0] + p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+0.02,fwhm_guess*2.0,fwhm_guess*2.0,1.0,1.0] + + + if bgtype=='linear': + num_pk_parms=len(p0tmp.ravel()) + p0=np.zeros(num_pk_parms+2) + lb=np.zeros(num_pk_parms+2) + ub=np.zeros(num_pk_parms+2) + p0[:num_pk_parms]=p0tmp.ravel() + lb[:num_pk_parms]=p0tmp_lb.ravel() + ub[:num_pk_parms]=p0tmp_ub.ravel() + + + p0[-2]=min_val + + lb[-2]=-float('inf') + lb[-1]=-float('inf') + + ub[-2]=float('inf') + ub[-1]=float('inf') + + elif bgtype=='constant': + num_pk_parms=len(p0tmp.ravel()) + p0=np.zeros(num_pk_parms+1) + lb=np.zeros(num_pk_parms+1) + ub=np.zeros(num_pk_parms+1) + p0[:num_pk_parms]=p0tmp.ravel() + lb[:num_pk_parms]=p0tmp_lb.ravel() + ub[:num_pk_parms]=p0tmp_ub.ravel() + + + p0[-1]=min_val + lb[-1]=-float('inf') + ub[-1]=float('inf') + + elif bgtype=='quadratic': + num_pk_parms=len(p0tmp.ravel()) + p0=np.zeros(num_pk_parms+3) + lb=np.zeros(num_pk_parms+3) + ub=np.zeros(num_pk_parms+3) + p0[:num_pk_parms]=p0tmp.ravel() + lb[:num_pk_parms]=p0tmp_lb.ravel() + ub[:num_pk_parms]=p0tmp_ub.ravel() + + + p0[-3]=min_val + lb[-3]=-float('inf') + lb[-2]=-float('inf') + lb[-1]=-float('inf') + ub[-3]=float('inf') + ub[-2]=float('inf') + ub[-1]=float('inf') + + bnds=(lb,ub) + + + + + return p0, bnds def eval_pk_deriv_1d(p,x,y0,pktype): @@ -511,7 +613,7 @@ def calc_pk_integrated_intensities(p,x,pktype,num_pks): elif pktype == 'pvoigt': p_fit=np.reshape(p[:4*num_pks],[num_pks,4]) elif pktype == 'split_pvoigt': - p_fit=np.reshape(p[:6*num_pks],[num_pks,5]) + p_fit=np.reshape(p[:6*num_pks],[num_pks,6]) for ii in np.arange(num_pks): if pktype == 'gaussian': diff --git a/hexrd/grainmap/nfutil.py b/hexrd/grainmap/nfutil.py index 5aa51e93..7df9901a 100644 --- a/hexrd/grainmap/nfutil.py +++ b/hexrd/grainmap/nfutil.py @@ -32,6 +32,7 @@ import cPickle as cpl import scipy.ndimage as img +import imageio as imgio import matplotlib.pyplot as plt @@ -965,7 +966,7 @@ def gen_nf_dark(data_folder,img_nums,num_for_dark,nrows,ncols,dark_type='median' print('Loading data for dark generation...') for ii in np.arange(num_for_dark): print('Image #: ' + str(ii)) - dark_stack[ii,:,:]=img.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext) + dark_stack[ii,:,:]=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext) #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) if dark_type=='median': @@ -987,7 +988,7 @@ def gen_nf_image_stack(data_folder,img_nums,dark,num_erosions,num_dilations,ome_ print('Loading and Cleaning Images...') for ii in np.arange(img_nums.shape[0]): print('Image #: ' + str(ii)) - tmp_img=img.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext)-dark + tmp_img=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext)-dark #image procesing image_stack[ii,:,:]=img.morphology.binary_erosion(tmp_img>threshold,iterations=num_erosions) image_stack[ii,:,:]=img.morphology.binary_dilation(image_stack[ii,:,:],iterations=num_dilations) diff --git a/hexrd/grainmap/tomoutil.py b/hexrd/grainmap/tomoutil.py index 7883f987..29c71b66 100644 --- a/hexrd/grainmap/tomoutil.py +++ b/hexrd/grainmap/tomoutil.py @@ -4,6 +4,9 @@ import scipy as sp import scipy.ndimage as img +import imageio as imgio +import skimage.transform as xformimg + from skimage.transform import iradon, radon, rescale @@ -23,7 +26,7 @@ def gen_bright_field(tbf_data_folder,tbf_img_start,tbf_num_imgs,nrows,ncols,stem print('Loading data for median bright field...') for ii in np.arange(tbf_num_imgs): print('Image #: ' + str(ii)) - tbf_stack[ii,:,:]=img.imread(tbf_data_folder+'%s'%(stem)+str(tbf_img_nums[ii]).zfill(num_digits)+ext) + tbf_stack[ii,:,:]=imgio.imread(tbf_data_folder+'%s'%(stem)+str(tbf_img_nums[ii]).zfill(num_digits)+ext) #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) print('making median...') @@ -45,7 +48,7 @@ def gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,nrows print('Loading and Calculating Absorption Radiographs ...') for ii in np.arange(tomo_num_imgs): print('Image #: ' + str(ii)) - tmp_img=img.imread(tomo_data_folder+'%s'%(stem)+str(tomo_img_nums[ii]).zfill(num_digits)+ext) + tmp_img=imgio.imread(tomo_data_folder+'%s'%(stem)+str(tomo_img_nums[ii]).zfill(num_digits)+ext) rad_stack[ii,:,:]=-np.log(tmp_img.astype(float)/tbf.astype(float)) @@ -122,7 +125,11 @@ def crop_and_rebin_tomo_layer(binary_recon,recon_thresh,voxel_spacing,pixel_size new_rows=np.round(rows/scaling).astype(int) new_cols=np.round(cols/scaling).astype(int) - binary_recon_bin=np.floor(sp.misc.imresize(binary_recon,[new_rows,new_cols])/255).astype(bool) + tmp_resize=xformimg.resize(binary_recon,[new_rows,new_cols],preserve_range=True) + #tmp_resize_norm=tmp_resize/255 + tmp_resize_norm_force=np.floor(tmp_resize) + + binary_recon_bin=tmp_resize_norm_force.astype(bool) cut_edge=int(np.round((binary_recon_bin.shape[0]*voxel_spacing-cross_sectional_dim)/2./voxel_spacing)) From a73e26b3a57dee5ad649709e384b6929e5d4095f Mon Sep 17 00:00:00 2001 From: Donald Boyce Date: Wed, 10 Oct 2018 19:49:13 -0400 Subject: [PATCH 169/253] improved performance for writing frame caches and loading imagefiles * fixed issue with multiple calls to fabio.open * changed warning for sparsity in frame-cache write (checks on 95% sparsity and continues execution) * dropped print statement from stats.percentile and had stats.median use stats.percentile(50%) --- hexrd/imageseries/load/imagefiles.py | 6 +++--- hexrd/imageseries/save.py | 13 ++++++++++--- hexrd/imageseries/stats.py | 9 +++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py index 24128aeb..36c554f1 100644 --- a/hexrd/imageseries/load/imagefiles.py +++ b/hexrd/imageseries/load/imagefiles.py @@ -50,9 +50,8 @@ def __getitem__(self, key): img = fabio.open(imgf) else: (fnum, frame) = self._file_and_frame(key) - imgf = self._files[fnum] - img0 = fabio.open(imgf) - img = img0.getframe(frame) + fimg = self.infolist[fnum].fabioimage + img = fimg.getframe(frame) return img.data @@ -197,6 +196,7 @@ def __init__(self, filename, **kwargs): self._fabioclass = img.classname self._imgframes = img.nframes self.dat = img.data + self.fabioimage = img d = kwargs.copy() self._empty = d.pop('empty', 0) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index ee6cbebb..50b5a564 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -2,6 +2,7 @@ from __future__ import print_function import abc import os +import warnings import numpy as np import h5py @@ -195,12 +196,18 @@ def _write_frames(self): frame = self._ims[i] mask = frame > self._thresh # FIXME: formalize this a little better??? - if np.sum(mask) / float(frame.shape[0]*frame.shape[1]) > 0.25: - raise Warning("frame %d is less than 75%% sparse" % i) + # -- maybe set a hard limit of total nonzeros for the imageseries + # -- could pass as a kwarg on open + fullness = np.sum(mask) / float(frame.shape[0]*frame.shape[1]) + if fullness > 0.05: + sparseness = 100.*(1 -fullness) + msg = "frame %d is %4.2f%% sparse (cutoff is 95%%)" % (i, sparseness) + warnings.warn(msg) + row, col = mask.nonzero() - arrd['%d_data' % i] = frame[mask] arrd['%d_row' % i] = row arrd['%d_col' % i] = col + arrd['%d_data' % i] = frame[mask] arrd['shape'] = self._ims.shape arrd['nframes'] = len(self._ims) arrd['dtype'] = str(self._ims.dtype) diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index b8ae1aee..3f81a0a7 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -26,11 +26,8 @@ def average(ims, nframes=0): def median(ims, nframes=0): """return image with median values over all frames""" - # could be done by rectangle by rectangle if full series - # too big for memory - nf = _nframes(ims, nframes) - out = np.empty(ims.shape, dtype=ims.dtype) - return np.median(_toarray(ims, nf), axis=0, out=out) + # use percentile since it has better performance + return percentile(ims, 50, nframes=nframes) def percentile(ims, pct, nframes=0): """return image with given percentile values over all frames""" @@ -40,7 +37,7 @@ def percentile(ims, pct, nframes=0): dt = ims.dtype (nr, nc) = ims.shape nrpb = _rows_in_buffer(nframes, nf*nc*dt.itemsize) - print('Buffering percentile calculation with', nrpb, 'rows per buffer.') + # now build the result a rectangle at a time img = np.zeros_like(ims[0]) for rr in _row_ranges(nr, nrpb): From dc72ef981a8ef9545c6b01b287d03ecdae4a9772 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 23 Jul 2018 12:35:17 -0700 Subject: [PATCH 170/253] changed tilt spec to exp map --- hexrd/instrument.py | 31 ++++++++++++++++++++++--------- hexrd/xrd/rotations.py | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 55ba5237..0aff07e7 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -49,7 +49,6 @@ from hexrd.xrd.transforms_CAPI import anglesToGVec, \ detectorXYToGvec, \ gvecToDetectorXY, \ - makeDetectorRotMat, \ makeOscillRotMat, \ makeRotMatOfExpMap, \ mapAngle, \ @@ -71,6 +70,8 @@ # PARAMETERS # ============================================================================= +instrument_name_DFLT = 'GE' + beam_energy_DFLT = 65.351 beam_vec_DFLT = ct.beam_vec @@ -81,7 +82,7 @@ ncols_DFLT = 2048 pixel_size_DFLT = (0.2, 0.2) -tilt_angles_DFLT = np.zeros(3) +tilt_params_DFLT = np.zeros(3) t_vec_d_DFLT = np.r_[0., 0., -1000.] chi_DFLT = 0. @@ -181,8 +182,8 @@ class HEDMInstrument(object): """ def __init__(self, instrument_config=None, image_series=None, eta_vector=None, - instrument_name="instrument"): - self._id = instrument_name + instrument_name=None): + self._id = instrument_name_DFLT if eta_vector is None: self._eta_vector = eta_vec_DFLT @@ -190,6 +191,8 @@ def __init__(self, instrument_config=None, self._eta_vector = eta_vector if instrument_config is None: + if instrument_name is not None: + self._id = instrument_name self._num_panels = 1 self._beam_energy = beam_energy_DFLT self._beam_vector = beam_vec_DFLT @@ -199,7 +202,7 @@ def __init__(self, instrument_config=None, rows=nrows_DFLT, cols=ncols_DFLT, pixel_size=pixel_size_DFLT, tvec=t_vec_d_DFLT, - tilt=tilt_angles_DFLT, + tilt=tilt_params_DFLT, bvec=self._beam_vector, evec=self._eta_vector, distortion=None), @@ -208,6 +211,11 @@ def __init__(self, instrument_config=None, self._tvec = t_vec_s_DFLT self._chi = chi_DFLT else: + if instrument_name is None: + if 'id' in instrument_config: + self._id = instrument_config['id'] + else: + self._id = instrument_name self._num_panels = len(instrument_config['detectors']) self._beam_energy = instrument_config['beam']['energy'] # keV self._beam_vector = calc_beam_vec( @@ -317,9 +325,14 @@ def beam_vector(self): @beam_vector.setter def beam_vector(self, x): x = np.array(x).flatten() - assert len(x) == 3 and sum(x*x) > 1-ct.sqrt_epsf, \ - 'input must have length = 3 and have unit magnitude' - self._beam_vector = x + if len(x) == 3: + assert sum(x*x) > 1-ct.sqrt_epsf, \ + 'input must have length = 3 and have unit magnitude' + self._beam_vector = x + elif len(x) == 2: + self._beam_vector = calc_beam_vec(*x) + else: + raise RuntimeError("input must be a unit vector or angle pair") # ...maybe change dictionary item behavior for 3.x compatibility? for detector_id in self.detectors: panel = self.detectors[detector_id] @@ -1264,7 +1277,7 @@ def distortion(self, x): @property def rmat(self): - return makeDetectorRotMat(self.tilt) + return makeRotMatOfExpMap(self.tilt) @property def normal(self): diff --git a/hexrd/xrd/rotations.py b/hexrd/xrd/rotations.py index d155441b..63ae23a0 100644 --- a/hexrd/xrd/rotations.py +++ b/hexrd/xrd/rotations.py @@ -10,9 +10,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the Free +# Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -33,8 +33,8 @@ import numpy from numpy import \ arange, arctan2, array, asarray, atleast_1d, average, \ - ndarray, diag, zeros, \ - cross, dot, pi, arccos, arcsin, cos, sin, sqrt, \ + ndarray, diag, eye, zeros, \ + cross, dot, outer, pi, arccos, arcsin, cos, sin, sqrt, \ sort, tile, vstack, hstack, c_, ix_, \ abs, mod, sign, \ finfo, isscalar @@ -643,18 +643,14 @@ def angleAxisOfRotMat(R): def make_rmat_euler(tilt_angles, axes_order, extrinsic=True): """ - extrinsic or intrinsic by kw + extrinsic (PASSIVE) or intrinsic (ACTIVE) by kw + tilt_angles are in RADIANS """ axes = numpy.eye(3) axes_dict = dict(x=0, y=1, z=2) - # orders = [] - # for l in [''.join(i) for i in itertools.product(['x', 'y', 'z'], repeat=3)]: - # if numpy.sum(numpy.array([j for j in l]) == l[0]) < 3: - # if l[1] != l[0] and l[2] != l[1]: - # orders.append(l) - + # axes orders, all permutations orders = [ 'xyz', 'zyx', 'zxy', 'yxz', @@ -709,6 +705,28 @@ def angles_from_rmat_xyz(rmat): return rx, ry, rz +def angles_from_rmat_zxz(rmat): + """ + calculate z-x-z euler angles from a rotation matrix in + the ACTIVE convention + + alpha, beta, gamma + """ + if abs(rmat[2, 2]) > 1. - sqrt(finfo('float').eps): + beta = 0. + alpha = arctan2(rmat[1, 0], rmat[0, 0]) + gamma = 0. + else: + xnew = rmat[:, 0] + znew = rmat[:, 2] + alpha = arctan2(znew[0], -znew[1]) + rma = rotMatOfExpMap(alpha*c_[0., 0., 1.].T) + znew1 = dot(rma.T, znew) + beta = arctan2(-znew1[1], znew1[2]) + rmb = rotMatOfExpMap(beta*c_[cos(alpha), sin(alpha), 0.].T) + xnew2 = dot(rma.T, dot(rmb.T, xnew)) + gamma = arctan2(xnew2[1], xnew2[0]) + return alpha, beta, gamma # # ==================== Fiber # From b66641ba12a668ee9bf906699f072bbc4f46f678 Mon Sep 17 00:00:00 2001 From: "Darren C. Pagan" Date: Wed, 24 Oct 2018 09:48:47 -0400 Subject: [PATCH 171/253] Edited the extract_line_postitions routine to take a list of tth values as input. Minor changes to fitpeak.py to improve estimate function for multipeak fitting. --- hexrd/fitting/fitpeak.py | 14 +++++++------- hexrd/instrument.py | 25 ++++++++++++++++--------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index b7b37070..607c8450 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -201,7 +201,7 @@ def fit_mpk_parms_1d(p0,x,f0,pktype,num_pks,bgtype=None,bnds=None): return p.x -def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_guess=0.07): +def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_guess=0.07,center_bnd=0.02): num_pks=len(pk_pos_0) @@ -219,8 +219,8 @@ def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_gues for ii in np.arange(num_pks): pt=np.argmin(np.abs(x-pk_pos_0[ii])) p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess] - p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-0.02,fwhm_guess*0.5] - p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+0.02,fwhm_guess*2.0] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5] + p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0] elif pktype == 'pvoigt': p0tmp=np.zeros([num_pks,4]) p0tmp_lb=np.zeros([num_pks,4]) @@ -231,8 +231,8 @@ def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_gues for ii in np.arange(num_pks): pt=np.argmin(np.abs(x-pk_pos_0[ii])) p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess,0.5] - p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-0.02,fwhm_guess*0.5,0.0] - p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+0.02,fwhm_guess*2.0,1.0] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5,0.0] + p0tmp_ub[ii,:]=[(f[pt]-min_val+1.)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0,1.0] elif pktype == 'split_pvoigt': p0tmp=np.zeros([num_pks,6]) p0tmp_lb=np.zeros([num_pks,6]) @@ -243,8 +243,8 @@ def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_gues for ii in np.arange(num_pks): pt=np.argmin(np.abs(x-pk_pos_0[ii])) p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess,fwhm_guess,0.5,0.5] - p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-0.02,fwhm_guess*0.5,fwhm_guess*0.5,0.0,0.0] - p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+0.02,fwhm_guess*2.0,fwhm_guess*2.0,1.0,1.0] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5,fwhm_guess*0.5,0.0,0.0] + p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0,fwhm_guess*2.0,1.0,1.0] if bgtype=='linear': diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 55ba5237..8b142b92 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -514,11 +514,14 @@ def extract_line_positions(self, plane_data, imgser_dict, FIXME: must handle merged ranges (fixed by JVB 2018/06/28) """ - plane_data = plane_data.makeNew() # make local copy to munge - if tth_tol is not None: - plane_data.tThWidth = np.radians(tth_tol) - tth_ranges = np.degrees(plane_data.getMergedRanges()[1]) - tth_tols = np.vstack([i[1] - i[0] for i in tth_ranges]) + if not hasattr(plane_data, '__len__'): + plane_data = plane_data.makeNew() # make local copy to munge + if tth_tol is not None: + plane_data.tThWidth = np.radians(tth_tol) + tth_ranges = np.degrees(plane_data.getMergedRanges()[1]) + tth_tols = np.vstack([i[1] - i[0] for i in tth_ranges]) + else: + tth_tols=np.ones(len(plane_data))*tth_tol # ===================================================================== # LOOP OVER DETECTORS @@ -542,14 +545,14 @@ def extract_line_positions(self, plane_data, imgser_dict, # make rings pow_angs, pow_xys = panel.make_powder_rings( - plane_data, merge_hkls=True, delta_eta=eta_tol) + plane_data, merge_hkls=True, delta_tth=tth_tol, delta_eta=eta_tol) # ================================================================= # LOOP OVER RING SETS # ================================================================= ring_data = [] for i_ring, these_data in enumerate(zip(pow_angs, pow_xys)): - print("working on ring %d..." % i_ring) + print("working on 2theta bin (ring) %d..." % i_ring) # points are already checked to fall on detector angs = these_data[0] @@ -588,8 +591,9 @@ def extract_line_positions(self, plane_data, imgser_dict, # interpolate if not collapse_tth: ims_data = [] - for j_p, image in enumerate(images): + for j_p in np.arange(len(images)): # catch interpolation type + image=images[j_p] if do_interpolation: tmp = panel.interpolate_bilinear( xy_eval, @@ -1624,11 +1628,13 @@ def make_powder_rings( neta = int(360./float(delta_eta)) eta = mapAngle( - np.radians(delta_eta*(np.linspace(0, neta - 1, num=neta) + 0.5)) + + #np.radians(delta_eta*(np.linspace(0., neta - 1, num=neta) + 0.5)) + #dp change, don't like 0.5 + np.radians(delta_eta*(np.linspace(0., neta - 1, num=neta))) + eta_period[0], eta_period ) angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] + # need xy coords and pixel sizes valid_ang = [] @@ -1662,6 +1668,7 @@ def make_powder_rings( # all vertices must be on... patch_is_on = np.all(on_panel.reshape(neta, npp), axis=1) patch_xys = all_xy.reshape(neta, 5, 2)[patch_is_on] + idx = np.where(patch_is_on)[0] From 80719a2dd9f9873078b1ca1219b63627128df4a3 Mon Sep 17 00:00:00 2001 From: rachelelim Date: Mon, 29 Oct 2018 22:17:40 -0400 Subject: [PATCH 172/253] fix reshape for split_pvoigt in _mpeak_1d_no_bg --- hexrd/fitting/peakfunctions.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/hexrd/fitting/peakfunctions.py b/hexrd/fitting/peakfunctions.py index 32f2b310..6364692a 100644 --- a/hexrd/fitting/peakfunctions.py +++ b/hexrd/fitting/peakfunctions.py @@ -298,7 +298,7 @@ def _split_pvoigt1d_no_bg(p,x): #+ r=np.where(xr)[0] - + f[r]=A*_unit_pvoigt1d(p[[1,3,5]],x[r]) #- @@ -571,7 +571,7 @@ def gaussian3d(p,x,y,z): -def _mpeak_1d_no_bg(p,x,pktype,num_pks): +def _mpeak_1d_no_bg(p,x,pktype,num_pks): """ Required Arguments: @@ -587,16 +587,16 @@ def _mpeak_1d_no_bg(p,x,pktype,num_pks): Outputs: f -- (n) ndarray of function values at positions (x) """ - + f=np.zeros(len(x)) - + if pktype == 'gaussian' or pktype == 'lorentzian': p_fit=np.reshape(p[:3*num_pks],[num_pks,3]) elif pktype == 'pvoigt': p_fit=np.reshape(p[:4*num_pks],[num_pks,4]) elif pktype == 'split_pvoigt': - p_fit=np.reshape(p[:6*num_pks],[num_pks,5]) - + p_fit=np.reshape(p[:6*num_pks],[num_pks,6]) + for ii in np.arange(num_pks): if pktype == 'gaussian': f=f+_gaussian1d_no_bg(p_fit[ii],x) @@ -606,10 +606,10 @@ def _mpeak_1d_no_bg(p,x,pktype,num_pks): f=f+_pvoigt1d_no_bg(p_fit[ii],x) elif pktype == 'split_pvoigt': f=f+_split_pvoigt1d_no_bg(p_fit[ii],x) - + return f - -def mpeak_1d(p,x,pktype,num_pks,bgtype=None): + +def mpeak_1d(p,x,pktype,num_pks,bgtype=None): """ Required Arguments: p -- (m x u) list of peak parameters for number of peaks (m is the number of @@ -628,18 +628,14 @@ def mpeak_1d(p,x,pktype,num_pks,bgtype=None): """ - + f=_mpeak_1d_no_bg(p,x,pktype,num_pks) - - if bgtype=='linear': + + if bgtype=='linear': f=f+p[-2]+p[-1]*x #c0=p[-2], c1=p[-1] elif bgtype=='constant': - f=f+p[-1] #c0=p[-1] + f=f+p[-1] #c0=p[-1] elif bgtype=='quadratic': - f=f+p[-3]+p[-2]*x+p[-1]*x**2 #c0=p[-3], c1=p[-2], c2=p[-1], - - return f - - - + f=f+p[-3]+p[-2]*x+p[-1]*x**2 #c0=p[-3], c1=p[-2], c2=p[-1], + return f From 36507538ae0ae25c36ed87a464ca6e62b846b073 Mon Sep 17 00:00:00 2001 From: "Joel V. Bernier" Date: Tue, 13 Nov 2018 11:51:01 -0800 Subject: [PATCH 173/253] fixed non-numba ValueError in distortion --- hexrd/xrd/distortion.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hexrd/xrd/distortion.py b/hexrd/xrd/distortion.py index b1b16dcd..47f0766b 100644 --- a/hexrd/xrd/distortion.py +++ b/hexrd/xrd/distortion.py @@ -141,11 +141,12 @@ def _ge_41rt_distortion(out, in_, rhoMax, params): rxi = 1.0/rhoMax xi, yi = in_[:, 0], in_[:,1] + + # !!! included fix on ValueError for array--like in_ ri = np.sqrt(xi*xi + yi*yi) - if ri < sqrt_epsf: - ri_inv = 0.0 - else: - ri_inv = 1.0/ri + ri[ri < sqrt_epsf] = np.inf + ri_inv = 1.0/ri + sinni = yi*ri_inv cosni = xi*ri_inv cos2ni = cosni*cosni - sinni*sinni From ad2282f8dc2ad046d2c62e00ac79fb9a17e9181c Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 14 Nov 2018 12:12:12 -0800 Subject: [PATCH 174/253] switch imageio to skimage.io --- hexrd/grainmap/nfutil.py | 8 +++++--- hexrd/grainmap/tomoutil.py | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/hexrd/grainmap/nfutil.py b/hexrd/grainmap/nfutil.py index 7df9901a..9d8a3156 100644 --- a/hexrd/grainmap/nfutil.py +++ b/hexrd/grainmap/nfutil.py @@ -32,8 +32,10 @@ import cPickle as cpl import scipy.ndimage as img -import imageio as imgio - +try: + import imageio as imgio +except(ImportError): + from skimage import io as imgio import matplotlib.pyplot as plt # ============================================================================== @@ -1146,4 +1148,4 @@ def plot_ori_map(grain_map, confidence_map, exp_maps, layer_no,id_remap=None): plt.hold(True) plt.imshow(conf_plot,vmin=0.0,vmax=1.,interpolation='none',cmap=plt.cm.gray,alpha=0.5) - \ No newline at end of file + diff --git a/hexrd/grainmap/tomoutil.py b/hexrd/grainmap/tomoutil.py index 29c71b66..326ce707 100644 --- a/hexrd/grainmap/tomoutil.py +++ b/hexrd/grainmap/tomoutil.py @@ -4,7 +4,10 @@ import scipy as sp import scipy.ndimage as img -import imageio as imgio +try: + import imageio as imgio +except(ImportError): + from skimage import io as imgio import skimage.transform as xformimg From 7546e9cb8fb9e6d46998aba08525b639c9b22220 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 14 Nov 2018 12:13:26 -0800 Subject: [PATCH 175/253] removed imageio dep in favor of skimage --- conda.recipe/meta.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index befdd265..4ef0292f 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -25,9 +25,7 @@ requirements: - python - setuptools run: - #- fabio - h5py - - imageio - matplotlib - numba - numpy From 4778931b5b0a0f66361e4bfc5069b5539a31f42b Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 29 Nov 2018 22:00:45 -0800 Subject: [PATCH 176/253] check on data dimension --- hexrd/imageseries/load/array.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/load/array.py b/hexrd/imageseries/load/array.py index 4ce73a47..76074aed 100644 --- a/hexrd/imageseries/load/array.py +++ b/hexrd/imageseries/load/array.py @@ -3,6 +3,9 @@ from . import ImageSeriesAdapter from ..imageseriesiter import ImageSeriesIterator +import numpy as np + + class ArrayImageSeriesAdapter(ImageSeriesAdapter): """collection of images in numpy array""" @@ -16,7 +19,17 @@ def __init__(self, fname, **kwargs): . 'data' = a 3D array (double/float) . 'metadata' = a dictionary """ - self._data = kwargs['data'] + data_arr = np.array(kwargs['data']) + if data_arr.ndim < 3: + self._data = np.tile(data_arr, (1, 1, 1)) + elif data_arr.ndim == 3: + self._data = data_arr + else: + raise RuntimeError( + 'input array must be 2-d or 3-d; you provided ndim=%d' + % data_arr.ndim + ) + self._meta = kwargs.pop('meta', dict()) self._shape = self._data.shape self._nframes = self._shape[0] @@ -44,7 +57,7 @@ def __getitem__(self, key): def __iter__(self): return ImageSeriesIterator(self) - #@memoize + # @memoize def __len__(self): return self._nframes From 1b894b0a1b9cb48ea0b3f38bec405f3f6e0fb415 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 2 Jan 2019 17:12:38 -0800 Subject: [PATCH 177/253] update build docs and example --- docs/build.rst | 34 +++++++++++++++++++++++++++++++-- hexrd/xrd/experiment.py | 1 - share/example_calibration.yml | 36 ++++++++++++++++++----------------- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/docs/build.rst b/docs/build.rst index c249e20b..d7fbcc5c 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -19,13 +19,43 @@ environments, you should be able to run:: Building -------- -The procedure for building/installing is as follows +First, the dependencies for building an environment to run hexrd:: + + - cython + - dask + - distributed + - fabio + - h5py + - matplotlib + - numba + - numpy + - progressbar >=2.3 + - python + - pyyaml + - scikit-image + - scikit-learn + - scipy + - wxpython ==3 + +If you will be running scripts of you own, I also strongly suggest adding spyder:: + + - spyder + +For example, to buid an environment to run hexrd v0.5.x, do the following:: + + conda create --name hexrd_0.5 cython dask distributed h5py matplotlib numba numpy=1.15 progressbar=2.3 python=2.7 pyyaml scikit-image scikit-learn scipy spyder wxpython=3 + +Then install in develop mode using disutils:: + + python setup.py develop + +The procedure for building/installing with conda-build is as follows First, update conda and conda-build:: conda update conda conda update conda-build - + Second, using ``conda-build``, purge previous builds (recommended, not strictly required):: diff --git a/hexrd/xrd/experiment.py b/hexrd/xrd/experiment.py index 9f98e04e..7b165606 100644 --- a/hexrd/xrd/experiment.py +++ b/hexrd/xrd/experiment.py @@ -883,7 +883,6 @@ def readImage(self, frameNum=1): # if (frameNum == self.__curFrame): return # NOTE: instantiate new reader even when requested frame is current # frame because reader properties may have changed - if haveReader and (frameNum > self.__curFrame): nskip = frameNum - self.__curFrame - 1 self.__active_img = self.__active_reader.read(nframes=rdFrames, diff --git a/share/example_calibration.yml b/share/example_calibration.yml index 0c3f6ae2..30ef1db8 100644 --- a/share/example_calibration.yml +++ b/share/example_calibration.yml @@ -1,23 +1,25 @@ +beam: + energy: 71.6760000000000 + vector: {azimuth: 90.0, polar_angle: 90.0} calibration_crystal: grain_id: 0 - inv_stretch: [0.9999910690173981, 0.9999801645520292, 1.0000532478330082, -7.6206360506652305e-06, - 2.3167627263591607e-06, -5.3587098260888435e-06] + inv_stretch: [1.0, 1.0, 1.0, 0.0, 0.0, 0.0] orientation: [0.6691581915988989, -0.9864605537384611, 0.7367040542122328] - position: [6.724776361571607e-05, 1.576112881919417e-05, 0.002666490586467067] -detector: - distortion: - function_name: GE_41RT - parameters: [-1.19340383e-05, -9.11913398e-05, -0.000511540815, 2.0, 2.0, 2.0] - id: GE - pixels: - columns: 2048 - rows: 2048 - size: [0.2, 0.2] - saturation_level: 14000.0 - transform: - t_vec_d: [-1.4481037291976095, -3.233712371397272, -1050.649899464198] - tilt_angles: [0.0005634301421575493, -0.003161010511305945, -0.00230772467063694] + position: [6.724776361571607e-05, 0.0, 0.002666490586467067] +detectors: + ge3: + distortion: + function_name: GE_41RT + parameters: [-1.19340383e-05, -9.11913398e-05, -0.000511540815, 2.0, 2.0, 2.0] + pixels: + columns: 2048 + rows: 2048 + size: [0.2, 0.2] + saturation_level: 14000.0 + transform: + t_vec_d: [-9.42714847e-01, -6.56494503e-01, -9.53912093e+02] + tilt_angles: [6.87105395e-03, 4.28568797e-04, 0.00000000e+00] oscillation_stage: - chi: -0.0009114106199393461 + chi: 0.0 t_vec_s: [0.0, 0.0, 0.0] From 9fedef94be3d4286881be42b62534e6958e3f687 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 23 Jan 2019 14:49:27 -0800 Subject: [PATCH 178/253] more cleanup for new tilt/cfg spec --- hexrd/instrument.py | 20 +++++++++++--------- hexrd/xrd/xrdutil.py | 26 +++++++++++++------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 4ea4320a..03af8d27 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -248,8 +248,8 @@ def __init__(self, instrument_config=None, PlanarDetector( rows=pix['rows'], cols=pix['columns'], pixel_size=pix['size'], - tvec=xform['t_vec_d'], - tilt=xform['tilt_angles'], + tvec=xform['translation'], + tilt=xform['tilt'], bvec=self._beam_vector, evec=ct.eta_vec, distortion=dist_list) @@ -258,7 +258,7 @@ def __init__(self, instrument_config=None, self._detectors = dict(zip(detector_ids, det_list)) self._tvec = np.r_[ - instrument_config['oscillation_stage']['t_vec_s'] + instrument_config['oscillation_stage']['translation'] ] self._chi = instrument_config['oscillation_stage']['chi'] @@ -400,6 +400,8 @@ def write_config(self, filename=None, calibration_dict={}): par_dict = {} + par_dict['id'] = self.id + azim, pola = calc_angles_from_beam_vec(self.beam_vector) beam = dict( energy=self.beam_energy, @@ -415,7 +417,7 @@ def write_config(self, filename=None, calibration_dict={}): ostage = dict( chi=self.chi, - t_vec_s=self.tvec.tolist() + translation=self.tvec.tolist() ) par_dict['oscillation_stage'] = ostage @@ -1342,8 +1344,8 @@ def config_dict(self, chi, t_vec_s, sat_level=None): d = dict( detector=dict( transform=dict( - tilt_angles=self.tilt.tolist(), - t_vec_d=self.tvec.tolist(), + tilt=self.tilt.tolist(), + translation=self.tvec.tolist(), ), pixels=dict( rows=self.rows, @@ -1353,7 +1355,7 @@ def config_dict(self, chi, t_vec_s, sat_level=None): ), oscillation_stage=dict( chi=chi, - t_vec_s=t_vec_s.tolist(), + translation=t_vec_s.tolist(), ), ) @@ -1647,7 +1649,7 @@ def make_powder_rings( ) angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] - + # need xy coords and pixel sizes valid_ang = [] @@ -1681,7 +1683,7 @@ def make_powder_rings( # all vertices must be on... patch_is_on = np.all(on_panel.reshape(neta, npp), axis=1) patch_xys = all_xy.reshape(neta, 5, 2)[patch_is_on] - + idx = np.where(patch_is_on)[0] diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 69adb30d..de1a3f3a 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -1797,10 +1797,10 @@ def __init__(self, image_series, instrument_params, planeData, active_hkls, # stack parameters detector_params = num.hstack([ - instrument_params['detector']['transform']['tilt_angles'], - instrument_params['detector']['transform']['t_vec_d'], + instrument_params['detector']['transform']['tilt'], + instrument_params['detector']['transform']['translation'], instrument_params['oscillation_stage']['chi'], - instrument_params['oscillation_stage']['t_vec_s'], + instrument_params['oscillation_stage']['translation'], ]) pixel_pitch = instrument_params['detector']['pixels']['size'] @@ -2112,10 +2112,10 @@ def __init__(self, ome_eta_archive): # not ready # ) # not ready # # stack parameters # not ready # detector_params = num.hstack([ -# not ready # instr_cfg['detector']['transform']['tilt_angles'], -# not ready # instr_cfg['detector']['transform']['t_vec_d'], +# not ready # instr_cfg['detector']['transform']['tilt'], +# not ready # instr_cfg['detector']['transform']['translation'], # not ready # instr_cfg['oscillation_stage']['chi'], -# not ready # instr_cfg['oscillation_stage']['t_vec_s'], +# not ready # instr_cfg['oscillation_stage']['translation'], # not ready # ]) # not ready # pixel_pitch = instr_cfg['detector']['pixels']['size'] # not ready # chi = self.instr_cfg['oscillation_stage']['chi'] # in DEGREES @@ -4056,10 +4056,10 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, npts = len(tth_eta) # detector frame - rMat_d = xfcapi.makeDetectorRotMat( - instr_cfg['detector']['transform']['tilt_angles'] + rMat_d = xfcapi.makeRotMatOfExpMap( + num.r_[instr_cfg['detector']['transform']['tilt']] ) - tVec_d = num.r_[instr_cfg['detector']['transform']['t_vec_d']] + tVec_d = num.r_[instr_cfg['detector']['transform']['translation']] pixel_size = instr_cfg['detector']['pixels']['size'] frame_nrows = instr_cfg['detector']['pixels']['rows'] @@ -4076,7 +4076,7 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, # sample frame chi = instr_cfg['oscillation_stage']['chi'] - tVec_s = num.r_[instr_cfg['oscillation_stage']['t_vec_s']] + tVec_s = num.r_[instr_cfg['oscillation_stage']['translation']] # beam vector if beamVec is None: @@ -4679,11 +4679,11 @@ def extract_detector_transformation(detector_params): """ # extract variables for convenience if isinstance(detector_params, dict): rMat_d = xfcapi.makeDetectorRotMat( - detector_params['detector']['transform']['tilt_angles'] + detector_params['detector']['transform']['tilt'] ) - tVec_d = num.r_[detector_params['detector']['transform']['t_vec_d']] + tVec_d = num.r_[detector_params['detector']['transform']['translation']] chi = detector_params['oscillation_stage']['chi'] - tVec_s = num.r_[detector_params['oscillation_stage']['t_vec_s']] + tVec_s = num.r_[detector_params['oscillation_stage']['translation']] else: assert len(detector_params >= 10), \ "list of detector parameters must have length >= 10" From 2c90d1babde62e242480876524b4ef0962b16613 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Thu, 7 Feb 2019 20:32:21 -0600 Subject: [PATCH 179/253] increase stats buffer, frame-cache updates --- hexrd/imageseries/save.py | 74 +++++++++++++++++++++++++++++++++----- hexrd/imageseries/stats.py | 2 +- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 50b5a564..1fa71439 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -5,9 +5,12 @@ import warnings import numpy as np + import h5py import yaml +MAX_NZ_FRACTION = 0.1 # 10% sparsity trigger for frame-cache write + def write(ims, fname, fmt, **kwargs): """write imageseries to file with options @@ -192,22 +195,25 @@ def _write_frames(self): """also save shape array as originally done (before yaml)""" arrd = dict() for i in range(len(self._ims)): - # RFE: make it so we can use emumerate on self._ims??? + # ???: make it so we can use emumerate on self._ims? + # FIXME: in __init__() of ProcessedImageSeries: + # 'ProcessedImageSeries' object has no attribute '_adapter' frame = self._ims[i] mask = frame > self._thresh - # FIXME: formalize this a little better??? - # -- maybe set a hard limit of total nonzeros for the imageseries - # -- could pass as a kwarg on open + # FIXME: formalize this a little better + # ???: maybe set a hard limit of total nonzeros for the imageseries + # ???: could pass as a kwarg on open fullness = np.sum(mask) / float(frame.shape[0]*frame.shape[1]) - if fullness > 0.05: - sparseness = 100.*(1 -fullness) - msg = "frame %d is %4.2f%% sparse (cutoff is 95%%)" % (i, sparseness) + if fullness > MAX_NZ_FRACTION: + sparseness = 100.*(1 - fullness) + msg = "frame %d is %4.2f%% sparse (cutoff is 95%%)" \ + % (i, sparseness) warnings.warn(msg) - row, col = mask.nonzero() arrd['%d_row' % i] = row arrd['%d_col' % i] = col arrd['%d_data' % i] = frame[mask] + pass arrd['shape'] = self._ims.shape arrd['nframes'] = len(self._ims) arrd['dtype'] = str(self._ims.dtype) @@ -222,3 +228,55 @@ def write(self, output_yaml=False): self._write_frames() if output_yaml: self._write_yml() + + +""" +# ============================================================================= +# Numba-fied frame cache writer +# ============================================================================= + + +if USE_NUMBA: + @numba.njit + def extract_ijv(in_array, threshold, out_i, out_j, out_v): + n = 0 + w, h = in_array.shape + + for i in range(w): + for j in range(h): + v = in_array[i, j] + if v > threshold: + out_v[n] = v + out_i[n] = i + out_j[n] = j + n += 1 + + return n + + class CooMatrixBuilder(object): + def __init__(self, shape, dtype=np.uint16): + self._shape = shape + self._dtype = dtype + self._size = self._shape[0]*self._shape[1] + self.v_buff = np.empty(self._size, dtype=self._dtype) + self.i_buff = np.empty(self._size, dtype=self._dtype) + self.j_buff = np.empty(self._size, dtype=self._dtype) + + def build_matrix(self, frame, threshold): + count = extract_ijv(frame, threshold, + self.i_buff, self.j_buff, self.v_buff) + return coo_matrix((self.v_buff[0:count].copy(), + (self.i_buff[0:count].copy(), + self.j_buff[0:count].copy())), + shape=frame.shape) +else: # not USE_NUMBA + class CooMatrixBuilder(object): + def __init__(self, shape, dtype=np.uint16): + self._shape = shape + self._dtype = dtype + + def build_matrix(self, frame, threshold): + mask = frame > threshold + return coo_matrix((frame[mask], mask.nonzero()), + shape=frame.shape) +""" diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index 3f81a0a7..415619af 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -7,7 +7,7 @@ from hexrd.imageseries.process import ProcessedImageSeries as PIS # Default Buffer: 100 MB -STATS_BUFFER = 1.e8 +STATS_BUFFER = 419430400 # 50 GE frames def max(ims, nframes=0): nf = _nframes(ims, nframes) From cefa048ba4042e309adcc4c01c7e63a238d7a3c7 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Fri, 8 Feb 2019 11:50:05 -0600 Subject: [PATCH 180/253] added numba helper for frame cache write --- hexrd/cacheframes.py | 4 +-- hexrd/imageseries/save.py | 61 +++++++++++++++------------------------ 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/hexrd/cacheframes.py b/hexrd/cacheframes.py index df15344e..69d6b0c4 100644 --- a/hexrd/cacheframes.py +++ b/hexrd/cacheframes.py @@ -92,6 +92,7 @@ def load_frames(reader, cfg, show_progress=False): return reader def cache_frames(reader, cfg, show_progress=False, overwrite=True): + start = time.time() cache_file = os.path.join(cfg.analysis_dir, 'frame_cache.npz') # load the data reader = load_frames(reader, cfg, show_progress) @@ -103,7 +104,6 @@ def cache_frames(reader, cfg, show_progress=False, overwrite=True): arrs['%d_data' % i] = coo.data arrs['%d_row' % i] = coo.row arrs['%d_col' % i] = coo.col - start = time.time() np.savez_compressed(cache_file, **arrs) elapsed = time.time()-start logger.info('wrote %d frames to cache in %g seconds', len(reader[0]), elapsed) @@ -123,7 +123,7 @@ def get_frames(reader, cfg, show_progress=False, force=False, clean=False): # temporary catch if reader is None; i.e. raw data not here but cache is # ...NEED TO FIX THIS WHEN AXING OLD READER CLASS! - # the stop is treated as total number of frames read, which is inconsistent with + # the stop is treated as total number of frames read, which is inconsistent with # how the start value is used, which specifies empty frames to skip at the start # of each image. What a fucking mess! if reader is not None: diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 1fa71439..05e33c35 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -4,7 +4,10 @@ import os import warnings +from hexrd import USE_NUMBA + import numpy as np +import numba import h5py import yaml @@ -193,26 +196,26 @@ def _write_yml(self): def _write_frames(self): """also save shape array as originally done (before yaml)""" + buff_size = self._ims.shape[0]*self._ims.shape[1] arrd = dict() for i in range(len(self._ims)): # ???: make it so we can use emumerate on self._ims? # FIXME: in __init__() of ProcessedImageSeries: # 'ProcessedImageSeries' object has no attribute '_adapter' - frame = self._ims[i] - mask = frame > self._thresh + rows, cols, vals = extract_ijv(self._ims[i], self._thresh) + count = len(vals) # FIXME: formalize this a little better # ???: maybe set a hard limit of total nonzeros for the imageseries # ???: could pass as a kwarg on open - fullness = np.sum(mask) / float(frame.shape[0]*frame.shape[1]) + fullness = count / float(buff_size) if fullness > MAX_NZ_FRACTION: sparseness = 100.*(1 - fullness) msg = "frame %d is %4.2f%% sparse (cutoff is 95%%)" \ % (i, sparseness) warnings.warn(msg) - row, col = mask.nonzero() - arrd['%d_row' % i] = row - arrd['%d_col' % i] = col - arrd['%d_data' % i] = frame[mask] + arrd['%d_row' % i] = rows + arrd['%d_col' % i] = cols + arrd['%d_data' % i] = vals pass arrd['shape'] = self._ims.shape arrd['nframes'] = len(self._ims) @@ -230,7 +233,6 @@ def write(self, output_yaml=False): self._write_yml() -""" # ============================================================================= # Numba-fied frame cache writer # ============================================================================= @@ -238,10 +240,9 @@ def write(self, output_yaml=False): if USE_NUMBA: @numba.njit - def extract_ijv(in_array, threshold, out_i, out_j, out_v): + def _extract_ijv(in_array, threshold, out_i, out_j, out_v): n = 0 w, h = in_array.shape - for i in range(w): for j in range(h): v = in_array[i, j] @@ -250,33 +251,19 @@ def extract_ijv(in_array, threshold, out_i, out_j, out_v): out_i[n] = i out_j[n] = j n += 1 - return n - class CooMatrixBuilder(object): - def __init__(self, shape, dtype=np.uint16): - self._shape = shape - self._dtype = dtype - self._size = self._shape[0]*self._shape[1] - self.v_buff = np.empty(self._size, dtype=self._dtype) - self.i_buff = np.empty(self._size, dtype=self._dtype) - self.j_buff = np.empty(self._size, dtype=self._dtype) - - def build_matrix(self, frame, threshold): - count = extract_ijv(frame, threshold, - self.i_buff, self.j_buff, self.v_buff) - return coo_matrix((self.v_buff[0:count].copy(), - (self.i_buff[0:count].copy(), - self.j_buff[0:count].copy())), - shape=frame.shape) + def extract_ijv(in_array, threshold): + assert in_array.ndim == 2, "input array must be 2-d" + buff_size = in_array.shape[0]*in_array.shape[1] + i = np.empty(buff_size, dtype=np.int) + j = np.empty(buff_size, dtype=np.int) + v = np.empty(buff_size, dtype=np.int) + count = _extract_ijv(in_array, threshold, i, j, v) + return i[:count], j[:count], v[:count] else: # not USE_NUMBA - class CooMatrixBuilder(object): - def __init__(self, shape, dtype=np.uint16): - self._shape = shape - self._dtype = dtype - - def build_matrix(self, frame, threshold): - mask = frame > threshold - return coo_matrix((frame[mask], mask.nonzero()), - shape=frame.shape) -""" + def extract_ijv(in_array, threshold): + mask = in_array > threshold + i, j = mask.nonzero() + v = in_array[mask] + return i, j, v From 59c6b6a4b4047280d8f1f80b2384470986b1e34b Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Fri, 8 Feb 2019 11:53:42 -0600 Subject: [PATCH 181/253] smaller dtype for index vectors --- hexrd/imageseries/save.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index 05e33c35..da97a97c 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -256,8 +256,8 @@ def _extract_ijv(in_array, threshold, out_i, out_j, out_v): def extract_ijv(in_array, threshold): assert in_array.ndim == 2, "input array must be 2-d" buff_size = in_array.shape[0]*in_array.shape[1] - i = np.empty(buff_size, dtype=np.int) - j = np.empty(buff_size, dtype=np.int) + i = np.empty(buff_size, dtype=np.uint16) + j = np.empty(buff_size, dtype=np.uint16) v = np.empty(buff_size, dtype=np.int) count = _extract_ijv(in_array, threshold, i, j, v) return i[:count], j[:count], v[:count] From 248d2febaf182b5dea36d523532879ff9d299694 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Fri, 8 Feb 2019 16:30:30 -0600 Subject: [PATCH 182/253] cleanup on frame-cache --- hexrd/imageseries/process.py | 21 ++++++++++-------- hexrd/imageseries/save.py | 42 +++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/hexrd/imageseries/process.py b/hexrd/imageseries/process.py index d58067c1..02173b83 100644 --- a/hexrd/imageseries/process.py +++ b/hexrd/imageseries/process.py @@ -5,6 +5,7 @@ from .baseclass import ImageSeries + class ProcessedImageSeries(ImageSeries): """Images series with mapping applied to frames""" FLIP = 'flip' @@ -55,24 +56,25 @@ def _process_frame(self, key): def _subtract_dark(self, img, dark): # need to check for values below zero - return np.where(img > dark, img-dark, 0) + # !!! careful, truncation going on here;necessary to promote dtype? + return np.where(img > dark, img - dark, 0) def _rectangle(self, img, r): # restrict to rectangle - return img[r[0,0]:r[0,1], r[1,0]:r[1,1]] + return img[r[0, 0]:r[0, 1], r[1, 0]:r[1, 1]] def _flip(self, img, flip): - if flip in ('y','v'): # about y-axis (vertical) + if flip in ('y', 'v'): # about y-axis (vertical) pimg = img[:, ::-1] - elif flip in ('x', 'h'): # about x-axis (horizontal) + elif flip in ('x', 'h'): # about x-axis (horizontal) pimg = img[::-1, :] - elif flip in ('vh', 'hv', 'r180'): # 180 degree rotation + elif flip in ('vh', 'hv', 'r180'): # 180 degree rotation pimg = img[::-1, ::-1] - elif flip in ('t', 'T'): # transpose (possible shape change) + elif flip in ('t', 'T'): # transpose (possible shape change) pimg = img.T - elif flip in ('ccw90', 'r90'): # rotate 90 (possible shape change) + elif flip in ('ccw90', 'r90'): # rotate 90 (possible shape change) pimg = img.T[::-1, :] - elif flip in ('cw90', 'r270'): # rotate 270 (possible shape change) + elif flip in ('cw90', 'r270'): # rotate 270 (possible shape change) pimg = img.T[:, ::-1] else: pimg = img @@ -81,6 +83,7 @@ def _flip(self, img, flip): # # ==================== API # + @property def dtype(self): return self[0].dtype @@ -108,4 +111,4 @@ def oplist(self): """list of operations to apply""" return self._oplist - pass # end class + pass # end class diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index da97a97c..d75ce704 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -197,13 +197,21 @@ def _write_yml(self): def _write_frames(self): """also save shape array as originally done (before yaml)""" buff_size = self._ims.shape[0]*self._ims.shape[1] + rows = np.empty(buff_size, dtype=np.uint16) + cols = np.empty(buff_size, dtype=np.uint16) + vals = np.empty(buff_size, dtype=self._ims.dtype) arrd = dict() for i in range(len(self._ims)): # ???: make it so we can use emumerate on self._ims? # FIXME: in __init__() of ProcessedImageSeries: # 'ProcessedImageSeries' object has no attribute '_adapter' - rows, cols, vals = extract_ijv(self._ims[i], self._thresh) - count = len(vals) + + # wrapper to find (sparse) pixels above threshold + count = extract_ijv(self._ims[i], self._thresh, + rows, cols, vals) + + # check the sparsity + # # FIXME: formalize this a little better # ???: maybe set a hard limit of total nonzeros for the imageseries # ???: could pass as a kwarg on open @@ -213,9 +221,9 @@ def _write_frames(self): msg = "frame %d is %4.2f%% sparse (cutoff is 95%%)" \ % (i, sparseness) warnings.warn(msg) - arrd['%d_row' % i] = rows - arrd['%d_col' % i] = cols - arrd['%d_data' % i] = vals + arrd['%d_row' % i] = rows[:count].copy() + arrd['%d_col' % i] = cols[:count].copy() + arrd['%d_data' % i] = vals[:count].copy() pass arrd['shape'] = self._ims.shape arrd['nframes'] = len(self._ims) @@ -240,30 +248,24 @@ def write(self, output_yaml=False): if USE_NUMBA: @numba.njit - def _extract_ijv(in_array, threshold, out_i, out_j, out_v): + def extract_ijv(in_array, threshold, out_i, out_j, out_v): n = 0 w, h = in_array.shape for i in range(w): for j in range(h): v = in_array[i, j] if v > threshold: - out_v[n] = v out_i[n] = i out_j[n] = j + out_v[n] = v n += 1 return n - - def extract_ijv(in_array, threshold): - assert in_array.ndim == 2, "input array must be 2-d" - buff_size = in_array.shape[0]*in_array.shape[1] - i = np.empty(buff_size, dtype=np.uint16) - j = np.empty(buff_size, dtype=np.uint16) - v = np.empty(buff_size, dtype=np.int) - count = _extract_ijv(in_array, threshold, i, j, v) - return i[:count], j[:count], v[:count] else: # not USE_NUMBA - def extract_ijv(in_array, threshold): + def extract_ijv(in_array, threshold, out_i, out_j, out_v): mask = in_array > threshold - i, j = mask.nonzero() - v = in_array[mask] - return i, j, v + n = sum(mask) + tmp_i, tmp_j = mask.nonzero() + out_i[:n] = tmp_i + out_j[:n] = tmp_j + out_v[:n] = in_array[mask] + return n From 91d3c085c8659b52d1a2aecaea29db48713560d2 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Fri, 8 Feb 2019 17:30:39 -0600 Subject: [PATCH 183/253] playing with STATS_BUFFER in imageseries --- hexrd/imageseries/stats.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index 415619af..4c1e394f 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -7,7 +7,8 @@ from hexrd.imageseries.process import ProcessedImageSeries as PIS # Default Buffer: 100 MB -STATS_BUFFER = 419430400 # 50 GE frames +#STATS_BUFFER = 419430400 # 50 GE frames +STATS_BUFFER = 838860800 # 100 GE frames def max(ims, nframes=0): nf = _nframes(ims, nframes) From d23f2221a6b6c8ab5f2b1ff1dd5ace95dcb09c11 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Sat, 9 Feb 2019 16:24:23 -0600 Subject: [PATCH 184/253] speedup to histogramming in eta-ome map generation --- hexrd/instrument.py | 96 +++++++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 8b142b92..cd4a7dcc 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -451,12 +451,14 @@ def extract_polar_maps(self, plane_data, imgser_dict, # native_area = panel.pixel_area # pixel ref area # make rings clipped to panel - pow_angs, pow_xys, eta_idx, full_etas = panel.make_powder_rings( + # !!! eta_idx has the same length as plane_data.exclusions + # !!! eta_edges is the list of eta bin EDGES + pow_angs, pow_xys, eta_idx, eta_edges = panel.make_powder_rings( plane_data, merge_hkls=False, delta_eta=eta_tol, full_output=True) - ptth, peta = panel.pixel_angles + ptth, peta = panel.pixel_angles() ring_maps = [] for i_r, tthr in enumerate(tth_ranges): print("working on ring %d..." % i_r) @@ -465,6 +467,7 @@ def extract_polar_maps(self, plane_data, imgser_dict, ) etas = pow_angs[i_r][:, 1] netas = len(etas) + """ eta_ranges = np.tile(etas, (2, 1)).T \ + np.tile(eta_tol_vec, (netas, 1)) ring_map = [] @@ -479,16 +482,23 @@ def extract_polar_maps(self, plane_data, imgser_dict, rtth_idx[1][reta_idx]) ring_map.append(ijs) pass - + """ try: omegas = imgser_dict[det_key].metadata['omega'] except(KeyError): msg = "imageseries for '%s' has no omega info" % det_key raise RuntimeError(msg) + + # initialize maps and assing by row (omega/frame) nrows_ome = len(omegas) - ncols_eta = len(full_etas) + ncols_eta = len(eta_edges - 1) this_map = np.nan*np.ones((nrows_ome, ncols_eta)) for i_row, image in enumerate(imgser_dict[det_key]): + intensity_weights = image[rtth_idx] + ring_etas = peta[rtth_idx] + this_map[i_row, eta_idx[i_r]], _ = np.histogram( + ring_etas, bins=eta_edges, weights=intensity_weights) + """ psum = np.zeros(len(ring_map)) for i_k, k in enumerate(ring_map): pdata = image[k[0], k[1]] @@ -499,10 +509,11 @@ def extract_polar_maps(self, plane_data, imgser_dict, # this_map[i_row, eta_idx[i_r]] = [ # np.sum(image[k[0], k[1]]) for k in ring_map # ] + """ ring_maps.append(this_map) pass ring_maps_panel[det_key] = ring_maps - return ring_maps_panel, full_etas + return ring_maps_panel, eta_edges def extract_line_positions(self, plane_data, imgser_dict, tth_tol=None, eta_tol=1., npdiv=2, @@ -1300,23 +1311,9 @@ def pixel_coords(self): indexing='ij') return pix_i, pix_j - # ...memoize??? - @property - def pixel_angles(self): - pix_i, pix_j = self.pixel_coords - xy = np.ascontiguousarray( - np.vstack([ - pix_j.flatten(), pix_i.flatten() - ]).T - ) - angs, g_vec = detectorXYToGvec( - xy, self.rmat, ct.identity_3x3, - self.tvec, ct.zeros_3, ct.zeros_3, - beamVec=self.bvec, etaVec=self.evec) - del(g_vec) - tth = angs[0].reshape(self.rows, self.cols) - eta = angs[1].reshape(self.rows, self.cols) - return tth, eta + """ + ##################### METHODS + """ def config_dict(self, chi, t_vec_s, sat_level=None): """ @@ -1356,9 +1353,23 @@ def config_dict(self, chi, t_vec_s, sat_level=None): d['detector']['distortion'] = dist_d return d - """ - ##################### METHODS - """ + def pixel_angles(self, origin=ct.zeros_3): + assert len(origin) == 3, "origin must have 3 elemnts" + pix_i, pix_j = self.pixel_coords + xy = np.ascontiguousarray( + np.vstack([ + pix_j.flatten(), pix_i.flatten() + ]).T + ) + angs, g_vec = detectorXYToGvec( + xy, self.rmat, ct.identity_3x3, + self.tvec, ct.zeros_3, origin, + beamVec=self.bvec, etaVec=self.evec) + del(g_vec) + tth = angs[0].reshape(self.rows, self.cols) + eta = angs[1].reshape(self.rows, self.cols) + return tth, eta + def cartToPixel(self, xy_det, pixels=False): """ Convert vstacked array or list of [x,y] points in the center-based @@ -1622,19 +1633,35 @@ def make_powder_rings( 0.0, 0.0] for i in tth_pm]) - # for generating rings + # for generating rings, make eta vector in correct period if eta_period is None: eta_period = (-np.pi, np.pi) - neta = int(360./float(delta_eta)) - eta = mapAngle( - #np.radians(delta_eta*(np.linspace(0., neta - 1, num=neta) + 0.5)) + #dp change, don't like 0.5 - np.radians(delta_eta*(np.linspace(0., neta - 1, num=neta))) + - eta_period[0], eta_period + + # this is the vector of ETA EDGES + eta_edges = mapAngle( + np.radians( + delta_eta*np.linspace(0., neta, num=neta + 1) + ) + eta_period[0], + eta_period ) - angs = [np.vstack([i*np.ones(neta), eta, np.zeros(neta)]) for i in tth] + # get eta bin centers from edges + """ + # !!! this way is probably overkill, since we have delta eta + eta_centers = np.average( + np.vstack([eta[:-1], eta[1:]), + axis=0) + """ + # !!! should be safe as eta_edges are monotonic + eta_centers = eta_edges[:-1] + del_eta + # make list of angle tuples + angs = [ + np.vstack( + [i*np.ones(neta), eta_centers, np.zeros(neta)] + ) for i in tth + ] # need xy coords and pixel sizes valid_ang = [] @@ -1668,17 +1695,18 @@ def make_powder_rings( # all vertices must be on... patch_is_on = np.all(on_panel.reshape(neta, npp), axis=1) patch_xys = all_xy.reshape(neta, 5, 2)[patch_is_on] - + # the surving indices idx = np.where(patch_is_on)[0] + # form output arrays valid_ang.append(these_angs[patch_is_on, :2]) valid_xy.append(patch_xys[:, -1, :].squeeze()) map_indices.append(idx) pass # ??? is this option necessary? if full_output: - return valid_ang, valid_xy, map_indices, eta + return valid_ang, valid_xy, map_indices, eta_edges else: return valid_ang, valid_xy From 6d8721cca33bef1e192faf776a1b9b38d146b6b0 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Sun, 10 Feb 2019 00:09:42 -0600 Subject: [PATCH 185/253] more fixes to eta-ome map gen --- hexrd/instrument.py | 49 +++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index cd4a7dcc..11b66754 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -439,8 +439,8 @@ def extract_polar_maps(self, plane_data, imgser_dict, "active_hkls must be an iterable with __len__" tth_ranges = tth_ranges[active_hkls] - # need this for making eta ranges - eta_tol_vec = 0.5*np.radians([-eta_tol, eta_tol]) + # # need this for making eta ranges + # eta_tol_vec = 0.5*np.radians([-eta_tol, eta_tol]) ring_maps_panel = dict.fromkeys(self.detectors) for i_d, det_key in enumerate(self.detectors): @@ -465,24 +465,8 @@ def extract_polar_maps(self, plane_data, imgser_dict, rtth_idx = np.where( np.logical_and(ptth >= tthr[0], ptth <= tthr[1]) ) - etas = pow_angs[i_r][:, 1] - netas = len(etas) - """ - eta_ranges = np.tile(etas, (2, 1)).T \ - + np.tile(eta_tol_vec, (netas, 1)) - ring_map = [] - for i_e, etar in enumerate(eta_ranges): - # WARNING: assuming start/stop - emin = np.r_[etar[0]] - emax = np.r_[etar[1]] - reta_idx = np.where( - validateAngleRanges(peta[rtth_idx], emin, emax) - ) - ijs = (rtth_idx[0][reta_idx], - rtth_idx[1][reta_idx]) - ring_map.append(ijs) - pass - """ + + # grab omegas from imageseries and squawk if missing try: omegas = imgser_dict[det_key].metadata['omega'] except(KeyError): @@ -491,25 +475,20 @@ def extract_polar_maps(self, plane_data, imgser_dict, # initialize maps and assing by row (omega/frame) nrows_ome = len(omegas) - ncols_eta = len(eta_edges - 1) + ncols_eta = len(eta_edges) - 1 this_map = np.nan*np.ones((nrows_ome, ncols_eta)) + + # histogram intensities over eta ranges for i_row, image in enumerate(imgser_dict[det_key]): intensity_weights = image[rtth_idx] ring_etas = peta[rtth_idx] + eta_idx_r = np.hstack([eta_idx[i_r], eta_idx[i_r][-1] + 1]) this_map[i_row, eta_idx[i_r]], _ = np.histogram( - ring_etas, bins=eta_edges, weights=intensity_weights) - """ - psum = np.zeros(len(ring_map)) - for i_k, k in enumerate(ring_map): - pdata = image[k[0], k[1]] - if threshold: - pdata[pdata <= threshold] = 0 - psum[i_k] = np.average(pdata) - this_map[i_row, eta_idx[i_r]] = psum - # this_map[i_row, eta_idx[i_r]] = [ - # np.sum(image[k[0], k[1]]) for k in ring_map - # ] - """ + ring_etas, + bins=eta_edges[eta_idx_r], + weights=intensity_weights + ) + pass ring_maps.append(this_map) pass ring_maps_panel[det_key] = ring_maps @@ -1655,7 +1634,7 @@ def make_powder_rings( """ # !!! should be safe as eta_edges are monotonic eta_centers = eta_edges[:-1] + del_eta - + # make list of angle tuples angs = [ np.vstack( From 9a5c3d1362284ee5ae6bbf35c8c86a7a9b8ac4ab Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sun, 10 Feb 2019 19:10:29 -0600 Subject: [PATCH 186/253] roi and fake planeData edits --- hexrd/instrument.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 03af8d27..ca0be8af 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1183,7 +1183,8 @@ def roi(self, vertex_array): does NOT need to repeat start vertex for closure """ - assert len(vertex_array) >= 3 + if vertex_array is not None: + assert len(vertex_array) >= 3 self._roi = vertex_array @property @@ -1612,7 +1613,13 @@ def make_powder_rings( ) else: # Okay, we have a PlaneData object - pd = PlaneData.makeNew(pd) # make a copy to munge + try: + pd = PlaneData.makeNew(pd) # make a copy to munge + except(TypeError): + # !!! have some other object here, likely a dummy plane data + # object of some sort... + pass + if delta_tth is not None: pd.tThWidth = np.radians(delta_tth) else: From 1e06b89c400078103632996e6f5e8d3fb746b7a3 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 26 Feb 2019 16:08:36 -0800 Subject: [PATCH 187/253] some speedups via numba and fast_histogram --- hexrd/imageseries/save.py | 41 +++---------- hexrd/instrument.py | 126 +++++++++++++++++++++++++++++++------- hexrd/matrixutil.py | 36 +++++++++++ 3 files changed, 147 insertions(+), 56 deletions(-) diff --git a/hexrd/imageseries/save.py b/hexrd/imageseries/save.py index d75ce704..f6240063 100644 --- a/hexrd/imageseries/save.py +++ b/hexrd/imageseries/save.py @@ -4,17 +4,20 @@ import os import warnings -from hexrd import USE_NUMBA - import numpy as np -import numba - import h5py import yaml +from hexrd.matrixutil import extract_ijv + MAX_NZ_FRACTION = 0.1 # 10% sparsity trigger for frame-cache write +# ============================================================================= +# METHODS +# ============================================================================= + + def write(ims, fname, fmt, **kwargs): """write imageseries to file with options @@ -239,33 +242,3 @@ def write(self, output_yaml=False): self._write_frames() if output_yaml: self._write_yml() - - -# ============================================================================= -# Numba-fied frame cache writer -# ============================================================================= - - -if USE_NUMBA: - @numba.njit - def extract_ijv(in_array, threshold, out_i, out_j, out_v): - n = 0 - w, h = in_array.shape - for i in range(w): - for j in range(h): - v = in_array[i, j] - if v > threshold: - out_i[n] = i - out_j[n] = j - out_v[n] = v - n += 1 - return n -else: # not USE_NUMBA - def extract_ijv(in_array, threshold, out_i, out_j, out_v): - mask = in_array > threshold - n = sum(mask) - tmp_i, tmp_j = mask.nonzero() - out_i[:n] = tmp_i - out_j[:n] = tmp_j - out_v[:n] = in_array[mask] - return n diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 11b66754..8dcf629e 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -47,6 +47,7 @@ from hexrd import matrixutil as mutil from hexrd.valunits import valWUnit from hexrd.xrd.transforms_CAPI import anglesToGVec, \ + angularDifference, \ detectorXYToGvec, \ gvecToDetectorXY, \ makeDetectorRotMat, \ @@ -67,6 +68,13 @@ from skimage.draw import polygon +try: + from fast_histogram import histogram1d + fast_histogram = True +except(ImportError): + from numpy import histogram as histogram1d + fast_histogram = False + # ============================================================================= # PARAMETERS # ============================================================================= @@ -452,45 +460,110 @@ def extract_polar_maps(self, plane_data, imgser_dict, # make rings clipped to panel # !!! eta_idx has the same length as plane_data.exclusions + # each entry are the integer indices into the bins # !!! eta_edges is the list of eta bin EDGES pow_angs, pow_xys, eta_idx, eta_edges = panel.make_powder_rings( plane_data, merge_hkls=False, delta_eta=eta_tol, full_output=True) - + delta_eta = eta_edges[1] - eta_edges[0] + + # pixel angular coords for the detector panel ptth, peta = panel.pixel_angles() + + # grab omegas from imageseries and squawk if missing + try: + omegas = imgser_dict[det_key].metadata['omega'] + except(KeyError): + msg = "imageseries for '%s' has no omega info" % det_key + raise RuntimeError(msg) + + # initialize maps and assing by row (omega/frame) + nrows_ome = len(omegas) + ncols_eta = len(eta_edges) - 1 + ring_maps = [] for i_r, tthr in enumerate(tth_ranges): print("working on ring %d..." % i_r) + # ???: faster to index with bool or use np.where, + # or recode in numba? rtth_idx = np.where( np.logical_and(ptth >= tthr[0], ptth <= tthr[1]) ) - # grab omegas from imageseries and squawk if missing - try: - omegas = imgser_dict[det_key].metadata['omega'] - except(KeyError): - msg = "imageseries for '%s' has no omega info" % det_key - raise RuntimeError(msg) - - # initialize maps and assing by row (omega/frame) - nrows_ome = len(omegas) - ncols_eta = len(eta_edges) - 1 - this_map = np.nan*np.ones((nrows_ome, ncols_eta)) + # grab relevant eta coords using histogram + # !!!: This allows use to calculate arc length and + # detect a branch cut. The histogram idx var + # is the left-hand edges... + retas = peta[rtth_idx] + if fast_histogram: + reta_hist = histogram1d( + retas, + len(eta_edges) - 1, + (eta_edges[0], eta_edges[-1]) + ) + else: + reta_hist, _ = histogram1d(retas, bins=eta_edges) + reta_idx = np.where(reta_hist)[0] + reta_bin_idx = np.hstack( + [reta_idx, + reta_idx[-1] + 1] + ) + + # ring arc lenght on panel + arc_length = angularDifference( + eta_edges[reta_bin_idx[0]], + eta_edges[reta_bin_idx[-1]] + ) + # Munge eta bins + # !!! need to work with the subset to preserve + # NaN values at panel extents! + # + # !!! MUST RE-MAP IF BRANCH CUT IS IN RANGE + # + # The logic below assumes that eta_edges span 2*pi to + # single precision + eta_bins = eta_edges[reta_bin_idx] + if arc_length < 2*np.pi - 1e-4: + # ring is incomplete + if arc_length < 1e-4: + # have branch cut in here + eta_stop_idx = np.where( + reta_idx + - np.arange(len(reta_idx)) + )[0][0] + eta_stop = eta_edges[eta_stop_idx] + new_period = np.cumsum([eta_stop, 2*np.pi]) + # remap + retas = mapAngle(retas, new_period) + tmp_bins = mapAngle(eta_edges[reta_idx], new_period) + reta_idx = np.argsort(tmp_bins) + eta_bins = np.hstack( + [tmp_bins[reta_idx], + tmp_bins[reta_idx][-1] + delta_eta] + ) + pass + pass # histogram intensities over eta ranges + this_map = np.nan*np.ones((nrows_ome, ncols_eta)) for i_row, image in enumerate(imgser_dict[det_key]): - intensity_weights = image[rtth_idx] - ring_etas = peta[rtth_idx] - eta_idx_r = np.hstack([eta_idx[i_r], eta_idx[i_r][-1] + 1]) - this_map[i_row, eta_idx[i_r]], _ = np.histogram( - ring_etas, - bins=eta_edges[eta_idx_r], - weights=intensity_weights - ) - pass + if fast_histogram: + this_map[i_row, reta_idx] = histogram1d( + retas, + len(eta_bins) - 1, + (eta_bins[0], eta_bins[-1]), + weights=image[rtth_idx] + ) + else: + this_map[i_row, reta_idx], _ = histogram1d( + retas, + bins=eta_bins, + weights=image[rtth_idx] + ) + pass # end loop on rows ring_maps.append(this_map) - pass + pass # end loop on rings ring_maps_panel[det_key] = ring_maps return ring_maps_panel, eta_edges @@ -565,6 +638,15 @@ def extract_line_positions(self, plane_data, imgser_dict, for i_p, patch in enumerate(patches): # strip relevant objects out of current patch vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch + + _, on_panel = panel.clip_to_panel( + np.vstack( + [xy_eval[0].flatten(), xy_eval[1].flatten()] + ).T + ) + if np.any(~on_panel): + continue + if collapse_tth: ang_data = (vtx_angs[0][0, [0, -1]], vtx_angs[1][[0, -1], 0]) diff --git a/hexrd/matrixutil.py b/hexrd/matrixutil.py index 1ddb4937..c21d6d8f 100644 --- a/hexrd/matrixutil.py +++ b/hexrd/matrixutil.py @@ -39,6 +39,11 @@ from scipy.linalg import svd import numpy as num +from hexrd import USE_NUMBA +if USE_NUMBA: + import numba + + # module variables sqr6i = 1./sqrt(6.) sqr3i = 1./sqrt(3.) @@ -802,3 +807,34 @@ def symmToVecds(A): vecds[4] = sqr2 * A[2,1] vecds[5] = traceToVecdsS(trace3(A)) return vecds + + + +# ============================================================================= +# Numba-fied frame cache writer +# ============================================================================= + + +if USE_NUMBA: + @numba.njit + def extract_ijv(in_array, threshold, out_i, out_j, out_v): + n = 0 + w, h = in_array.shape + for i in range(w): + for j in range(h): + v = in_array[i, j] + if v > threshold: + out_i[n] = i + out_j[n] = j + out_v[n] = v + n += 1 + return n +else: # not USE_NUMBA + def extract_ijv(in_array, threshold, out_i, out_j, out_v): + mask = in_array > threshold + n = sum(mask) + tmp_i, tmp_j = mask.nonzero() + out_i[:n] = tmp_i + out_j[:n] = tmp_j + out_v[:n] = in_array[mask] + return n From ea2c4fe1f57551a2a83f117f28720b10ba83a3bd Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 27 Feb 2019 15:04:33 -0800 Subject: [PATCH 188/253] fix to branch cut mapping --- hexrd/instrument.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 8dcf629e..2cb2093b 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -467,7 +467,7 @@ def extract_polar_maps(self, plane_data, imgser_dict, merge_hkls=False, delta_eta=eta_tol, full_output=True) delta_eta = eta_edges[1] - eta_edges[0] - + # pixel angular coords for the detector panel ptth, peta = panel.pixel_angles() @@ -509,7 +509,7 @@ def extract_polar_maps(self, plane_data, imgser_dict, [reta_idx, reta_idx[-1] + 1] ) - + # ring arc lenght on panel arc_length = angularDifference( eta_edges[reta_bin_idx[0]], @@ -525,14 +525,15 @@ def extract_polar_maps(self, plane_data, imgser_dict, # The logic below assumes that eta_edges span 2*pi to # single precision eta_bins = eta_edges[reta_bin_idx] - if arc_length < 2*np.pi - 1e-4: - # ring is incomplete - if arc_length < 1e-4: - # have branch cut in here - eta_stop_idx = np.where( - reta_idx - - np.arange(len(reta_idx)) - )[0][0] + if arc_length < 1e-4: + # have branch cut in here + ring_gap = np.where( + reta_idx + - np.arange(len(reta_idx)) + )[0] + if len(ring_gap) > 0: + # have incomplete ring + eta_stop_idx = ring_gap[0] eta_stop = eta_edges[eta_stop_idx] new_period = np.cumsum([eta_stop, 2*np.pi]) # remap @@ -560,7 +561,7 @@ def extract_polar_maps(self, plane_data, imgser_dict, retas, bins=eta_bins, weights=image[rtth_idx] - ) + ) pass # end loop on rows ring_maps.append(this_map) pass # end loop on rings From e3ef4df66e7133334d41df9c0fb44bf7550e2aa9 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 18 Mar 2019 15:10:22 -0700 Subject: [PATCH 189/253] improvements to peak fitting initial guess, config updates --- hexrd/config/findorientations.py | 46 ++-- hexrd/fitting/fitpeak.py | 353 +++++++++++++++++-------------- 2 files changed, 210 insertions(+), 189 deletions(-) diff --git a/hexrd/config/findorientations.py b/hexrd/config/findorientations.py index a35bc6f6..5b1495cf 100644 --- a/hexrd/config/findorientations.py +++ b/hexrd/config/findorientations.py @@ -1,4 +1,3 @@ -import logging import os import numpy as np @@ -6,20 +5,16 @@ from .config import Config - class FindOrientationsConfig(Config): - @property def clustering(self): return ClusteringConfig(self._cfg) - @property def eta(self): return EtaConfig(self._cfg) - @property def extract_measured_g_vectors(self): return self._cfg.get( @@ -27,27 +22,22 @@ def extract_measured_g_vectors(self): False ) - @property def omega(self): return OmegaConfig(self._cfg) - @property def orientation_maps(self): return OrientationMapsConfig(self._cfg) - @property def seed_search(self): return SeedSearchConfig(self._cfg) - @property def threshold(self): return self._cfg.get('find_orientations:threshold', 1) - @property def use_quaternion_grid(self): key = 'find_orientations:use_quaternion_grid' @@ -65,7 +55,6 @@ def use_quaternion_grid(self): class ClusteringConfig(Config): - @property def algorithm(self): key = 'find_orientations:clustering:algorithm' @@ -78,7 +67,6 @@ def algorithm(self): % (key, temp, choices) ) - @property def completeness(self): key = 'find_orientations:clustering:completeness' @@ -89,7 +77,6 @@ def completeness(self): '"%s" must be specified' % key ) - @property def radius(self): key = 'find_orientations:clustering:radius' @@ -101,18 +88,24 @@ def radius(self): ) - class OmegaConfig(Config): - @property def period(self): key = 'find_orientations:omega:period' ome_start = self._cfg.image_series.omega.start - ome_step = self._cfg.image_series.omega.step range = 360 if self._cfg.image_series.omega.step > 0 else -360 - temp = self._cfg.get(key, [ome_start, ome_start + range]) - range = np.abs(temp[1]-temp[0]) + try: + temp = self._cfg.get(key, [ome_start, ome_start + range]) + except(TypeError): + temp = self._cfg.get(key, None) + + try: + range = np.abs(temp[1] - temp[0]) + except(TypeError): + raise RuntimeError( + "without imageseries spec, must specify period" + ) if range != 360: raise RuntimeError( '"%s": range must be 360 degrees, range of %s is %g' @@ -120,7 +113,6 @@ def period(self): ) return temp - @property def tolerance(self): return self._cfg.get( @@ -131,7 +123,6 @@ def tolerance(self): class EtaConfig(Config): - @property def tolerance(self): return self._cfg.get( @@ -139,32 +130,28 @@ def tolerance(self): 2 * self._cfg.image_series.omega.step ) - @property def mask(self): return self._cfg.get('find_orientations:eta:mask', 5) - @property def range(self): mask = self.mask if mask is None: return mask return [[-90 + mask, 90 - mask], - [ 90 + mask, 270 - mask]] - + [90 + mask, 270 - mask]] class SeedSearchConfig(Config): - @property def hkl_seeds(self): key = 'find_orientations:seed_search:hkl_seeds' try: temp = self._cfg.get(key) if isinstance(temp, int): - temp = [temp,] + temp = [temp, ] return temp except: if self._cfg.find_orientations.use_quaternion_grid is None: @@ -172,7 +159,6 @@ def hkl_seeds(self): '"%s" must be defined for seeded search' % key ) - @property def fiber_step(self): return self._cfg.get( @@ -180,16 +166,13 @@ def fiber_step(self): self._cfg.find_orientations.omega.tolerance ) - @property def fiber_ndiv(self): return int(360.0 / self.fiber_step) - class OrientationMapsConfig(Config): - @property def active_hkls(self): temp = self._cfg.get( @@ -197,14 +180,12 @@ def active_hkls(self): ) return [temp] if isinstance(temp, int) else temp - @property def bin_frames(self): return self._cfg.get( 'find_orientations:orientation_maps:bin_frames', default=1 ) - @property def file(self): temp = self._cfg.get('find_orientations:orientation_maps:file') @@ -212,7 +193,6 @@ def file(self): temp = os.path.join(self._cfg.working_dir, temp) return temp - @property def threshold(self): return self._cfg.get('find_orientations:orientation_maps:threshold') diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index 607c8450..1fd60707 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -1,3 +1,4 @@ + # ============================================================ # Copyright (c) 2012, Lawrence Livermore National Security, LLC. # Produced at the Lawrence Livermore National Laboratory. @@ -10,9 +11,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the +# Free Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -25,147 +26,187 @@ # Boston, MA 02111-1307 USA or visit . # ============================================================ +import numpy as np +from scipy import integrate +from scipy import ndimage as imgproc +from scipy import optimize + +from hexrd import constants +from hexrd.fitting import peakfunctions as pkfuncs -import numpy as np -import scipy.optimize as optimize -import hexrd.fitting.peakfunctions as pkfuncs -import scipy.ndimage as imgproc -import scipy.integrate as integrate -#import copy import matplotlib.pyplot as plt -#### 1-D Peak Fitting -def estimate_pk_parms_1d(x,f,pktype='pvoigt'): - """ - Gives initial guess of parameters for analytic fit of one dimensional peak - data. +# ============================================================================= +# Helper Functions and Module Vars +# ============================================================================= - Required Arguments: - x -- (n) ndarray of coordinate positions - f -- (n) ndarray of intensity measurements at coordinate positions x - pktype -- string, type of analytic function that will be used to fit the data, - current options are "gaussian","lorentzian","pvoigt" (psuedo voigt), and - "split_pvoigt" (split psuedo voigt) +ftol = constants.sqrt_epsf +xtol = constants.sqrt_epsf - Outputs: - p -- (m) ndarray containing initial guesses for parameters for the input peaktype - (see peak function help for what each parameters corresponds to) - """ +def snip1d(y, w=4, numiter=2): + """Return SNIP-estimated baseline-background for given spectrum y.""" + z = np.log(np.log(np.sqrt(y + 1) + 1) + 1) + b = z + for i in range(numiter): + for p in range(w, 0, -1): + kernel = np.zeros(p*2 + 1) + kernel[0] = kernel[-1] = 1./2. + b = np.minimum( + b, + imgproc.convolve1d(z, kernel, mode='nearest') + ) + z = b + # bfull = np.zeros_like(y) + # bfull[~zeros_idx] = b + bkg = (np.exp(np.exp(b) - 1) - 1)**2 - 1 + return bkg - data_max=np.max(f) -# lbg=np.mean(f[:2]) -# rbg=np.mean(f[:2]) - if((f[0]> (0.25*data_max)) and (f[-1]> (0.25*data_max))):#heuristic for wide peaks - bg0=0. - elif (f[0]> (0.25*data_max)): #peak cut off on the left - bg0=f[-1] - elif (f[-1]> (0.25*data_max)): #peak cut off on the right - bg0=f[0] - else: - bg0=(f[0]+f[-1])/2. - #bg1=(rbg-lbg)/(x[-1]-x[0]) +def lin_fit_obj(x, m, b): + return m*np.asarray(x) + b - cen_index=np.argmax(f) - x0=x[cen_index] - A=data_max-bg0#-(bg0+bg1*x0) - num_pts=len(f) +def lin_fit_jac(x, m, b): + return np.vstack([x, np.ones_like(x)]).T - #checks for peaks that are cut off - if cen_index == (num_pts-1): - FWHM=x[cen_index]-x[np.argmin(np.abs(f[:cen_index]-A/2.))]#peak cut off on the left - elif cen_index == 0: - FWHM=x[cen_index+np.argmin(np.abs(f[cen_index+1:]-A/2.))]-x[0] #peak cut off on the right - else: - FWHM=x[cen_index+np.argmin(np.abs(f[cen_index+1:]-A/2.))]-x[np.argmin(np.abs(f[:cen_index]-A/2.))] - if FWHM <=0:##uh,oh something went bad - FWHM=(x[-1]-x[0])/4. #completely arbitrary, set peak width to 1/4 window size +# ============================================================================= +# 1-D Peak Fitting +# ============================================================================= + +def estimate_pk_parms_1d(x, f, pktype='pvoigt'): + """ + Gives initial guess of parameters for analytic fit of one dimensional peak + data. + Required Arguments: + x -- (n) ndarray of coordinate positions + f -- (n) ndarray of intensity measurements at coordinate positions x + pktype -- string, type of analytic function that will be used to fit the + data, current options are "gaussian", "lorentzian", + "pvoigt" (psuedo voigt), and "split_pvoigt" (split psuedo voigt) - if pktype=='gaussian' or pktype=='lorentzian': - p=[A,x0,FWHM,bg0,0.] - elif pktype=='pvoigt': - p=[A,x0,FWHM,0.5,bg0,0.] - elif pktype=='split_pvoigt': - p=[A,x0,FWHM,FWHM,0.5,0.5,bg0,0.] + Outputs: + p -- (m) ndarray containing initial guesses for parameters for the input + peaktype + (see peak function help for what each parameters corresponds to) + """ + npts = len(x) + assert len(f) == npts, "ordinate and data must be same length!" + + bkg = snip1d(f, w=int(2*npts/3.)) + + bp, _ = optimize.curve_fit(lin_fit_obj, x, bkg, jac=lin_fit_jac) + bg0 = bp[-1] + bg1 = bp[0] + + pint = f - lin_fit_obj(x, *bp) + cen_index = np.argmax(pint) + A = pint[cen_index] + x0 = x[cen_index] + + # generically robust fwhm extimation for data with a peak + bkg + left_hm = np.argmin(abs(pint[:cen_index] - 0.5*A)) + right_hm = np.argmin(abs(pint[cen_index:] - 0.5*A)) + FWHM = x[cen_index + right_hm] - x[left_hm] + if FWHM <= 0 or FWHM > 0.75*npts: + # something is weird, so punt... + FWHM = 0.25*(x[-1] - x[0]) + + if pktype in ['gaussian', 'lorentzian']: + p = [A, x0, FWHM, bg0, bg1] + elif pktype == 'pvoigt': + p = [A, x0, FWHM, 0.5, bg0, bg1] + elif pktype == 'split_pvoigt': + p = [A, x0, FWHM, FWHM, 0.5, 0.5, bg0, bg1] - p=np.array(p) - return p + return np.r_[p] -def fit_pk_parms_1d(p0,x,f,pktype='pvoigt'): +def fit_pk_parms_1d(p0, x, f, pktype='pvoigt'): """ Performs least squares fit to find parameters for 1d analytic functions fit to diffraction data Required Arguments: - p0 -- (m) ndarray containing initial guesses for parameters for the input peaktype + p0 -- (m) ndarray containing initial guesses for parameters + for the input peaktype x -- (n) ndarray of coordinate positions f -- (n) ndarray of intensity measurements at coordinate positions x - pktype -- string, type of analytic function that will be used to fit the data, + pktype -- string, type of analytic function that will be used to + fit the data, current options are "gaussian","lorentzian","pvoigt" (psuedo voigt), and "split_pvoigt" (split psuedo voigt) Outputs: - p -- (m) ndarray containing fit parameters for the input peaktype (see peak function - help for what each parameters corresponds to) + p -- (m) ndarray containing fit parameters for the input peaktype + (see peak function help for what each parameters corresponds to) Notes: - 1. Currently no checks are in place to make sure that the guess of parameters - has a consistent number of parameters with the requested peak type + 1. Currently no checks are in place to make sure that the guess of + parameters has a consistent number of parameters with the requested + peak type """ - - fitArgs=(x,f,pktype) - - ftol=1e-6 - xtol=1e-6 - - weight=np.max(f)*10.#hard coded should be changed - + weight = np.max(f)*10. # hard coded should be changed + fitArgs = (x, f, pktype) if pktype == 'gaussian': - p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,Dfun=eval_pk_deriv_1d,ftol=ftol,xtol=xtol) + p, outflag = optimize.leastsq( + fit_pk_obj_1d, p0, + args=fitArgs, Dfun=eval_pk_deriv_1d, + ftol=ftol, xtol=xtol + ) elif pktype == 'lorentzian': - p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,Dfun=eval_pk_deriv_1d,ftol=ftol,xtol=xtol) + p, outflag = optimize.leastsq( + fit_pk_obj_1d, p0, + args=fitArgs, Dfun=eval_pk_deriv_1d, + ftol=ftol, xtol=xtol + ) elif pktype == 'pvoigt': - lb=[p0[0]*0.5,np.min(x),0., 0., 0.,None] - ub=[p0[0]*2.0,np.max(x),4.*p0[2],1., 2.*p0[4],None] - - fitArgs=(x,f,pktype,weight,lb,ub) - p, outflag = optimize.leastsq(fit_pk_obj_1d_bnded, p0, args=fitArgs,ftol=ftol,xtol=xtol) + lb = [p0[0]*0.5, np.min(x), 0., 0., 0., None] + ub = [p0[0]*2.0, np.max(x), 4.*p0[2], 1., 2.*p0[4], None] + + fitArgs = (x, f, pktype, weight, lb, ub) + p, outflag = optimize.leastsq( + fit_pk_obj_1d_bnded, p0, + args=fitArgs, + ftol=ftol, xtol=xtol + ) elif pktype == 'split_pvoigt': - lb=[p0[0]*0.5,np.min(x),0., 0., 0., 0., 0.,None] - ub=[p0[0]*2.0,np.max(x),4.*p0[2],4.*p0[2],1., 1., 2.*p0[4],None] - fitArgs=(x,f,pktype,weight,lb,ub) - p, outflag = optimize.leastsq(fit_pk_obj_1d_bnded, p0, args=fitArgs,ftol=ftol,xtol=xtol) - + lb = [p0[0]*0.5, np.min(x), 0., 0., 0., 0., 0., None] + ub = [p0[0]*2.0, np.max(x), 4.*p0[2], 4.*p0[2], 1., 1., 2.*p0[4], None] + fitArgs = (x, f, pktype, weight, lb, ub) + p, outflag = optimize.leastsq( + fit_pk_obj_1d_bnded, p0, + args=fitArgs, + ftol=ftol, xtol=xtol + ) elif pktype == 'tanh_stepdown': - p, outflag = optimize.leastsq(fit_pk_obj_1d, p0, args=fitArgs,ftol=ftol,xtol=xtol) + p, outflag = optimize.leastsq( + fit_pk_obj_1d, p0, + args=fitArgs, + ftol=ftol, xtol=xtol) else: - p=p0 + p = p0 print('non-valid option, returning guess') - if np.any(np.isnan(p)): - p=p0 + p = p0 print('failed fitting, returning guess') return p - def fit_mpk_parms_1d(p0,x,f0,pktype,num_pks,bgtype=None,bnds=None): """ Performs least squares fit to find parameters for MULTIPLE 1d analytic functions fit - to diffraction data + to diffraction data Required Arguments: @@ -192,18 +233,18 @@ def fit_mpk_parms_1d(p0,x,f0,pktype,num_pks,bgtype=None,bnds=None): ftol=1e-6 xtol=1e-6 - + if bnds != None: p = optimize.least_squares(fit_mpk_obj_1d, p0,bounds=bnds, args=fitArgs,ftol=ftol,xtol=xtol) else: - p = optimize.least_squares(fit_mpk_obj_1d, p0, args=fitArgs,ftol=ftol,xtol=xtol) - + p = optimize.least_squares(fit_mpk_obj_1d, p0, args=fitArgs,ftol=ftol,xtol=xtol) + return p.x def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_guess=0.07,center_bnd=0.02): - + num_pks=len(pk_pos_0) min_val=np.min(f) @@ -213,95 +254,95 @@ def estimate_mpk_parms_1d(pk_pos_0,x,f,pktype='pvoigt',bgtype='linear',fwhm_gues p0tmp=np.zeros([num_pks,3]) p0tmp_lb=np.zeros([num_pks,3]) p0tmp_ub=np.zeros([num_pks,3]) - + #x is just 2theta values #make guess for the initital parameters for ii in np.arange(num_pks): - pt=np.argmin(np.abs(x-pk_pos_0[ii])) + pt=np.argmin(np.abs(x-pk_pos_0[ii])) p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess] - p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5] - p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5] + p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0] elif pktype == 'pvoigt': p0tmp=np.zeros([num_pks,4]) p0tmp_lb=np.zeros([num_pks,4]) p0tmp_ub=np.zeros([num_pks,4]) - + #x is just 2theta values #make guess for the initital parameters for ii in np.arange(num_pks): - pt=np.argmin(np.abs(x-pk_pos_0[ii])) + pt=np.argmin(np.abs(x-pk_pos_0[ii])) p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess,0.5] - p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5,0.0] - p0tmp_ub[ii,:]=[(f[pt]-min_val+1.)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0,1.0] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5,0.0] + p0tmp_ub[ii,:]=[(f[pt]-min_val+1.)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0,1.0] elif pktype == 'split_pvoigt': p0tmp=np.zeros([num_pks,6]) p0tmp_lb=np.zeros([num_pks,6]) p0tmp_ub=np.zeros([num_pks,6]) - + #x is just 2theta values #make guess for the initital parameters for ii in np.arange(num_pks): - pt=np.argmin(np.abs(x-pk_pos_0[ii])) + pt=np.argmin(np.abs(x-pk_pos_0[ii])) p0tmp[ii,:]=[(f[pt]-min_val),pk_pos_0[ii],fwhm_guess,fwhm_guess,0.5,0.5] - p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5,fwhm_guess*0.5,0.0,0.0] - p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0,fwhm_guess*2.0,1.0,1.0] + p0tmp_lb[ii,:]=[(f[pt]-min_val)*0.1,pk_pos_0[ii]-center_bnd,fwhm_guess*0.5,fwhm_guess*0.5,0.0,0.0] + p0tmp_ub[ii,:]=[(f[pt]-min_val)*10.0,pk_pos_0[ii]+center_bnd,fwhm_guess*2.0,fwhm_guess*2.0,1.0,1.0] - if bgtype=='linear': - num_pk_parms=len(p0tmp.ravel()) + if bgtype=='linear': + num_pk_parms=len(p0tmp.ravel()) p0=np.zeros(num_pk_parms+2) lb=np.zeros(num_pk_parms+2) ub=np.zeros(num_pk_parms+2) p0[:num_pk_parms]=p0tmp.ravel() lb[:num_pk_parms]=p0tmp_lb.ravel() ub[:num_pk_parms]=p0tmp_ub.ravel() - - + + p0[-2]=min_val - + lb[-2]=-float('inf') lb[-1]=-float('inf') - + ub[-2]=float('inf') - ub[-1]=float('inf') - + ub[-1]=float('inf') + elif bgtype=='constant': - num_pk_parms=len(p0tmp.ravel()) + num_pk_parms=len(p0tmp.ravel()) p0=np.zeros(num_pk_parms+1) lb=np.zeros(num_pk_parms+1) ub=np.zeros(num_pk_parms+1) p0[:num_pk_parms]=p0tmp.ravel() lb[:num_pk_parms]=p0tmp_lb.ravel() ub[:num_pk_parms]=p0tmp_ub.ravel() - - + + p0[-1]=min_val lb[-1]=-float('inf') - ub[-1]=float('inf') - + ub[-1]=float('inf') + elif bgtype=='quadratic': - num_pk_parms=len(p0tmp.ravel()) + num_pk_parms=len(p0tmp.ravel()) p0=np.zeros(num_pk_parms+3) lb=np.zeros(num_pk_parms+3) ub=np.zeros(num_pk_parms+3) p0[:num_pk_parms]=p0tmp.ravel() lb[:num_pk_parms]=p0tmp_lb.ravel() ub[:num_pk_parms]=p0tmp_ub.ravel() - - - p0[-3]=min_val + + + p0[-3]=min_val lb[-3]=-float('inf') lb[-2]=-float('inf') - lb[-1]=-float('inf') + lb[-1]=-float('inf') ub[-3]=float('inf') - ub[-2]=float('inf') - ub[-1]=float('inf') - + ub[-2]=float('inf') + ub[-1]=float('inf') + bnds=(lb,ub) - - - - + + + + return p0, bnds def eval_pk_deriv_1d(p,x,y0,pktype): @@ -354,8 +395,8 @@ def fit_pk_obj_1d_bnded(p,x,f0,pktype,weight,lb,ub): return resd -def fit_mpk_obj_1d(p,x,f0,pktype,num_pks,bgtype): - +def fit_mpk_obj_1d(p,x,f0,pktype,num_pks,bgtype): + f=pkfuncs.mpeak_1d(p,x,pktype,num_pks,bgtype='linear') resd = f-f0 return resd @@ -531,33 +572,33 @@ def direct_pk_analysis(x,f,remove_bg=True,low_int=1.,edge_pts=3,pts_per_meas=100 - + plt.plot(x,f) #subtract background, assumed linear - if remove_bg: + if remove_bg: bg_data=np.hstack((f[:(edge_pts+1)],f[-edge_pts:])) bg_pts=np.hstack((x[:(edge_pts+1)],x[-edge_pts:])) - + bg_parm=np.polyfit(bg_pts,bg_data,1) - + f=f-(bg_parm[0]*x+bg_parm[1])#pull out high background - + f=f-np.min(f)#set the minimum to 0 - + plt.plot(bg_pts,bg_data,'x') plt.plot(x,f,'r') - + spacing=np.diff(x)[0]/pts_per_meas xfine=np.arange(np.min(x),np.max(x)+spacing,spacing)# make a fine grid of points ffine=np.interp(xfine,x,f) - + data_max=np.max(f)#find max intensity values total_int=integrate.simps(ffine,xfine)#numerically integrate the peak using the simpson rule - - cen_index=np.argmax(ffine) - A=data_max + + cen_index=np.argmax(ffine) + A=data_max if(total_int Date: Mon, 1 Apr 2019 19:37:40 -0700 Subject: [PATCH 190/253] enhancements to fitting parameter estimation --- hexrd/fitting/fitpeak.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index 1fd60707..d9389277 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -98,25 +98,42 @@ def estimate_pk_parms_1d(x, f, pktype='pvoigt'): npts = len(x) assert len(f) == npts, "ordinate and data must be same length!" + # handle background + # ??? make kernel width a kwarg? bkg = snip1d(f, w=int(2*npts/3.)) + # fit linear bg and grab params bp, _ = optimize.curve_fit(lin_fit_obj, x, bkg, jac=lin_fit_jac) bg0 = bp[-1] bg1 = bp[0] + # set remaining params pint = f - lin_fit_obj(x, *bp) cen_index = np.argmax(pint) A = pint[cen_index] x0 = x[cen_index] - # generically robust fwhm extimation for data with a peak + bkg - left_hm = np.argmin(abs(pint[:cen_index] - 0.5*A)) - right_hm = np.argmin(abs(pint[cen_index:] - 0.5*A)) - FWHM = x[cen_index + right_hm] - x[left_hm] + # fix center index + if cen_index > 0 and cen_index < npts - 1: + left_hm = np.argmin(abs(pint[:cen_index] - 0.5*A)) + right_hm = np.argmin(abs(pint[cen_index:] - 0.5*A)) + elif cen_index == 0: + right_hm = np.argmin(abs(pint[cen_index:] - 0.5*A)) + left_hm = right_hm + elif cen_index == npts - 1: + left_hm = np.argmin(abs(pint[:cen_index] - 0.5*A)) + right_hm = left_hm + + # FWHM estimation + try: + FWHM = x[cen_index + right_hm] - x[left_hm] + except(IndexError): + FWHM = 0 if FWHM <= 0 or FWHM > 0.75*npts: # something is weird, so punt... FWHM = 0.25*(x[-1] - x[0]) + # set params if pktype in ['gaussian', 'lorentzian']: p = [A, x0, FWHM, bg0, bg1] elif pktype == 'pvoigt': From b1581d4abc9a354dfbd9d75c9622e5943ec56c8d Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 2 Apr 2019 15:33:05 -0700 Subject: [PATCH 191/253] added 'dtype' override in image-files adaptor --- hexrd/imageseries/load/imagefiles.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py index 36c554f1..3456b278 100644 --- a/hexrd/imageseries/load/imagefiles.py +++ b/hexrd/imageseries/load/imagefiles.py @@ -52,8 +52,14 @@ def __getitem__(self, key): (fnum, frame) = self._file_and_frame(key) fimg = self.infolist[fnum].fabioimage img = fimg.getframe(frame) - - return img.data + if self._dtype is not None: + # !!! handled in self._process_files + iinfo = np.iinfo(self._dtype) + if np.max(img.data) > iinfo.max: + raise RuntimeError("specified dtype will truncate image") + return np.array(img.data, dtype=self._dtype) + else: + return img.data def __iter__(self): return ImageSeriesIterator(self) @@ -74,7 +80,7 @@ def _load_yml(self): EMPTY = 'empty-frames' MAXTOTF = 'max-total-frames' MAXFILF = 'max-file-frames' - + DTYPE = 'dtype' with open(self._fname, "r") as f: d = yaml.load(f) imgsd = d['image-files'] @@ -88,6 +94,7 @@ def _load_yml(self): self._empty = self.optsd[EMPTY] if EMPTY in self.optsd else 0 self._maxframes_tot = self.optsd[MAXTOTF] if MAXTOTF in self.optsd else 0 self._maxframes_file = self.optsd[MAXFILF] if MAXFILF in self.optsd else 0 + self._dtype = np.dtype(self.optsd[DTYPE]) if DTYPE in self.optsd else None self._meta = yamlmeta(d['meta']) #, path=imgsd) @@ -104,8 +111,13 @@ def _process_files(self): infolist.append(info) shp = self._checkvalue(shp, info.shape, "inconsistent image shapes") - dtp = self._checkvalue(dtp, info.dtype, - "inconsistent image dtypes") + if self._dtype is not None: + dtp = self._dtype + + else: + dtp = self._checkvalue( + dtp, info.dtype, + "inconsistent image dtypes") fcl = self._checkvalue(fcl, info.fabioclass, "inconsistent image types") nf += info.nframes From df0fd0bceda7f4c380c26e77dbf428fcb6557d6a Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 10 Apr 2019 15:45:40 -0700 Subject: [PATCH 192/253] Update fitpeak.py catch on invalid pktype kwarg --- hexrd/fitting/fitpeak.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hexrd/fitting/fitpeak.py b/hexrd/fitting/fitpeak.py index d9389277..e0c05c6e 100644 --- a/hexrd/fitting/fitpeak.py +++ b/hexrd/fitting/fitpeak.py @@ -140,6 +140,8 @@ def estimate_pk_parms_1d(x, f, pktype='pvoigt'): p = [A, x0, FWHM, 0.5, bg0, bg1] elif pktype == 'split_pvoigt': p = [A, x0, FWHM, FWHM, 0.5, 0.5, bg0, bg1] + else: + raise RuntimeError("pktype '%s' not understood" % pktype) return np.r_[p] From d686d35294b58e27c181e86f149a683b8c530992 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 17 Apr 2019 16:56:40 -0700 Subject: [PATCH 193/253] Update build.rst --- docs/build.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/build.rst b/docs/build.rst index d7fbcc5c..93d16730 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -32,6 +32,7 @@ First, the dependencies for building an environment to run hexrd:: - progressbar >=2.3 - python - pyyaml + - setuptools - scikit-image - scikit-learn - scipy @@ -41,9 +42,9 @@ If you will be running scripts of you own, I also strongly suggest adding spyder - spyder -For example, to buid an environment to run hexrd v0.5.x, do the following:: +For example, to buid an environment to run hexrd v0.6.x, do the following:: - conda create --name hexrd_0.5 cython dask distributed h5py matplotlib numba numpy=1.15 progressbar=2.3 python=2.7 pyyaml scikit-image scikit-learn scipy spyder wxpython=3 + conda create --name hexrd_0.6 cython dask distributed h5py matplotlib numba numpy=1.15 progressbar=2.3 python=2.7 pyyaml setuptools scikit-image scikit-learn scipy spyder wxpython=3 Then install in develop mode using disutils:: @@ -95,7 +96,7 @@ Installation Findally, run ``conda install`` using the local package:: - conda install hexrd=0.5 --use-local + conda install hexrd=0.6 --use-local Conda should echo the proper version number package in the package install list, which includes all dependencies. @@ -105,4 +106,4 @@ directory) and run:: hexrd --verison -It should currently read ``hexrd 0.5.14`` +It should currently read ``hexrd 0.6.0`` From b5ab401b49c7ad33fbd48738dcf5de7d67b65ccd Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 7 May 2019 15:49:57 -0700 Subject: [PATCH 194/253] minor bugfix for check on float image types --- hexrd/imageseries/load/imagefiles.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py index 3456b278..431f7e5f 100644 --- a/hexrd/imageseries/load/imagefiles.py +++ b/hexrd/imageseries/load/imagefiles.py @@ -54,8 +54,11 @@ def __getitem__(self, key): img = fimg.getframe(frame) if self._dtype is not None: # !!! handled in self._process_files - iinfo = np.iinfo(self._dtype) - if np.max(img.data) > iinfo.max: + try: + dinfo = np.iinfo(self._dtype) + except(ValueError): + dinfo = np.finfo(self._dtype) + if np.max(img.data) > dinfo.max: raise RuntimeError("specified dtype will truncate image") return np.array(img.data, dtype=self._dtype) else: From e799d8fd92f1b44a43371b7390d5a4ee41ae75ae Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 22 May 2019 12:08:30 -0700 Subject: [PATCH 195/253] fixed rmat_s handling --- hexrd/instrument.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 79de6b71..8beabd49 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1669,6 +1669,8 @@ def make_powder_rings( rmat_s=ct.identity_3x3, tvec_s=ct.zeros_3, tvec_c=ct.zeros_3, full_output=False): """ + !!! it is assuming that rmat_s is built from (chi, ome) + !!! as it the case for HEDM """ # in case you want to give it tth angles directly if hasattr(pd, '__len__'): @@ -1740,10 +1742,14 @@ def make_powder_rings( # !!! should be safe as eta_edges are monotonic eta_centers = eta_edges[:-1] + del_eta + # !!! get chi and ome from rmat_s + chi = np.arctan2(rmat_s[2, 1], rmat_s[1, 1]) + ome = np.arctan2(rmat_s[0, 2], rmat_s[0, 0]) + # make list of angle tuples angs = [ np.vstack( - [i*np.ones(neta), eta_centers, np.zeros(neta)] + [i*np.ones(neta), eta_centers, ome*np.ones(neta)] ) for i in tth ] From 1b56abcbec42791f4a7cbab8fe33d6695163114d Mon Sep 17 00:00:00 2001 From: "Joel V. Bernier" Date: Wed, 7 Aug 2019 15:55:04 -0500 Subject: [PATCH 196/253] DeprecationWarning fixes --- hexrd/config/__init__.py | 2 +- hexrd/imageseries/load/imagefiles.py | 2 +- hexrd/xrd/indexer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hexrd/config/__init__.py b/hexrd/config/__init__.py index 912ebb9a..bff7c58a 100644 --- a/hexrd/config/__init__.py +++ b/hexrd/config/__init__.py @@ -16,7 +16,7 @@ def open(file_name=None): with file(file_name) as f: res = [] - for cfg in yaml.load_all(f): + for cfg in yaml.load_all(f, Loader=yaml.SafeLoader): try: # take the previous config section and update with values # from the current one diff --git a/hexrd/imageseries/load/imagefiles.py b/hexrd/imageseries/load/imagefiles.py index 431f7e5f..4dfa7c12 100644 --- a/hexrd/imageseries/load/imagefiles.py +++ b/hexrd/imageseries/load/imagefiles.py @@ -85,7 +85,7 @@ def _load_yml(self): MAXFILF = 'max-file-frames' DTYPE = 'dtype' with open(self._fname, "r") as f: - d = yaml.load(f) + d = yaml.load(f, Loader=yaml.SafeLoader) imgsd = d['image-files'] dname = imgsd['directory'] fglob = imgsd['files'] diff --git a/hexrd/xrd/indexer.py b/hexrd/xrd/indexer.py index 5835bfee..3c3b0d45 100644 --- a/hexrd/xrd/indexer.py +++ b/hexrd/xrd/indexer.py @@ -807,7 +807,7 @@ def paintGrid(quats, etaOmeMaps, # symHKLs_ix provides the start/end index for each subarray # of symHKLs. symHKLs_ix = num.add.accumulate([0] + [s.shape[1] for s in symHKLs]) - symHKLs = num.vstack(s.T for s in symHKLs) + symHKLs = num.vstack([s.T for s in symHKLs]) # Pack together the common parameters for processing params = { From 4224e338c8c41ed1b3b595e9e99248efef0e89f5 Mon Sep 17 00:00:00 2001 From: "Joel V. Bernier" Date: Thu, 8 Aug 2019 15:11:10 -0500 Subject: [PATCH 197/253] minor change to use capi function --- hexrd/instrument.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 8beabd49..69a94da5 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -130,8 +130,8 @@ def calc_angles_from_beam_vec(bvec): Return the azimuth and polar angle from a beam vector """ - bvec = np.atleast_2d(bvec).reshape(3, 1) - nvec = mutil.unitVector(-bvec) + bvec = np.atleast_1d(bvec).flatten() + nvec = unitRowVector(-bvec) azim = float( np.degrees(np.arctan2(nvec[2], nvec[0])) ) From b315297f3982d2a76b8c22dfab5d461f33506877 Mon Sep 17 00:00:00 2001 From: Oscar Villellas Date: Fri, 23 Aug 2019 15:51:35 +0200 Subject: [PATCH 198/253] FIX: keep hdf5 file open for the whole life of imageseries. Previous code was opening and closing for every __get_item__, which is far from ideal. --- hexrd/imageseries/load/hdf5.py | 76 ++++++++++++++++------------------ 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/hexrd/imageseries/load/hdf5.py b/hexrd/imageseries/load/hdf5.py index 78f633c9..7da6f1f5 100644 --- a/hexrd/imageseries/load/hdf5.py +++ b/hexrd/imageseries/load/hdf5.py @@ -1,6 +1,7 @@ """HDF5 adapter class """ import h5py +import warnings from . import ImageSeriesAdapter from ..imageseriesiter import ImageSeriesIterator @@ -21,42 +22,53 @@ def __init__(self, fname, **kwargs): self.__path = kwargs['path'] self.__dataname = kwargs.pop('dataname', 'images') self.__images = '/'.join([self.__path, self.__dataname]) + self.__h5file = h5py.File(self.__h5name, 'r') + self.__image_dataset = self.__h5file[self.__images] + self.__data_group = self.__h5file[self.__path] self._meta = self._getmeta() + + def close(self): + self.__image_dataset = None + self.__data_group = None + self.__h5file.close() + self.__h5file = None + + + def __del__(self): + # Note this is not ideal, as the use of __del__ is problematic. However, + # it is highly unlikely that the usage of a ImageSeries would pose + # a problem. + # + # A warning will (hopefully) be emitted if an issue arises at some point. + try: + self.close() + except: + warnings.warn("HDF5ImageSeries could not close h5file") + pass + + def __getitem__(self, key): - with self._dset as dset: - return dset.__getitem__(key) + return self.__image_dataset[key] + def __iter__(self): return ImageSeriesIterator(self) - #@memoize - def __len__(self): - with self._dset as dset: - return len(dset) - @property - def _dgroup(self): - # return a context manager to ensure proper file handling - # always use like: "with self._dgroup as dgroup:" - return H5ContextManager(self.__h5name, self.__path) + def __len__(self): + return len(self.__image_dataset) - @property - def _dset(self): - # return a context manager to ensure proper file handling - # always use like: "with self._dset as dset:" - return H5ContextManager(self.__h5name, self.__images) def _getmeta(self): mdict = {} - with self._dgroup as dgroup: - for k, v in dgroup.attrs.items(): - mdict[k] = v + for k, v in self.__data_group.attrs.items(): + mdict[k] = v return mdict + @property - #@memoize def metadata(self): """(read-only) Image sequence metadata @@ -64,30 +76,14 @@ def metadata(self): """ return self._meta + @property def dtype(self): - with self._dset as dset: - return dset.dtype + return self.__image_dataset.dtype + @property - #@memoize so you only need to do this once def shape(self): - with self._dset as dset: - return dset.shape[1:] + return self.__image_dataset.shape[1:] pass # end class - - -class H5ContextManager: - - def __init__(self, fname, path): - self._fname = fname - self._path = path - self._f = None - - def __enter__(self): - self._f = h5py.File(self._fname, 'r') - return self._f[self._path] - - def __exit__(self, *args): - self._f.close() From 3b472e94e6e3c416f00cb4c6a76bed98ef58fdb0 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 19 Sep 2019 16:10:41 -0700 Subject: [PATCH 199/253] Update instrument.py forgot import after changing to CAPI function. --- hexrd/instrument.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 69a94da5..e9bef112 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -55,7 +55,7 @@ mapAngle, \ oscillAnglesOfHKLs, \ rowNorm, \ - validateAngleRanges + unitRowVector from hexrd.xrd import xrdutil from hexrd.xrd.crystallography import PlaneData from hexrd import constants as ct @@ -600,7 +600,7 @@ def extract_line_positions(self, plane_data, imgser_dict, tth_ranges = np.degrees(plane_data.getMergedRanges()[1]) tth_tols = np.vstack([i[1] - i[0] for i in tth_ranges]) else: - tth_tols=np.ones(len(plane_data))*tth_tol + tth_tols = np.ones(len(plane_data))*tth_tol # ===================================================================== # LOOP OVER DETECTORS @@ -624,7 +624,8 @@ def extract_line_positions(self, plane_data, imgser_dict, # make rings pow_angs, pow_xys = panel.make_powder_rings( - plane_data, merge_hkls=True, delta_tth=tth_tol, delta_eta=eta_tol) + plane_data, merge_hkls=True, + delta_tth=tth_tol, delta_eta=eta_tol) # ================================================================= # LOOP OVER RING SETS @@ -681,7 +682,7 @@ def extract_line_positions(self, plane_data, imgser_dict, ims_data = [] for j_p in np.arange(len(images)): # catch interpolation type - image=images[j_p] + image = images[j_p] if do_interpolation: tmp = panel.interpolate_bilinear( xy_eval, @@ -1552,13 +1553,13 @@ def clip_to_panel(self, xy, buffer_edges=True): ) on_panel = np.logical_and(on_panel_x, on_panel_y) elif not buffer_edges: - on_panel_x = np.logical_and( - xy[:, 0] >= -xlim, xy[:, 0] <= xlim - ) - on_panel_y = np.logical_and( - xy[:, 1] >= -ylim, xy[:, 1] <= ylim - ) - on_panel = np.logical_and(on_panel_x, on_panel_y) + on_panel_x = np.logical_and( + xy[:, 0] >= -xlim, xy[:, 0] <= xlim + ) + on_panel_y = np.logical_and( + xy[:, 1] >= -ylim, xy[:, 1] <= ylim + ) + on_panel = np.logical_and(on_panel_x, on_panel_y) return xy[on_panel, :], on_panel def cart_to_angles(self, xy_data): @@ -1743,7 +1744,7 @@ def make_powder_rings( eta_centers = eta_edges[:-1] + del_eta # !!! get chi and ome from rmat_s - chi = np.arctan2(rmat_s[2, 1], rmat_s[1, 1]) + # chi = np.arctan2(rmat_s[2, 1], rmat_s[1, 1]) ome = np.arctan2(rmat_s[0, 2], rmat_s[0, 0]) # make list of angle tuples From d0f301aa46f370d68c78b7f1fd0c2fa166802f36 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 26 Sep 2019 15:00:10 -0700 Subject: [PATCH 200/253] udpates for wxpython v4 --- hexrd/wx/fitparampanel.py | 8 ++++---- hexrd/wx/floatcontrol.py | 2 +- hexrd/wx/guiutil.py | 2 +- hexrd/wx/indexpanel.py | 2 +- hexrd/wx/listeditor.py | 2 +- hexrd/wx/materialspanel.py | 2 +- hexrd/wx/planedataeditor.py | 2 +- hexrd/wx/ringsubpanel.py | 4 ++-- hexrd/wx/selecthkls.py | 8 ++++---- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hexrd/wx/fitparampanel.py b/hexrd/wx/fitparampanel.py index e65b7933..1058c46b 100644 --- a/hexrd/wx/fitparampanel.py +++ b/hexrd/wx/fitparampanel.py @@ -97,12 +97,12 @@ def __makeObjects(self): def __makeTitleBar(self, t): """Add titlebar""" self.titlebar = wx.StaticText(self, -1, t, - style=wx.ALIGN_CENTER|wx.SIMPLE_BORDER) + style=wx.ALIGN_CENTER|wx.SIMPLE_BORDER) self.titlebar.SetBackgroundColour(WP.TITLEBAR_BG_COLOR) myToolTip = r""" -PANEL FOR managing data for fit parameters -""" - self.titlebar.SetToolTipString(myToolTip) + PANEL FOR managing data for fit parameters + """ + self.titlebar.SetToolTip(myToolTip) return diff --git a/hexrd/wx/floatcontrol.py b/hexrd/wx/floatcontrol.py index 70255eb3..798ec25f 100644 --- a/hexrd/wx/floatcontrol.py +++ b/hexrd/wx/floatcontrol.py @@ -98,7 +98,7 @@ def __init__(self, parent, id, **kwargs): The spinner increment is shown in the gray box to the right of the spinner. """ - self.SetToolTipString(myToolTip) + self.SetToolTip(myToolTip) self.SetAutoLayout(True) self.SetSizerAndFit(self.sizer) diff --git a/hexrd/wx/guiutil.py b/hexrd/wx/guiutil.py index 4b88623f..f3c70a6a 100644 --- a/hexrd/wx/guiutil.py +++ b/hexrd/wx/guiutil.py @@ -111,7 +111,7 @@ def makeTitleBar(p, t, **kwargs): # Keyword args # tt = 'tooltip' - if tt in kwargs: titlebar.SetToolTipString(kwargs[tt]) + if tt in kwargs: titlebar.SetToolTip(kwargs[tt]) cl = 'color' if cl in kwargs: diff --git a/hexrd/wx/indexpanel.py b/hexrd/wx/indexpanel.py index bbe69467..6edda0ab 100644 --- a/hexrd/wx/indexpanel.py +++ b/hexrd/wx/indexpanel.py @@ -204,7 +204,7 @@ def __makeObjects(self): self.friedel_cbox = wx.CheckBox(self, wx.NewId(), 'Friedel Only') self.friedel_cbox.SetValue(iopts.friedelOnly) - self.claims_cbox = wx.CheckBox(self, wx.NewId(), 'Preserve Claiims') + self.claims_cbox = wx.CheckBox(self, wx.NewId(), 'Preserve Claims') self.claims_cbox.SetValue(iopts.preserveClaims) self.refine_cbox = wx.CheckBox(self, wx.NewId(), 'Do Refinement') self.refine_cbox.SetValue(iopts.doRefinement) diff --git a/hexrd/wx/listeditor.py b/hexrd/wx/listeditor.py index 7502e720..df63c4fe 100644 --- a/hexrd/wx/listeditor.py +++ b/hexrd/wx/listeditor.py @@ -101,7 +101,7 @@ def __makeTitleBar(self, t): myToolTip = r""" PANEL FOR ... """ - self.titlebar.SetToolTipString(myToolTip) + self.titlebar.SetToolTip(myToolTip) return diff --git a/hexrd/wx/materialspanel.py b/hexrd/wx/materialspanel.py index 3a82fbb4..388a471f 100644 --- a/hexrd/wx/materialspanel.py +++ b/hexrd/wx/materialspanel.py @@ -189,7 +189,7 @@ def __makeTitleBar(self, t): myToolTip = r""" PANEL FOR ... """ - self.titlebar.SetToolTipString(myToolTip) + self.titlebar.SetToolTip(myToolTip) return diff --git a/hexrd/wx/planedataeditor.py b/hexrd/wx/planedataeditor.py index 5968f3d4..a162fe95 100644 --- a/hexrd/wx/planedataeditor.py +++ b/hexrd/wx/planedataeditor.py @@ -172,7 +172,7 @@ def __makeTitleBar(self, t): myToolTip = r""" FRAME FOR editing the list of calibrants """ - self.titlebar.SetToolTipString(myToolTip) + self.titlebar.SetToolTip(myToolTip) return diff --git a/hexrd/wx/ringsubpanel.py b/hexrd/wx/ringsubpanel.py index d403d045..5a16c03c 100644 --- a/hexrd/wx/ringsubpanel.py +++ b/hexrd/wx/ringsubpanel.py @@ -95,7 +95,7 @@ def __makeObjects(self): # b. Wavelength # self.dfwv_but = wx.Button(self, wx.NewId(), 'Make Default') - self.dfwv_but.SetToolTipString(dfltToolTip) + self.dfwv_but.SetToolTip(dfltToolTip) self.wave_lab = wx.StaticText(self, wx.NewId(), 'Wavelength:', @@ -120,7 +120,7 @@ def __makeObjects(self): # d. Ring widths # self.dfwd_but = wx.Button(self, wx.NewId(), 'Make Default') - self.dfwd_but.SetToolTipString(dfltToolTip) + self.dfwd_but.SetToolTip(dfltToolTip) self.width_lab = wx.StaticText(self, wx.NewId(), 'Ring Width:', diff --git a/hexrd/wx/selecthkls.py b/hexrd/wx/selecthkls.py index ab7b77bb..ac4fe661 100644 --- a/hexrd/wx/selecthkls.py +++ b/hexrd/wx/selecthkls.py @@ -87,7 +87,7 @@ def __makeTitleBar(self, t): myToolTip = r""" PANEL FOR ... """ - self.titlebar.SetToolTipString(myToolTip) + self.titlebar.SetToolTip(myToolTip) return @@ -119,12 +119,12 @@ def __makeListCtrl(self): hklData = hkls[i] hkl = hklData['hkl'] hklStr = '(%d, %d, %d)' % (hkl[0], hkl[1], hkl[2]) - index = listctrl.InsertStringItem(sys.maxint, hklStr) + index = listctrl.InsertItem(sys.maxint, hklStr) dspace = '%.6g' % hklData['dSpacings'] tth = hklData['tTheta'] * (180/math.pi) tTheta = '%.6g' % tth - listctrl.SetStringItem(index, 1, dspace) - listctrl.SetStringItem(index, 2, tTheta) + listctrl.SetItem(index, 1, dspace) + listctrl.SetItem(index, 2, tTheta) # # Show exclusions by background color # From 67f17e6b723e80413e5c2e1af8e3a2bafc755334 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 29 Oct 2019 20:27:51 -0700 Subject: [PATCH 201/253] Add files via upload conversion routine for instrument yaml file from v0.5.x format to v0.6.x format --- scripts/convert_instrument_config.yml | 113 ++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 scripts/convert_instrument_config.yml diff --git a/scripts/convert_instrument_config.yml b/scripts/convert_instrument_config.yml new file mode 100644 index 00000000..d924fd33 --- /dev/null +++ b/scripts/convert_instrument_config.yml @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jul 29 13:33:57 2019 + +@author: joel +""" +from __future__ import print_function + +import argparse +import yaml +from hexrd.xrd.rotations import make_rmat_euler, angleAxisOfRotMat +from hexrd.xrd import transforms_CAPI as xfcapi + + +# top level keys +ROOT_KEYS = ['beam', 'oscillation_stage', 'detectors'] +OLD_OSCILL_KEYS = ['t_vec_s', 'chi'] +NEW_OSCILL_KEYS = ['translation', 'chi'] +OLD_TRANSFORM_KEYS = ['t_vec_d', 'tilt_angles'] +NEW_TRANSFORM_KEYS = ['translation', 'tilt'] + + +def convert_instrument_config(old_cfg, output=None): + """ + convert v0.5.x style YMAL config to v0.6.x + """ + icfg = yaml.safe_load(open(old_cfg, 'r')) + + new_cfg = dict.fromkeys(icfg) + + # %% first beam + new_cfg['beam'] = icfg['beam'] + + # %% next, calibration crystal if applicable + calib_key = 'calibration_crystal' + if calib_key in icfg.keys(): + new_cfg[calib_key] = icfg[calib_key] + + + # %% sample stage + old_dict = icfg['oscillation_stage'] + tmp_dict = dict.fromkeys(NEW_OSCILL_KEYS) + for tk in zip(OLD_OSCILL_KEYS, NEW_OSCILL_KEYS): + tmp_dict[tk[1]] = old_dict[tk[0]] + new_cfg['oscillation_stage'] = tmp_dict + + # %% detectors + new_cfg['detectors'] = dict.fromkeys(icfg['detectors']) + det_block_keys = ['pixels', 'saturation_level', 'transform', 'distortion'] + for det_id, source_params in icfg['detectors'].items(): + new_dict = {} + for key in det_block_keys: + if key != 'transform': + try: + new_dict[key] = source_params[key] + except(KeyError): + if key == 'distortion': + continue + elif key == 'saturation_level': + new_dict[key] = 2**16 + else: + raise RuntimeError("unrecognized parameter key '%s'" % key) + else: + old_dict = source_params[key] + tmp_dict = dict.fromkeys(NEW_TRANSFORM_KEYS) + for tk in zip(OLD_TRANSFORM_KEYS, NEW_TRANSFORM_KEYS): + if tk[0] == 't_vec_d': + tmp_dict[tk[1]] = old_dict[tk[0]] + elif tk[0] == 'tilt_angles': + xyz_angles = old_dict[tk[0]] + rmat = make_rmat_euler(xyz_angles, + 'xyz', + extrinsic=True) + phi, n = angleAxisOfRotMat(rmat) + tmp_dict[tk[1]] = (phi*n.flatten()).tolist() + new_dict[key] = tmp_dict + new_cfg['detectors'][det_id] = new_dict + + # %% dump new file + if output is None: + print(new_cfg) + else: + yaml.dump(new_cfg, open(output, 'w')) + return + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description="convert a v0.5.x instrument config to v0.6.x format" + ) + + parser.add_argument( + 'instrument_cfg', + help="v0.5 style instrument config YAML file" + ) + + parser.add_argument( + '-o', '--output-file', + help="output file name", + type=str, + default="" + ) + + args = parser.parse_args() + + old_cfg = args.instrument_cfg + output_file = args.output_file + + if len(output_file) == 0: + output_file = None + + convert_instrument_config(old_cfg, output=output_file) From e794ac200fbac37b98e228f4885cb4aa075f0ed2 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 30 Oct 2019 09:32:08 -0700 Subject: [PATCH 202/253] Update build.rst --- docs/build.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/build.rst b/docs/build.rst index 93d16730..35d8bb73 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -22,8 +22,6 @@ Building First, the dependencies for building an environment to run hexrd:: - cython - - dask - - distributed - fabio - h5py - matplotlib @@ -36,7 +34,7 @@ First, the dependencies for building an environment to run hexrd:: - scikit-image - scikit-learn - scipy - - wxpython ==3 + - wxpython If you will be running scripts of you own, I also strongly suggest adding spyder:: @@ -44,7 +42,11 @@ If you will be running scripts of you own, I also strongly suggest adding spyder For example, to buid an environment to run hexrd v0.6.x, do the following:: - conda create --name hexrd_0.6 cython dask distributed h5py matplotlib numba numpy=1.15 progressbar=2.3 python=2.7 pyyaml setuptools scikit-image scikit-learn scipy spyder wxpython=3 + conda create --name hexrd_0.6 cython h5py matplotlib numba numpy python=2.7 pyyaml setuptools scikit-image scikit-learn scipy spyder + conda install -c conda-forge --name hexrd_0.6 wxpython + conda install -c conda-forge --name hexrd_0.6 progressbar + conda activate hexrd_0.6 + Then install in develop mode using disutils:: @@ -106,4 +108,4 @@ directory) and run:: hexrd --verison -It should currently read ``hexrd 0.6.0`` +It should currently read ``hexrd 0.6.5`` From 4be88da57374b88f00ace87901c686964cda0999 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 30 Oct 2019 09:36:17 -0700 Subject: [PATCH 203/253] Update build.rst --- docs/build.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/build.rst b/docs/build.rst index 35d8bb73..52e132b9 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -48,11 +48,12 @@ For example, to buid an environment to run hexrd v0.6.x, do the following:: conda activate hexrd_0.6 -Then install in develop mode using disutils:: +Then install using setuptools:: - python setup.py develop + python setup.py install -The procedure for building/installing with conda-build is as follows +Note, you will have to install fabio in the same environment using ``setup.py`` as well. +The procedure for building/installing with conda-build is as follows (*this is curently broken*) First, update conda and conda-build:: From bbad52960b55373f8cc61c2b1f84ca04d3eb219b Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 30 Oct 2019 10:11:29 -0700 Subject: [PATCH 204/253] Update build.rst --- docs/build.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/build.rst b/docs/build.rst index 52e132b9..df87cc83 100644 --- a/docs/build.rst +++ b/docs/build.rst @@ -43,8 +43,8 @@ If you will be running scripts of you own, I also strongly suggest adding spyder For example, to buid an environment to run hexrd v0.6.x, do the following:: conda create --name hexrd_0.6 cython h5py matplotlib numba numpy python=2.7 pyyaml setuptools scikit-image scikit-learn scipy spyder - conda install -c conda-forge --name hexrd_0.6 wxpython - conda install -c conda-forge --name hexrd_0.6 progressbar + conda install -c anaconda --name hexrd_0.6 wxpython + conda install -c anaconda --name hexrd_0.6 progressbar conda activate hexrd_0.6 From 8aa8846a8ed9dd90ea88bb5596cc9476b6ae2d46 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 3 Feb 2020 16:20:23 -0800 Subject: [PATCH 205/253] Indexing fix Fixes error for map generation with incomplete rings and a branch cut. --- hexrd/instrument.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index e9bef112..8ffbe1e3 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -554,10 +554,11 @@ def extract_polar_maps(self, plane_data, imgser_dict, # remap retas = mapAngle(retas, new_period) tmp_bins = mapAngle(eta_edges[reta_idx], new_period) - reta_idx = np.argsort(tmp_bins) + tmp_idx = np.argsort(tmp_bins) + reta_idx = reta_idx[np.argsort(tmp_bins)] eta_bins = np.hstack( - [tmp_bins[reta_idx], - tmp_bins[reta_idx][-1] + delta_eta] + [tmp_bins[tmp_idx], + tmp_bins[tmp_idx][-1] + delta_eta] ) pass pass From 86cda2551abeca0bd440bcb9a8c7bf20a2ddd97f Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 3 Feb 2020 16:48:54 -0800 Subject: [PATCH 206/253] fix recipe --- conda.recipe/meta.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 4ef0292f..b5f0a98d 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -29,14 +29,14 @@ requirements: - matplotlib - numba - numpy - - progressbar >=2.3 + - progressbar - python - python.app # [osx] - pyyaml - scikit-image - scikit-learn - scipy - - wxpython ==3 + - wxpython test: imports: From a769281a8bdb60a5be40f25ffb6d85ce3d9ebb9f Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 3 Feb 2020 21:54:18 -0800 Subject: [PATCH 207/253] added blob detection; fixed python version spec --- conda.recipe/meta.yaml | 6 ++--- hexrd/findorientations.py | 49 ++++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index b5f0a98d..4bc8f6fa 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -3,8 +3,6 @@ package: version: {{ environ.get('GIT_DESCRIBE_TAG', '')[1:] }} source: - #git_url: https://github.com/joelvbernier/hexrd.git - #git_tag: v0.3.x git_url: ../ build: @@ -22,7 +20,7 @@ requirements: build: - numba - numpy - - python + - python <3 - setuptools run: - h5py @@ -30,7 +28,7 @@ requirements: - numba - numpy - progressbar - - python + - python <3 - python.app # [osx] - pyyaml - scikit-image diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index d4578bd2..8e616ba3 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -13,6 +13,7 @@ import scipy.cluster as cluster from scipy import ndimage +from skimage.feature import blob_log from hexrd import matrixutil as mutil from hexrd.xrd import indexer as idx @@ -28,7 +29,8 @@ logger = logging.getLogger(__name__) -save_as_ascii = False # FIX LATER... +save_as_ascii = False # FIXME LATER... +method = 'blob_log' # FIXME LATER... # just require scikit-learn? have_sklearn = False @@ -82,21 +84,36 @@ def generate_orientation_fibers( numSpots = [] coms = [] for i in seed_hkl_ids: - # First apply filter - this_map_f = -ndimage.filters.gaussian_laplace( - eta_ome.dataStore[i], filt_stdev) - - labels_t, numSpots_t = ndimage.label( - this_map_f > threshold, - structureNDI_label - ) - coms_t = np.atleast_2d( - ndimage.center_of_mass( - this_map_f, - labels=labels_t, - index=np.arange(1, np.amax(labels_t)+1) + if method == "label": + # First apply filter + this_map_f = -ndimage.filters.gaussian_laplace( + eta_ome.dataStore[i], filt_stdev) + + labels_t, numSpots_t = ndimage.label( + this_map_f > threshold, + structureNDI_label + ) + coms_t = np.atleast_2d( + ndimage.center_of_mass( + this_map_f, + labels=labels_t, + index=np.arange(1, np.amax(labels_t)+1) + ) ) + elif method == "blob_log": + # must scale map + this_map = eta_ome.dataStore[i] + this_map[np.isnan(this_map)] = 0. + this_map -= np.min(this_map) + scl_map = 2*this_map/np.max(this_map) - 1. + + # FIXME: need to expose the parameters to config options. + blobs_log = np.atleast_2d( + blob_log(scl_map, min_sigma=0.5, max_sigma=5, + num_sigma=10, threshold=0.01, overlap=0.1) ) + numSpots_t = len(blobs_log) + coms_t = blobs_log[:, 2] numSpots.append(numSpots_t) coms.append(coms_t) pass @@ -151,7 +168,7 @@ def discretefiber_reduced(params_in): fiber_ndiv = paramMP['fiber_ndiv'] hkl = params_in[:3].reshape(3, 1) - + gVec_s = xfcapi.anglesToGVec( np.atleast_2d(params_in[3:]), chi=chi, @@ -489,7 +506,7 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): ) pass pass # close conditional on grid search - + # generate the completion maps logger.info("Running paintgrid on %d trial orientations", quats.shape[1]) if profile: From 7eaa0a02e3a18d302b105af87a819a9c16c4ffd3 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 3 Feb 2020 22:38:44 -0800 Subject: [PATCH 208/253] indexing typo --- hexrd/findorientations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 8e616ba3..cc4d6e31 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -97,7 +97,7 @@ def generate_orientation_fibers( ndimage.center_of_mass( this_map_f, labels=labels_t, - index=np.arange(1, np.amax(labels_t)+1) + index=np.arange(1, np.amax(labels_t) + 1) ) ) elif method == "blob_log": @@ -106,7 +106,7 @@ def generate_orientation_fibers( this_map[np.isnan(this_map)] = 0. this_map -= np.min(this_map) scl_map = 2*this_map/np.max(this_map) - 1. - + import pdb; pbd.set_trace() # FIXME: need to expose the parameters to config options. blobs_log = np.atleast_2d( blob_log(scl_map, min_sigma=0.5, max_sigma=5, From 7982e0261dc0ef9b052b11739fa60ff8aa6cec77 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 3 Feb 2020 22:51:18 -0800 Subject: [PATCH 209/253] added blob_dog option --- hexrd/findorientations.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index cc4d6e31..8412135a 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -13,7 +13,7 @@ import scipy.cluster as cluster from scipy import ndimage -from skimage.feature import blob_log +from skimage.feature import blob_dog, blob_log from hexrd import matrixutil as mutil from hexrd.xrd import indexer as idx @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) save_as_ascii = False # FIXME LATER... -method = 'blob_log' # FIXME LATER... +method = 'blob_dog' # FIXME LATER... # just require scikit-learn? have_sklearn = False @@ -100,20 +100,24 @@ def generate_orientation_fibers( index=np.arange(1, np.amax(labels_t) + 1) ) ) - elif method == "blob_log": + elif method in ["blob_log", "blob_dog"]: # must scale map this_map = eta_ome.dataStore[i] this_map[np.isnan(this_map)] = 0. this_map -= np.min(this_map) scl_map = 2*this_map/np.max(this_map) - 1. - import pdb; pbd.set_trace() + # FIXME: need to expose the parameters to config options. - blobs_log = np.atleast_2d( - blob_log(scl_map, min_sigma=0.5, max_sigma=5, - num_sigma=10, threshold=0.01, overlap=0.1) - ) - numSpots_t = len(blobs_log) - coms_t = blobs_log[:, 2] + if method == "blob_log": + blobs = np.atleast_2d( + blob_log(scl_map, min_sigma=0.5, max_sigma=5, + num_sigma=10, threshold=0.01, overlap=0.1) + ) + else: + blobs = blob_dog(scl_map, min_sigma=0.5, max_sigma=5, + sigma_ratio=1.6, threshold=0.01, overlap=0.1) + numSpots_t = len(blobs) + coms_t = blobs[:, :2] numSpots.append(numSpots_t) coms.append(coms_t) pass From 0fcf1eeeb98b6b2f9214a6c6abd31232fc247cd5 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 19 Feb 2020 11:54:47 -0800 Subject: [PATCH 210/253] fixes to package dependencies --- conda.recipe/meta.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 4ef0292f..1dc6c32e 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -22,21 +22,21 @@ requirements: build: - numba - numpy - - python + - python ==2 - setuptools run: - h5py - matplotlib - numba - numpy - - progressbar >=2.3 - - python + - progressbar + - python ==2 - python.app # [osx] - pyyaml - scikit-image - scikit-learn - scipy - - wxpython ==3 + - wxpython test: imports: From ecaeeeae3817c1bfb907740f526b8f2918b2d8ca Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Fri, 21 Feb 2020 10:15:32 -0800 Subject: [PATCH 211/253] added blob finding to fiber generation --- conda.recipe/meta.yaml | 4 +-- hexrd/findorientations.py | 66 ++++++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 4ef0292f..b5f0a98d 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -29,14 +29,14 @@ requirements: - matplotlib - numba - numpy - - progressbar >=2.3 + - progressbar - python - python.app # [osx] - pyyaml - scikit-image - scikit-learn - scipy - - wxpython ==3 + - wxpython test: imports: diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index d4578bd2..ba721fd8 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -26,9 +26,7 @@ from hexrd.fitgrains import get_instrument_parameters -logger = logging.getLogger(__name__) - -save_as_ascii = False # FIX LATER... +from skimage.feature import blob_dog, blob_log # just require scikit-learn? have_sklearn = False @@ -43,9 +41,20 @@ pass +method = "blob_dog" # !!! have to get this from the config +save_as_ascii = False # FIX LATER... + +logger = logging.getLogger(__name__) + + +# ============================================================================= +# FUNCTIONS +# ============================================================================= + + def generate_orientation_fibers( eta_ome, chi, threshold, seed_hkl_ids, fiber_ndiv, - filt_stdev=0.8, ncpus=1): + method='blob_dog', filt_stdev=0.8, ncpus=1): """ From ome-eta maps and hklid spec, generate list of quaternions from fibers @@ -82,21 +91,42 @@ def generate_orientation_fibers( numSpots = [] coms = [] for i in seed_hkl_ids: - # First apply filter - this_map_f = -ndimage.filters.gaussian_laplace( - eta_ome.dataStore[i], filt_stdev) - - labels_t, numSpots_t = ndimage.label( - this_map_f > threshold, - structureNDI_label - ) - coms_t = np.atleast_2d( - ndimage.center_of_mass( - this_map_f, - labels=labels_t, - index=np.arange(1, np.amax(labels_t)+1) + if method == 'label': + # First apply filter + this_map_f = -ndimage.filters.gaussian_laplace( + eta_ome.dataStore[i], filt_stdev) + + labels_t, numSpots_t = ndimage.label( + this_map_f > threshold, + structureNDI_label ) - ) + coms_t = np.atleast_2d( + ndimage.center_of_mass( + this_map_f, + labels=labels_t, + index=np.arange(1, np.amax(labels_t) + 1) + ) + ) + elif method in ['blob_log', 'blob_dog']: + # must scale map + this_map = eta_ome.dataStore[i] + this_map[np.isnan(this_map)] = 0. + this_map -= np.min(this_map) + scl_map = 2*this_map/np.max(this_map) - 1. + + # FIXME: need to expose the parameters to config options. + if method == 'blob_log': + blobs = np.atleast_2d( + blob_log(scl_map, min_sigma=0.5, max_sigma=5, + num_sigma=10, threshold=0.01, overlap=0.1) + ) + else: + blobs = np.atleast_2d( + blob_dog(scl_map, min_sigma=0.5, max_sigma=5, + sigma_ratio=1.6, threshold=0.01, overlap=0.1) + ) + numSpots_t = len(blobs) + coms_t = blobs[:, :2] numSpots.append(numSpots_t) coms.append(coms_t) pass From 34ca77a96cdf6452bab2e13944d2ad788f35fff6 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 21 Feb 2020 13:29:29 -0800 Subject: [PATCH 212/253] force 2d output --- hexrd/findorientations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 8412135a..3d51a4c7 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -114,8 +114,10 @@ def generate_orientation_fibers( num_sigma=10, threshold=0.01, overlap=0.1) ) else: - blobs = blob_dog(scl_map, min_sigma=0.5, max_sigma=5, - sigma_ratio=1.6, threshold=0.01, overlap=0.1) + blobs = np.atleast_2d( + blob_dog(scl_map, min_sigma=0.5, max_sigma=5, + sigma_ratio=1.6, threshold=0.01, overlap=0.1) + ) numSpots_t = len(blobs) coms_t = blobs[:, :2] numSpots.append(numSpots_t) From 1d7a5348e5482e01425d4a9c6e49fd21d3c38b36 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 9 Mar 2020 21:01:52 -0700 Subject: [PATCH 213/253] auto sizing for imageseries stats buffer --- conda.recipe/meta.yaml | 1 + hexrd/imageseries/stats.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 581cba43..a17ba78d 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -29,6 +29,7 @@ requirements: - matplotlib - numba - numpy + - psutil - progressbar - python - python.app # [osx] diff --git a/hexrd/imageseries/stats.py b/hexrd/imageseries/stats.py index 4c1e394f..3247f936 100644 --- a/hexrd/imageseries/stats.py +++ b/hexrd/imageseries/stats.py @@ -1,14 +1,18 @@ """Stats for imageseries""" from __future__ import print_function -import numpy as np import logging +import numpy as np + +from psutil import virtual_memory from hexrd.imageseries.process import ProcessedImageSeries as PIS # Default Buffer: 100 MB #STATS_BUFFER = 419430400 # 50 GE frames -STATS_BUFFER = 838860800 # 100 GE frames +#STATS_BUFFER = 838860800 # 100 GE frames +vmem = virtual_memory() +STATS_BUFFER = int(0.5*vmem.available) def max(ims, nframes=0): nf = _nframes(ims, nframes) From a2c9fdee4cf72c3188afe955cde104870e813a4f Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 12 Mar 2020 10:59:20 -0700 Subject: [PATCH 214/253] change to calibration parameter I/O --- hexrd/instrument.py | 93 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 8ffbe1e3..be00b035 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -96,9 +96,9 @@ chi_DFLT = 0. t_vec_s_DFLT = np.zeros(3) -# [wavelength, chi, tvec_s, expmap_c, tec_c], len is 11 +# [wavelength, beam azim, beam pola, chi, tvec_s], len is 7 instr_param_flags_DFLT = np.array( - [0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1], + [0, 0, 0, 1, 0, 0, 0], dtype=bool) panel_param_flags_DFLT = np.array( [1, 1, 1, 1, 1, 1], @@ -368,7 +368,7 @@ def param_flags(self): @param_flags.setter def param_flags(self, x): x = np.array(x, dtype=bool).flatten() - assert len(x) == 11 + 6*self.num_panels, \ + assert len(x) == 7 + 6*self.num_panels, \ "length of parameter list must be %d; you gave %d" \ % (len(self._param_flags), len(x)) self._param_flags = x @@ -377,31 +377,94 @@ def param_flags(self, x): # METHODS # ========================================================================= - def calibration_params(self, expmap_c, tvec_c): - plist = np.zeros(11 + 6*self.num_panels) + def calibration_params(self): + """ + Yield the full list of adjustable parameters for + instument calibration. + + Parameters + ---------- + None + + Returns + ------- + retval : array + concatenated list of calibration parameters. + """ + azim, pola = calc_angles_from_beam_vec(self.beam_vector) + + ni = 7 + np = 6 + ng = 12 + + plist = np.zeros(ni + np*self.num_panels) plist[0] = self.beam_wavelength - plist[1] = self.chi - plist[2], plist[3], plist[4] = self.tvec - plist[5], plist[6], plist[7] = expmap_c - plist[8], plist[9], plist[10] = tvec_c + plist[1] = azim + plist[2] = pola + plist[3] = self.chi + plist[4], plist[5], plist[6] = self.tvec - ii = 11 + ii = ni for panel in self.detectors.itervalues(): - plist[ii:ii + 6] = np.hstack([ + plist[ii:ii + np] = np.hstack([ panel.tilt.flatten(), panel.tvec.flatten(), ]) - ii += 6 + ii += np + + # FIXME: FML!!! + # this assumes old style distiortion = (func, params) + for panel in self.detectors.itervalues(): + if panel.distortion is not None: + plist = np.concatenate( + [plist, panel.distortion[1]] + ) + + return plist + + + def update_from_calibration_params(plist): + """ + """ + ni = 7 + np = 6 + ng = 12 + + # check total length + len_plist = ni + np*self.num_panels + for panel in self.detectors.itervalues(): + if panel.distortion is not None: + len_plist += len(panel.distortion[1]) + if len(plist) > len_plist: + # ??? could have grains on here + raise RuntimeError("input plist is not the correct length") + + # updates + self.beam_wavelength = plist[0] + bvec = calc_beam_vec(plist[1], plist[2]) + self.beam_vector = bvec + self.chi = plist[3] + self.tvec = plist[4:7] + + ii = ni + for panel in self.detectors.itervalues(): + tilt_n_trans = plist[ii:ii + np] + panel.tilt = tilt_n_trans[:3] + panel.tvec = tilt_n_trans[3:] + ii += np # FIXME: FML!!! # this assumes old style distiortion = (func, params) - retval = plist for panel in self.detectors.itervalues(): if panel.distortion is not None: - retval = np.hstack([retval, panel.distortion[1]]) - return retval + ldp = len(panel.distortion[1]) + panel.distortion[1] = plist[ii:ii + ldp] + ii += ldp + + return + def write_config(self, filename=None, calibration_dict={}): """ WRITE OUT YAML FILE """ # initialize output dictionary From edebfc1d86913eec079bc9f5e1a9cf777bb3cc82 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Fri, 13 Mar 2020 11:17:23 -0700 Subject: [PATCH 215/253] more fixes to calibration param I/O --- hexrd/instrument.py | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index be00b035..7c99f2c3 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -104,6 +104,10 @@ [1, 1, 1, 1, 1, 1], dtype=bool) +NP_INS = 7 +NP_DET = 6 +NP_GRN = 12 + # ============================================================================= # UTILITY METHODS # ============================================================================= @@ -368,7 +372,7 @@ def param_flags(self): @param_flags.setter def param_flags(self, x): x = np.array(x, dtype=bool).flatten() - assert len(x) == 7 + 6*self.num_panels, \ + assert len(x) == NP_INS + NP_DET*self.num_panels, \ "length of parameter list must be %d; you gave %d" \ % (len(self._param_flags), len(x)) self._param_flags = x @@ -393,25 +397,21 @@ def calibration_params(self): """ azim, pola = calc_angles_from_beam_vec(self.beam_vector) - ni = 7 - np = 6 - ng = 12 - - plist = np.zeros(ni + np*self.num_panels) + plist = np.zeros(NP_INS + NP_DET*self.num_panels) - plist[0] = self.beam_wavelength + plist[0] = self.beam_energy plist[1] = azim plist[2] = pola plist[3] = self.chi plist[4], plist[5], plist[6] = self.tvec - ii = ni + ii = NP_INS for panel in self.detectors.itervalues(): - plist[ii:ii + np] = np.hstack([ + plist[ii:ii + NP_DET] = np.hstack([ panel.tilt.flatten(), panel.tvec.flatten(), ]) - ii += np + ii += NP_DET # FIXME: FML!!! # this assumes old style distiortion = (func, params) @@ -423,36 +423,31 @@ def calibration_params(self): return plist - - def update_from_calibration_params(plist): + def update_from_calibration_params(self, plist): """ """ - ni = 7 - np = 6 - ng = 12 - # check total length - len_plist = ni + np*self.num_panels + min_len_plist = NP_INS + NP_DET*self.num_panels for panel in self.detectors.itervalues(): if panel.distortion is not None: - len_plist += len(panel.distortion[1]) - if len(plist) > len_plist: + min_len_plist += len(panel.distortion[1]) + if len(plist) < min_len_plist: # ??? could have grains on here raise RuntimeError("input plist is not the correct length") # updates - self.beam_wavelength = plist[0] + self.beam_energy = plist[0] bvec = calc_beam_vec(plist[1], plist[2]) self.beam_vector = bvec self.chi = plist[3] self.tvec = plist[4:7] - ii = ni + ii = NP_INS for panel in self.detectors.itervalues(): - tilt_n_trans = plist[ii:ii + np] + tilt_n_trans = plist[ii:ii + NP_DET] panel.tilt = tilt_n_trans[:3] panel.tvec = tilt_n_trans[3:] - ii += np + ii += NP_DET # FIXME: FML!!! # this assumes old style distiortion = (func, params) @@ -461,10 +456,9 @@ def update_from_calibration_params(plist): ldp = len(panel.distortion[1]) panel.distortion[1] = plist[ii:ii + ldp] ii += ldp - + return - def write_config(self, filename=None, calibration_dict={}): """ WRITE OUT YAML FILE """ # initialize output dictionary From 47f3bf42f44810594d84c4ab67a3bfb6956d3dc2 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 19 Mar 2020 11:05:28 -0700 Subject: [PATCH 216/253] splash screen badness --- hexrd/wx/mainapp.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/hexrd/wx/mainapp.py b/hexrd/wx/mainapp.py index 413ba4e5..4c03cb6a 100644 --- a/hexrd/wx/mainapp.py +++ b/hexrd/wx/mainapp.py @@ -32,6 +32,7 @@ import sys import wx +from wx.adv import SplashScreen from hexrd.wx.mainframe import MainFrame from hexrd.xrd.experiment import loadExp, ImageModes @@ -130,11 +131,13 @@ def execute(*args): # splashFile = 'hexrd.png' splashDir = os.path.dirname(__file__) - splashImage = wx.Bitmap(os.path.join(splashDir, splashFile)) + splashImage = wx.Bitmap(os.path.join(splashDir, splashFile), + wx.BITMAP_TYPE_PNG) # - wx.SplashScreen(splashImage, - wx.SPLASH_CENTRE_ON_PARENT | wx.SPLASH_TIMEOUT, - 1000, app.mframe) + splash = SplashScreen( + splashImage, + wx.adv.SPLASH_CENTRE_ON_PARENT | wx.adv.SPLASH_TIMEOUT, + 1000, app.mframe) # # Main frame # From e9d92731f6469e47b1851b9ac49748949c82dd98 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Sun, 29 Mar 2020 23:25:44 -0700 Subject: [PATCH 217/253] modify setup.py for pip install; wxpython 4 updates --- hexrd/wx/caking.py | 108 ++++++++++++++++++------------------ hexrd/wx/cakingcanvas.py | 28 +++++----- hexrd/wx/canvaspanel.py | 22 ++++---- hexrd/wx/canvasutil.py | 22 ++++---- hexrd/wx/detectorpanel.py | 82 +++++++++++++-------------- hexrd/wx/fitparampanel.py | 4 +- hexrd/wx/floatcontrol.py | 6 +- hexrd/wx/gereader.py | 48 ++++++++-------- hexrd/wx/guiutil.py | 4 +- hexrd/wx/indexpanel.py | 66 +++++++++++----------- hexrd/wx/listeditor.py | 12 ++-- hexrd/wx/logwindows.py | 4 +- hexrd/wx/mainapp.py | 2 +- hexrd/wx/mainframe.py | 50 ++++++++--------- hexrd/wx/materialspanel.py | 66 +++++++++++----------- hexrd/wx/planedataeditor.py | 60 ++++++++++---------- hexrd/wx/readerinfo_dlg.py | 20 +++---- hexrd/wx/readerpanel.py | 4 +- hexrd/wx/ringsubpanel.py | 24 ++++---- hexrd/wx/selecthkls.py | 4 +- hexrd/wx/spotspanel.py | 50 ++++++++--------- hexrd/wx/xrdnotebook.py | 12 ++-- setup.py | 16 ++++++ 23 files changed, 365 insertions(+), 349 deletions(-) diff --git a/hexrd/wx/caking.py b/hexrd/wx/caking.py index 80e634d8..5fbfdebd 100644 --- a/hexrd/wx/caking.py +++ b/hexrd/wx/caking.py @@ -89,18 +89,18 @@ def __makeObjects(self): self.tbarSizer = makeTitleBar(self, 'Polar Rebinning') # - self.std_pan = standardOptsPanel(self, wx.NewId()) - self.mrb_pan = multiringOptsPanel(self, wx.NewId()) - self.sph_pan = sphericalOptsPanel(self, wx.NewId()) + self.std_pan = standardOptsPanel(self, wx.NewIdRef()) + self.mrb_pan = multiringOptsPanel(self, wx.NewIdRef()) + self.sph_pan = sphericalOptsPanel(self, wx.NewIdRef()) # # Method # - self.method_cho = wx.Choice(self, wx.NewId(), choices=prOpts.cakeMethods) + self.method_cho = wx.Choice(self, wx.NewIdRef(), choices=prOpts.cakeMethods) self.method_cho.SetSelection(2) # # Run # - self.run_but = wx.Button(self, wx.NewId(), 'Run Polar Rebin') + self.run_but = wx.Button(self, wx.NewIdRef(), 'Run Polar Rebin') # # Canvas for figures moved to separate window: see cakingCanvas.py # @@ -168,7 +168,7 @@ def __cake_img(self): 'args' : (), 'kwargs': dict() } - logwin = logWindow(self, wx.NewId(), action, 'Standard Polar Rebinning') + logwin = logWindow(self, wx.NewIdRef(), action, 'Standard Polar Rebinning') logwin.ShowModal() # # ============================== @@ -177,7 +177,7 @@ def __cake_img(self): # # Now draw # - cCan = cakeDisplay(self, wx.NewId(), prOpts.CAKE_IMG, self.img_info) + cCan = cakeDisplay(self, wx.NewIdRef(), prOpts.CAKE_IMG, self.img_info) # pass @@ -191,12 +191,12 @@ def __cake_rng(self): # 'args' : (), # 'kwargs': dict() # } - # logwin = logWindow(self, wx.NewId(), action, 'Multiring Binning') + # logwin = logWindow(self, wx.NewIdRef(), action, 'Multiring Binning') # logwin.ShowModal() # ==================== - cCan = cakeDisplay(self, wx.NewId(), prOpts.CAKE_RNG, self.mrb) + cCan = cakeDisplay(self, wx.NewIdRef(), prOpts.CAKE_RNG, self.mrb) return @@ -276,7 +276,7 @@ def __cake_sph(self): self.omeEta = CollapseOmeEta(reader, pdata, hklIDs, det, **kwargs) - cCan = cakeDisplay(self, wx.NewId(), prOpts.CAKE_SPH, self.omeEta) + cCan = cakeDisplay(self, wx.NewIdRef(), prOpts.CAKE_SPH, self.omeEta) return # @@ -355,7 +355,7 @@ def __init__(self, parent, id, **kwargs): # self.titlebar = wx.StaticText(self, -1, 'cakingDialog', style=wx.ALIGN_CENTER|wx.SIMPLE_BORDER) - self.dataPanel = cakingPanel(self, wx.NewId()) + self.dataPanel = cakingPanel(self, wx.NewIdRef()) # # Bindings. # @@ -445,33 +445,33 @@ def __makeObjects(self): # # Labels # - self.min_lab = wx.StaticText(self, wx.NewId(), 'min', style=wx.ALIGN_CENTER) - self.max_lab = wx.StaticText(self, wx.NewId(), 'max', style=wx.ALIGN_CENTER) - self.num_lab = wx.StaticText(self, wx.NewId(), 'num', style=wx.ALIGN_CENTER) - self.rho_lab = wx.StaticText(self, wx.NewId(), 'rho', style=wx.ALIGN_CENTER) - self.eta_lab = wx.StaticText(self, wx.NewId(), 'eta', style=wx.ALIGN_CENTER) + self.min_lab = wx.StaticText(self, wx.NewIdRef(), 'min', style=wx.ALIGN_CENTER) + self.max_lab = wx.StaticText(self, wx.NewIdRef(), 'max', style=wx.ALIGN_CENTER) + self.num_lab = wx.StaticText(self, wx.NewIdRef(), 'num', style=wx.ALIGN_CENTER) + self.rho_lab = wx.StaticText(self, wx.NewIdRef(), 'rho', style=wx.ALIGN_CENTER) + self.eta_lab = wx.StaticText(self, wx.NewIdRef(), 'eta', style=wx.ALIGN_CENTER) # # Rho # - self.rmin_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=10000, value=str(100)) - self.rmax_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=10000, value=str(1000)) - self.rnum_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=10000, value=str(500)) + self.rmin_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=10000, value=str(100)) + self.rmax_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=10000, value=str(1000)) + self.rnum_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=10000, value=str(500)) # # Eta # - self.emin_spn = wx.SpinCtrl(self, wx.NewId(), min=-360, max=360, value=str(0)) - self.emax_spn = wx.SpinCtrl(self, wx.NewId(), min=-360, max=360, value=str(360)) - self.enum_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=360, value=str(36)) + self.emin_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=-360, max=360, value=str(0)) + self.emax_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=-360, max=360, value=str(360)) + self.enum_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=360, value=str(36)) # # Other options # - self.corr_cbox = wx.CheckBox(self, wx.NewId(), 'corrected') - self.npdv_lab = wx.StaticText(self, wx.NewId(), 'pixel divisions', + self.corr_cbox = wx.CheckBox(self, wx.NewIdRef(), 'corrected') + self.npdv_lab = wx.StaticText(self, wx.NewIdRef(), 'pixel divisions', style=wx.ALIGN_CENTER) - self.npdv_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=10, initial=1) - self.frame_lab = wx.StaticText(self, wx.NewId(), 'frame', + self.npdv_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=10, initial=1) + self.frame_lab = wx.StaticText(self, wx.NewIdRef(), 'frame', style=wx.ALIGN_CENTER) - self.frame_cho = wx.Choice(self, wx.NewId(), choices=['frame 1']) + self.frame_cho = wx.Choice(self, wx.NewIdRef(), choices=['frame 1']) return @@ -629,28 +629,28 @@ def __makeObjects(self): self.tbarSizer = makeTitleBar(self, 'Options for Multiring Rebin') - self.ring_pan = ringPanel(self, wx.NewId()) + self.ring_pan = ringPanel(self, wx.NewIdRef()) - self.emin_lab = wx.StaticText(self, wx.NewId(), + self.emin_lab = wx.StaticText(self, wx.NewIdRef(), 'Eta min', style=wx.ALIGN_RIGHT) - self.emax_lab = wx.StaticText(self, wx.NewId(), + self.emax_lab = wx.StaticText(self, wx.NewIdRef(), 'Eta max', style=wx.ALIGN_RIGHT) - self.emin_txt = wx.TextCtrl(self, wx.NewId(), + self.emin_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) - self.emax_txt = wx.TextCtrl(self, wx.NewId(), + self.emax_txt = wx.TextCtrl(self, wx.NewIdRef(), value='360', style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) - self.numEta_lab = wx.StaticText(self, wx.NewId(), + self.numEta_lab = wx.StaticText(self, wx.NewIdRef(), 'Number of Eta Bins', style=wx.ALIGN_RIGHT) - self.numRho_lab = wx.StaticText(self, wx.NewId(), + self.numRho_lab = wx.StaticText(self, wx.NewIdRef(), 'Rho Bins Per Ring', style=wx.ALIGN_RIGHT) - self.numEta_spn = wx.SpinCtrl(self, wx.NewId(), min=1, value=str(36) ) - self.numRho_spn = wx.SpinCtrl(self, wx.NewId(), min=1, value=str(20) ) + self.numEta_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, value=str(36) ) + self.numRho_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, value=str(20) ) return @@ -783,45 +783,45 @@ def __makeObjects(self): # # Integer inputs # - self.lump_lab = wx.StaticText(self, wx.NewId(), '# lumped frames (omega)', style=wx.ALIGN_CENTER) - self.lump_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=1000, initial=1) + self.lump_lab = wx.StaticText(self, wx.NewIdRef(), '# lumped frames (omega)', style=wx.ALIGN_CENTER) + self.lump_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=1000, initial=1) - self.bins_lab = wx.StaticText(self, wx.NewId(), 'azimuthal bins (eta)', style=wx.ALIGN_CENTER) - self.bins_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=10000, initial=600) + self.bins_lab = wx.StaticText(self, wx.NewIdRef(), 'azimuthal bins (eta)', style=wx.ALIGN_CENTER) + self.bins_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=10000, initial=600) - self.thresh_lab = wx.StaticText(self, wx.NewId(), 'threshold', + self.thresh_lab = wx.StaticText(self, wx.NewIdRef(), 'threshold', style=wx.ALIGN_CENTER) - self.thresh_spn = wx.SpinCtrl(self, wx.NewId(), min=0, max=10000, initial=20) + self.thresh_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=0, max=10000, initial=20) # # Material and HKLs selector # exp = wx.GetApp().ws - self.matl_cho = wx.Choice(self, wx.NewId(), choices=exp.matNames) + self.matl_cho = wx.Choice(self, wx.NewIdRef(), choices=exp.matNames) self.matl_cho.SetSelection(0) - self.read_cho = wx.Choice(self, wx.NewId(), choices=exp.readerNames) + self.read_cho = wx.Choice(self, wx.NewIdRef(), choices=exp.readerNames) self.read_cho.SetSelection(0) - self.hkls_but = wx.Button(self, wx.NewId(), 'Select HKL') + self.hkls_but = wx.Button(self, wx.NewIdRef(), 'Select HKL') # # Angle/axis # name = 'angle' - self.angle_lab = wx.StaticText(self, wx.NewId(), name, style=wx.ALIGN_CENTER) - self.angle_flt = FloatControl(self, wx.NewId()) + self.angle_lab = wx.StaticText(self, wx.NewIdRef(), name, style=wx.ALIGN_CENTER) + self.angle_flt = FloatControl(self, wx.NewIdRef()) self.angle_flt.SetValue(1.0) name = 'axis x' - self.axis1_lab = wx.StaticText(self, wx.NewId(), name, style=wx.ALIGN_CENTER) - self.axis1_flt = FloatControl(self, wx.NewId()) + self.axis1_lab = wx.StaticText(self, wx.NewIdRef(), name, style=wx.ALIGN_CENTER) + self.axis1_flt = FloatControl(self, wx.NewIdRef()) self.axis1_flt.SetValue(1.0) name = 'axis y' - self.axis2_lab = wx.StaticText(self, wx.NewId(), name, style=wx.ALIGN_CENTER) - self.axis2_flt = FloatControl(self, wx.NewId()) + self.axis2_lab = wx.StaticText(self, wx.NewIdRef(), name, style=wx.ALIGN_CENTER) + self.axis2_flt = FloatControl(self, wx.NewIdRef()) self.axis2_flt.SetValue(1.0) name = 'axis z' - self.axis3_lab = wx.StaticText(self, wx.NewId(), name, style=wx.ALIGN_CENTER) - self.axis3_flt = FloatControl(self, wx.NewId()) + self.axis3_lab = wx.StaticText(self, wx.NewIdRef(), name, style=wx.ALIGN_CENTER) + self.axis3_flt = FloatControl(self, wx.NewIdRef()) self.axis3_flt.SetValue(1.0) @@ -904,7 +904,7 @@ def OnSelectHKLs(self, evt): exp = app.ws mat = exp.activeMaterial - dlg = hklsDlg(self, wx.NewId(), mat) + dlg = hklsDlg(self, wx.NewIdRef(), mat) if dlg.ShowModal() == wx.ID_OK: mat.planeData.exclusions = dlg.getExclusions() diff --git a/hexrd/wx/cakingcanvas.py b/hexrd/wx/cakingcanvas.py index d3bcea27..49b98dbe 100644 --- a/hexrd/wx/cakingcanvas.py +++ b/hexrd/wx/cakingcanvas.py @@ -106,16 +106,16 @@ def __makeObjects(self): self.tbarSizer = makeTitleBar(self, 'cakeCanvas') # if self.cakeType == prOpts.CAKE_IMG: - self.opt_pan = imgOpts(self, wx.NewId()) + self.opt_pan = imgOpts(self, wx.NewIdRef()) # full image caking panel pass elif self.cakeType == prOpts.CAKE_RNG: # multiring panel - self.opt_pan = rngOpts(self, wx.NewId()) + self.opt_pan = rngOpts(self, wx.NewIdRef()) pass elif self.cakeType == prOpts.CAKE_SPH: # omega-eta panel - self.opt_pan = sphOpts(self, wx.NewId()) + self.opt_pan = sphOpts(self, wx.NewIdRef()) pass self._makeFigureCanvas() @@ -126,7 +126,7 @@ def __makeObjects(self): def _makeFigureCanvas(self): """Build figure canvas""" self.figure = Figure() - self.canvas = FigureCanvas(self, wx.NewId(), self.figure) + self.canvas = FigureCanvas(self, wx.NewIdRef(), self.figure) self.axes = self.figure.gca() self.axes.set_aspect('equal') @@ -217,7 +217,7 @@ def __makeObjects(self): # # Add canvas panel # - self.cpan = cakeCanvas(self, wx.NewId(), self.cakeType, self.data) + self.cpan = cakeCanvas(self, wx.NewIdRef(), self.cakeType, self.data) # # A Statusbar in the bottom of the window # @@ -298,8 +298,8 @@ def __makeObjects(self): self.tbarSizer = makeTitleBar(self, 'Multiring Rebinning Results') # - self.unit_cho = wx.Choice(self, wx.NewId(), choices=rngOpts.unitList) - self.exp_but = wx.Button(self, wx.NewId(), 'Export') + self.unit_cho = wx.Choice(self, wx.NewIdRef(), choices=rngOpts.unitList) + self.exp_but = wx.Button(self, wx.NewIdRef(), 'Export') #self.Bind(wx.EVT_CHOICE, self.OnChoice, self.choice) return @@ -408,8 +408,8 @@ def __init__(self, parent, id, **kwargs): def __makeObjects(self): """Add interactors""" self.tbarSizer = makeTitleBar(self, 'Full Image Rebinning Results') - self.cmPanel = cmapPanel(self, wx.NewId()) - self.exp_but = wx.Button(self, wx.NewId(), 'Export') + self.cmPanel = cmapPanel(self, wx.NewIdRef()) + self.exp_but = wx.Button(self, wx.NewIdRef(), 'Export') # return @@ -574,24 +574,24 @@ def __init__(self, parent, id, **kwargs): def __makeObjects(self): """Add interactors""" exp = wx.GetApp().ws - self.cmPanel = cmapPanel(self, wx.NewId()) + self.cmPanel = cmapPanel(self, wx.NewIdRef()) self.tbarSizer = makeTitleBar(self, 'Omega-Eta Plots', color=WP.BG_COLOR_TITLEBAR_PANEL1) # choice interactor for HKL hkls = exp.activeMaterial.planeData.getHKLs(asStr=True) - self.hkl_cho = wx.Choice(self, wx.NewId(), choices=hkls) + self.hkl_cho = wx.Choice(self, wx.NewIdRef(), choices=hkls) self.hkl_cho.SetSelection(0) - self.disp_cho = wx.Choice(self, wx.NewId(), choices=self.DISP_METHODS) + self.disp_cho = wx.Choice(self, wx.NewIdRef(), choices=self.DISP_METHODS) self.disp_cho.SetSelection(0) self.idata = 0 self.dispm = self.DISP_RAW self.coms = None # centers of mass from optional labeling - self.exp_but = wx.Button(self, wx.NewId(), 'Export') - self.lab_but = wx.Button(self, wx.NewId(), 'Label Spots') + self.exp_but = wx.Button(self, wx.NewIdRef(), 'Export') + self.lab_but = wx.Button(self, wx.NewIdRef(), 'Label Spots') return def __makeBindings(self): diff --git a/hexrd/wx/canvaspanel.py b/hexrd/wx/canvaspanel.py index 31161095..d10777d2 100644 --- a/hexrd/wx/canvaspanel.py +++ b/hexrd/wx/canvaspanel.py @@ -95,7 +95,7 @@ def __makeObjects(self): # # * show image # - self.showImage_box = wx.CheckBox(self, wx.NewId(), + self.showImage_box = wx.CheckBox(self, wx.NewIdRef(), 'Show Image') self.showImage_box.SetValue(True) self.optSizer.Add(self.showImage_box, 0, wx.LEFT | wx.EXPAND) @@ -103,7 +103,7 @@ def __makeObjects(self): # # * show rings # - self.showCalRings_box = wx.CheckBox(self, wx.NewId(), + self.showCalRings_box = wx.CheckBox(self, wx.NewIdRef(), 'Show Rings') self.showCalRings_box.SetValue(False) # default self.optSizer.Add(self.showCalRings_box, 0, wx.LEFT | wx.EXPAND) @@ -111,7 +111,7 @@ def __makeObjects(self): # # * show ranges # - self.showCalRanges_box = wx.CheckBox(self, wx.NewId(), + self.showCalRanges_box = wx.CheckBox(self, wx.NewIdRef(), 'Show Ranges') self.showCalRanges_box.SetValue(False) # default self.optSizer.Add(self.showCalRanges_box, 0, wx.LEFT | wx.EXPAND) @@ -119,23 +119,23 @@ def __makeObjects(self): # # Add image list management # - self.ail_lab = wx.StaticText(self, wx.NewId(), 'Load Image', style=wx.ALIGN_CENTER) - self.ail_cho = wx.Choice(self, wx.NewId(), choices=[]) - self.nam_lab = wx.StaticText(self, wx.NewId(), 'Name Image', style=wx.ALIGN_CENTER) - self.nam_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.ail_lab = wx.StaticText(self, wx.NewIdRef(), 'Load Image', style=wx.ALIGN_CENTER) + self.ail_cho = wx.Choice(self, wx.NewIdRef(), choices=[]) + self.nam_lab = wx.StaticText(self, wx.NewIdRef(), 'Name Image', style=wx.ALIGN_CENTER) + self.nam_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.eil_but = wx.Button(self, wx.NewId(), 'Edit List') + self.eil_but = wx.Button(self, wx.NewIdRef(), 'Edit List') # # Add colormap panel # - self.cmPanel = cmapPanel(self, wx.NewId()) + self.cmPanel = cmapPanel(self, wx.NewIdRef()) # # ===== FIGURE CANVAS # self.figure = Figure() self.axes = self.figure.gca() self.axes.set_aspect('equal') - self.canvas = FigureCanvas(self, wx.NewId(), self.figure) + self.canvas = FigureCanvas(self, wx.NewIdRef(), self.figure) self.__add_toolbar() # comment this out for no toolbar @@ -426,7 +426,7 @@ def OnEditImg(self, evt): ssel = self.ail_cho.GetStringSelection() - dlg = ListEditDlg(self, wx.NewId(), nilist) + dlg = ListEditDlg(self, wx.NewIdRef(), nilist) dlg.ShowModal() dlg.Destroy() diff --git a/hexrd/wx/canvasutil.py b/hexrd/wx/canvasutil.py index a2656cbb..98f11d48 100644 --- a/hexrd/wx/canvasutil.py +++ b/hexrd/wx/canvasutil.py @@ -95,7 +95,7 @@ def __makeObjects(self): # # * choose colormap and vmin and vmax # - self.cmap_lab = wx.StaticText(self, wx.NewId(), + self.cmap_lab = wx.StaticText(self, wx.NewIdRef(), 'Colormap: ', style=wx.ALIGN_RIGHT) @@ -103,39 +103,39 @@ def __makeObjects(self): 'flag', 'gray', 'gray_r', 'hot', 'hot_r', 'hsv', 'jet', 'pink', 'prism', 'spring', 'summer', 'winter', 'spectral'] - self.cmap_cho = wx.Choice(self, wx.NewId(), + self.cmap_cho = wx.Choice(self, wx.NewIdRef(), choices=self.cmap_nameList) self.cmap_name = 'bone' self.cmap_cho.SetStringSelection(self.cmap_name) self.cmin_val = 0 - self.cmin_lab = wx.StaticText(self, wx.NewId(), + self.cmin_lab = wx.StaticText(self, wx.NewIdRef(), 'Minimum: ', style=wx.ALIGN_RIGHT) - self.cmin_txt = wx.TextCtrl(self, wx.NewId(), + self.cmin_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(self.cmin_val), style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) - self.cmUnder_box = wx.CheckBox(self, wx.NewId(), 'show under') + self.cmUnder_box = wx.CheckBox(self, wx.NewIdRef(), 'show under') self.cmax_val = 2000 - self.cmax_lab = wx.StaticText(self, wx.NewId(), + self.cmax_lab = wx.StaticText(self, wx.NewIdRef(), 'Maximum: ', style=wx.ALIGN_RIGHT) - self.cmax_txt = wx.TextCtrl(self, wx.NewId(), + self.cmax_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(self.cmax_val), style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) - self.cmOver_box = wx.CheckBox(self, wx.NewId(), 'show over') + self.cmOver_box = wx.CheckBox(self, wx.NewIdRef(), 'show over') self.apply_filter = False self.filter_val = 0.8 - self.applyFilter_txt = wx.TextCtrl(self, wx.NewId(), + self.applyFilter_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(self.filter_val), style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) - self.applyFilter_lab = wx.StaticText(self, wx.NewId(), + self.applyFilter_lab = wx.StaticText(self, wx.NewIdRef(), 'Apply filter: ', style=wx.ALIGN_RIGHT) - self.applyFilter_box = wx.CheckBox(self, wx.NewId(), 'apply filter') + self.applyFilter_box = wx.CheckBox(self, wx.NewIdRef(), 'apply filter') return diff --git a/hexrd/wx/detectorpanel.py b/hexrd/wx/detectorpanel.py index 76e5ea6c..25c3f981 100644 --- a/hexrd/wx/detectorpanel.py +++ b/hexrd/wx/detectorpanel.py @@ -109,14 +109,14 @@ def __makeObjects(self): # # Material Selection # - self.mats_lab = wx.StaticText(self, wx.NewId(), + self.mats_lab = wx.StaticText(self, wx.NewIdRef(), 'Active Material', style=wx.ALIGN_CENTER) - self.mats_cho = wx.Choice(self, wx.NewId(), + self.mats_cho = wx.Choice(self, wx.NewIdRef(), choices=[m.name for m in exp.matList]) # # Rings panel # - self.ring_pan = ringPanel(self, wx.NewId()) + self.ring_pan = ringPanel(self, wx.NewIdRef()) # # II. Geometry # @@ -128,47 +128,47 @@ def __makeObjects(self): app = wx.GetApp() det = app.ws.detector - self.nrows_txt = wx.TextCtrl(self, wx.NewId(), value=str(det.nrows), style=wx.RAISED_BORDER) - self.ncols_txt = wx.TextCtrl(self, wx.NewId(), value=str(det.ncols), style=wx.RAISED_BORDER) - self.pixel_txt = wx.TextCtrl(self, wx.NewId(), value=str(det.pixelPitch), style=wx.RAISED_BORDER) - self.pixel_txt_s = wx.TextCtrl(self, wx.NewId(), value=str(det.pixelPitch), style=wx.RAISED_BORDER|wx.TE_READONLY) + self.nrows_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(det.nrows), style=wx.RAISED_BORDER) + self.ncols_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(det.ncols), style=wx.RAISED_BORDER) + self.pixel_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(det.pixelPitch), style=wx.RAISED_BORDER) + self.pixel_txt_s = wx.TextCtrl(self, wx.NewIdRef(), value=str(det.pixelPitch), style=wx.RAISED_BORDER|wx.TE_READONLY) name = 'x Center' - self.cbox_xc = wx.CheckBox(self, wx.NewId(), name) - self.float_xc = FloatControl(self, wx.NewId()) + self.cbox_xc = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_xc = FloatControl(self, wx.NewIdRef()) self.float_xc.SetValue(det.xc) self.float_xc.SetDelta(0.5*det.pixelPitch) name = 'y Center' - self.cbox_yc = wx.CheckBox(self, wx.NewId(), name) - self.float_yc = FloatControl(self, wx.NewId()) + self.cbox_yc = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_yc = FloatControl(self, wx.NewIdRef()) self.float_yc.SetValue(det.yc) self.float_yc.SetDelta(0.5*det.pixelPitch) name = 'Distance' - self.cbox_D = wx.CheckBox(self, wx.NewId(), name) - self.float_D = FloatControl(self, wx.NewId()) + self.cbox_D = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_D = FloatControl(self, wx.NewIdRef()) self.float_D.SetValue(det.workDist) self.float_D.SetDelta(10*det.pixelPitch) name = 'x Tilt' - self.cbox_xt = wx.CheckBox(self, wx.NewId(), name) - self.float_xt = FloatControl(self, wx.NewId()) + self.cbox_xt = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_xt = FloatControl(self, wx.NewIdRef()) self.float_xt.SetValue(det.xTilt) name = 'y Tilt' - self.cbox_yt = wx.CheckBox(self, wx.NewId(), name) - self.float_yt = FloatControl(self, wx.NewId()) + self.cbox_yt = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_yt = FloatControl(self, wx.NewIdRef()) self.float_yt.SetValue(det.yTilt) name = 'z Tilt' - self.cbox_zt = wx.CheckBox(self, wx.NewId(), name) - self.float_zt = FloatControl(self, wx.NewId()) + self.cbox_zt = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_zt = FloatControl(self, wx.NewIdRef()) self.float_zt.SetValue(det.zTilt) name = 'chi Tilt' - self.cbox_ct = wx.CheckBox(self, wx.NewId(), name) - self.float_ct = FloatControl(self, wx.NewId()) + self.cbox_ct = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_ct = FloatControl(self, wx.NewIdRef()) self.float_ct.SetValue(det.chiTilt) # @@ -179,57 +179,57 @@ def __makeObjects(self): # number (if any at all) will change for each # detector type. name = 'p0' - self.cbox_d1 = wx.CheckBox(self, wx.NewId(), name) - self.float_d1 = FloatControl(self, wx.NewId()) + self.cbox_d1 = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_d1 = FloatControl(self, wx.NewIdRef()) self.float_d1.SetValue(det.dparms[0]) name = 'p1' - self.cbox_d2 = wx.CheckBox(self, wx.NewId(), name) - self.float_d2 = FloatControl(self, wx.NewId()) + self.cbox_d2 = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_d2 = FloatControl(self, wx.NewIdRef()) self.float_d2.SetValue(det.dparms[1]) name = 'p2' - self.cbox_d3 = wx.CheckBox(self, wx.NewId(), name) - self.float_d3 = FloatControl(self, wx.NewId()) + self.cbox_d3 = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_d3 = FloatControl(self, wx.NewIdRef()) self.float_d3.SetValue(det.dparms[2]) name = 'n0' - self.cbox_d4 = wx.CheckBox(self, wx.NewId(), name) - self.float_d4 = FloatControl(self, wx.NewId()) + self.cbox_d4 = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_d4 = FloatControl(self, wx.NewIdRef()) self.float_d4.SetValue(det.dparms[3]) name = 'n1' - self.cbox_d5 = wx.CheckBox(self, wx.NewId(), name) - self.float_d5 = FloatControl(self, wx.NewId()) + self.cbox_d5 = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_d5 = FloatControl(self, wx.NewIdRef()) self.float_d5.SetValue(det.dparms[4]) name = 'n2' - self.cbox_d6 = wx.CheckBox(self, wx.NewId(), name) - self.float_d6 = FloatControl(self, wx.NewId()) + self.cbox_d6 = wx.CheckBox(self, wx.NewIdRef(), name) + self.float_d6 = FloatControl(self, wx.NewIdRef()) self.float_d6.SetValue(det.dparms[5]) # # Fitting method # self.fitLabelSizer = makeTitleBar(self, 'Fitting Method', color=WP.TITLEBAR_BG_COLOR_PANEL1) - self.fitDir_rb = wx.RadioButton(self, wx.NewId(), 'Direct Fit', + self.fitDir_rb = wx.RadioButton(self, wx.NewIdRef(), 'Direct Fit', style=wx.RB_GROUP) - self.fitBin_rb = wx.RadioButton(self, wx.NewId(), 'Binned Fit') + self.fitBin_rb = wx.RadioButton(self, wx.NewIdRef(), 'Binned Fit') # # III. Caking # - self.numEta_lab = wx.StaticText(self, wx.NewId(), + self.numEta_lab = wx.StaticText(self, wx.NewIdRef(), 'Azimuthal bins', style=wx.ALIGN_RIGHT) - self.numRho_lab = wx.StaticText(self, wx.NewId(), + self.numRho_lab = wx.StaticText(self, wx.NewIdRef(), 'Radial bins per ring', style=wx.ALIGN_RIGHT) - self.numEta_spn = wx.SpinCtrl(self, wx.NewId(), min=12, initial=36) - self.numRho_spn = wx.SpinCtrl(self, wx.NewId(), min=10, initial=20) + self.numEta_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=12, initial=36) + self.numRho_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=10, initial=20) # # Fit button with options (at some point) # - self.runFit_but = wx.Button(self, wx.NewId(), 'Run Fit') + self.runFit_but = wx.Button(self, wx.NewIdRef(), 'Run Fit') return @@ -498,7 +498,7 @@ def OnRunFit(self, evt): # 'args': (), # 'kwargs': dict() # } - # logwin = logWindow(self, wx.NewId(), action, 'Fitting Log') + # logwin = logWindow(self, wx.NewIdRef(), action, 'Fitting Log') # logwin.ShowModal() # # except Exception as e: diff --git a/hexrd/wx/fitparampanel.py b/hexrd/wx/fitparampanel.py index 1058c46b..d4490b90 100644 --- a/hexrd/wx/fitparampanel.py +++ b/hexrd/wx/fitparampanel.py @@ -87,8 +87,8 @@ def __makeObjects(self): for p in self.fParams: name = p.getProp('name') valu = p.getProp('value') - cbox = wx.CheckBox(self, wx.NewId(), name) - spin = wx.SpinCtrl(self, wx.NewId(), str(valu), initial=50, name=name) + cbox = wx.CheckBox(self, wx.NewIdRef(), name) + spin = wx.SpinCtrl(self, wx.NewIdRef(), str(valu), initial=50, name=name) self.rowDict[name] = [cbox, spin] pass diff --git a/hexrd/wx/floatcontrol.py b/hexrd/wx/floatcontrol.py index 798ec25f..d3829449 100644 --- a/hexrd/wx/floatcontrol.py +++ b/hexrd/wx/floatcontrol.py @@ -109,14 +109,14 @@ def __init__(self, parent, id, **kwargs): # def __makeObjects(self): """Add interactors""" - self.value_txt = wx.TextCtrl(self, wx.NewId(), + self.value_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(self.value), style=wx.RAISED_BORDER| wx.TE_PROCESS_ENTER) - self.delta_txt = wx.TextCtrl(self, wx.NewId(), + self.delta_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(self.delta), style=wx.RAISED_BORDER| wx.TE_PROCESS_ENTER) self.delta_txt.SetBackgroundColour( (230, 230, 230) ) - self.spin_but = wx.SpinButton(self, wx.NewId()) + self.spin_but = wx.SpinButton(self, wx.NewIdRef()) self.spin_but.SetRange(-1,1) return diff --git a/hexrd/wx/gereader.py b/hexrd/wx/gereader.py index 7d02e537..28a19a15 100644 --- a/hexrd/wx/gereader.py +++ b/hexrd/wx/gereader.py @@ -151,58 +151,58 @@ def __makeObjects(self): # # Reader List # - self.curr_lab = wx.StaticText(self, wx.NewId(), + self.curr_lab = wx.StaticText(self, wx.NewIdRef(), 'Current Reader', style=wx.ALIGN_CENTER) - self.rdrs_cho = wx.Choice(self, wx.NewId(), + self.rdrs_cho = wx.Choice(self, wx.NewIdRef(), choices=[r.name for r in exp.savedReaders]) - self.new_but = wx.Button(self, wx.NewId(), 'New Reader') + self.new_but = wx.Button(self, wx.NewIdRef(), 'New Reader') # # Reader Name # - self.name_lab = wx.StaticText(self, wx.NewId(), + self.name_lab = wx.StaticText(self, wx.NewIdRef(), 'READER NAME', style=wx.ALIGN_CENTER) - self.name_txt = wx.TextCtrl(self, wx.NewId(), value=ReaderInput.DFLT_NAME, + self.name_txt = wx.TextCtrl(self, wx.NewIdRef(), value=ReaderInput.DFLT_NAME, style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) # # Mode interactors # - self.mode_lab = wx.StaticText(self, wx.NewId(), 'Image Mode', + self.mode_lab = wx.StaticText(self, wx.NewIdRef(), 'Image Mode', style=wx.ALIGN_RIGHT) - self.mode_cho = wx.Choice(self, wx.NewId(), choices=MODE_CHOICES) + self.mode_cho = wx.Choice(self, wx.NewIdRef(), choices=MODE_CHOICES) # # Aggregation # - self.agg_lab = wx.StaticText(self, wx.NewId(), 'Frame Aggregation', + self.agg_lab = wx.StaticText(self, wx.NewIdRef(), 'Frame Aggregation', style=wx.ALIGN_RIGHT) - self.agg_cho = wx.Choice(self, wx.NewId(), choices=AGG_CHOICES) + self.agg_cho = wx.Choice(self, wx.NewIdRef(), choices=AGG_CHOICES) # # # Image and dark file names # - self.img_but = wx.Button(self, wx.NewId(), 'Select Imageseries File') - self.dir_but = wx.Button(self, wx.NewId(), 'Change Image Folder') + self.img_but = wx.Button(self, wx.NewIdRef(), 'Select Imageseries File') + self.dir_but = wx.Button(self, wx.NewIdRef(), 'Change Image Folder') # # Action buttons # - self.files_lab = wx.StaticText(self, wx.NewId(), 'Image Files', + self.files_lab = wx.StaticText(self, wx.NewIdRef(), 'Image Files', style=wx.ALIGN_RIGHT) - self.read_lab = wx.StaticText(self, wx.NewId(), 'Read', + self.read_lab = wx.StaticText(self, wx.NewIdRef(), 'Read', style=wx.ALIGN_RIGHT) - self.read_but = wx.Button(self, wx.NewId(), 'Load') - self.browse_lab = wx.StaticText(self, wx.NewId(), 'Browse Frames', + self.read_but = wx.Button(self, wx.NewIdRef(), 'Load') + self.browse_lab = wx.StaticText(self, wx.NewIdRef(), 'Browse Frames', style=wx.ALIGN_RIGHT) - self.browse_spn = wx.SpinCtrl(self, wx.NewId(), min=0, initial=0) - self.browse_inf = wx.TextCtrl(self, wx.NewId(), value='', + self.browse_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=0, initial=0) + self.browse_inf = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_READONLY) self.sizer = wx.BoxSizer(wx.VERTICAL) # # Subpanels # - self.sp_single = SF_Subpanel(self, wx.NewId()) - self.sp_multi = MF_Subpanel(self, wx.NewId()) - self.sp_info = infoPanel(self, wx.NewId()) + self.sp_single = SF_Subpanel(self, wx.NewIdRef()) + self.sp_multi = MF_Subpanel(self, wx.NewIdRef()) + self.sp_info = infoPanel(self, wx.NewIdRef()) return @@ -562,7 +562,7 @@ def __makeListCtrl(self): # LStyle = wx.LC_REPORT|wx.LC_SINGLE_SEL # - listctrl = myListCtrl(self, wx.NewId(), style=LStyle) + listctrl = myListCtrl(self, wx.NewIdRef(), style=LStyle) listctrl.InsertColumn(0, 'Image File') listctrl.InsertColumn(1, 'Empty Frames') listctrl.InsertColumn(2, 'Total Frames') @@ -674,7 +674,7 @@ def __makeListCtrl(self): # LStyle = wx.LC_REPORT|wx.LC_SINGLE_SEL # - listctrl = wx.ListCtrl(self, wx.NewId(), style=LStyle) + listctrl = wx.ListCtrl(self, wx.NewIdRef(), style=LStyle) listctrl.InsertColumn(0, 'Image File') listctrl.InsertColumn(1, 'Omega') listctrl.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER) @@ -750,9 +750,9 @@ def __makeObjects(self): # File lists for display. # - self.img_txt_lab = wx.StaticText(self, wx.NewId(), 'Image Directory', + self.img_txt_lab = wx.StaticText(self, wx.NewIdRef(), 'Image Directory', style=wx.ALIGN_CENTER) - self.img_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.img_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER) return diff --git a/hexrd/wx/guiutil.py b/hexrd/wx/guiutil.py index f3c70a6a..e92e71af 100644 --- a/hexrd/wx/guiutil.py +++ b/hexrd/wx/guiutil.py @@ -105,7 +105,7 @@ def makeTitleBar(p, t, **kwargs): We use a workaround by creating a sizer with colored boxes on either side. """ - titlebar = wx.StaticText(p, wx.NewId(), t, + titlebar = wx.StaticText(p, wx.NewIdRef(), t, style=wx.ALIGN_CENTER|wx.SIMPLE_BORDER) # # Keyword args @@ -142,7 +142,7 @@ def makeTitleBar(p, t, **kwargs): def callJoel(p): """Return message to display on empty pages""" - hpage = wx.html.HtmlWindow(p, wx.NewId()) + hpage = wx.html.HtmlWindow(p, wx.NewIdRef()) msg = r""" diff --git a/hexrd/wx/indexpanel.py b/hexrd/wx/indexpanel.py index 6edda0ab..b892117a 100644 --- a/hexrd/wx/indexpanel.py +++ b/hexrd/wx/indexpanel.py @@ -86,13 +86,13 @@ def __makeObjects(self): ind_opts = exp.index_opts self.sz_titlebar = makeTitleBar(self, 'Indexing') - self.method_cho = wx.Choice(self, wx.NewId(), + self.method_cho = wx.Choice(self, wx.NewIdRef(), choices=ind_opts.INDEX_CHOICES) self.method_cho.SetSelection(ind_opts.IND_FIBER) - self.run_but = wx.Button(self, wx.NewId(), 'Run Indexer') + self.run_but = wx.Button(self, wx.NewIdRef(), 'Run Indexer') - self.fiber_pan = FiberSearchPanel(self, wx.NewId()) - self.gspot_pan = GrainSpotterPanel(self, wx.NewId()) + self.fiber_pan = FiberSearchPanel(self, wx.NewIdRef()) + self.gspot_pan = GrainSpotterPanel(self, wx.NewIdRef()) return @@ -202,59 +202,59 @@ def __makeObjects(self): # checkboxes - self.friedel_cbox = wx.CheckBox(self, wx.NewId(), 'Friedel Only') + self.friedel_cbox = wx.CheckBox(self, wx.NewIdRef(), 'Friedel Only') self.friedel_cbox.SetValue(iopts.friedelOnly) - self.claims_cbox = wx.CheckBox(self, wx.NewId(), 'Preserve Claims') + self.claims_cbox = wx.CheckBox(self, wx.NewIdRef(), 'Preserve Claims') self.claims_cbox.SetValue(iopts.preserveClaims) - self.refine_cbox = wx.CheckBox(self, wx.NewId(), 'Do Refinement') + self.refine_cbox = wx.CheckBox(self, wx.NewIdRef(), 'Do Refinement') self.refine_cbox.SetValue(iopts.doRefinement) - self.multi_cbox = wx.CheckBox(self, wx.NewId(), 'Use Multiprocessing') + self.multi_cbox = wx.CheckBox(self, wx.NewIdRef(), 'Use Multiprocessing') self.multi_cbox.SetValue(iopts.doMultiProc) # value boxes - self.etol_lab = wx.StaticText(self, wx.NewId(), 'Eta Tolerance', + self.etol_lab = wx.StaticText(self, wx.NewIdRef(), 'Eta Tolerance', style=wx.ALIGN_RIGHT) - self.etol_txt = wx.TextCtrl(self, wx.NewId(), value=str(iopts.etaTol), + self.etol_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(iopts.etaTol), style=wx.RAISED_BORDER) - self.otol_lab = wx.StaticText(self, wx.NewId(), 'Omega Tolerance', + self.otol_lab = wx.StaticText(self, wx.NewIdRef(), 'Omega Tolerance', style=wx.ALIGN_RIGHT) - self.otol_txt = wx.TextCtrl(self, wx.NewId(), value=str(iopts.omeTol), + self.otol_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(iopts.omeTol), style=wx.RAISED_BORDER) - self.steps_lab = wx.StaticText(self, wx.NewId(), 'Number of Steps', + self.steps_lab = wx.StaticText(self, wx.NewIdRef(), 'Number of Steps', style=wx.ALIGN_RIGHT) - self.steps_spn = wx.SpinCtrl(self, wx.NewId(), + self.steps_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=36, max=36000, initial=iopts.nsteps) label = 'Minimum Completeness' - self.comp_lab = wx.StaticText(self, wx.NewId(), label, + self.comp_lab = wx.StaticText(self, wx.NewIdRef(), label, style=wx.ALIGN_RIGHT) - self.comp_txt = wx.TextCtrl(self, wx.NewId(), + self.comp_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(iopts.minCompleteness), style=wx.RAISED_BORDER) label = 'Minimum Fraction Claimed' - self.claim_lab = wx.StaticText(self, wx.NewId(), label, + self.claim_lab = wx.StaticText(self, wx.NewIdRef(), label, style=wx.ALIGN_RIGHT) - self.claim_txt = wx.TextCtrl(self, wx.NewId(), + self.claim_txt = wx.TextCtrl(self, wx.NewIdRef(), value=str(iopts.minPctClaimed), style=wx.RAISED_BORDER) label = 'Number of CPUs' - self.ncpus_lab = wx.StaticText(self, wx.NewId(), label, + self.ncpus_lab = wx.StaticText(self, wx.NewIdRef(), label, style=wx.ALIGN_RIGHT) - self.ncpus_spn = wx.SpinCtrl(self, wx.NewId(), + self.ncpus_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=ncpus_DFLT, initial=ncpus_DFLT) label = 'Quit After This Many' - self.qafter_lab = wx.StaticText(self, wx.NewId(), label, + self.qafter_lab = wx.StaticText(self, wx.NewIdRef(), label, style=wx.ALIGN_RIGHT) - self.qafter_spn = wx.SpinCtrl(self, wx.NewId(), + self.qafter_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=0, max=100000, initial=0) - self.hkls_but = wx.Button(self, wx.NewId(), 'HKLs') + self.hkls_but = wx.Button(self, wx.NewIdRef(), 'HKLs') return @@ -344,7 +344,7 @@ def OnRunHKLs(self, evt): iopts = exp.index_opts pd = exp.activeMaterial.planeData - hkls_dlg = HklsDlg(self, wx.NewId(), exp.activeMaterial) + hkls_dlg = HklsDlg(self, wx.NewIdRef(), exp.activeMaterial) if hkls_dlg.ShowModal() == wx.ID_OK: # pd.exclusions = hkls_dlg.getExclusions() @@ -394,29 +394,29 @@ def __makeObjects(self): self.tbarSizer = makeTitleBar(self, 'Grain Spotter Options', color=WP.BG_COLOR_PANEL1_TITLEBAR) - self.pfit_cbox = wx.CheckBox(self, wx.NewId(), 'Position Fit') + self.pfit_cbox = wx.CheckBox(self, wx.NewIdRef(), 'Position Fit') label = 'Minimum Completeness' - self.comp_lab = wx.StaticText(self, wx.NewId(), label, + self.comp_lab = wx.StaticText(self, wx.NewIdRef(), label, style=wx.ALIGN_CENTER) - self.comp_txt = wx.TextCtrl(self, wx.NewId(), value='0.5', + self.comp_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0.5', style=wx.RAISED_BORDER) label = 'Minimum Fraction of G-Vectors' - self.fracG_lab = wx.StaticText(self, wx.NewId(), label, + self.fracG_lab = wx.StaticText(self, wx.NewIdRef(), label, style=wx.ALIGN_CENTER) - self.fracG_txt = wx.TextCtrl(self, wx.NewId(), value='0.5', + self.fracG_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0.5', style=wx.RAISED_BORDER) label = 'Sigmas' - self.sigmas_lab = wx.StaticText(self, wx.NewId(), label, + self.sigmas_lab = wx.StaticText(self, wx.NewIdRef(), label, style=wx.ALIGN_CENTER) - self.sigmas_txt = wx.TextCtrl(self, wx.NewId(), value='2.0', + self.sigmas_txt = wx.TextCtrl(self, wx.NewIdRef(), value='2.0', style=wx.RAISED_BORDER) - self.trials_lab = wx.StaticText(self, wx.NewId(), 'Number of Trials', + self.trials_lab = wx.StaticText(self, wx.NewIdRef(), 'Number of Trials', style=wx.ALIGN_CENTER) - self.trials_spn = wx.SpinCtrl(self, wx.NewId(), + self.trials_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1000, max=1000000, initial=100000) diff --git a/hexrd/wx/listeditor.py b/hexrd/wx/listeditor.py index df63c4fe..cf3e862b 100644 --- a/hexrd/wx/listeditor.py +++ b/hexrd/wx/listeditor.py @@ -83,13 +83,13 @@ def __makeObjects(self): """Add interactors""" #self.__makeTitleBar('List Editor') - self.main_lbx = wx.ListBox(self, wx.NewId(), + self.main_lbx = wx.ListBox(self, wx.NewIdRef(), style = wx.LB_SINGLE, choices = [item.name for item in self.mylist]) - self.up_but = wx.Button(self, wx.NewId(), 'up') - self.down_but = wx.Button(self, wx.NewId(), 'down') - self.del_but = wx.Button(self, wx.NewId(), 'del') - self.copy_but = wx.Button(self, wx.NewId(), 'copy') + self.up_but = wx.Button(self, wx.NewIdRef(), 'up') + self.down_but = wx.Button(self, wx.NewIdRef(), 'down') + self.del_but = wx.Button(self, wx.NewIdRef(), 'del') + self.copy_but = wx.Button(self, wx.NewIdRef(), 'copy') return @@ -237,7 +237,7 @@ def __init__(self, parent, id, mylist, **kwargs): self.titlebar = wx.StaticText(self, -1, 'List Editor', style=wx.ALIGN_CENTER|wx.SIMPLE_BORDER) # - self.list_ed = ListEditor(self, wx.NewId(), mylist) + self.list_ed = ListEditor(self, wx.NewIdRef(), mylist) # # Bindings. # diff --git a/hexrd/wx/logwindows.py b/hexrd/wx/logwindows.py index 72434f6a..bd399fa5 100644 --- a/hexrd/wx/logwindows.py +++ b/hexrd/wx/logwindows.py @@ -84,7 +84,7 @@ def __makeObjects(self): self.tbarSizer = makeTitleBar(self, 'Log') # - self.log_pan = logPanel(self, wx.NewId()) + self.log_pan = logPanel(self, wx.NewIdRef()) # return @@ -166,7 +166,7 @@ def __init__(self, parent, id): def __makeObjects(self): """Add interactors""" - self.log_txt = wx.TextCtrl(self, wx.NewId(), value='', size=(500,700), + self.log_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', size=(500,700), style=wx.RAISED_BORDER|wx.TE_MULTILINE|wx.TE_READONLY) # return diff --git a/hexrd/wx/mainapp.py b/hexrd/wx/mainapp.py index 4c03cb6a..e27e8e9d 100644 --- a/hexrd/wx/mainapp.py +++ b/hexrd/wx/mainapp.py @@ -114,7 +114,7 @@ def execute(*args): # Run program stand-alone. # app = xrdApp(*args) - app.mframe = MainFrame(None, wx.NewId()) + app.mframe = MainFrame(None, wx.NewIdRef()) app.SetTopWindow(app.mframe) # if len(sys.argv) == 1: diff --git a/hexrd/wx/mainframe.py b/hexrd/wx/mainframe.py index 9b059d92..f2fab900 100644 --- a/hexrd/wx/mainframe.py +++ b/hexrd/wx/mainframe.py @@ -96,11 +96,11 @@ def __makeObjects(self): # Canvas panel will update on all pages. Create this before # creating the notebook. # - self.canvasPanel = CanvasPanel(self, wx.NewId()) + self.canvasPanel = CanvasPanel(self, wx.NewIdRef()) # # Notebook # - self.nBook = xrdNoteBook(self, wx.NewId()) + self.nBook = xrdNoteBook(self, wx.NewIdRef()) # # A Statusbar in the bottom of the window # @@ -162,19 +162,19 @@ def __makeMaterialMenu(self): # Load, save and edit the material list # # ===== Load List - self.materialMenu.IDload = wx.NewId() + self.materialMenu.IDload = wx.NewIdRef() self.materialMenu.Append(self.materialMenu.IDload, "Load material list", "Load a saved material list") self.Bind(wx.EVT_MENU, self.OnMaterialsLoad, id=self.materialMenu.IDload) # ===== Edit List - self.materialMenu.IDedit = wx.NewId() + self.materialMenu.IDedit = wx.NewIdRef() self.materialMenu.Append(self.materialMenu.IDedit, "Edit material list", "Rearrange/remove list items") self.Bind(wx.EVT_MENU, self.OnMaterialsEdit, id=self.materialMenu.IDedit) # ===== Save List - self.materialMenu.IDsave = wx.NewId() + self.materialMenu.IDsave = wx.NewIdRef() self.materialMenu.Append(self.materialMenu.IDsave, "Save material list", "Save the material list to a file.") @@ -187,19 +187,19 @@ def __makeReaderMenu(self): self.readerMenu = wx.Menu('Readers') # ===== Load List - self.readerMenu.IDloadl = wx.NewId() + self.readerMenu.IDloadl = wx.NewIdRef() self.readerMenu.Append(self.readerMenu.IDloadl, "Load reader list", "Load the reader list to from a file") self.Bind(wx.EVT_MENU, self.OnReadersLoad, id=self.readerMenu.IDloadl) # ===== Edit - self.readerMenu.IDedit = wx.NewId() + self.readerMenu.IDedit = wx.NewIdRef() self.readerMenu.Append(self.readerMenu.IDedit, "Edit reader list", "Rearrange/remove list items") self.Bind(wx.EVT_MENU, self.OnReadersEdit, id=self.readerMenu.IDedit) # ===== Save List - self.readerMenu.IDsave = wx.NewId() + self.readerMenu.IDsave = wx.NewIdRef() self.readerMenu.Append(self.readerMenu.IDsave, "Save reader list", "Save the reader list to a file") @@ -209,7 +209,7 @@ def __makeReaderMenu(self): # # ===== Hydra # - self.readerMenu.IDhydra = wx.NewId() + self.readerMenu.IDhydra = wx.NewIdRef() self.readerMenu.Append(self.readerMenu.IDhydra, "Hydra interface", "Open the hydra interface") @@ -223,13 +223,13 @@ def __makeDetectorMenu(self): self.detectorMenu = wx.Menu('Detector') # ===== Load - self.detectorMenu.IDload = wx.NewId() + self.detectorMenu.IDload = wx.NewIdRef() self.detectorMenu.Append(self.detectorMenu.IDload, "Load detector", "Load a saved detector from a file") self.Bind(wx.EVT_MENU, self.OnDetectorLoad, id=self.detectorMenu.IDload) # ===== Save - self.detectorMenu.IDsave = wx.NewId() + self.detectorMenu.IDsave = wx.NewIdRef() self.detectorMenu.Append(self.detectorMenu.IDsave, "Save detector", "Save the detector to a file") @@ -240,7 +240,7 @@ def __makeDetectorMenu(self): # Polar Rebin # # ===== Save - self.detectorMenu.IDcake = wx.NewId() + self.detectorMenu.IDcake = wx.NewIdRef() self.detectorMenu.Append(self.detectorMenu.IDcake, "Polar Rebinning", "Bring up a window for polar rebinning (caking)") @@ -251,31 +251,31 @@ def __makeDetectorMenu(self): def __makeSpotsMenu(self): self.spotsMenu = wx.Menu('Spots') # - self.spotsMenu.IDloadRaw = wx.NewId() + self.spotsMenu.IDloadRaw = wx.NewIdRef() self.spotsMenu.Append(self.spotsMenu.IDloadRaw, "Load raw spots", "Load the raw spots to a file") self.Bind(wx.EVT_MENU, self.OnSpotsLoadRaw, id=self.spotsMenu.IDloadRaw) # - self.spotsMenu.IDsaveRaw = wx.NewId() + self.spotsMenu.IDsaveRaw = wx.NewIdRef() self.spotsMenu.Append(self.spotsMenu.IDsaveRaw, "Save raw spots", "Save the raw spots to a file") self.Bind(wx.EVT_MENU, self.OnSpotsSaveRaw, id=self.spotsMenu.IDsaveRaw) # - ## self.spotsMenu.IDsave = wx.NewId() + ## self.spotsMenu.IDsave = wx.NewIdRef() ## self.spotsMenu.Append(self.spotsMenu.IDsave, ## "Save post-processed spots", ## "Save the post-processed Spots class") ## self.Bind(wx.EVT_MENU, self.OnSpotsSave, id=self.spotsMenu.IDsave) ## # - ## self.spotsMenu.IDexportFLT = wx.NewId() + ## self.spotsMenu.IDexportFLT = wx.NewIdRef() ## self.spotsMenu.Append(self.spotsMenu.IDexportFLT, ## "Export flt", ## "Export a fable flt file") ## self.Bind(wx.EVT_MENU, self.OnSpotsExportFLT, id=self.spotsMenu.IDexportFLT) ## # - ## self.spotsMenu.IDexportGVE = wx.NewId() + ## self.spotsMenu.IDexportGVE = wx.NewIdRef() ## self.spotsMenu.Append(self.spotsMenu.IDexportGVE, ## "Export gve", ## "Export a fable gve file") @@ -285,25 +285,25 @@ def __makeSpotsMenu(self): def __makeIndexerMenu(self): self.indexerMenu = wx.Menu('Indexing') # - ## self.indexerMenu.IDloadRaw = wx.NewId() + ## self.indexerMenu.IDloadRaw = wx.NewIdRef() ## self.indexerMenu.Append(self.indexerMenu.IDloadRMats, ## "Load rMats", ## "Load an array of rotation matrices") ## self.Bind(wx.EVT_MENU, self.OnLoadRMats, id=self.indexerMenu.IDloadRMats) # - self.indexerMenu.IDsaveRMats = wx.NewId() + self.indexerMenu.IDsaveRMats = wx.NewIdRef() self.indexerMenu.Append(self.indexerMenu.IDsaveRMats, "Save rMats array", "Save the indexed rotations matrices to binary (.npy)") self.Bind(wx.EVT_MENU, self.OnSaveRMats, id=self.indexerMenu.IDsaveRMats) - self.indexerMenu.IDexportGrainLog = wx.NewId() + self.indexerMenu.IDexportGrainLog = wx.NewIdRef() self.indexerMenu.Append(self.indexerMenu.IDexportGrainLog, "Export grains log file", "Export the log file for all indexed rotations to ASCII") self.Bind(wx.EVT_MENU, self.OnExportGrainLog, id=self.indexerMenu.IDexportGrainLog) - self.indexerMenu.IDdumpGrainList = wx.NewId() + self.indexerMenu.IDdumpGrainList = wx.NewIdRef() self.indexerMenu.Append(self.indexerMenu.IDdumpGrainList, "Dump grain list", "Export the grainList to a cPickle") @@ -493,7 +493,7 @@ def OnCaking(self, e): wx.MessageBox('No Image is loaded') return - dlg = cakingDialog(self, wx.NewId()) + dlg = cakingDialog(self, wx.NewIdRef()) dlg.ShowModal() app.getCanvas().update() @@ -622,7 +622,7 @@ def OnReadersEdit(self, e): """Edit Reader list""" exp = wx.GetApp().ws - dlg = ListEditDlg(self, wx.NewId(), exp.savedReaders) + dlg = ListEditDlg(self, wx.NewIdRef(), exp.savedReaders) dlg.ShowModal() dlg.Destroy() @@ -677,7 +677,7 @@ def OnReadersLoad(self, e): def OnHydra(self, e): """Raise hydra interface""" - h = HydraControlFrame(self, wx.NewId()) + h = HydraControlFrame(self, wx.NewIdRef()) return # # ========== FILE MENU @@ -813,7 +813,7 @@ def OnMaterialsEdit(self, e): """Edit the materials list""" exp = wx.GetApp().ws - dlg = ListEditDlg(self, wx.NewId(), exp.matList) + dlg = ListEditDlg(self, wx.NewIdRef(), exp.matList) dlg.ShowModal() dlg.Destroy() diff --git a/hexrd/wx/materialspanel.py b/hexrd/wx/materialspanel.py index 388a471f..7475c95d 100644 --- a/hexrd/wx/materialspanel.py +++ b/hexrd/wx/materialspanel.py @@ -83,55 +83,55 @@ def __makeObjects(self): # # Material List # - self.curr_lab = wx.StaticText(self, wx.NewId(), + self.curr_lab = wx.StaticText(self, wx.NewIdRef(), 'Active Material', style=wx.ALIGN_CENTER) - self.mats_cho = wx.Choice(self, wx.NewId(), + self.mats_cho = wx.Choice(self, wx.NewIdRef(), choices=[m.name for m in exp.matList]) - self.new_but = wx.Button(self, wx.NewId(), 'New Material') + self.new_but = wx.Button(self, wx.NewIdRef(), 'New Material') # # Material Name # - self.name_lab = wx.StaticText(self, wx.NewId(), + self.name_lab = wx.StaticText(self, wx.NewIdRef(), 'MATERIAL NAME', style=wx.ALIGN_CENTER) - self.name_txt = wx.TextCtrl(self, wx.NewId(), value=Material.DFLT_NAME, + self.name_txt = wx.TextCtrl(self, wx.NewIdRef(), value=Material.DFLT_NAME, style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) # # Rings panel # - self.ring_pan = ringPanel(self, wx.NewId()) + self.ring_pan = ringPanel(self, wx.NewIdRef()) # # Categories # # ========== Lattice Params # - self.lp_a_lab = wx.StaticText(self, wx.NewId(), 'a', style=wx.ALIGN_CENTER) - self.lp_a_txt = wx.TextCtrl(self, wx.NewId(), value='0', + self.lp_a_lab = wx.StaticText(self, wx.NewIdRef(), 'a', style=wx.ALIGN_CENTER) + self.lp_a_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.lp_b_lab = wx.StaticText(self, wx.NewId(), 'b', style=wx.ALIGN_CENTER) - self.lp_b_txt = wx.TextCtrl(self, wx.NewId(), value='0', + self.lp_b_lab = wx.StaticText(self, wx.NewIdRef(), 'b', style=wx.ALIGN_CENTER) + self.lp_b_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.lp_c_lab = wx.StaticText(self, wx.NewId(), 'c', style=wx.ALIGN_CENTER) - self.lp_c_txt = wx.TextCtrl(self, wx.NewId(), value='0', + self.lp_c_lab = wx.StaticText(self, wx.NewIdRef(), 'c', style=wx.ALIGN_CENTER) + self.lp_c_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.alpha_lab = wx.StaticText(self, wx.NewId(), 'alpha', style=wx.ALIGN_CENTER) - self.alpha_txt = wx.TextCtrl(self, wx.NewId(), value='90', + self.alpha_lab = wx.StaticText(self, wx.NewIdRef(), 'alpha', style=wx.ALIGN_CENTER) + self.alpha_txt = wx.TextCtrl(self, wx.NewIdRef(), value='90', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.beta_lab = wx.StaticText(self, wx.NewId(), 'beta', style=wx.ALIGN_CENTER) - self.beta_txt = wx.TextCtrl(self, wx.NewId(), value='90', + self.beta_lab = wx.StaticText(self, wx.NewIdRef(), 'beta', style=wx.ALIGN_CENTER) + self.beta_txt = wx.TextCtrl(self, wx.NewIdRef(), value='90', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.gamma_lab = wx.StaticText(self, wx.NewId(), 'gamma', style=wx.ALIGN_CENTER) - self.gamma_txt = wx.TextCtrl(self, wx.NewId(), value='90', + self.gamma_lab = wx.StaticText(self, wx.NewIdRef(), 'gamma', style=wx.ALIGN_CENTER) + self.gamma_txt = wx.TextCtrl(self, wx.NewIdRef(), value='90', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.units_lab = wx.StaticText(self, wx.NewId(), 'UNITS', style=wx.ALIGN_CENTER) - self.dunits_cho = wx.Choice(self, wx.NewId(), choices=['angstroms']) + self.units_lab = wx.StaticText(self, wx.NewIdRef(), 'UNITS', style=wx.ALIGN_CENTER) + self.dunits_cho = wx.Choice(self, wx.NewIdRef(), choices=['angstroms']) self.dunits_cho.SetSelection(0) - self.aunits_cho = wx.Choice(self, wx.NewId(), choices=['degrees']) + self.aunits_cho = wx.Choice(self, wx.NewIdRef(), choices=['degrees']) self.aunits_cho.SetSelection(0) # # Save list of lattice parameter windows. @@ -150,33 +150,33 @@ def __makeObjects(self): # # ========== Space group info # - self.sg_lab = wx.StaticText(self, wx.NewId(), 'Space Group', + self.sg_lab = wx.StaticText(self, wx.NewIdRef(), 'Space Group', style=wx.ALIGN_CENTER) - self.sg_spn = wx.SpinCtrl(self, wx.NewId(), min=1, max=230, initial=mat.spaceGroup.sgnum) + self.sg_spn = wx.SpinCtrl(self, wx.NewIdRef(), min=1, max=230, initial=mat.spaceGroup.sgnum) - self.hall_lab = wx.StaticText(self, wx.NewId(), 'Hall Symbol', + self.hall_lab = wx.StaticText(self, wx.NewIdRef(), 'Hall Symbol', style=wx.ALIGN_CENTER) - self.hall_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.hall_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_READONLY) - self.herm_lab = wx.StaticText(self, wx.NewId(), 'Hermann-Mauguin', + self.herm_lab = wx.StaticText(self, wx.NewIdRef(), 'Hermann-Mauguin', style=wx.ALIGN_CENTER) - self.herm_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.herm_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_READONLY) - self.laue_lab = wx.StaticText(self, wx.NewId(), 'Laue Group', + self.laue_lab = wx.StaticText(self, wx.NewIdRef(), 'Laue Group', style=wx.ALIGN_CENTER) - self.laue_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.laue_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_READONLY) - self.ltype_lab = wx.StaticText(self, wx.NewId(), 'Lattice Type', + self.ltype_lab = wx.StaticText(self, wx.NewIdRef(), 'Lattice Type', style=wx.ALIGN_CENTER) - self.ltype_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.ltype_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_READONLY) - self.hkls_lab = wx.StaticText(self, wx.NewId(), 'HKLs Max (sum of squares)', + self.hkls_lab = wx.StaticText(self, wx.NewIdRef(), 'HKLs Max (sum of squares)', style=wx.ALIGN_CENTER) - self.hkls_txt = wx.TextCtrl(self, wx.NewId(), value='10', + self.hkls_txt = wx.TextCtrl(self, wx.NewIdRef(), value='10', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) return diff --git a/hexrd/wx/planedataeditor.py b/hexrd/wx/planedataeditor.py index a162fe95..68caa2d3 100644 --- a/hexrd/wx/planedataeditor.py +++ b/hexrd/wx/planedataeditor.py @@ -96,70 +96,70 @@ def __makeObjects(self): # # Text control for name # - self.name_lab = wx.StaticText(self, wx.NewId(), 'Name', style=wx.ALIGN_CENTER) - self.name_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.name_lab = wx.StaticText(self, wx.NewIdRef(), 'Name', style=wx.ALIGN_CENTER) + self.name_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) self.name_txt.ChangeValue(self.mat.name) # # Two-Theta and wavelength selectors, with units. # - self.tthmin_lab = wx.StaticText(self, wx.NewId(), 'Two Theta Min', + self.tthmin_lab = wx.StaticText(self, wx.NewIdRef(), 'Two Theta Min', style=wx.ALIGN_CENTER) - self.tthmin_txt = wx.TextCtrl(self, wx.NewId(), value='0.0', + self.tthmin_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0.0', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.tthmin_uni = wx.Choice(self, wx.NewId(), choices=AngleUnits) + self.tthmin_uni = wx.Choice(self, wx.NewIdRef(), choices=AngleUnits) #self.Bind(wx.EVT_CHOICE, self.OnChoice, self.choice) - self.tthmax_lab = wx.StaticText(self, wx.NewId(), 'Two Theta Max', + self.tthmax_lab = wx.StaticText(self, wx.NewIdRef(), 'Two Theta Max', style=wx.ALIGN_CENTER) - self.tthmax_txt = wx.TextCtrl(self, wx.NewId(), value='20.0', + self.tthmax_txt = wx.TextCtrl(self, wx.NewIdRef(), value='20.0', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.tthmax_uni = wx.Choice(self, wx.NewId(), choices=AngleUnits) + self.tthmax_uni = wx.Choice(self, wx.NewIdRef(), choices=AngleUnits) - self.wave_lab = wx.StaticText(self, wx.NewId(), 'Wavelength', + self.wave_lab = wx.StaticText(self, wx.NewIdRef(), 'Wavelength', style=wx.ALIGN_CENTER) - self.wave_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.wave_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) self.wave_txt.ChangeValue(str(self.pData.wavelength)) - self.wave_uni = wx.Choice(self, wx.NewId(), choices=AngleUnits) + self.wave_uni = wx.Choice(self, wx.NewIdRef(), choices=AngleUnits) # # Group selectors # - self.laue_lab = wx.StaticText(self, wx.NewId(), + self.laue_lab = wx.StaticText(self, wx.NewIdRef(), 'Select the Laue group', style=wx.ALIGN_RIGHT) - self.laue_cho = wx.Choice(self, wx.NewId(), choices=['Laue Groups']) + self.laue_cho = wx.Choice(self, wx.NewIdRef(), choices=['Laue Groups']) - self.space_lab = wx.StaticText(self, wx.NewId(), + self.space_lab = wx.StaticText(self, wx.NewIdRef(), 'Select the Space group', style=wx.ALIGN_RIGHT) - self.space_cho = wx.Choice(self, wx.NewId(), choices=['Space Groups']) + self.space_cho = wx.Choice(self, wx.NewIdRef(), choices=['Space Groups']) # # Add HKL list # - self.hkls_clb = wx.CheckListBox(self, wx.NewId(), choices = self.__getHKLs()) + self.hkls_clb = wx.CheckListBox(self, wx.NewIdRef(), choices = self.__getHKLs()) [self.hkls_clb.Check(i, not self.exclude[i]) for i in range(len(self.exclude))] # # Lattice Parameters # - self.lp_a_lab = wx.StaticText(self, wx.NewId(), 'a', style=wx.ALIGN_CENTER) - self.lp_a_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.lp_a_lab = wx.StaticText(self, wx.NewIdRef(), 'a', style=wx.ALIGN_CENTER) + self.lp_a_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.lp_b_lab = wx.StaticText(self, wx.NewId(), 'b', style=wx.ALIGN_CENTER) - self.lp_b_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.lp_b_lab = wx.StaticText(self, wx.NewIdRef(), 'b', style=wx.ALIGN_CENTER) + self.lp_b_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.lp_c_lab = wx.StaticText(self, wx.NewId(), 'c', style=wx.ALIGN_CENTER) - self.lp_c_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.lp_c_lab = wx.StaticText(self, wx.NewIdRef(), 'c', style=wx.ALIGN_CENTER) + self.lp_c_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.lp_alpha_lab = wx.StaticText(self, wx.NewId(), 'alpha', style=wx.ALIGN_CENTER) - self.lp_alpha_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.lp_alpha_lab = wx.StaticText(self, wx.NewIdRef(), 'alpha', style=wx.ALIGN_CENTER) + self.lp_alpha_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.lp_beta_lab = wx.StaticText(self, wx.NewId(), 'beta', style=wx.ALIGN_CENTER) - self.lp_beta_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.lp_beta_lab = wx.StaticText(self, wx.NewIdRef(), 'beta', style=wx.ALIGN_CENTER) + self.lp_beta_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) - self.lp_gamma_lab = wx.StaticText(self, wx.NewId(), 'gamma', style=wx.ALIGN_CENTER) - self.lp_gamma_txt = wx.TextCtrl(self, wx.NewId(), value='', + self.lp_gamma_lab = wx.StaticText(self, wx.NewIdRef(), 'gamma', style=wx.ALIGN_CENTER) + self.lp_gamma_txt = wx.TextCtrl(self, wx.NewIdRef(), value='', style=wx.RAISED_BORDER|wx.TE_PROCESS_ENTER) return @@ -290,8 +290,8 @@ def __init__(self, parent, id, mat, **kwargs): # self.titlebar = wx.StaticText(self, -1, 'PlaneDataDialog', style=wx.ALIGN_CENTER|wx.SIMPLE_BORDER) - self.pdPanel = PlaneDataPanel(self, wx.NewId(), self.mat) - self.quitBut = wx.Button(self, wx.NewId(), 'QUIT') + self.pdPanel = PlaneDataPanel(self, wx.NewIdRef(), self.mat) + self.quitBut = wx.Button(self, wx.NewIdRef(), 'QUIT') # # Bindings. # diff --git a/hexrd/wx/readerinfo_dlg.py b/hexrd/wx/readerinfo_dlg.py index 456e8cf9..44ddebe0 100644 --- a/hexrd/wx/readerinfo_dlg.py +++ b/hexrd/wx/readerinfo_dlg.py @@ -40,36 +40,36 @@ def __make_objects(self): """Add interactors""" self.tbarSizer = makeTitleBar(self, 'Reader Info') - self.file_but = wx.Button(self, wx.NewId(), + self.file_but = wx.Button(self, wx.NewIdRef(), 'File' ) - self.file_txt = wx.TextCtrl(self, wx.NewId(), + self.file_txt = wx.TextCtrl(self, wx.NewIdRef(), value="", style=wx.RAISED_BORDER|wx.TE_READONLY ) - self.format_lab = wx.StaticText(self, wx.NewId(), + self.format_lab = wx.StaticText(self, wx.NewIdRef(), 'Format', style=wx.ALIGN_RIGHT ) - self.format_cho = wx.Choice(self, wx.NewId(), + self.format_cho = wx.Choice(self, wx.NewIdRef(), choices=['hdf5', 'frame-cache'] ) - self.pixel_lab = wx.StaticText(self, wx.NewId(), + self.pixel_lab = wx.StaticText(self, wx.NewIdRef(), 'Pixel Pitch', style=wx.ALIGN_RIGHT ) - self.pixel_txt = wx.TextCtrl(self, wx.NewId(), + self.pixel_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0.2', style=wx.RAISED_BORDER ) - self.option_lab = wx.StaticText(self, wx.NewId(), + self.option_lab = wx.StaticText(self, wx.NewIdRef(), 'Option', style=wx.ALIGN_RIGHT ) - self.value_lab = wx.StaticText(self, wx.NewId(), + self.value_lab = wx.StaticText(self, wx.NewIdRef(), 'Value', style=wx.ALIGN_LEFT ) - self.option_cho = wx.Choice(self, wx.NewId(), + self.option_cho = wx.Choice(self, wx.NewIdRef(), choices=['path', 'pixel pitch'] ) - self.value_txt = wx.TextCtrl(self, wx.NewId(), + self.value_txt = wx.TextCtrl(self, wx.NewIdRef(), value="/imageseries", style=wx.RAISED_BORDER ) diff --git a/hexrd/wx/readerpanel.py b/hexrd/wx/readerpanel.py index da46b791..a3598d9d 100644 --- a/hexrd/wx/readerpanel.py +++ b/hexrd/wx/readerpanel.py @@ -82,9 +82,9 @@ def __makeObjects(self): # # Add detector choice # - self.det_cho = wx.Choice(self, wx.NewId(), + self.det_cho = wx.Choice(self, wx.NewIdRef(), choices=DET_CHOICES) - self.rdr_pan = geReaderPanel(self, wx.NewId()) + self.rdr_pan = geReaderPanel(self, wx.NewIdRef()) return diff --git a/hexrd/wx/ringsubpanel.py b/hexrd/wx/ringsubpanel.py index 5a16c03c..674ee4cc 100644 --- a/hexrd/wx/ringsubpanel.py +++ b/hexrd/wx/ringsubpanel.py @@ -94,40 +94,40 @@ def __makeObjects(self): # # b. Wavelength # - self.dfwv_but = wx.Button(self, wx.NewId(), 'Make Default') + self.dfwv_but = wx.Button(self, wx.NewIdRef(), 'Make Default') self.dfwv_but.SetToolTip(dfltToolTip) - self.wave_lab = wx.StaticText(self, wx.NewId(), + self.wave_lab = wx.StaticText(self, wx.NewIdRef(), 'Wavelength:', style=wx.ALIGN_RIGHT) - self.waveAng_txt = wx.TextCtrl(self, wx.NewId(), + self.waveAng_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) - self.waveAng_lab = wx.StaticText(self, wx.NewId(), + self.waveAng_lab = wx.StaticText(self, wx.NewIdRef(), WAVELENGTH_UNIT, style=wx.ALIGN_RIGHT) - self.waveKEV_txt = wx.TextCtrl(self, wx.NewId(), + self.waveKEV_txt = wx.TextCtrl(self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) - self.waveKEV_lab = wx.StaticText(self, wx.NewId(), + self.waveKEV_lab = wx.StaticText(self, wx.NewIdRef(), 'keV', style=wx.ALIGN_RIGHT) # # c. Edit HKLs # - self.hkl_but = wx.Button(self, wx.NewId(), 'Edit HKLs') + self.hkl_but = wx.Button(self, wx.NewIdRef(), 'Edit HKLs') # # d. Ring widths # - self.dfwd_but = wx.Button(self, wx.NewId(), 'Make Default') + self.dfwd_but = wx.Button(self, wx.NewIdRef(), 'Make Default') self.dfwd_but.SetToolTip(dfltToolTip) - self.width_lab = wx.StaticText(self, wx.NewId(), + self.width_lab = wx.StaticText(self, wx.NewIdRef(), 'Ring Width:', style=wx.ALIGN_RIGHT) - self.width_cho = wx.Choice(self, wx.NewId(), choices=widChoices) + self.width_cho = wx.Choice(self, wx.NewIdRef(), choices=widChoices) - self.width_txt = wx.TextCtrl(self, wx.NewId(), + self.width_txt = wx.TextCtrl(self, wx.NewIdRef(), value='1.0e-3', style=wx.RAISED_BORDER | wx.TE_PROCESS_ENTER) # @@ -242,7 +242,7 @@ def OnEditHKLs(self, evt): exp = app.ws mat = exp.activeMaterial - dlg = hklsDlg(self, wx.NewId(), mat) + dlg = hklsDlg(self, wx.NewIdRef(), mat) if dlg.ShowModal() == wx.ID_OK: mat.planeData.exclusions = dlg.getExclusions() diff --git a/hexrd/wx/selecthkls.py b/hexrd/wx/selecthkls.py index ac4fe661..8e013ca1 100644 --- a/hexrd/wx/selecthkls.py +++ b/hexrd/wx/selecthkls.py @@ -96,7 +96,7 @@ def __makeListCtrl(self): # LStyle = wx.LC_REPORT # - listctrl = wx.ListView(self, wx.NewId(), style=LStyle) + listctrl = wx.ListView(self, wx.NewIdRef(), style=LStyle) listctrl.InsertColumn(0, 'HKL') listctrl.InsertColumn(1, 'd-spacing') listctrl.InsertColumn(2, '2-theta (deg)') @@ -211,7 +211,7 @@ def __init__(self, parent, id, mat, **kwargs): # self.titlebar = wx.StaticText(self, -1, 'selectHKLsDialog', style=wx.ALIGN_CENTER|wx.SIMPLE_BORDER) - self.dataPanel = selectHKLsPanel(self, wx.NewId(), mat) + self.dataPanel = selectHKLsPanel(self, wx.NewIdRef(), mat) self.dataPanel.SetMinSize((400,400)) # # Bindings. diff --git a/hexrd/wx/spotspanel.py b/hexrd/wx/spotspanel.py index 13643fb0..2860ffca 100644 --- a/hexrd/wx/spotspanel.py +++ b/hexrd/wx/spotspanel.py @@ -85,78 +85,78 @@ def __makeObjects(self): # Booleans - self.disc_box = wx.CheckBox(self, wx.NewId(), 'Discard at bounds') - self.bbox_box = wx.CheckBox(self, wx.NewId(), 'Keep in bounding box') - self.pado_box = wx.CheckBox(self, wx.NewId(), 'Pad Omega') - self.pads_box = wx.CheckBox(self, wx.NewId(), 'Pad Spots') + self.disc_box = wx.CheckBox(self, wx.NewIdRef(), 'Discard at bounds') + self.bbox_box = wx.CheckBox(self, wx.NewIdRef(), 'Keep in bounding box') + self.pado_box = wx.CheckBox(self, wx.NewIdRef(), 'Pad Omega') + self.pads_box = wx.CheckBox(self, wx.NewIdRef(), 'Pad Spots') # Threshold self.thresh_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), 'Threshold', style=wx.ALIGN_CENTER) self.thresh_txt = wx.TextCtrl( - self, wx.NewId(), value='500', + self, wx.NewIdRef(), value='500', style=wx.RAISED_BORDER) # Min PX self.minpx_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), 'Min PX', style=wx.ALIGN_CENTER) self.minpx_txt = wx.TextCtrl( - self, wx.NewId(), value='4', + self, wx.NewIdRef(), value='4', style=wx.RAISED_BORDER) # Spots info self.aread_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), 'Active Reader', style=wx.ALIGN_RIGHT) - self.aread_cho = wx.Choice(self, wx.NewId(), choices=['reader list']) + self.aread_cho = wx.Choice(self, wx.NewIdRef(), choices=['reader list']) self.rdr_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), 'Used Readers', style=wx.ALIGN_RIGHT) - self.rdr_lbx = wx.ListBox(self, wx.NewId(), choices = ['r1', 'r2']) + self.rdr_lbx = wx.ListBox(self, wx.NewIdRef(), choices = ['r1', 'r2']) self.nspot_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), 'Number of Spots', style=wx.ALIGN_RIGHT) self.nspot_txt = wx.TextCtrl( - self, wx.NewId(), value='0', + self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER) # Run button - self.run = wx.Button(self, wx.NewId(), 'Add Spots') - self.clear_but = wx.Button(self, wx.NewId(), 'Clear Spots') + self.run = wx.Button(self, wx.NewIdRef(), 'Add Spots') + self.clear_but = wx.Button(self, wx.NewIdRef(), 'Clear Spots') # Spots for Indexing info self.amat_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), 'Active Material', style=wx.ALIGN_RIGHT) - self.amat_cho = wx.Choice(self, wx.NewId(), choices=['mat list']) + self.amat_cho = wx.Choice(self, wx.NewIdRef(), choices=['mat list']) self.Bind(wx.EVT_CHOICE, self.OnMatChoice, self.aread_cho) self.hkls_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), '', style=wx.ALIGN_RIGHT) - self.hkls_but = wx.Button(self, wx.NewId(), 'HKLs') + self.hkls_but = wx.Button(self, wx.NewIdRef(), 'HKLs') self.nspotind_lab = wx.StaticText( - self, wx.NewId(), + self, wx.NewIdRef(), 'Number of Spots', style=wx.ALIGN_RIGHT) self.nspotind_txt = wx.TextCtrl( - self, wx.NewId(), value='0', + self, wx.NewIdRef(), value='0', style=wx.RAISED_BORDER) # Run button for indexing spots - self.run_ind = wx.Button(self, wx.NewId(), 'Process Spots\nfor Indexing') + self.run_ind = wx.Button(self, wx.NewIdRef(), 'Process Spots\nfor Indexing') return @@ -340,7 +340,7 @@ def OnRun(self, evt): 'args' : (), 'kwargs': dict() } - logwin = logWindow(self, wx.NewId(), action, 'Finding Spots') + logwin = logWindow(self, wx.NewIdRef(), action, 'Finding Spots') logwin.ShowModal() self.updateFromExp() @@ -357,7 +357,7 @@ def OnRunInd(self, evt): def OnRunHKLs(self, evt): """Select HKLs to use for indexing""" exp = wx.GetApp().ws - hkls_dlg = HklsDlg(self, wx.NewId(), exp.activeMaterial) + hkls_dlg = HklsDlg(self, wx.NewIdRef(), exp.activeMaterial) if hkls_dlg.ShowModal() == wx.ID_OK: exp.activeMaterial.planeData.exclusions = hkls_dlg.getExclusions() diff --git a/hexrd/wx/xrdnotebook.py b/hexrd/wx/xrdnotebook.py index ead1a11d..9ff729e9 100644 --- a/hexrd/wx/xrdnotebook.py +++ b/hexrd/wx/xrdnotebook.py @@ -70,30 +70,30 @@ def __init__(self, parent, id, **kwargs): self.pageDict = dict() # title = 'Materials' - panel = matPanel(self, wx.NewId()) + panel = matPanel(self, wx.NewIdRef()) self.materialsPanel = panel self.AddPage(panel, title) self.pageDict[title] = panel # title = 'Reader' - self.readerPanel = readerPanel(self, wx.NewId()) + self.readerPanel = readerPanel(self, wx.NewIdRef()) self.AddPage(self.readerPanel, title) self.pageDict[title] = self.readerPanel # title = 'Detector' - self.detectorPanel = detectorPanel(self, wx.NewId()) + self.detectorPanel = detectorPanel(self, wx.NewIdRef()) self.AddPage(self.detectorPanel, title) self.pageDict[title] = self.detectorPanel # title = 'Spots' - self.spotsPanel = spotsPanel(self, wx.NewId()) + self.spotsPanel = spotsPanel(self, wx.NewIdRef()) self.AddPage(self.spotsPanel, title) self.pageDict[title] = self.spotsPanel # # - self.AddPage(indexPanel(self, wx.NewId()), + self.AddPage(indexPanel(self, wx.NewIdRef()), 'Indexing') - self.AddPage(grainPanel(self, wx.NewId()), + self.AddPage(grainPanel(self, wx.NewIdRef()), 'Grains') # # Make sure page is updated on page change. diff --git a/setup.py b/setup.py index cb8bfd5c..1d67e38c 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,21 @@ import versioneer +install_reqs = [ + 'fabio@git+https://github.com/joelvbernier/fabio.git@master', + 'h5py', + 'matplotlib', + 'numba', + 'numpy', + 'psutil', + 'progressbar', + 'python', + 'pyyaml', + 'scikit-image', + 'scikit-learn', + 'scipy', + 'wxpython' +] cmdclass = versioneer.get_cmdclass() @@ -141,4 +156,5 @@ def run(self): data_files = data_files, package_data = package_data, cmdclass = cmdclass, + install_requires=install_reqs ) From 47c8d6261b674ed623e1c6d121325206c27bf8a5 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 8 Apr 2020 17:26:43 -0700 Subject: [PATCH 218/253] updated hedm config and instrument init --- hexrd/config/config.py | 56 +++++ hexrd/config/findorientations.py | 73 ++++--- hexrd/config/imageseries.py | 108 +++------- hexrd/config/instrument.py | 83 ++------ hexrd/config/material.py | 13 +- hexrd/config/root.py | 99 +++------ hexrd/config/tests/test_find_orientations.py | 15 +- hexrd/config/tests/test_image_series.py | 56 ++--- hexrd/config/tests/test_instrument.py | 212 ++++++++++--------- hexrd/instrument.py | 114 ++++++---- 10 files changed, 391 insertions(+), 438 deletions(-) diff --git a/hexrd/config/config.py b/hexrd/config/config.py index 55901727..f9df78c6 100644 --- a/hexrd/config/config.py +++ b/hexrd/config/config.py @@ -1,4 +1,60 @@ +"""Base Config class""" + +import logging + +from .utils import null + +logger = logging.getLogger('hexrd.config') + class Config(object): + _dirty = False + def __init__(self, cfg): self._cfg = cfg + + @property + def dirty(self): + return self._dirty + + def get(self, key, default=null): + args = key.split(':') + args, item = args[:-1], args[-1] + temp = self._cfg + for arg in args: + temp = temp.get(arg, {}) + # intermediate block may be None: + temp = {} if temp is None else temp + try: + res = temp[item] + except KeyError: + if default is not null: + logger.info( + '%s not specified, defaulting to %s', key, default + ) + res = temp.get(item, default) + else: + raise RuntimeError( + '%s must be specified in configuration file' % key + ) + return res + + def set(self, key, val): + args = key.split(':') + args, item = args[:-1], args[-1] + temp = self._cfg + for arg in args: + temp = temp.get(arg, {}) + # intermediate block may be None: + temp = {} if temp is None else temp + if temp.get(item, null) != val: + temp[item] = val + self._dirty = True + + + def dump(self, filename): + import yaml + + with open(filename, 'w') as f: + yaml.dump(self._cfg, f) + self._dirty = False diff --git a/hexrd/config/findorientations.py b/hexrd/config/findorientations.py index 5b1495cf..f8abc729 100644 --- a/hexrd/config/findorientations.py +++ b/hexrd/config/findorientations.py @@ -1,3 +1,4 @@ +import logging import os import numpy as np @@ -7,6 +8,14 @@ class FindOrientationsConfig(Config): + # Subsections + @property + def orientation_maps(self): + return OrientationMapsConfig(self._cfg) + + @property + def seed_search(self): + return SeedSearchConfig(self._cfg) @property def clustering(self): return ClusteringConfig(self._cfg) @@ -15,25 +24,12 @@ def clustering(self): def eta(self): return EtaConfig(self._cfg) - @property - def extract_measured_g_vectors(self): - return self._cfg.get( - 'find_orientations:extract_measured_g_vectors', - False - ) - @property def omega(self): return OmegaConfig(self._cfg) - @property - def orientation_maps(self): - return OrientationMapsConfig(self._cfg) - - @property - def seed_search(self): - return SeedSearchConfig(self._cfg) + # Simple Values @property def threshold(self): return self._cfg.get('find_orientations:threshold', 1) @@ -52,6 +48,13 @@ def use_quaternion_grid(self): '"%s": "%s" does not exist' % (key, temp) ) + @property + def extract_measured_g_vectors(self): + return self._cfg.get( + 'find_orientations:extract_measured_g_vectors', + False + ) + class ClusteringConfig(Config): @@ -90,22 +93,14 @@ def radius(self): class OmegaConfig(Config): + tolerance_dflt = 0.5 + @property def period(self): + # ??? maybe should get from image_series like before in v0.3.x key = 'find_orientations:omega:period' - ome_start = self._cfg.image_series.omega.start - range = 360 if self._cfg.image_series.omega.step > 0 else -360 - try: - temp = self._cfg.get(key, [ome_start, ome_start + range]) - except(TypeError): - temp = self._cfg.get(key, None) - - try: - range = np.abs(temp[1] - temp[0]) - except(TypeError): - raise RuntimeError( - "without imageseries spec, must specify period" - ) + temp = self._cfg.get(key, [-180., 180]) + range = np.abs(temp[1]-temp[0]) if range != 360: raise RuntimeError( '"%s": range must be 360 degrees, range of %s is %g' @@ -117,17 +112,19 @@ def period(self): def tolerance(self): return self._cfg.get( 'find_orientations:omega:tolerance', - 2 * self._cfg.image_series.omega.step + self.tolerance_dflt ) class EtaConfig(Config): + tolerance_dflt = 0.5 + @property def tolerance(self): return self._cfg.get( 'find_orientations:eta:tolerance', - 2 * self._cfg.image_series.omega.step + self.tolerance_dflt ) @property @@ -140,7 +137,7 @@ def range(self): if mask is None: return mask return [[-90 + mask, 90 - mask], - [90 + mask, 270 - mask]] + [ 90 + mask, 270 - mask]] class SeedSearchConfig(Config): @@ -151,7 +148,7 @@ def hkl_seeds(self): try: temp = self._cfg.get(key) if isinstance(temp, int): - temp = [temp, ] + temp = [temp,] return temp except: if self._cfg.find_orientations.use_quaternion_grid is None: @@ -178,7 +175,11 @@ def active_hkls(self): temp = self._cfg.get( 'find_orientations:orientation_maps:active_hkls', default='all' ) - return [temp] if isinstance(temp, int) else temp + if isinstance(temp, int): + temp = [temp] + if temp == 'all': + temp = None + return temp @property def bin_frames(self): @@ -188,9 +189,11 @@ def bin_frames(self): @property def file(self): - temp = self._cfg.get('find_orientations:orientation_maps:file') - if not os.path.isabs(temp): - temp = os.path.join(self._cfg.working_dir, temp) + temp = self._cfg.get('find_orientations:orientation_maps:file', + default=None) + if temp is not None: + if not os.path.isabs(temp): + temp = os.path.join(self._cfg.working_dir, temp) return temp @property diff --git a/hexrd/config/imageseries.py b/hexrd/config/imageseries.py index 83bc945c..435ae73a 100644 --- a/hexrd/config/imageseries.py +++ b/hexrd/config/imageseries.py @@ -7,98 +7,40 @@ from hexrd import imageseries -class ImageSeriesConfig(Config): +class ImageSeries(Config): + + BASEKEY = 'image_series' def __init__(self, cfg): - super(ImageSeriesConfig, self).__init__(cfg) - self._imser = None - self._omseries = None + super(ImageSeries, self).__init__(cfg) + self._image_dict = None - @property - def imageseries(self): - """return the imageseries without checking for omega metadata""" - if self._imser is None: - self._imser = imageseries.open(self.filename, self.format, **self.args) - plist = self.process.process_list - if plist: - self._imser = imageseries.process.ProcessedImageSeries(self._imser, plist) - return self._imser + def get(self, key): + """get item with given key""" + return self._cfg.get(':'.join([self.BASEKEY, key])) @property - def omegaseries(self): - """return the imageseries and ensure it has omega metadata""" - if self._omseries is None: - self._omseries = imageseries.omega.OmegaImageSeries(self.imageseries) - return self._omseries + def imageseries(self): + """return the imageseries dictionary""" + if self._image_dict is None: + self._image_dict = dict() + fmt = self.format + for ispec in self.data: + fname = ispec['file'] + args = ispec['args'] + ims = imageseries.open(fname, fmt, **args) + oms = imageseries.omega.OmegaImageSeries(ims) + panel = oms.metadata['panel'] + self._image_dict[panel] = oms + + return self._image_dict # ========== yaml inputs @property - def filename(self): - temp = self._cfg.get('image_series:filename') - if not os.path.isabs(temp): - temp = os.path.join(self._cfg.working_dir, temp) - return temp + def data(self): + return self.get('data') @property def format(self): - return self._cfg.get('image_series:format') - - @property - def args(self): - return self._cfg.get('image_series:args', default={}) - - # ========== Other Configs - - @property - def omega(self): - return OmegaConfig(self._cfg) - - @property - def process(self): - return ProcessConfig(self._cfg) - - @property - def stop(self): - return self._cfg.get('image_series:omega:stop', default=None) - - -class ProcessConfig(Config): - - @property - def process_list(self): - plist = [] - dark = self.dark - if self.dark is not None: - plist.append(('dark', dark)) - flip = self.flip - if self.flip is not None: - plist.append(('flip', flip)) - - return plist - - @property - def flip(self): - return self._cfg.get('image_series:process:flip', default=None) - - @property - def dark(self): - # fixed bug that returned np.load(None) - fname = self._cfg.get('image_series:process:dark', default=None) - if fname is not None: - return np.load(fname) - - -class OmegaConfig(Config): - - @property - def step(self): - return self._cfg.get('image_series:omega:step') - - @property - def start(self): - return self._cfg.get('image_series:omega:start') - - @property - def stop(self): - return self._cfg.get('image_series:omega:stop') + return self.get('format') diff --git a/hexrd/config/instrument.py b/hexrd/config/instrument.py index f60e9e87..b68698e8 100644 --- a/hexrd/config/instrument.py +++ b/hexrd/config/instrument.py @@ -1,74 +1,31 @@ import os -from .config import Config - - - -class PixelsConfig(Config): - - - @property - def columns(self): - return self._cfg.get('instrument:detector:pixels:columns') - - - @property - def size(self): - temp = self._cfg.get('instrument:detector:pixels:size') - if isinstance(temp, (int, float)): - temp = [temp, temp] - return temp - +import numpy as np - @property - def rows(self): - return self._cfg.get('instrument:detector:pixels:rows') - - - -class DetectorConfig(Config): - - - @property - def parameters_old(self): - key = 'instrument:detector:parameters_old' - temp = self._cfg.get(key, default=None) - if temp is None: - return temp - if not os.path.isabs(temp): - temp = os.path.join(self._cfg.working_dir, temp) - if os.path.exists(temp): - return temp - raise IOError( - '"%s": "%s" does not exist' % (key, temp) - ) - - - @property - def pixels(self): - return PixelsConfig(self._cfg) +from hexrd import constants +from hexrd import instrument +from hexrd.xrd.distortion import GE_41RT as dfunc # !!! UGH, FIXME !!! +from .config import Config -class InstrumentConfig(Config): +class Instrument(Config): + def __init__(self, icfg): + self._hedm = instrument.HEDMInstrument(icfg) + # Note: instrument is instantiated with a yaml dictionary; use self + # to instantiate classes based on this one @property - def detector(self): - return DetectorConfig(self._cfg) - + def hedm(self): + return self._hedm + @hedm.setter + def hedm(self, yml): + with open(yml, 'r') as f: + icfg = yaml.safe_load(f) + self._hedm = instrument.HEDMInstrument(icfg) @property - def parameters(self): - key = 'instrument:parameters' - temp = self._cfg.get(key) - if not os.path.isabs(temp): - temp = os.path.join(self._cfg.working_dir, temp) - if os.path.exists(temp): - return temp - if self.detector.parameters_old is not None: - # converting old parameter file - return temp - raise IOError( - '"%s": "%s" does not exist' % (key, temp) - ) + def detector_dict(self): + """returns dictionary of detectors""" + return self.hedm.detectors diff --git a/hexrd/config/material.py b/hexrd/config/material.py index 37a490c5..7837c081 100644 --- a/hexrd/config/material.py +++ b/hexrd/config/material.py @@ -1,11 +1,15 @@ import os +try: + import dill as cpl +except(ImportError): + import cPickle as cpl + from .config import Config class MaterialConfig(Config): - @property def definitions(self): temp = self._cfg.get('material:definitions') @@ -17,7 +21,12 @@ def definitions(self): '"material:definitions": "%s" does not exist' ) - @property def active(self): return self._cfg.get('material:active') + + @property + def plane_data(self): + with file(self.definitions, "r") as matf: + mat_list = cpl.load(matf) + return dict(zip([i.name for i in mat_list], mat_list))[self.active].planeData diff --git a/hexrd/config/root.py b/hexrd/config/root.py index bda741d8..2080f674 100644 --- a/hexrd/config/root.py +++ b/hexrd/config/root.py @@ -3,27 +3,22 @@ import multiprocessing as mp import sys +import yaml + from hexrd.utils.decorators import memoized +from hexrd import imageseries from .config import Config -from .instrument import InstrumentConfig +from .instrument import Instrument from .findorientations import FindOrientationsConfig from .fitgrains import FitGrainsConfig -from .imageseries import ImageSeriesConfig from .material import MaterialConfig -from .utils import null - logger = logging.getLogger('hexrd.config') - class RootConfig(Config): - - _dirty = False - - @property def analysis_name(self): return str(self.get('analysis_name', default='analysis')) @@ -31,42 +26,29 @@ def analysis_name(self): def analysis_name(self, val): self.set('analysis_name', val) - @property def analysis_dir(self): return os.path.join(self.working_dir, self.analysis_name) - - @property - def dirty(self): - return self._dirty - - @property def find_orientations(self): return FindOrientationsConfig(self) - @property def fit_grains(self): return FitGrainsConfig(self) - - @property - def image_series(self): - return ImageSeriesConfig(self) - - @property def instrument(self): - return InstrumentConfig(self) - + instr_file = self.get('instrument') + with open(instr_file, 'r') as f: + icfg = yaml.safe_load(f) + return Instrument(icfg) @property def material(self): return MaterialConfig(self) - @property def multiprocessing(self): # determine number of processes to run in parallel @@ -105,6 +87,7 @@ def multiprocessing(self): ) res = temp return res + @multiprocessing.setter def multiprocessing(self, val): if val in ('half', 'all', -1): @@ -117,7 +100,6 @@ def multiprocessing(self, val): % (mp.cpu_count(), val) ) - @property def working_dir(self): try: @@ -137,6 +119,7 @@ def working_dir(self): '"working_dir" not specified, defaulting to "%s"' % temp ) return temp + @working_dir.setter def working_dir(self, val): val = os.path.abspath(val) @@ -144,46 +127,22 @@ def working_dir(self, val): raise IOError('"working_dir": "%s" does not exist' % val) self.set('working_dir', val) - - def dump(self, filename): - import yaml - - with open(filename, 'w') as f: - yaml.dump(self._cfg, f) - self._dirty = False - - - def get(self, key, default=null): - args = key.split(':') - args, item = args[:-1], args[-1] - temp = self._cfg - for arg in args: - temp = temp.get(arg, {}) - # intermediate block may be None: - temp = {} if temp is None else temp - try: - res = temp[item] - except KeyError: - if default is not null: - logger.info( - '%s not specified, defaulting to %s', key, default - ) - res = temp.get(item, default) - else: - raise RuntimeError( - '%s must be specified in configuration file' % key - ) - return res - - - def set(self, key, val): - args = key.split(':') - args, item = args[:-1], args[-1] - temp = self._cfg - for arg in args: - temp = temp.get(arg, {}) - # intermediate block may be None: - temp = {} if temp is None else temp - if temp.get(item, null) != val: - temp[item] = val - self._dirty = True + @property + def image_series(self): + """return the imageseries dictionary""" + if not hasattr(self, '_image_dict'): + self._image_dict = dict() + fmt = self.get('image_series:format') + imsdata = self.get('image_series:data') + for ispec in imsdata: + fname = ispec['file'] + args = ispec['args'] + ims = imageseries.open(fname, fmt, **args) + oms = imageseries.omega.OmegaImageSeries(ims) + try: + panel=ispec['panel'] + except(KeyError): + panel = oms.metadata['panel'] + self._image_dict[panel] = oms + + return self._image_dict diff --git a/hexrd/config/tests/test_find_orientations.py b/hexrd/config/tests/test_find_orientations.py index b3f6cf79..1dccbf17 100644 --- a/hexrd/config/tests/test_find_orientations.py +++ b/hexrd/config/tests/test_find_orientations.py @@ -7,10 +7,6 @@ """ analysis_name: analysis working_dir: %(tempdir)s -image_series: - omega: - start: -180 - step: 0.25 --- find_orientations: orientation_maps: @@ -181,10 +177,11 @@ def test_period(self): self.cfgs[1].find_orientations.omega.period, [0, 360] ) - self.assertEqual( - self.cfgs[2].find_orientations.omega.period, - [0, -360] - ) + ## Do we allow ranges going backwards? + #self.assertEqual( + # self.cfgs[2].find_orientations.omega.period, + # [0, -360] + # ) self.assertRaises( RuntimeError, getattr, self.cfgs[3].find_orientations.omega, 'period' @@ -308,7 +305,7 @@ def get_reference_data(cls): def test_active_hkls(self): self.assertEqual( self.cfgs[0].find_orientations.orientation_maps.active_hkls, - 'all' + None ) self.assertEqual( self.cfgs[1].find_orientations.orientation_maps.active_hkls, diff --git a/hexrd/config/tests/test_image_series.py b/hexrd/config/tests/test_image_series.py index 86891334..ece0e333 100644 --- a/hexrd/config/tests/test_image_series.py +++ b/hexrd/config/tests/test_image_series.py @@ -3,26 +3,19 @@ from .common import TestConfig, test_data - reference_data = \ """ -analysis_name: analysis -working_dir: %(tempdir)s ---- -image_series: - filename: %(nonexistent_file)s - format: hdf5 - args: - path: %(nonexistent_path)s ---- image_series: - filename: %(nonexistent_file)s - format: frame-cache - args: + format: array + data: + - filename: f1 + args: a1 + - filename: f2 + args: a2 """ % test_data -class TestImageSeriesConfig(TestConfig): +class TestImageSeries(TestConfig): @classmethod @@ -30,31 +23,24 @@ def get_reference_data(cls): return reference_data - def test_filename(self): + def test_format(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].image_series, 'filename' + self.assertEqual( + 'array', + self.cfgs[0].get('image_series:format') ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].image_series, 'format' - ) + def test_data(self): - self.assertEqual( - self.cfgs[1].image_series.filename, - os.path.join(test_data['tempdir'], test_data['nonexistent_file']) - ) + d = self.cfgs[0].get('image_series:data') + self.assertEqual(len(d), 2) - self.assertEqual( - self.cfgs[1].image_series.format, 'hdf5' - ) + def test_data_filename(self): - a = self.cfgs[1].image_series.args - self.assertEqual( - a['path'], test_data['nonexistent_path'] - ) + d = self.cfgs[0].get('image_series:data') + self.assertEqual(d[0]['filename'], 'f1') + + def test_data_args(self): - a = self.cfgs[2].image_series.args - self.assertEqual(a, None) + d = self.cfgs[0].get('image_series:data') + self.assertEqual(d[1]['args'], 'a2') diff --git a/hexrd/config/tests/test_instrument.py b/hexrd/config/tests/test_instrument.py index dfec1d37..0c11e8b0 100644 --- a/hexrd/config/tests/test_instrument.py +++ b/hexrd/config/tests/test_instrument.py @@ -1,146 +1,148 @@ import os +import hexrd.instrument from .common import TestConfig, test_data - +from ..instrument import Instrument, Beam, OscillationStage reference_data = \ """ -analysis_name: foo -working_dir: %(tempdir)s +beam: {} --- -instrument: +beam: + energy: 2.0 + vector: {azimuth: 0.0, polar_angle: 0.0} --- -instrument: - parameters: %(nonexistent_file)s - detector: - parameters_old: %(nonexistent_file)s - pixels: +oscillation_stage: + chi: 0.05 + t_vec_s: [1., 2., 3.] --- -instrument: - parameters: %(existing_file)s - detector: +detectors: + GE1: + distortion: + function_name: GE_41RT + parameters: [7.617424115028922e-05, -1.01006559390677e-06, -0.00016461139058911365, + 2.0, 2.0, 2.0] pixels: - size: 1 - rows: 1024 columns: 2048 ---- -instrument: - parameters: %(nonexistent_file)s - detector: - parameters_old: %(existing_file)s + rows: 2048 + size: [0.2, 0.2] + saturation_level: 14000.0 + transform: + t_vec_d: [94.51351402409436, -337.4575337059045, -1921.058935922086] + tilt_angles: [0.002314455268055846, 6.288758382211901e-05, 1.0938371193555785] + GE2: + distortion: + function_name: GE_41RT + parameters: [5.245111176545523e-05, -3.165350904260842e-05, -0.00020774139197230943, + 2.0, 2.0, 2.0] pixels: - size: [1, 2] + columns: 2048 + rows: 2048 + size: [0.2, 0.2] + saturation_level: 14000.0 + transform: + t_vec_d: [-320.190205619744, -95.95873622987875, -1920.07233414923] + tilt_angles: [0.00044459111576242654, 0.003958638944891969, -0.47488346109306645] +--- +instrument: instrument.yaml """ % test_data -class TestInstrumentConfig(TestConfig): - +class TestInstrument(TestConfig): @classmethod def get_reference_data(cls): return reference_data + def test_beam(self): + icfg = Instrument(self.cfgs[1]) + b = icfg.beam + self.assertTrue(isinstance(b, hexrd.instrument.beam.Beam), "Failed to produce a Beam instance") - def test_parameters(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].instrument, 'parameters' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[1].instrument, 'parameters' - ) - self.assertRaises( - IOError, - getattr, self.cfgs[2].instrument, 'parameters' - ) - self.assertEqual( - self.cfgs[3].instrument.parameters, - test_data['existing_file'] - ) - # next test should succeed, converting from old parameters - self.assertEqual( - self.cfgs[4].instrument.parameters, - os.path.join(test_data['tempdir'], test_data['nonexistent_file']) - ) + def test_oscillation_stage(self): + icfg = Instrument(self.cfgs[2]) + ostage = icfg.oscillation_stage + self.assertTrue(isinstance(ostage, hexrd.instrument.oscillation_stage.OscillationStage), + "Failed to produce an OscillationStage instance") + def test_detector(self): + icfg = Instrument(self.cfgs[3]) + det = icfg.get_detector('GE1') + self.assertTrue(isinstance(det, hexrd.instrument.PlanarDetector), + "Failed to produce an Detector instance") + def test_detector_dict(self): + icfg = Instrument(self.cfgs[3]) + dd = icfg.detector_dict + self.assertTrue(isinstance(dd, dict), + "Failed to produce an Detector Dictionary instance") + for k in dd: + d = dd[k] + self.assertTrue(isinstance(d, hexrd.instrument.PlanarDetector), + "Detector dictionary values are not detector instances") -class TestDetectorConfig(TestConfig): +class TestBeam(TestConfig): @classmethod def get_reference_data(cls): return reference_data + def test_beam_energy_dflt(self): + bcfg = Beam(self.cfgs[0]) + energy = bcfg.energy + self.assertEqual(energy, Beam.beam_energy_DFLT, "Incorrect default beam energy") - def test_parameters_old(self): - self.assertEqual(self.cfgs[0].instrument.detector.parameters_old, None) - self.assertEqual(self.cfgs[1].instrument.detector.parameters_old, None) - self.assertRaises( - IOError, - getattr, self.cfgs[2].instrument.detector, 'parameters_old' - ) - self.assertEqual( - self.cfgs[4].instrument.detector.parameters_old, - os.path.join(test_data['tempdir'], test_data['existing_file']) - ) + def test_beam_energy(self): + bcfg = Beam(self.cfgs[1]) + energy = bcfg.energy + self.assertEqual(energy, 2.0, "Incorrect beam energy") + def test_beam_vector_dflt(self): + bcfg = Beam(self.cfgs[0]) + bvecdflt = Beam.beam_vec_DFLT + bvec = bcfg.vector + self.assertEqual(bvec[0], bvecdflt[0], "Incorrect default beam vector") + self.assertEqual(bvec[1], bvecdflt[1], "Incorrect default beam vector") + self.assertEqual(bvec[2], bvecdflt[2], "Incorrect default beam vector") -class TestDetectorPixelsConfig(TestConfig): + def test_beam_vector(self): + bcfg = Beam(self.cfgs[1]) + bvec = bcfg.vector + self.assertEqual(bvec[0], 0.0, "Incorrect default beam vector") + self.assertEqual(bvec[1], -1.0, "Incorrect default beam vector") + self.assertEqual(bvec[2], 0.0, "Incorrect default beam vector") + + +class TestOscillationStage(TestConfig): @classmethod def get_reference_data(cls): return reference_data + def test_chi_dflt(self): + oscfg = OscillationStage(self.cfgs[0]) + self.assertEqual(oscfg.chi, OscillationStage.chi_DFLT, "Incorrect default chi for oscillation stage") + + def test_chi(self): + oscfg = OscillationStage(self.cfgs[2]) + self.assertEqual(oscfg.chi, 0.05, "Incorrect default chi for oscillation stage") + + def test_tvec_dflt(self): + oscfg = OscillationStage(self.cfgs[0]) + tvec_dflt = OscillationStage.tvec_DFLT + tvec = oscfg.tvec + + self.assertEqual(tvec[0], tvec_dflt[0], "Incorrect default translation vector") + self.assertEqual(tvec[1], tvec_dflt[1], "Incorrect default translation vector") + self.assertEqual(tvec[2], tvec_dflt[2], "Incorrect default translation vector") + + def test_tvec(self): + oscfg = OscillationStage(self.cfgs[2]) + tvec = oscfg.tvec - def test_columns(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].instrument.detector.pixels, 'columns' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[1].instrument.detector.pixels, 'columns' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[2].instrument.detector.pixels, 'columns' - ) - self.assertEqual(self.cfgs[3].instrument.detector.pixels.columns, 2048) - - - - def test_size(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].instrument.detector.pixels, 'size' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[1].instrument.detector.pixels, 'size' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[2].instrument.detector.pixels, 'size' - ) - self.assertEqual(self.cfgs[3].instrument.detector.pixels.size, [1, 1]) - self.assertEqual(self.cfgs[4].instrument.detector.pixels.size, [1, 2]) - - - def test_rows(self): - self.assertRaises( - RuntimeError, - getattr, self.cfgs[0].instrument.detector.pixels, 'rows' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[1].instrument.detector.pixels, 'rows' - ) - self.assertRaises( - RuntimeError, - getattr, self.cfgs[2].instrument.detector.pixels, 'rows' - ) - self.assertEqual(self.cfgs[3].instrument.detector.pixels.rows, 1024) + self.assertEqual(tvec[0], 1., "Incorrect translation vector") + self.assertEqual(tvec[1], 2., "Incorrect translation vector") + self.assertEqual(tvec[2], 3., "Incorrect translation vector") diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 7c99f2c3..23b16887 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -108,6 +108,9 @@ NP_DET = 6 NP_GRN = 12 +buffer_key = 'buffer' +distortion_key = 'distortion' + # ============================================================================= # UTILITY METHODS # ============================================================================= @@ -234,40 +237,45 @@ def __init__(self, instrument_config=None, instrument_config['beam']['vector']['azimuth'], instrument_config['beam']['vector']['polar_angle'], ) - ct.eta_vec + # now build detector dict - detector_ids = instrument_config['detectors'].keys() - pixel_info = [instrument_config['detectors'][i]['pixels'] - for i in detector_ids] - affine_info = [instrument_config['detectors'][i]['transform'] - for i in detector_ids] - distortion = [] - for i in detector_ids: - try: - distortion.append( - instrument_config['detectors'][i]['distortion'] - ) - except KeyError: - distortion.append(None) - det_list = [] - for pix, xform, dist in zip(pixel_info, affine_info, distortion): - # HARD CODED GE DISTORTION !!! FIX - dist_list = None - if dist is not None: - dist_list = [GE_41RT, dist['parameters']] - - det_list.append( - PlanarDetector( - rows=pix['rows'], cols=pix['columns'], - pixel_size=pix['size'], - tvec=xform['translation'], - tilt=xform['tilt'], + detectors_config = instrument_config['detectors'] + det_dict = dict.fromkeys(detectors_config) + for det_id, det_info in detectors_config.iteritems(): + pixel_info = det_info['pixels'] + saturation_level = det_info['saturation_level'] + affine_info = det_info['transform'] + + panel_buffer = None + if buffer_key in det_info: + det_buffer = det_info[buffer_key] + if det_buffer is not None: + if isinstance(det_buffer, str): + panel_buffer = np.load(det_buffer) + elif isinstance(det_buffer, list): + panel_buffer = det_buffer + + # FIXME: must promote this to a class w/ registry + distortion = None + if distortion_key in det_info: + distortion = det_info[distortion_key] + if det_info[distortion_key] is not None: + # !!! hard-coded GE distortion + distortion = [GE_41RT, distortion['parameters']] + + det_dict[det_id] = PlanarDetector( + rows=pixel_info['rows'], + cols=pixel_info['columns'], + pixel_size=pixel_info['size'], + panel_buffer=panel_buffer, + saturation_level=saturation_level, + tvec=affine_info['translation'], + tilt=affine_info['tilt'], bvec=self._beam_vector, - evec=ct.eta_vec, - distortion=dist_list) - ) - pass - self._detectors = dict(zip(detector_ids, det_list)) + evec=self._eta_vector, + distortion=distortion) + + self._detectors = det_dict self._tvec = np.r_[ instrument_config['oscillation_stage']['translation'] @@ -1201,9 +1209,45 @@ def __init__(self, roi=None, distortion=None): """ - panel buffer is in pixels... + Instantiate PlanarDetector object. + + Parameters + ---------- + rows : TYPE, optional + DESCRIPTION. The default is 2048. + cols : TYPE, optional + DESCRIPTION. The default is 2048. + pixel_size : TYPE, optional + DESCRIPTION. The default is (0.2, 0.2). + tvec : TYPE, optional + DESCRIPTION. The default is np.r_[0., 0., -1000.]. + tilt : TYPE, optional + DESCRIPTION. The default is ct.zeros_3. + name : TYPE, optional + DESCRIPTION. The default is 'default'. + bvec : TYPE, optional + DESCRIPTION. The default is ct.beam_vec. + evec : TYPE, optional + DESCRIPTION. The default is ct.eta_vec. + saturation_level : TYPE, optional + DESCRIPTION. The default is None. + panel_buffer : array_like, optional + If panel_buffer has size 2, it is interpreted as a buffer in mm + in the row and column dimensions, respectively. If it is a 2-d + array with shape (rows, cols), then it is interpreted as a boolean + mask where valid pixels are marked with True. Refer to + self.clip_to_panel for usage. The default is None. + roi : TYPE, optional + DESCRIPTION. The default is None. + distortion : TYPE, optional + DESCRIPTION. The default is None. + + Returns + ------- + None. """ + self._name = name self._rows = rows @@ -1214,9 +1258,7 @@ def __init__(self, self._saturation_level = saturation_level - if panel_buffer is None: - self._panel_buffer = 20*np.r_[self._pixel_size_col, - self._pixel_size_row] + self._panel_buffer = panel_buffer self._roi = roi From e31d93ba24f4a11d61f810a2c3bb27ffe6feaf13 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 8 Apr 2020 20:35:41 -0700 Subject: [PATCH 219/253] moved panel buffer to instrument spec --- hexrd/config/fitgrains.py | 8 -------- hexrd/instrument.py | 13 ++++++++++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/hexrd/config/fitgrains.py b/hexrd/config/fitgrains.py index 0d9b6a8d..0a3dec26 100644 --- a/hexrd/config/fitgrains.py +++ b/hexrd/config/fitgrains.py @@ -61,14 +61,6 @@ def npdiv(self): return self._cfg.get('fit_grains:npdiv', 2) - @property - def panel_buffer(self): - temp = self._cfg.get('fit_grains:panel_buffer') - if isinstance(temp, (int, float)): - temp = [temp, temp] - return temp - - @property def threshold(self): return self._cfg.get('fit_grains:threshold') diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 23b16887..57998eac 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -245,6 +245,8 @@ def __init__(self, instrument_config=None, pixel_info = det_info['pixels'] saturation_level = det_info['saturation_level'] affine_info = det_info['transform'] + + shape = (pixel_info['rows'], pixel_info['columns']) panel_buffer = None if buffer_key in det_info: @@ -252,8 +254,16 @@ def __init__(self, instrument_config=None, if det_buffer is not None: if isinstance(det_buffer, str): panel_buffer = np.load(det_buffer) + assert panel_buffer.shape == shape, \ + "buffer shape must match detector" elif isinstance(det_buffer, list): - panel_buffer = det_buffer + panel_buffer = np.asarray(det_buffer) + elif np.isscalar(det_buffer): + panel_buffer = det_buffer*np.ones(2) + else: + raise RuntimeError( + "panel buffer spec invalid for %s" % det_id + ) # FIXME: must promote this to a class w/ registry distortion = None @@ -1630,6 +1640,7 @@ def clip_to_panel(self, xy, buffer_edges=True): xlim = 0.5*self.col_dim ylim = 0.5*self.row_dim if buffer_edges and self.panel_buffer is not None: + # ok, panel_buffer should be array-like if self.panel_buffer.ndim == 2: pix = self.cartToPixel(xy, pixels=True) From af9f3c55971afb9799c64b6b61d5200465db8f7e Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 9 Apr 2020 14:40:19 -0700 Subject: [PATCH 220/253] fix to panel buffer handling; added field to config --- hexrd/config/findorientations.py | 31 ++++++++++++++++++++++++ hexrd/findorientations.py | 41 ++++++++++++++++++++++---------- hexrd/instrument.py | 2 +- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/hexrd/config/findorientations.py b/hexrd/config/findorientations.py index f8abc729..9393ffec 100644 --- a/hexrd/config/findorientations.py +++ b/hexrd/config/findorientations.py @@ -5,6 +5,15 @@ from .config import Config +seed_search_methods = { + 'label':dict(filter_radius=1, threshold=1), + 'blob_log':dict(min_sigma=0.5, max_sigma=5, + num_sigma=10, threshold=0.01, + overlap=0.1), + 'blob_dog':dict(min_sigma=0.5, max_sigma=5, + sigma_ratio=1.6, + threshold=0.01, overlap=0.1) +} class FindOrientationsConfig(Config): @@ -16,6 +25,7 @@ def orientation_maps(self): @property def seed_search(self): return SeedSearchConfig(self._cfg) + @property def clustering(self): return ClusteringConfig(self._cfg) @@ -163,6 +173,27 @@ def fiber_step(self): self._cfg.find_orientations.omega.tolerance ) + @property + def method(self): + key = 'find_orientations:seed_search:method' + try: + temp = self._cfg.get(key) + assert len(temp) == 1., \ + "method must have exactly one key" + if isinstance(temp, dict): + method_spec = next(temp.iterkeys()) + if method_spec.lower() not in seed_search_methods: + raise RuntimeError( + 'invalid seed search method "%s"' + % method_spec + ) + else: + return temp + except: + raise RuntimeError( + '"%s" must be defined for seeded search' % key + ) + @property def fiber_ndiv(self): return int(360.0 / self.fiber_step) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 8114e66b..458f15a9 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -42,8 +42,8 @@ pass -method = "blob_dog" # !!! have to get this from the config save_as_ascii = False # FIX LATER... +fwhm_to_stdev = 1./np.sqrt(8*np.log(2)) logger = logging.getLogger(__name__) @@ -53,13 +53,23 @@ # ============================================================================= -def generate_orientation_fibers( - eta_ome, chi, threshold, seed_hkl_ids, fiber_ndiv, - method='blob_dog', filt_stdev=0.8, ncpus=1): +def generate_orientation_fibers(cfg, eta_ome, chi, ncpus=1): """ From ome-eta maps and hklid spec, generate list of quaternions from fibers """ + # grab the relevant parameters from the root config + seed_hkl_ids = cfg.find_orientations.seed_search.hkl_seeds + fiber_ndiv = cfg.find_orientations.seed_search.fiber_ndiv + method_dict = cfg.find_orientations.seed_search.method + + # strip out method name and kwargs + # !!! note that the config enforces that method is a dict with length 1 + # TODO: put a consistency check on required kwargs, or otherwise specify + # default values for each case? They must be specified as of now. + method = next(method_dict.iterkeys()) + method_kwargs = method_dict[method] + # seed_hkl_ids must be consistent with this... pd_hkl_ids = eta_ome.iHKLList[seed_hkl_ids] @@ -94,11 +104,12 @@ def generate_orientation_fibers( for i in seed_hkl_ids: if method == 'label': # First apply filter + filt_stdev = fwhm_to_stdev * method_kwargs['filter_radius'] this_map_f = -ndimage.filters.gaussian_laplace( eta_ome.dataStore[i], filt_stdev) labels_t, numSpots_t = ndimage.label( - this_map_f > threshold, + this_map_f > method_kwargs['threshold'], structureNDI_label ) coms_t = np.atleast_2d( @@ -110,21 +121,25 @@ def generate_orientation_fibers( ) elif method in ['blob_log', 'blob_dog']: # must scale map + # TODO: we should so a parameter study here this_map = eta_ome.dataStore[i] this_map[np.isnan(this_map)] = 0. this_map -= np.min(this_map) scl_map = 2*this_map/np.max(this_map) - 1. - # FIXME: need to expose the parameters to config options. + # TODO: Currently the method kwargs must be explicitly specified + # in the config, and there are no checks + # for 'blob_log': min_sigma=0.5, max_sigma=5, + # num_sigma=10, threshold=0.01, overlap=0.1 + # for 'blob_dog': min_sigma=0.5, max_sigma=5, + # sigma_ratio=1.6, threshold=0.01, overlap=0.1 if method == 'blob_log': blobs = np.atleast_2d( - blob_log(scl_map, min_sigma=0.5, max_sigma=5, - num_sigma=10, threshold=0.01, overlap=0.1) + blob_log(scl_map, **method_kwargs) ) - else: + else: # blob_dog blobs = np.atleast_2d( - blob_dog(scl_map, min_sigma=0.5, max_sigma=5, - sigma_ratio=1.6, threshold=0.01, overlap=0.1) + blob_dog(scl_map, **method_kwargs) ) numSpots_t = len(blobs) coms_t = blobs[:, :2] @@ -152,9 +167,9 @@ def generate_orientation_fibers( qfib = None if ncpus > 1: # multiple process version - # QUESTION: Need a chunksize? + # ???: Need a chunksize in map? pool = mp.Pool(ncpus, discretefiber_init, (params, )) - qfib = pool.map(discretefiber_reduced, input_p) # chunksize=chunksize) + qfib = pool.map(discretefiber_reduced, input_p) pool.close() else: # single process version. diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 57998eac..8e7b6c2f 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1663,7 +1663,7 @@ def clip_to_panel(self, xy, buffer_edges=True): xy[:, 1] >= -ylim, xy[:, 1] <= ylim ) on_panel = np.logical_and(on_panel_x, on_panel_y) - elif not buffer_edges: + elif not buffer_edges or self.panel_buffer is None: on_panel_x = np.logical_and( xy[:, 0] >= -xlim, xy[:, 0] <= xlim ) From 272e3d48eaf296eee601f917954b6bbe836771fb Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 9 Apr 2020 14:48:28 -0700 Subject: [PATCH 221/253] cleanup to function API --- hexrd/findorientations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 458f15a9..c90ae12a 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -53,12 +53,14 @@ # ============================================================================= -def generate_orientation_fibers(cfg, eta_ome, chi, ncpus=1): +def generate_orientation_fibers(cfg, eta_ome): """ From ome-eta maps and hklid spec, generate list of quaternions from fibers """ # grab the relevant parameters from the root config + ncpus = cfg.multiprocessing + chi = cfg.instrument.hedm.chi seed_hkl_ids = cfg.find_orientations.seed_search.hkl_seeds fiber_ndiv = cfg.find_orientations.seed_search.fiber_ndiv method_dict = cfg.find_orientations.seed_search.method From efbd26c23d19f8990f8c10023e13807d0d0d9dc7 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 9 Apr 2020 14:53:49 -0700 Subject: [PATCH 222/253] added logging output --- hexrd/findorientations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index c90ae12a..cb2284ff 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -71,6 +71,8 @@ def generate_orientation_fibers(cfg, eta_ome): # default values for each case? They must be specified as of now. method = next(method_dict.iterkeys()) method_kwargs = method_dict[method] + logger.info('using "%s" method for fiber generation' + % method) # seed_hkl_ids must be consistent with this... pd_hkl_ids = eta_ome.iHKLList[seed_hkl_ids] From 4a9a4519c87c060025d2488441952677dfa69b2b Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 9 Apr 2020 14:57:24 -0700 Subject: [PATCH 223/253] changed kwarg to addres np.load error --- hexrd/xrd/xrdutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index de1a3f3a..f1b99e51 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -1936,7 +1936,7 @@ class EtaOmeMaps(object): def __init__(self, ome_eta_archive): - ome_eta = num.load(ome_eta_archive) + ome_eta = num.load(ome_eta_archive, allow_pickle=True) planeData_args = ome_eta['planeData_args'] planeData_hkls = ome_eta['planeData_hkls'] From fcc1c683f2d54eafcf83559088d51af4b4bd5083 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Fri, 10 Apr 2020 22:50:42 -0700 Subject: [PATCH 224/253] added instr config filename to RootConfig --- hexrd/config/instrument.py | 18 +++++++++++------- hexrd/config/root.py | 12 ++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hexrd/config/instrument.py b/hexrd/config/instrument.py index b68698e8..7c4391dd 100644 --- a/hexrd/config/instrument.py +++ b/hexrd/config/instrument.py @@ -1,24 +1,28 @@ -import os - -import numpy as np - -from hexrd import constants from hexrd import instrument -from hexrd.xrd.distortion import GE_41RT as dfunc # !!! UGH, FIXME !!! from .config import Config +import yaml + class Instrument(Config): - def __init__(self, icfg): + def __init__(self, instr_file): + self._configuration = instr_file + with open(instr_file, 'r') as f: + icfg = yaml.safe_load(f) self._hedm = instrument.HEDMInstrument(icfg) # Note: instrument is instantiated with a yaml dictionary; use self # to instantiate classes based on this one + @property + def configuration(self): + return self._configuration + @property def hedm(self): return self._hedm + @hedm.setter def hedm(self, yml): with open(yml, 'r') as f: diff --git a/hexrd/config/root.py b/hexrd/config/root.py index 2080f674..36c399f8 100644 --- a/hexrd/config/root.py +++ b/hexrd/config/root.py @@ -1,11 +1,8 @@ import os import logging import multiprocessing as mp -import sys -import yaml - -from hexrd.utils.decorators import memoized +# from hexrd.utils.decorators import memoized from hexrd import imageseries from .config import Config @@ -22,6 +19,7 @@ class RootConfig(Config): @property def analysis_name(self): return str(self.get('analysis_name', default='analysis')) + @analysis_name.setter def analysis_name(self, val): self.set('analysis_name', val) @@ -41,9 +39,7 @@ def fit_grains(self): @property def instrument(self): instr_file = self.get('instrument') - with open(instr_file, 'r') as f: - icfg = yaml.safe_load(f) - return Instrument(icfg) + return Instrument(instr_file) @property def material(self): @@ -140,7 +136,7 @@ def image_series(self): ims = imageseries.open(fname, fmt, **args) oms = imageseries.omega.OmegaImageSeries(ims) try: - panel=ispec['panel'] + panel = ispec['panel'] except(KeyError): panel = oms.metadata['panel'] self._image_dict[panel] = oms From e01a5011dbe53caaf4c3ae06d3cf8bb9c3db72a7 Mon Sep 17 00:00:00 2001 From: Joel Vincent Bernier Date: Fri, 10 Apr 2020 23:29:55 -0700 Subject: [PATCH 225/253] force detector to always write sat_level and buffer --- hexrd/instrument.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 8e7b6c2f..74a11ee2 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -247,7 +247,7 @@ def __init__(self, instrument_config=None, affine_info = det_info['transform'] shape = (pixel_info['rows'], pixel_info['columns']) - + panel_buffer = None if buffer_key in det_info: det_buffer = det_info[buffer_key] @@ -264,7 +264,7 @@ def __init__(self, instrument_config=None, raise RuntimeError( "panel buffer spec invalid for %s" % det_id ) - + # FIXME: must promote this to a class w/ registry distortion = None if distortion_key in det_info: @@ -272,7 +272,7 @@ def __init__(self, instrument_config=None, if det_info[distortion_key] is not None: # !!! hard-coded GE distortion distortion = [GE_41RT, distortion['parameters']] - + det_dict[det_id] = PlanarDetector( rows=pixel_info['rows'], cols=pixel_info['columns'], @@ -284,7 +284,7 @@ def __init__(self, instrument_config=None, bvec=self._beam_vector, evec=self._eta_vector, distortion=distortion) - + self._detectors = det_dict self._tvec = np.r_[ @@ -1243,7 +1243,7 @@ def __init__(self, DESCRIPTION. The default is None. panel_buffer : array_like, optional If panel_buffer has size 2, it is interpreted as a buffer in mm - in the row and column dimensions, respectively. If it is a 2-d + in the row and column dimensions, respectively. If it is a 2-d array with shape (rows, cols), then it is interpreted as a boolean mask where valid pixels are marked with True. Refer to self.clip_to_panel for usage. The default is None. @@ -1504,12 +1504,18 @@ def pixel_coords(self): ##################### METHODS """ - def config_dict(self, chi, t_vec_s, sat_level=None): + def config_dict(self, chi, t_vec_s, panel_buffer=None, sat_level=None): """ """ if sat_level is None: sat_level = self.saturation_level + if panel_buffer is None: + # FIXME: won't work right if it is an array + panel_buffer = self.panel_buffer + if isinstance(panel_buffer, np.ndarray): + panel_buffer = panel_buffer.flatten().tolist() + t_vec_s = np.atleast_1d(t_vec_s) d = dict( @@ -1530,8 +1536,8 @@ def config_dict(self, chi, t_vec_s, sat_level=None): ), ) - if sat_level is not None: - d['detector']['saturation_level'] = sat_level + d['detector']['saturation_level'] = sat_level + d['detector']['buffer'] = panel_buffer if self.distortion is not None: """...HARD CODED DISTORTION! FIX THIS!!!""" From 10f8b0747be40c1a0c62fe013bf023ba091ac63d Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 14 Apr 2020 12:17:38 -0700 Subject: [PATCH 226/253] updates to config dict behavior --- hexrd/config/findorientations.py | 2 +- hexrd/config/instrument.py | 4 +- hexrd/config/root.py | 7 ++ hexrd/findorientations.py | 72 +++++++++--------- hexrd/instrument.py | 122 +++++++++++++++++++++---------- hexrd/xrd/fitting.py | 10 ++- hexrd/xrd/xrdutil.py | 88 ++++++++++++++-------- 7 files changed, 192 insertions(+), 113 deletions(-) diff --git a/hexrd/config/findorientations.py b/hexrd/config/findorientations.py index 9393ffec..00078da0 100644 --- a/hexrd/config/findorientations.py +++ b/hexrd/config/findorientations.py @@ -1,10 +1,10 @@ -import logging import os import numpy as np from .config import Config +# TODO: set these as defaults seed_search_methods = { 'label':dict(filter_radius=1, threshold=1), 'blob_log':dict(min_sigma=0.5, max_sigma=5, diff --git a/hexrd/config/instrument.py b/hexrd/config/instrument.py index 7c4391dd..4172e956 100644 --- a/hexrd/config/instrument.py +++ b/hexrd/config/instrument.py @@ -1,8 +1,8 @@ -from hexrd import instrument +import yaml from .config import Config -import yaml +from hexrd import instrument class Instrument(Config): diff --git a/hexrd/config/root.py b/hexrd/config/root.py index 36c399f8..6add97c4 100644 --- a/hexrd/config/root.py +++ b/hexrd/config/root.py @@ -45,6 +45,13 @@ def instrument(self): def material(self): return MaterialConfig(self) + @property + def analysis_id(self): + return '_'.join( + self.analysis_name.strip().replace(' ', '-'), + self.material.active.strip().replace(' ', '-'), + ) + @property def multiprocessing(self): # determine number of processes to run in parallel diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index cb2284ff..78f8cba7 100644 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -4,9 +4,7 @@ import logging import multiprocessing as mp import os -import time - -import yaml +import timeit import numpy as np # np.seterr(over='ignore', invalid='ignore') @@ -16,19 +14,16 @@ from skimage.feature import blob_dog, blob_log from hexrd import matrixutil as mutil -from hexrd.xrd import indexer as idx +from hexrd.xrd import indexer +from hexrd import instrument from hexrd.xrd import rotations as rot from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.xrd.xrdutil import GenerateEtaOmeMaps, EtaOmeMaps, simulateGVecs - from hexrd.xrd import distortion as dFuncs from hexrd.fitgrains import get_instrument_parameters -from skimage.feature import blob_dog, blob_log - # just require scikit-learn? have_sklearn = False try: @@ -167,7 +162,7 @@ def generate_orientation_fibers(cfg, eta_ome): pass # do the mapping - start = time.time() + start = timeit.default_timer() qfib = None if ncpus > 1: # multiple process version @@ -181,7 +176,7 @@ def generate_orientation_fibers(cfg, eta_ome): discretefiber_init(params) # sets paramMP qfib = map(discretefiber_reduced, input_p) paramMP = None # clear paramMP - elapsed = (time.time() - start) + elapsed = (timeit.default_timer() - start) logger.info("fiber generation took %.3f seconds", elapsed) return np.hstack(qfib) @@ -236,7 +231,7 @@ def run_cluster(compl, qfib, qsym, cfg, min_samples=None, compl_thresh=None, rad if radius is not None: cl_radius = radius - start = time.clock() # time this + start = timeit.default_timer() # timeit this num_above = sum(np.array(compl) > min_compl) if num_above == 0: @@ -370,7 +365,7 @@ def quat_distance(x, y): pass pass - logger.info("clustering took %f seconds", time.clock() - start) + logger.info("clustering took %f seconds", timeit.default_timer() - start) logger.info( "Found %d orientation clusters with >=%.1f%% completeness" " and %2f misorientation", @@ -388,7 +383,7 @@ def load_eta_ome_maps(cfg, pd, image_series, hkls=None, clean=False): cfg.find_orientations.orientation_maps.file ) - # ...necessary? + # ???: necessary? if fn.split('.')[-1] != 'npz': fn = fn + '.npz' @@ -408,42 +403,49 @@ def load_eta_ome_maps(cfg, pd, image_series, hkls=None, clean=False): logger.info('clean option specified; recomputing eta/ome orientation maps') return generate_eta_ome_maps(cfg, pd, image_series, hkls) -def generate_eta_ome_maps(cfg, pd, image_series, hkls=None): - available_hkls = pd.hkls.T - # default to all hkls defined for material - active_hkls = range(available_hkls.shape[0]) - # override with hkls from config, if specified +def generate_eta_ome_maps(cfg, hkls=None): + # extract PlaneData from config and set active hkls + plane_data = cfg.material.plane_data + + # handle logicl for active hkl spec + # !!!: default to all hkls defined for material, + # override with + # 1) hkls from config, if specified; or + # 2) hkls from kwarg, if specified + available_hkls = plane_data.hkls.T + active_hkls = range(len(available_hkls)) temp = cfg.find_orientations.orientation_maps.active_hkls active_hkls = active_hkls if temp == 'all' else temp - # override with hkls from command line, if specified active_hkls = hkls if hkls is not None else active_hkls + # logging output + hklseedstr = ', '.join( + [str(available_hkls[i]) for i in active_hkls] + ) logger.info( - "using hkls to generate orientation maps: %s", - ', '.join([str(i) for i in available_hkls[active_hkls]]) + "building eta_ome maps using hkls: %s", + hklseedstr ) - bin_frames = cfg.find_orientations.orientation_maps.bin_frames - ome_step = cfg.image_series.omega.step*bin_frames - instrument_params = yaml.load(open(cfg.instrument.parameters, 'r')) - - # generate maps - eta_ome = GenerateEtaOmeMaps( - image_series, instrument_params, pd, active_hkls, - ome_step=ome_step, - threshold=cfg.find_orientations.orientation_maps.threshold - ) + # make eta_ome maps + eta_ome = instrument.GenerateEtaOmeMaps( + cfg.image_series, cfg.instrument.hedm, plane_data, + active_hkls=active_hkls, + threshold=cfg.find_orientations.orientation_maps.threshold, + ome_period=cfg.find_orientations.omega.period) + map_fname = cfg.find_orientations.orientation_maps.file \ + or '_'.join(cfg.analysis_id, 'maps.npz') fn = os.path.join( cfg.working_dir, - cfg.find_orientations.orientation_maps.file - ) + map_fname + ) fd = os.path.split(fn)[0] if not os.path.isdir(fd): os.makedirs(fd) eta_ome.save(fn) - logger.info("saved eta/ome orientation maps to %s", fn) + logger.info('saved eta/ome orientation maps to "%s"', fn) return eta_ome @@ -550,7 +552,7 @@ def find_orientations(cfg, hkls=None, clean=False, profile=False): logger.info( "%d of %d available processors requested", ncpus, mp.cpu_count() ) - compl = idx.paintGrid( + compl = indexer.paintGrid( quats, eta_ome, etaRange=np.radians(cfg.find_orientations.eta.range), diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 74a11ee2..db217669 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -274,6 +274,7 @@ def __init__(self, instrument_config=None, distortion = [GE_41RT, distortion['parameters']] det_dict[det_id] = PlanarDetector( + name=det_id, rows=pixel_info['rows'], cols=pixel_info['columns'], pixel_size=pixel_info['size'], @@ -311,13 +312,6 @@ def num_panels(self): def detectors(self): return self._detectors - @property - def detector_parameters(self): - pdict = {} - for key, panel in self.detectors.iteritems(): - pdict[key] = panel.config_dict(self.chi, self.tvec) - return pdict - @property def tvec(self): return self._tvec @@ -504,12 +498,9 @@ def write_config(self, filename=None, calibration_dict={}): ) par_dict['oscillation_stage'] = ostage - det_names = self.detectors.keys() - det_dict = dict.fromkeys(det_names) - for det_name in det_names: - panel = self.detectors[det_name] - pdict = panel.config_dict(self.chi, self.tvec) - det_dict[det_name] = pdict['detector'] + det_dict = dict.fromkeys(self.detectors) + for det_name, panel in self.detectors.iteritems(): + det_dict[det_name] = panel.config_dict()['detector'] par_dict['detectors'] = det_dict if filename is not None: with open(filename, 'w') as f: @@ -687,7 +678,11 @@ def extract_line_positions(self, plane_data, imgser_dict, # pbar.update(i_det + 1) # grab panel panel = self.detectors[detector_id] - instr_cfg = panel.config_dict(self.chi, self.tvec) + # !!! + instr_cfg = panel.config_dict( + self.chi, self.tvec, + self.beam_energy, self.beam_vector + ) native_area = panel.pixel_area # pixel ref area images = imgser_dict[detector_id] if images.ndim == 2: @@ -718,9 +713,7 @@ def extract_line_positions(self, plane_data, imgser_dict, patches = xrdutil.make_reflection_patches( instr_cfg, angs, panel.angularPixelSize(xys), tth_tol=tth_tols[i_ring], eta_tol=eta_tol, - distortion=panel.distortion, - npdiv=npdiv, quiet=True, - beamVec=self.beam_vector) + npdiv=npdiv, quiet=True) # loop over patches # FIXME: fix initialization @@ -906,7 +899,11 @@ def pull_spots(self, plane_data, grain_params, # grab panel panel = self.detectors[detector_id] - instr_cfg = panel.config_dict(self.chi, self.tvec) + # !!! + instr_cfg = panel.config_dict( + self.chi, self.tvec, + self.beam_energy, self.beam_vector + ) native_area = panel.pixel_area # pixel ref area # pull out the OmegaImageSeries for this panel from input dict @@ -986,13 +983,12 @@ def pull_spots(self, plane_data, grain_params, else: # make the tth,eta patches for interpolation patches = xrdutil.make_reflection_patches( - instr_cfg, ang_centers[:, :2], ang_pixel_size, + instr_cfg, + ang_centers[:, :2], ang_pixel_size, omega=ang_centers[:, 2], tth_tol=tth_tol, eta_tol=eta_tol, - rMat_c=rMat_c, tVec_c=tVec_c, - distortion=panel.distortion, - npdiv=npdiv, quiet=True, - beamVec=self.beam_vector) + rmat_c=rMat_c, tvec_c=tVec_c, + npdiv=npdiv, quiet=True) # GRAND LOOP over reflections for this panel patch_output = [] @@ -1504,9 +1500,40 @@ def pixel_coords(self): ##################### METHODS """ - def config_dict(self, chi, t_vec_s, panel_buffer=None, sat_level=None): + def config_dict(self, chi=0, tvec=ct.zeros_3, + beam_energy=beam_energy_DFLT, beam_vector=ct.beam_vec, + sat_level=None, panel_buffer=None): """ + Return a dictionary of detector parameters, with optional instrument + level parameters. This is a convenience function to work with the + APIs in several functions in xrdutil. + + Parameters + ---------- + chi : float, optional + DESCRIPTION. The default is 0. + tvec : array_like (3,), optional + DESCRIPTION. The default is ct.zeros_3. + beam_energy : float, optional + DESCRIPTION. The default is beam_energy_DFLT. + beam_vector : aray_like (3,), optional + DESCRIPTION. The default is ct.beam_vec. + sat_level : scalar, optional + DESCRIPTION. The default is None. + panel_buffer : scalar, array_like (2,), optional + DESCRIPTION. The default is None. + + Returns + ------- + config_dict : dict + DESCRIPTION. + """ + config_dict = {} + + # ===================================================================== + # DETECTOR PARAMETERS + # ===================================================================== if sat_level is None: sat_level = self.saturation_level @@ -1516,10 +1543,7 @@ def config_dict(self, chi, t_vec_s, panel_buffer=None, sat_level=None): if isinstance(panel_buffer, np.ndarray): panel_buffer = panel_buffer.flatten().tolist() - t_vec_s = np.atleast_1d(t_vec_s) - - d = dict( - detector=dict( + det_dict = dict( transform=dict( tilt=self.tilt.tolist(), translation=self.tvec.tolist(), @@ -1528,16 +1552,15 @@ def config_dict(self, chi, t_vec_s, panel_buffer=None, sat_level=None): rows=self.rows, columns=self.cols, size=[self.pixel_size_row, self.pixel_size_col], - ), - ), - oscillation_stage=dict( - chi=chi, - translation=t_vec_s.tolist(), - ), - ) + ) + ) - d['detector']['saturation_level'] = sat_level - d['detector']['buffer'] = panel_buffer + # saturation level + det_dict['saturation_level'] = sat_level + + # panel buffer + # FIXME if it is an array, the write will be a mess + det_dict['panel_buffer'] = panel_buffer if self.distortion is not None: """...HARD CODED DISTORTION! FIX THIS!!!""" @@ -1545,8 +1568,29 @@ def config_dict(self, chi, t_vec_s, panel_buffer=None, sat_level=None): function_name='GE_41RT', parameters=np.r_[self.distortion[1]].tolist() ) - d['detector']['distortion'] = dist_d - return d + det_dict['distortion'] = dist_d + + # ===================================================================== + # SAMPLE STAGE PARAMETERS + # ===================================================================== + stage_dict = dict( + chi=chi, + translation=tvec.tolist() + ) + + # ===================================================================== + # BEAM PARAMETERS + # ===================================================================== + beam_dict = dict( + energy=beam_energy, + vector=beam_vector + ) + + config_dict['detector'] = det_dict + config_dict['oscillation_stage'] = stage_dict + config_dict['beam'] = beam_dict + + return config_dict def pixel_angles(self, origin=ct.zeros_3): assert len(origin) == 3, "origin must have 3 elemnts" diff --git a/hexrd/xrd/fitting.py b/hexrd/xrd/fitting.py index 791f0313..baca376d 100644 --- a/hexrd/xrd/fitting.py +++ b/hexrd/xrd/fitting.py @@ -36,7 +36,6 @@ from hexrd.xrd import transforms_CAPI as xfcapi from hexrd.xrd import distortion as dFuncs -from hexrd.xrd.xrdutil import extract_detector_transformation return_value_flag = None epsf = np.finfo(float).eps # ~2.2e-16 @@ -440,9 +439,12 @@ def objFuncFitGrain(gFit, gFull, gFlag, for det_key, panel in instrument.detectors.iteritems(): det_keys_ordered.append(det_key) - rMat_d, tVec_d, chi, tVec_s = extract_detector_transformation( - instrument.detector_parameters[det_key]) - + # extract transformation quantities + rMat_d = instrument.detectors[det_key].rmat + tVec_d = instrument.detectors[det_key].tvec + chi = instrument.chi + tVec_s = instrument.tvec + results = reflections_dict[det_key] if len(results) == 0: continue diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index f1b99e51..2150ac16 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -35,6 +35,8 @@ import h5py import numpy as num +from numpy.ctypeslib import ctypes + from scipy import sparse from scipy.linalg import svd from scipy import ndimage @@ -74,7 +76,7 @@ from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.xrd import distortion +from hexrd.xrd import distortion as distortion_module #from hexrd.cacheframes import get_frames #from hexrd.coreutil import get_instrument_parameters @@ -95,7 +97,7 @@ debugDflt = False -dFunc_ref = distortion.dummy +dFunc_ref = distortion_module.dummy dParams_ref = [] d2r = piby180 = num.pi/180. @@ -108,6 +110,10 @@ bHat_l_DFLT = constants.beam_vec.flatten() eHat_l_DFLT = constants.eta_vec.flatten() +nans_2 = num.nan*num.ones(2) +nans_3 = num.nan*num.ones(3) +nans_6 = num.nan*num.ones(6) + class FormatEtaOme: 'for plotting data as a matrix, with ijAsXY=True' @@ -4015,14 +4021,12 @@ def _coo_build_window(frame_i, min_row, max_row, min_col, max_col): return window -def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, - omega=None, +def make_reflection_patches(instr_cfg, + tth_eta, ang_pixel_size, omega=None, tth_tol=0.2, eta_tol=1.0, - rMat_c=num.eye(3), tVec_c=num.zeros((3, 1)), - distortion=None, + rmat_c=num.eye(3), tvec_c=num.zeros((3, 1)), npdiv=1, quiet=False, - compute_areas_func=gutil.compute_areas, - beamVec=None): + compute_areas_func=gutil.compute_areas): """ prototype function for making angular patches on a detector @@ -4055,11 +4059,11 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, """ npts = len(tth_eta) - # detector frame - rMat_d = xfcapi.makeRotMatOfExpMap( + # detector quantities + rmat_d = xfcapi.makeRotMatOfExpMap( num.r_[instr_cfg['detector']['transform']['tilt']] ) - tVec_d = num.r_[instr_cfg['detector']['transform']['translation']] + tvec_d = num.r_[instr_cfg['detector']['transform']['translation']] pixel_size = instr_cfg['detector']['pixels']['size'] frame_nrows = instr_cfg['detector']['pixels']['rows'] @@ -4074,13 +4078,28 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, col_edges = num.arange(frame_ncols + 1)*pixel_size[0] \ + panel_dims[0][0] + # grab distortion + # FIXME: distortion function is still hard-coded here + try: + dfunc_name = instr_cfg['detector']['distortion']['function_name'] + except(KeyError): + dfunc_name = None + + if dfunc_name is None: + distortion = None + else: + # !!!: warning -- hard-coded distortion + distortion = ( + distortion_module.GE_41RT, + num.r_[instr_cfg['detector']['distortion']['parameters']] + ) + # sample frame chi = instr_cfg['oscillation_stage']['chi'] - tVec_s = num.r_[instr_cfg['oscillation_stage']['translation']] + tvec_s = num.r_[instr_cfg['oscillation_stage']['translation']] # beam vector - if beamVec is None: - beamVec = xfcapi.bVec_ref + bvec = num.r_[instr_cfg['beam']['vector']] # data to loop # ...WOULD IT BE CHEAPER TO CARRY ZEROS OR USE CONDITIONAL? @@ -4091,17 +4110,22 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, patches = [] for angs, pix in zip(full_angs, ang_pixel_size): - ndiv_tth = npdiv*num.ceil(tth_tol/num.degrees(pix[0])) - ndiv_eta = npdiv*num.ceil(eta_tol/num.degrees(pix[1])) - - tth_del = num.arange(0, ndiv_tth + 1)*tth_tol/float(ndiv_tth) \ - - 0.5*tth_tol - eta_del = num.arange(0, ndiv_eta + 1)*eta_tol/float(ndiv_eta) \ - - 0.5*eta_tol - + # calculate bin edges for patch based on local angular pixel size + # tth + tth_binw = num.ceil(tth_tol/num.degrees(pix[0])) + tth_edges = gutil.make_tolerance_grid( + bin_width=tth_binw, window_width=tth_tol, num_subdivisions=npdiv + ) + + # eta + eta_binw = num.ceil(eta_tol/num.degrees(pix[1])) + eta_edges = gutil.make_tolerance_grid( + bin_width=eta_binw, window_width=eta_tol, num_subdivisions=npdiv + ) + # store dimensions for convenience # * etas and tths are bin vertices, ome is already centers - sdims = [len(eta_del) - 1, len(tth_del) - 1] + sdims = [len(eta_edges) - 1, len(tth_edges) - 1] # FOR ANGULAR MESH conn = gutil.cellConnectivity( @@ -4111,7 +4135,7 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, ) # meshgrid args are (cols, rows), a.k.a (fast, slow) - m_tth, m_eta = num.meshgrid(tth_del, eta_del) + m_tth, m_eta = num.meshgrid(tth_edges, eta_edges) npts_patch = m_tth.size # calculate the patch XY coords from the (tth, eta) angles @@ -4127,11 +4151,11 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, xy_eval_vtx, _ = _project_on_detector_plane( gVec_angs_vtx, - rMat_d, rMat_c, + rmat_d, rmat_c, chi, - tVec_d, tVec_c, tVec_s, + tvec_d, tvec_c, tvec_s, distortion, - beamVec=beamVec) + beamVec=bvec) areas = compute_areas_func(xy_eval_vtx, conn) @@ -4147,11 +4171,11 @@ def make_reflection_patches(instr_cfg, tth_eta, ang_pixel_size, xy_eval, _ = _project_on_detector_plane( gVec_angs, - rMat_d, rMat_c, + rmat_d, rmat_c, chi, - tVec_d, tVec_c, tVec_s, + tvec_d, tvec_c, tvec_s, distortion, - beamVec=beamVec) + beamVec=bvec) row_indices = gutil.cellIndices(row_edges, xy_eval[:, 1]) col_indices = gutil.cellIndices(col_edges, xy_eval[:, 0]) @@ -4315,7 +4339,7 @@ def pullSpots(pd, detector_params, grain_params, reader, # store dimensions for convenience # * etas and tths are bin vertices, ome is already centers - sdims = [ len(ome_del), len(eta_del)-1, len(tth_del)-1 ] + sdims = [ len(ome_del), len(eta_edges)-1, len(tth_del)-1 ] # meshgrid args are (cols, rows), a.k.a (fast, slow) m_tth, m_eta = num.meshgrid(tth_del, eta_del) @@ -4699,7 +4723,7 @@ class GrainDataWriter_h5(object): TODO: add material spec """ def __init__(self, filename, detector_params, grain_params): - use_attr = True + #use_attr = True if isinstance(filename, h5py.File): self.fid = filename else: From 0eab5c214487f3ae1f139defcceff47e3e71e4d4 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 20 Apr 2020 10:23:41 -0700 Subject: [PATCH 227/253] fix for rings not on a particular detector --- hexrd/instrument.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index db217669..f4a72e50 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -566,11 +566,23 @@ def extract_polar_maps(self, plane_data, imgser_dict, ring_maps = [] for i_r, tthr in enumerate(tth_ranges): print("working on ring %d..." % i_r) + + # init map with NaNs + this_map = np.nan*np.ones((nrows_ome, ncols_eta)) + + # mark pixels in the spec'd tth range + pixels_in_tthr = np.logical_and( + ptth >= tthr[0], ptth <= tthr[1] + ) + + # catch case where ring isn't on detector + if not np.any(pixels_in_tthr): + ring_maps.append(this_map) + continue + # ???: faster to index with bool or use np.where, # or recode in numba? - rtth_idx = np.where( - np.logical_and(ptth >= tthr[0], ptth <= tthr[1]) - ) + rtth_idx = np.where(pixels_in_tthr) # grab relevant eta coords using histogram # !!!: This allows use to calculate arc length and @@ -629,7 +641,6 @@ def extract_polar_maps(self, plane_data, imgser_dict, pass pass # histogram intensities over eta ranges - this_map = np.nan*np.ones((nrows_ome, ncols_eta)) for i_row, image in enumerate(imgser_dict[det_key]): if fast_histogram: this_map[i_row, reta_idx] = histogram1d( From 95e0c410116ee467cfa505bec3b66927d049072c Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Tue, 21 Apr 2020 23:51:03 -0700 Subject: [PATCH 228/253] insert gridutil with correction --- hexrd/xrd/xrdutil.py | 48 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 2150ac16..4799d4cb 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -4102,7 +4102,7 @@ def make_reflection_patches(instr_cfg, bvec = num.r_[instr_cfg['beam']['vector']] # data to loop - # ...WOULD IT BE CHEAPER TO CARRY ZEROS OR USE CONDITIONAL? + # ??? WOULD IT BE CHEAPER TO CARRY ZEROS OR USE CONDITIONAL? if omega is None: full_angs = num.hstack([tth_eta, num.zeros((npts, 1))]) else: @@ -4111,26 +4111,24 @@ def make_reflection_patches(instr_cfg, patches = [] for angs, pix in zip(full_angs, ang_pixel_size): # calculate bin edges for patch based on local angular pixel size - # tth - tth_binw = num.ceil(tth_tol/num.degrees(pix[0])) - tth_edges = gutil.make_tolerance_grid( - bin_width=tth_binw, window_width=tth_tol, num_subdivisions=npdiv + # tth + ntths, tth_edges = gutil.make_tolerance_grid( + bin_width=num.degrees(pix[0]), + window_width=tth_tol, + num_subdivisions=npdiv ) # eta - eta_binw = num.ceil(eta_tol/num.degrees(pix[1])) - eta_edges = gutil.make_tolerance_grid( - bin_width=eta_binw, window_width=eta_tol, num_subdivisions=npdiv + netas, eta_edges = gutil.make_tolerance_grid( + bin_width=num.degrees(pix[1]), + window_width=eta_tol, + num_subdivisions=npdiv ) - # store dimensions for convenience - # * etas and tths are bin vertices, ome is already centers - sdims = [len(eta_edges) - 1, len(tth_edges) - 1] - # FOR ANGULAR MESH conn = gutil.cellConnectivity( - sdims[0], - sdims[1], + netas, + ntths, origin='ll' ) @@ -4139,8 +4137,8 @@ def make_reflection_patches(instr_cfg, npts_patch = m_tth.size # calculate the patch XY coords from the (tth, eta) angles - # * will CHEAT and ignore the small perturbation the different - # omega angle values causes and simply use the central value + # !!! will CHEAT and ignore the small perturbation the different + # omega angle values causes and simply use the central value gVec_angs_vtx = num.tile(angs, (npts_patch, 1)) \ + num.radians( num.vstack([m_tth.flatten(), @@ -4160,14 +4158,16 @@ def make_reflection_patches(instr_cfg, areas = compute_areas_func(xy_eval_vtx, conn) # EVALUATION POINTS - # * for lack of a better option will use centroids + # !!! for lack of a better option will use centroids tth_eta_cen = gutil.cellCentroids( num.atleast_2d(gVec_angs_vtx[:, :2]), conn ) - gVec_angs = num.hstack([tth_eta_cen, - num.tile(angs[2], (len(tth_eta_cen), 1))]) + gVec_angs = num.hstack( + [tth_eta_cen, + num.tile(angs[2], (len(tth_eta_cen), 1))] + ) xy_eval, _ = _project_on_detector_plane( gVec_angs, @@ -4187,11 +4187,11 @@ def make_reflection_patches(instr_cfg, (xy_eval_vtx[:, 0].reshape(m_tth.shape), xy_eval_vtx[:, 1].reshape(m_tth.shape)), conn, - areas.reshape(sdims[0], sdims[1]), - (xy_eval[:, 0].reshape(sdims[0], sdims[1]), - xy_eval[:, 1].reshape(sdims[0], sdims[1])), - (row_indices.reshape(sdims[0], sdims[1]), - col_indices.reshape(sdims[0], sdims[1]))) + areas.reshape(netas, ntths), + (xy_eval[:, 0].reshape(netas, ntths), + xy_eval[:, 1].reshape(netas, ntths)), + (row_indices.reshape(netas, ntths), + col_indices.reshape(netas, ntths))) ) pass # close loop over angles return patches From 16c32bf7f9e2d5864c99e3c483bf871c6b615b90 Mon Sep 17 00:00:00 2001 From: kenygren <40276151+kenygren@users.noreply.github.com> Date: Fri, 1 May 2020 23:23:03 -0400 Subject: [PATCH 229/253] Add files via upload --- .../grain_averaged_scripts/material_ti7.yml | 43 ++ .../near-field_uniform.py | 214 ++++++++ .../nf_uniformrecons.sh | 26 + .../GOE_builder_one_load.py | 450 +++++++++++++++++ .../ighexrd_v1/intragrain_scripts/GOE_ti7.yml | 31 ++ .../intragrain_scripts/build_GOEs.sh | 33 ++ .../intragrain_scripts/gen_eta_ome_maps.py | 466 ++++++++++++++++++ .../near-field_intragrain.py | 337 +++++++++++++ .../intragrain_scripts/nf_intragrainrecons.sh | 13 + .../intragrain_scripts/stitch_nf_diffvols.py | 116 +++++ .../intragrain_scripts/stitch_nf_grains.py | 136 +++++ .../mg_centroid_conver.py | 88 ++++ .../missing_grains_scripts/mg_finder.py | 116 +++++ .../mg_orientation_finder.py | 239 +++++++++ 14 files changed, 2308 insertions(+) create mode 100644 scripts/ighexrd_v1/grain_averaged_scripts/material_ti7.yml create mode 100644 scripts/ighexrd_v1/grain_averaged_scripts/near-field_uniform.py create mode 100644 scripts/ighexrd_v1/grain_averaged_scripts/nf_uniformrecons.sh create mode 100644 scripts/ighexrd_v1/intragrain_scripts/GOE_builder_one_load.py create mode 100644 scripts/ighexrd_v1/intragrain_scripts/GOE_ti7.yml create mode 100644 scripts/ighexrd_v1/intragrain_scripts/build_GOEs.sh create mode 100644 scripts/ighexrd_v1/intragrain_scripts/gen_eta_ome_maps.py create mode 100644 scripts/ighexrd_v1/intragrain_scripts/near-field_intragrain.py create mode 100644 scripts/ighexrd_v1/intragrain_scripts/nf_intragrainrecons.sh create mode 100644 scripts/ighexrd_v1/intragrain_scripts/stitch_nf_diffvols.py create mode 100644 scripts/ighexrd_v1/intragrain_scripts/stitch_nf_grains.py create mode 100644 scripts/ighexrd_v1/missing_grains_scripts/mg_centroid_conver.py create mode 100644 scripts/ighexrd_v1/missing_grains_scripts/mg_finder.py create mode 100644 scripts/ighexrd_v1/missing_grains_scripts/mg_orientation_finder.py diff --git a/scripts/ighexrd_v1/grain_averaged_scripts/material_ti7.yml b/scripts/ighexrd_v1/grain_averaged_scripts/material_ti7.yml new file mode 100644 index 00000000..3dfceed4 --- /dev/null +++ b/scripts/ighexrd_v1/grain_averaged_scripts/material_ti7.yml @@ -0,0 +1,43 @@ +analysis_name: ti7-05-scan-11 +find_orientations: + clustering: {algorithm: dbscan, completeness: 0.8, radius: 0.75} + eta: {mask: 10, tolerance: 0.5} + omega: + period: [0, 360] + tolerance: 0.5 + orientation_maps: + active_hkls: [0, 1, 2, 3, 4] + file: junk + threshold: 50.0 + seed_search: + fiber_step: 0.25 + hkl_seeds: [4] + threshold: 2.0 +fit_grains: + do_fit: true + estimate: ti7-05-scan-11/grains.out + npdiv: 2 + panel_buffer: 3 + refit: [3.0, 1.5] + threshold: 50.0 + tolerance: + eta: [2.5, 1.0, 1.0] + omega: [1.0, 0.75, 0.75] + tth: [0.5, 0.35, 0.35] + tth_max: 8.0 +image_series: + file: + ids: [] + stem: null + images: {start: 0} + omega: {start: 0, step: 0.2498265093684941, stop: 360.0} +instrument: + detector: + parameters_old: dummy.par + pixels: + columns: 3073 + rows: 3889 + size: [0.0748, 0.0748] + parameters: dexela_instrument_calibrated_ruby.yml +material: {active: ti7al, definitions: materials.cpl} +multiprocessing: -1 diff --git a/scripts/ighexrd_v1/grain_averaged_scripts/near-field_uniform.py b/scripts/ighexrd_v1/grain_averaged_scripts/near-field_uniform.py new file mode 100644 index 00000000..f4844e4c --- /dev/null +++ b/scripts/ighexrd_v1/grain_averaged_scripts/near-field_uniform.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Tue Aug 14 11:57:34 2018 + +@author: ken38 +""" + +#%% Necessary Dependencies + + +# PROCESSING NF GRAINS WITH MISORIENTATION +#============================================================================== +import numpy as np + +import matplotlib.pyplot as plt + +import multiprocessing as mp + +import os + +from hexrd.grainmap import nfutil +from hexrd.grainmap import tomoutil +from hexrd.grainmap import vtkutil + +#============================================================================== +# %% FILES TO LOAD -CAN BE EDITED +#============================================================================== +#These files are attached, retiga.yml is a detector configuration file +#The near field detector was already calibrated + +#A materials file, is a cPickle file which contains material information like lattice +#parameters necessary for the reconstruction + +main_dir = '/nfs/chess/user/ken38/Ti7_project/ti7-11-1percent/' + +det_file = main_dir + 'retiga.yml' +mat_file= main_dir + 'materials.cpl' + +#============================================================================== +# %% OUTPUT INFO -CAN BE EDITED +#============================================================================== + +output_dir = main_dir + +#============================================================================== + +# %% TOMOGRAPHY DATA FILES -CAN BE EDITED - ZERO LOAD SCAN +#============================================================================== + +#Locations of tomography bright field images +tbf_data_folder='/nfs/chess/raw/2018-1/f2/miller-774-1/ti7-11/2/nf/' + +tbf_img_start=31171 #for this rate, this is the 6th file in the folder +tbf_num_imgs=10 + +#Locations of tomography images +tomo_data_folder='/nfs/chess/raw/2018-1/f2/miller-774-1/ti7-11/3/nf/' + +tomo_img_start=31187#for this rate, this is the 6th file in the folder +tomo_num_imgs=360 + +#============================================================================== +# %% NEAR FIELD DATA FILES -CAN BE EDITED - ZERO LOAD SCAN +#============================================================================== +#These are the near field data files used for the reconstruction, a grains.out file +#from the far field analaysis is used as orientation guess for the grid that will +grain_out_file = main_dir + 'ti7-05-scan-11/grains.out' + +#%% +#Locations of near field images +#Locations of near field images +data_folder='/nfs/chess/raw/2018-1/f2/miller-774-1/ti7-11/7/nf/' #layer 1 + +#img_start=46501#for 0.25 degree/steps and 5 s exposure, end up with 5 junk frames up front, this is the 6th +img_start=31602#for 0.25 degree/steps and 5 s exposure, end up with 6 junk frames up front, this is the 7th +num_imgs=1440 +img_nums=np.arange(img_start,img_start+num_imgs,1) + +output_stem='initial_nf_uniform_diffvol_1' +#============================================================================== +# %% USER OPTIONS -CAN BE EDITED +#============================================================================== +x_ray_energy=61.332 #keV + +#name of the material for the reconstruction +mat_name='ti7al' + +#reconstruction with misorientation included, for many grains, this will quickly +#make the reconstruction size unmanagable +misorientation_bnd=0.0 #degrees +misorientation_spacing=0.25 #degrees + +beam_stop_width=0.6#mm, assumed to be in the center of the detector + +ome_range_deg=[(0.,359.75)] #degrees + +max_tth=-1. #degrees, if a negative number is input, all peaks that will hit the detector are calculated + +#image processing +num_for_dark=250#num images to use for median data +threshold=1.5 +num_erosions=2 #num iterations of images erosion, don't mess with unless you know what you're doing +num_dilations=3 #num iterations of images erosion, don't mess with unless you know what you're doing +ome_dilation_iter=1 #num iterations of 3d image stack dilations, don't mess with unless you know what you're doing + +chunk_size=500#chunksize for multiprocessing, don't mess with unless you know what you're doing + +#thresholds for grains in reconstructions +comp_thresh=0.75 #only use orientations from grains with completnesses ABOVE this threshold +chi2_thresh=0.005 #only use orientations from grains BELOW this chi^2 + +#tomography options +layer_row=1024 # row of layer to use to find the cross sectional specimen shape +recon_thresh=0.00025#usually varies between 0.0001 and 0.0005 +#Don't change these unless you know what you are doing, this will close small holes +#and remove noise +noise_obj_size=500 +min_hole_size=500 + +cross_sectional_dim=1.35 #cross sectional to reconstruct (should be at least 20%-30% over sample width) +#voxel spacing for the near field reconstruction +voxel_spacing = 0.005#in mm +##vertical (y) reconstruction voxel bounds in mm +v_bnds=[-0.085,0.085] +#v_bnds=[-0.,0.] +#======================= +#============================================================================== +# %% LOAD GRAIN AND EXPERIMENT DATA +#============================================================================== + +experiment, nf_to_ff_id_map = nfutil.gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ + misorientation_spacing,ome_range_deg, num_imgs, beam_stop_width) + +#============================================================================== +# %% TOMO PROCESSING - GENERATE BRIGHT FIELD +#============================================================================== + +tbf=tomoutil.gen_bright_field(tbf_data_folder,tbf_img_start,tbf_num_imgs,experiment.nrows,experiment.ncols,num_digits=6) + +#============================================================================== +# %% TOMO PROCESSING - BUILD RADIOGRAPHS +#============================================================================== + +rad_stack=tomoutil.gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,experiment.nrows,experiment.ncols,num_digits=6) + +#============================================================================== +# %% TOMO PROCESSING - INVERT SINOGRAM +#============================================================================== + +reconstruction_fbp=tomoutil.tomo_reconstruct_layer(rad_stack,cross_sectional_dim,layer_row=layer_row,\ + start_tomo_ang=ome_range_deg[0][0],end_tomo_ang=ome_range_deg[0][1],\ + tomo_num_imgs=tomo_num_imgs, center=experiment.detector_params[3]) + +#============================================================================== +# %% TOMO PROCESSING - CLEAN TOMO RECONSTRUCTION +#============================================================================== + +binary_recon=tomoutil.threshold_and_clean_tomo_layer(reconstruction_fbp,recon_thresh, noise_obj_size,min_hole_size) + +#============================================================================== +# %% TOMO PROCESSING - RESAMPLE TOMO RECONSTRUCTION +#============================================================================== + +tomo_mask=tomoutil.crop_and_rebin_tomo_layer(binary_recon,recon_thresh,voxel_spacing,experiment.pixel_size[0],cross_sectional_dim) + +#============================================================================== +# %% TOMO PROCESSING - CONSTRUCT DATA GRID +#============================================================================== + +test_crds, n_crds, Xs, Ys, Zs = nfutil.gen_nf_test_grid_tomo(tomo_mask.shape[1], tomo_mask.shape[0], v_bnds, voxel_spacing) + +#============================================================================== +# %% NEAR FIELD - MAKE MEDIAN DARK +#============================================================================== + +dark=nfutil.gen_nf_dark(data_folder,img_nums,num_for_dark,experiment.nrows,experiment.ncols,dark_type='median',num_digits=6) + +#============================================================================== +# %% NEAR FIELD - LOAD IMAGE DATA AND PROCESS +#============================================================================== + +image_stack=gen_nf_image_stack(data_folder,img_nums,dark,ome_dilation_iter,threshold,experiment.nrows,experiment.ncols,num_digits=6)#,grey_bnds=(5,5), gaussian=4.5) + +#============================================================================== +# %% INSTANTIATE CONTROLLER - RUN BLOCK NO EDITING +#============================================================================== + +progress_handler = nfutil.progressbar_progress_observer() +save_handler=nfutil.forgetful_result_handler() + +controller = nfutil.ProcessController(save_handler, progress_handler, + ncpus=mp.cpu_count(), chunk_size=chunk_size) + +multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + +#============================================================================== +# %% TEST ORIENTATIONS - RUN BLOCK NO EDITING +#============================================================================== + +raw_confidence=nfutil.test_orientations(image_stack, experiment, test_crds, + controller,multiprocessing_start_method) + +#============================================================================== +# %% POST PROCESS W WHEN TOMOGRAPHY HAS BEEN USED +#============================================================================== + +grain_map, confidence_map = nfutil.process_raw_confidence(raw_confidence,Xs.shape,tomo_mask=tomo_mask,id_remap=nf_to_ff_id_map) + +#============================================================================== +# %% SAVE PROCESSED GRAIN MAP DATA +#============================================================================== + +nfutil.save_nf_data(output_dir,output_stem,grain_map,confidence_map,Xs,Ys,Zs,experiment.exp_maps,id_remap=nf_to_ff_id_map) diff --git a/scripts/ighexrd_v1/grain_averaged_scripts/nf_uniformrecons.sh b/scripts/ighexrd_v1/grain_averaged_scripts/nf_uniformrecons.sh new file mode 100644 index 00000000..60973c1d --- /dev/null +++ b/scripts/ighexrd_v1/grain_averaged_scripts/nf_uniformrecons.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# -*- coding: utf-8 -*- + + +cd /workingdirectory/ + +source activate hexrd_environment + +echo starting near-field uniform orientation field + +echo diff_vol_1 +python near-field_uniform_diffvol_1.py + +echo diff_vol_2 +python near-field_uniform_diffvol_2.py + +echo diff_vol_3 +python near-field_uniform_diffvol_3.py + +echo diff_vol_4 +python near-field_uniform_diffvol_4.py + +echo diff_vol_5 +python near-field_uniform_diffvol_5.py + +echo All done diff --git a/scripts/ighexrd_v1/intragrain_scripts/GOE_builder_one_load.py b/scripts/ighexrd_v1/intragrain_scripts/GOE_builder_one_load.py new file mode 100644 index 00000000..893f1937 --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/GOE_builder_one_load.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Tue Jul 10 09:37:05 2018 + +@author: ken38 +"" +#original findorientations first +Created on Wed Mar 22 19:04:10 2017 + +@author: bernier2 + +""" +#%% +# MUST BE RUN FROM FOLDER WHERE ETA OMEGA MAPS LIVE +# THIS SCRIPT IS DESIGNED TO BE USED ONCE ALL ETA OME MAPS HAVE BEEN FORMED AND ONLY FOR SUBSEQUENT CLOUD SEARCHES. +# IF ETA OMEGA MAP IS NOT OKAY THEN PLEASE RUN WITH ANOTHER SCRIPT TO GENERATE - OR - CHANGE CLOBBER_MAPS TO TRUE +# it is better to not change clobber maps to true but simply generate a new series of eta_ome maps and change yml directory + +#%% +from __future__ import print_function + +import time +import logging + +import os + +import glob + +import multiprocessing + +import numpy as np + +from scipy import ndimage + +import timeit + +import argparse + + +try: + import dill as cpl +except(ImportError): + import cPickle as cpl + +import yaml + +from hexrd import constants as cnst +from hexrd import config +from hexrd import imageseries +from hexrd.imageseries.omega import OmegaImageSeries +from hexrd import instrument +from hexrd.findorientations import \ + generate_orientation_fibers, \ + run_cluster +from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd import indexer +from matplotlib import pyplot as plt +from hexrd.xrd.xrdutil import EtaOmeMaps + +from hexrd.xrd import rotations as rot + +logger = logging.getLogger(__name__) + +# just require scikit-learn? +have_sklearn = False +try: + import sklearn + vstring = sklearn.__version__.split('.') + if vstring[0] == '0' and int(vstring[1]) >= 14: + from sklearn.cluster import dbscan + from sklearn.metrics.pairwise import pairwise_distances + have_sklearn = True +except ImportError: + pass + + +# plane data +def load_pdata(cpkl, key): + with file(cpkl, "r") as matf: + mat_list = cpl.load(matf) + return dict(zip([i.name for i in mat_list], mat_list))[key].planeData + + +# images +def load_images(yml): + return imageseries.open(yml, format="frame-cache", style="npz") + + +# instrument +def load_instrument(yml): + with file(yml, 'r') as f: + icfg = yaml.load(f) + return instrument.HEDMInstrument(instrument_config=icfg) + +#%% +if __name__ == '__main__': + # + # Run preprocessor + # + parser = argparse.ArgumentParser( + description="batchfndorigrains") + + parser.add_argument('grain_num', + help="grain_num", type=int) + parser.add_argument('yaml_file', + help="yaml file", type=str) + parser.add_argument('sample_name', + help="sample name", type=str) + parser.add_argument('initial_or_final', + help="intial or final", type=str) + parser.add_argument('scan_num', + help="scan num to fit grains", type=int) + + + args = parser.parse_args() + cfg_filename = args.yaml_file + scan_number = args.scan_num + samp_name = args.sample_name + grain = args.grain_num + initial_or_final = args.initial_or_final + +# %% +# ============================================================================= +# START USER INPUT +# ============================================================================= +#----- The following parameters are passed through as arguements if running from command line ------ + +#cfg_filename = 'GOE_ti705.yml' +#samp_name = 'ti7-05' +#scan_number = 68 +#initial_or_final = final +#grain_= 0 +#---------------------------------------------------------------------------------------------------- + +#NEW GOE VARIABLES --- USER SHOULD EDIT ONCE -- same for all loadsteps + +scan_to_center_GOE = 11 #name of scan - typically taken to be the initial zero load step +misorientation_bnd = 3.0 #in degrees +misorientation_spacing = 0.20 #in degrees +id_analysisname = 'ti7-11' + +#location of master grains.out +dir_string = '/nfs/chess/user/ken38/Ti7_project/ti7-11-1percent/' +master_scan = dir_string + 'ti7-11-scan-%d' % scan_to_center_GOE + +#location and name of npz file output +npz_string = 'grain_%d' %grain + '_goe_map_data_%s.npz' % initial_or_final +goe_path = '/nfs/chess/user/ken38/Ti7_project/ti7-11-1percent/GOE/' +npz_save_dir = goe_path + '%s_goe/' % initial_or_final #for npz file +GOE_directory = goe_path + '%s_goe/' % initial_or_final #for grains.out file + +analysis_id = goe_path + id_analysisname + '-grain-%d' % grain + '-%s' % initial_or_final + +# make output directory if doesn't exist +if not os.path.exists(npz_save_dir): + os.mkdir(npz_save_dir) + +# %% +# ============================================================================= +# END USER INPUT +# ============================================================================= +# ------------------------------------------------------------------------------ +#cfg file -- currently ignores image_series block + +data_dir = os.getcwd() +fc_stem = "%s_%s_%%s*.npz" % (samp_name, scan_number) + +make_max_frames = False +use_direct_search = False + +# for clustering neighborhood +# FIXME +min_samples = 2 + +# maps options +clobber_maps = False +show_maps = False + +# ============================================================================= +# END USER INPUT +# ============================================================================= +# %% +cfg = config.open(cfg_filename)[0] + +active_hkls = cfg.find_orientations.orientation_maps.active_hkls +if active_hkls == 'all': + active_hkls = None + +max_tth = cfg.fit_grains.tth_max +if max_tth: + if type(cfg.fit_grains.tth_max) != bool: + max_tth = np.degrees(float(max_tth)) +else: + max_tth = None + +# load plane data +plane_data = load_pdata(cfg.material.definitions, cfg.material.active) +plane_data.tThMax = max_tth + +# load instrument +instr = load_instrument(cfg.instrument.parameters) +det_keys = instr.detectors.keys() + +# !!! panel buffer setting is global and assumes same typ of panel! +for det_key in det_keys: + instr.detectors[det_key].panel_buffer = \ + np.array(cfg.fit_grains.panel_buffer) + +# grab eta ranges +eta_ranges = cfg.find_orientations.eta.range + +# for indexing +build_map_threshold = cfg.find_orientations.orientation_maps.threshold + +on_map_threshold = cfg.find_orientations.threshold +fiber_ndiv = cfg.find_orientations.seed_search.fiber_ndiv +fiber_seeds = cfg.find_orientations.seed_search.hkl_seeds + +tth_tol = np.degrees(plane_data.tThWidth) +eta_tol = cfg.find_orientations.eta.tolerance +ome_tol = cfg.find_orientations.omega.tolerance +# omega period... +# QUESTION: necessary??? +ome_period = np.radians(cfg.find_orientations.omega.period) + +npdiv = cfg.fit_grains.npdiv + +compl_thresh = cfg.find_orientations.clustering.completeness +cl_radius = cfg.find_orientations.clustering.radius + +# %% + +imsd = dict.fromkeys(det_keys) +for det_key in det_keys: + fc_file = sorted( + glob.glob( + os.path.join( + data_dir, + fc_stem % det_key.lower() + ) + ) + ) + if len(fc_file) != 1: + raise(RuntimeError, 'cache file not found, or multiple found') + else: + ims = load_images(fc_file[0]) + imsd[det_key] = OmegaImageSeries(ims) + + +if make_max_frames: + max_frames_output_name = os.path.join( + data_dir, + "%s_%d-maxframes.hdf5" % (samp_name, scan_number) + ) + + if os.path.exists(max_frames_output_name): + os.remove(max_frames_output_name) + + max_frames = dict.fromkeys(det_keys) + for det_key in det_keys: + max_frames[det_key] = imageseries.stats.max(imsd[det_key]) + + ims_out = imageseries.open( + None, 'array', + data=np.array([max_frames[i] for i in max_frames]), + meta={'panels': max_frames.keys()} + ) + imageseries.write( + ims_out, max_frames_output_name, + 'hdf5', path='/imageseries' + ) +# %% + +maps_fname = analysis_id + "_maps.npz" +if os.path.exists(maps_fname) and not clobber_maps: + eta_ome = EtaOmeMaps(maps_fname) +else: + print("INFO:\tbuilding eta_ome maps") + start = timeit.default_timer() + + # make eta_ome maps + eta_ome = instrument.GenerateEtaOmeMaps( + imsd, instr, plane_data, + active_hkls=active_hkls, threshold=build_map_threshold, + ome_period=cfg.find_orientations.omega.period) + + print("INFO:\t\t...took %f seconds" % (timeit.default_timer() - start)) + + # save them + eta_ome.save(maps_fname) +#%% +# ============================================================================= +# BOX TEST POINT GENERATION +# ============================================================================= +# ============================================================================= +# Set up multiprocessing from yml +# ============================================================================= + +ncpus = cfg.multiprocessing +#Reload original data always to start from master grains.out + +exp_maps = np.zeros([1,3]) +grain_id = np.zeros([1,1]) + +grain_out = '/grains.out' +load_data_master = np.loadtxt(master_scan + grain_out) +exp_map1 = load_data_master[grain,3:6] +grain_id1 = load_data_master[grain,0] + +exp_maps[0,:] = exp_map1 +grain_id[0,:] = grain_id1 + +mis_amt=misorientation_bnd*np.pi/180 +spacing=misorientation_spacing*np.pi/180 + +ori_pts = np.arange(-mis_amt, (mis_amt+(spacing*0.999)), spacing) +num_ori_grid_pts=ori_pts.shape[0]**3 +num_oris = exp_maps.shape[0] + +Xs0, Ys0, Zs0 = np.meshgrid(ori_pts, ori_pts, ori_pts) +grid0 = np.vstack([Xs0.flatten(), Ys0.flatten(), Zs0.flatten()]).T + +exp_maps_expanded=np.zeros([num_ori_grid_pts*num_oris,3]) + + + +for ii in np.arange(num_oris): + pts_to_use=np.arange(num_ori_grid_pts) + ii*num_ori_grid_pts + exp_maps_expanded[pts_to_use,:] =grid0 + np.r_[exp_maps[ii,:]] + +exp_maps=exp_maps_expanded + +rMat_c = rot.quatOfExpMap(exp_maps.T) + +qfib=rMat_c +print("INFO: will test %d quaternions using %d processes" + % (qfib.shape[1], ncpus)) + +# %% +# ============================================================================= +# ORIENTATION SCORING +# ============================================================================= + +if use_direct_search: + def test_orientation_FF_init(params): + global paramMP + paramMP = params + + def test_orientation_FF_reduced(quat): + """ + input parameters are [ + plane_data, instrument, imgser_dict, + tth_tol, eta_tol, ome_tol, npdiv, threshold + ] + """ + plane_data = paramMP['plane_data'] + instrument = paramMP['instrument'] + imgser_dict = paramMP['imgser_dict'] + tth_tol = paramMP['tth_tol'] + eta_tol = paramMP['eta_tol'] + ome_tol = paramMP['ome_tol'] + npdiv = paramMP['npdiv'] + threshold = paramMP['threshold'] + + phi = 2*np.arccos(quat[0]) + n = xfcapi.unitRowVector(quat[1:]) + grain_params = np.hstack([ + phi*n, cnst.zeros_3, cnst.identity_6x1, + ]) + + compl, scrap = instrument.pull_spots( + plane_data, grain_params, imgser_dict, + tth_tol=tth_tol, eta_tol=eta_tol, ome_tol=ome_tol, + npdiv=npdiv, threshold=threshold, + eta_ranges=np.radians(cfg.find_orientations.eta.range), + ome_period=(-np.pi, np.pi), + check_only=True) + + return sum(compl)/float(len(compl)) + + params = dict( + plane_data=plane_data, + instrument=instr, + imgser_dict=imsd, + tth_tol=tth_tol, + eta_tol=eta_tol, + ome_tol=ome_tol, + npdiv=npdiv, + threshold=cfg.fit_grains.threshold) + + print("INFO:\tusing direct seach") + pool = multiprocessing.Pool(ncpus, test_orientation_FF_init, (params, )) + completeness = pool.map(test_orientation_FF_reduced, qfib.T) + pool.close() +else: + print("INFO:\tusing map search with paintGrid on %d processes" + % ncpus) + start = timeit.default_timer() + + completeness = indexer.paintGrid( + qfib, + eta_ome, + etaRange=np.radians(cfg.find_orientations.eta.range), + omeTol=np.radians(cfg.find_orientations.omega.tolerance), + etaTol=np.radians(cfg.find_orientations.eta.tolerance), + omePeriod=np.radians(cfg.find_orientations.omega.period), + threshold=on_map_threshold, + doMultiProc=ncpus > 1, + nCPUs=ncpus + ) + + + print("INFO:\t\t...took %f seconds" % (timeit.default_timer() - start)) +completeness = np.array(completeness) + +# %% +# ============================================================================= +# SAVE AS NPZ IN NEW FOLDER +# ============================================================================= + +goe_box_quat = np.zeros([1,4, len(pts_to_use)]) +goe_box_con = np.zeros([1,len(pts_to_use)]) + +#for grain in range (0,len(grain_id)) : +goe_box_quat[0,:,:] = qfib[:,:] +goe_box_con[0,:] = completeness[:] + +np.savez(npz_save_dir + npz_string,goe_box_con=goe_box_con,goe_box_quat=goe_box_quat,Xs0=Xs0,Ys0=Ys0,Zs0=Zs0) + +#%%#============================================================================== +#GRAINS.OUT #currently used for nf - will eliminate +#============================================================================== + +#if not os.path.exists(cfg.analysis_dir): +# os.makedirs(cfg.analysis_dir) + +print("INFO:writing misorientation clouds to grain_id_#.out files" ) + +gw = instrument.GrainDataWriter(os.path.join(GOE_directory, 'grain_id_%i.out') % grain ) +grain_params_list = [] + +for gid, q in enumerate(goe_box_quat[0,:,:].T): + phi = 2*np.arccos(q[0]) + n = xfcapi.unitRowVector(q[1:]) + grain_params = np.hstack([phi*n, cnst.zeros_3, cnst.identity_6x1]) + com = goe_box_con[0,gid] + gw.dump_grain(grain, com, 0., grain_params) + grain_params_list.append(grain_params) +gw.close() diff --git a/scripts/ighexrd_v1/intragrain_scripts/GOE_ti7.yml b/scripts/ighexrd_v1/intragrain_scripts/GOE_ti7.yml new file mode 100644 index 00000000..c50f4d4e --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/GOE_ti7.yml @@ -0,0 +1,31 @@ +analysis_name: analysis_goe_name +find_orientations: + clustering: {algorithm: dbscan, completeness: 0.8, radius: 0.75} + eta: {mask: 10, tolerance: 0.5} + omega: + period: [0, 360] + tolerance: 0.5 + orientation_maps: + active_hkls: [0, 1, 2, 3, 4] + file: junk + threshold: 50.0 + seed_search: + fiber_step: 0.25 + hkl_seeds: [3,4] + threshold: 2.0 +image_series: + file: + ids: [] + stem: null + images: {start: 0} + omega: {start: 0, step: 0.2498265093684941, stop: 360.0} +instrument: + detector: + parameters_old: dummy.par + pixels: + columns: 3073 + rows: 3889 + size: [0.0748, 0.0748] + parameters: ff_detector_dexela.yml +material: {active: materialid, definitions: materials.cpl} +multiprocessing: -1 diff --git a/scripts/ighexrd_v1/intragrain_scripts/build_GOEs.sh b/scripts/ighexrd_v1/intragrain_scripts/build_GOEs.sh new file mode 100644 index 00000000..4c5cf1eb --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/build_GOEs.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# -*- coding: utf-8 -*- + +cd /nfs/chess/aux/user/ken38/Ti7_project/ti7-05-3percent/ +source activate hexrd_0.5.29_working +#for first load use initial 11, for last load use final 68 +#counter set to increment up to the number of grains total (see grains.out to decide) + +echo starting eta_ome_maps initial +python generate_eta_ome_maps_parallel_initial.py GOE_ti705.yml ti7-05 initial 11 +echo Done building maps + +echo starting GOE builder initial +counter=0 +while [ $counter -le 792 ]; do +echo grain $counter +python GOE_builder_one_load.py $counter GOE_ti705.yml ti7-05 initial 11 +((counter++)) +done +echo Done + +echo starting eta_ome_maps final +python generate_eta_ome_maps_parallel_final.py GOE_ti705.yml ti7-05 final 68 +echo Done building maps + +echo starting GOE builder final +counter=0 +while [ $counter -le 792 ]; do +echo grain $counter +python GOE_builder_one_load.py $counter GOE_ti705.yml ti7-05 final 68 +((counter++)) +done +echo Done diff --git a/scripts/ighexrd_v1/intragrain_scripts/gen_eta_ome_maps.py b/scripts/ighexrd_v1/intragrain_scripts/gen_eta_ome_maps.py new file mode 100644 index 00000000..23cc3e2d --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/gen_eta_ome_maps.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Tue Jul 10 09:37:05 2018 + +@author: ken38 +"" +#original findorientations first +Created on Wed Mar 22 19:04:10 2017 + +@author: bernier2 + +""" +#%% +# MUST BE RUN FROM FOLDER WHERE ETA OMEGA MAPS LIVE +# THIS SCRIPT IS DESIGNED TO BE USED ONCE ALL ETA OME MAPS HAVE BEEN FORMED AND ONLY FOR SUBSEQUENT CLOUD SEARCHES. +# IF ETA OMEGA MAP IS NOT OKAY THEN PLEASE RUN WITH ANOTHER SCRIPT TO GENERATE - OR - CHANGE CLOBBER_MAPS TO TRUE +# it is better to not change clobber maps to true but simply generate a new series of eta_ome maps and change yml directory + +#%% +from __future__ import print_function + +import time +import logging + +import os + +import glob + +import multiprocessing + +import numpy as np + +from scipy import ndimage + +import timeit + +import argparse + +try: + import dill as cpl +except(ImportError): + import cPickle as cpl + +import yaml + +from hexrd import constants as cnst +from hexrd import config +from hexrd import imageseries +from hexrd.imageseries.omega import OmegaImageSeries +from hexrd import instrument +from hexrd.findorientations import \ + generate_orientation_fibers, \ + run_cluster +from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd import indexer +from matplotlib import pyplot as plt +from hexrd.xrd.xrdutil import EtaOmeMaps + +from hexrd.xrd import rotations as rot + +logger = logging.getLogger(__name__) + +# just require scikit-learn? +have_sklearn = False +try: + import sklearn + vstring = sklearn.__version__.split('.') + if vstring[0] == '0' and int(vstring[1]) >= 14: + from sklearn.cluster import dbscan + from sklearn.metrics.pairwise import pairwise_distances + have_sklearn = True +except ImportError: + pass + +# plane data +def load_pdata(cpkl, key): + with file(cpkl, "r") as matf: + mat_list = cpl.load(matf) + return dict(zip([i.name for i in mat_list], mat_list))[key].planeData + +# images +def load_images(yml): + return imageseries.open(yml, format="frame-cache", style="npz") + +# instrument +def load_instrument(yml): + with file(yml, 'r') as f: + icfg = yaml.load(f) + return instrument.HEDMInstrument(instrument_config=icfg) + +""" +Created on Fri Dec 9 13:05:27 2016 + +@author: bernier2 +""" + +import os + +import yaml + +import h5py + +import numpy as np + +from scipy import ndimage +from scipy.linalg.matfuncs import logm + +from hexrd.gridutil import cellIndices, make_tolerance_grid +from hexrd import matrixutil as mutil +from hexrd.valunits import valWUnit +from hexrd.xrd.transforms_CAPI import anglesToGVec, \ + detectorXYToGvec, \ + gvecToDetectorXY, \ + makeDetectorRotMat, \ + makeOscillRotMat, \ + makeRotMatOfExpMap, \ + mapAngle, \ + oscillAnglesOfHKLs, \ + rowNorm, \ + validateAngleRanges +from hexrd.xrd import xrdutil +from hexrd.xrd.crystallography import PlaneData +from hexrd import constants as ct + +# from hexrd.utils.progressbar import ProgressBar, Bar, ETA, ReverseBar + +# FIXME: distortion kludge +from hexrd.xrd.distortion import GE_41RT # BAD, VERY BAD!!! + +from skimage.draw import polygon + +#%% +if __name__ == '__main__': + + Run preprocessor + + parser = argparse.ArgumentParser( + description="batchfndorigrains") + + parser.add_argument('yaml_file', + help="yaml file", type=str) + parser.add_argument('sample_name', + help="sample name", type=str) + parser.add_argument('scan_num', + help="scan num to fit grains", type=int) + + args = parser.parse_args() + cfg_filename = args.yaml_file + scan_number = args.scan_num + samp_name = args.sample_name +# %% +#----- The following parameters are passed through as arguements if running from command line ------ +#cfg_filename = 'ti7-05-cloud-tvecs.yml' +#samp_name = 'ti7-05' +#scan_number = 68 +#inital_or_final = 'final'#'initial' + +# ============================================================================= +# START USER INPUT +# ============================================================================= + +#NEW GOE VARIABLES --- USER SHOULD EDIT ONCE -- same for all loadsteps +start_scan = 11 #must match GOE +misorientation_bnd = 3.0 #must match GOE +misorientation_spacing = 0.25 #must match GOE +id_analysisname = 'ti7-11' # must match GOE builder + +#location of master grains.out +dir_string = '/nfs/chess/user/ken38/Ti7_project/ti7-11-1percent/' +load_step_zero_dir = dir_string + 'ti7-11-scan-%d' % start_scan +save_folder = 'saved_GOEs_centered/' + +#%% +#location and name of npz file output +npz_save_dir = dir_string + save_folder + inital_or_final + '/' +# make output directory if doesn't exist +if not os.path.exists(npz_save_dir): + os.mkdir(npz_save_dir) +#%% +# ------------------------------------------------------------------------------ +#cfg file -- currently ignores image_series block + +data_dir = dir_string +fc_stem = "%s_%s_%%s*.npz" % (samp_name, scan_number) + +make_max_frames = False +use_direct_search = False + +# for clustering neighborhood +# FIXME +min_samples = 2 + +# maps options +clobber_maps = False +show_maps = False + +#%% one grain only +grain_out = '/grains.out' +load_data_zero = np.loadtxt(load_step_zero_dir + grain_out) +grain_id = load_data_zero[145:,0] + +#%% LOAD YML FILE +cfg = config.open(cfg_filename)[0] + +#analysis_id = '%s_%s' % ( +# cfg.analysis_name.strip().replace(' ', '-'), +# cfg.material.active.strip().replace(' ', '-'), +# ) + +active_hkls = cfg.find_orientations.orientation_maps.active_hkls +if active_hkls == 'all': + active_hkls = None + +max_tth = cfg.fit_grains.tth_max +if max_tth: + if type(cfg.fit_grains.tth_max) != bool: + max_tth = np.degrees(float(max_tth)) +else: + max_tth = None + +# load plane data +plane_data = load_pdata(cfg.material.definitions, cfg.material.active) +plane_data.tThMax = max_tth + +# load instrument +instr = load_instrument(cfg.instrument.parameters) +det_keys = instr.detectors.keys() + +# !!! panel buffer setting is global and assumes same typ of panel! +for det_key in det_keys: + instr.detectors[det_key].panel_buffer = \ + np.array(cfg.fit_grains.panel_buffer) + +# grab eta ranges +eta_ranges = cfg.find_orientations.eta.range + +# for indexing +build_map_threshold = cfg.find_orientations.orientation_maps.threshold + +on_map_threshold = cfg.find_orientations.threshold +fiber_ndiv = cfg.find_orientations.seed_search.fiber_ndiv +fiber_seeds = cfg.find_orientations.seed_search.hkl_seeds + +tth_tol = np.degrees(plane_data.tThWidth) +eta_tol = cfg.find_orientations.eta.tolerance +ome_tol = cfg.find_orientations.omega.tolerance +# omega period... +# QUESTION: necessary??? +ome_period = np.radians(cfg.find_orientations.omega.period) + +npdiv = cfg.fit_grains.npdiv + +compl_thresh = cfg.find_orientations.clustering.completeness +cl_radius = cfg.find_orientations.clustering.radius + +# % + +imsd = dict.fromkeys(det_keys) +for det_key in det_keys: + fc_file = sorted( + glob.glob( + os.path.join( + data_dir, + fc_stem % det_key.lower() + ) + ) + ) + if len(fc_file) != 1: + raise(RuntimeError, 'cache file not found, or multiple found') + else: + ims = load_images(fc_file[0]) + imsd[det_key] = OmegaImageSeries(ims) + + +if make_max_frames: + max_frames_output_name = os.path.join( + data_dir, + "%s_%d-maxframes.hdf5" % (samp_name, scan_number) + ) + + if os.path.exists(max_frames_output_name): + os.remove(max_frames_output_name) + + max_frames = dict.fromkeys(det_keys) + for det_key in det_keys: + max_frames[det_key] = imageseries.stats.max(imsd[det_key]) + + ims_out = imageseries.open( + None, 'array', + data=np.array([max_frames[i] for i in max_frames]), + meta={'panels': max_frames.keys()} + ) + imageseries.write( + ims_out, max_frames_output_name, + 'hdf5', path='/imageseries' + ) + +#%% +class GenerateEtaOmeMaps(object): + """ + eta-ome map class derived from new image_series and YAML config + + ...for now... + + must provide: + + self.dataStore + self.planeData + self.iHKLList + self.etaEdges # IN RADIANS + self.omeEdges # IN RADIANS + self.etas # IN RADIANS + self.omegas # IN RADIANS + + """ + def __init__(self, grain, image_series_dict, instrument, plane_data, + eta_step=0.25, threshold=None, + ome_period=(0, 360)): + """ + image_series must be OmegaImageSeries class + instrument_params must be a dict (loaded from yaml spec) + active_hkls must be a list (required for now) + """ + grain_params = np.squeeze(load_data_zero[grain,:]) + + analysis_id = id_analysisname + '-grain-%d' % grain + '-%s' % initial_or_final + + self._planeData = plane_data + + # ???: change name of iHKLList? + # ???: can we change the behavior of iHKLList? + active_hkls = [0,1,2,3,4] + + if active_hkls is None: + n_rings = len(plane_data.getTTh()) + self._iHKLList = range(n_rings) + else: + self._iHKLList = active_hkls + n_rings = len(active_hkls) + + # ???: need to pass a threshold? + eta_mapping, etas = instrument.extract_polar_maps_grain( + plane_data, image_series_dict, grain_params, + active_hkls=active_hkls, threshold=threshold, + tth_tol=None, eta_tol=eta_step) + + + # grab a det key + # WARNING: this process assumes that the imageseries for all panels + # have the same length and omegas + det_key = eta_mapping.keys()[0] + data_store = [] + for i_ring in range(n_rings): + full_map = np.zeros_like(eta_mapping[det_key][i_ring]) + nan_mask_full = np.zeros( + (len(eta_mapping), full_map.shape[0], full_map.shape[1]) + ) + i_p = 0 + for det_key, eta_map in eta_mapping.iteritems(): + nan_mask = ~np.isnan(eta_map[i_ring]) + nan_mask_full[i_p] = nan_mask + full_map[nan_mask] += eta_map[i_ring][nan_mask] + i_p += 1 + re_nan_these = np.sum(nan_mask_full, axis=0) == 0 + full_map[re_nan_these] = np.nan + data_store.append(full_map) + self._dataStore = data_store + + # handle omegas + omegas_array = image_series_dict[det_key].metadata['omega'] + self._omegas = mapAngle( + np.radians(np.average(omegas_array, axis=1)), + np.radians(ome_period) + ) + self._omeEdges = mapAngle( + np.radians(np.r_[omegas_array[:, 0], omegas_array[-1, 1]]), + np.radians(ome_period) + ) + + # !!! must avoid the case where omeEdges[0] = omeEdges[-1] for the + # indexer to work properly + if abs(self._omeEdges[0] - self._omeEdges[-1]) <= ct.sqrt_epsf: + # !!! SIGNED delta ome + del_ome = np.radians(omegas_array[0, 1] - omegas_array[0, 0]) + self._omeEdges[-1] = self._omeEdges[-2] + del_ome + + # handle etas + # WARNING: unlinke the omegas in imageseries metadata, + # these are in RADIANS and represent bin centers + self._etas = etas + self._etaEdges = np.r_[ + etas - 0.5*np.radians(eta_step), + etas[-1] + 0.5*np.radians(eta_step)] + + self.save(npz_save_dir + analysis_id + "_maps.npz") + + @property + def dataStore(self): + return self._dataStore + + @property + def planeData(self): + return self._planeData + + @property + def iHKLList(self): + return np.atleast_1d(self._iHKLList).flatten() + + @property + def etaEdges(self): + return self._etaEdges + + @property + def omeEdges(self): + return self._omeEdges + + @property + def etas(self): + return self._etas + + @property + def omegas(self): + return self._omegas + + def save(self, filename): + """ + self.dataStore + self.planeData + self.iHKLList + self.etaEdges + self.omeEdges + self.etas + self.omegas + """ + args = np.array(self.planeData.getParams())[:4] + args[2] = valWUnit('wavelength', 'length', args[2], 'angstrom') + hkls = self.planeData.hkls + save_dict = {'dataStore': self.dataStore, + 'etas': self.etas, + 'etaEdges': self.etaEdges, + 'iHKLList': self.iHKLList, + 'omegas': self.omegas, + 'omeEdges': self.omeEdges, + 'planeData_args': args, + 'planeData_hkls': hkls} + np.savez_compressed(filename, **save_dict) + return + pass # end of class: GenerateEtaOmeMaps + +#%% +from multiprocessing import Pool +from functools import partial + +num_processors=24 +grain_id= list(np.array(grain_id).astype('int').T) +#%% +print('building eta_ome maps using multiprocessing...') + +pool = Pool(processes=num_processors) + +#active hkls hardcoded to [0,1,2,3,4] +eta_ome_partial = partial(GenerateEtaOmeMaps, image_series_dict=imsd, instrument=instr, plane_data=plane_data, threshold=build_map_threshold, ome_period=cfg.find_orientations.omega.period) + +eta_ome = pool.map(eta_ome_partial, grain_id, chunksize=1) +pool.close() diff --git a/scripts/ighexrd_v1/intragrain_scripts/near-field_intragrain.py b/scripts/ighexrd_v1/intragrain_scripts/near-field_intragrain.py new file mode 100644 index 00000000..a81ab473 --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/near-field_intragrain.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Mon Sep 10 13:48:29 2018 + +@author: ken38 +""" + +import numpy as np +import matplotlib.pyplot as plt +from cycler import cycler +import os +import copy + +from hexrd import valunits +from hexrd.grainmap import nfutil + +from hexrd.xrd import rotations as rot +from hexrd.xrd import symmetry as sym +from hexrd.xrd import transforms as xf +from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd import xrdutil +from hexrd.xrd.transforms_CAPI import anglesToGVec, \ + makeRotMatOfExpMap, makeDetectorRotMat, makeOscillRotMat, \ + gvecToDetectorXY, detectorXYToGvec +import numba +import argparse +import contextlib +import multiprocessing +import tempfile +import shutil + +import yaml +import cPickle as cpl + +#============================================================================== +# %% INPUT FILES: Location of layer data - nf map and nf images +#============================================================================== + +#location of specific layer of near-field .NPZ array (initial) +file_dir='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/' +npz_file_stem = 'initial_nf_uniform_diffvol_1' + +#location of nearfield images +data_folder='/nfs/chess/raw/2018-1/f2/miller-774-1/ti7-11/7/nf/' #layer 1 +img_start=31602#for 0.25 degree/steps and 5 s exposure, end up with 6 junk frames up front, this is the 7th +num_imgs=1440 +img_nums=np.arange(img_start,img_start+num_imgs,1) + +#location of detector file (.yml) +det_file='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/retiga.yml' +#location of material file (.cpl) +mat_file='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/materials.cpl' + + +#grain_id.out file generated per grain with misorientation orientations for guesses +grain_id_out_dir='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/GOE/initial_goe/' + +grain_out_file='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-scan-14/grains.out' + +#============================================================================== +# %% OUTPUT FILES: Location to save new .npz +#============================================================================== + +output_dir = '/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/NEAR-FIELD/initial_volume_1/' + +# make output directory if doesn't exist +if not os.path.exists(output_dir): + os.mkdir(output_dir) + +#============================================================================== +# %% USER INPUT - X-RAY DATA experiment and analysis parameters (copy from nf) +#============================================================================== +x_ray_energy=61.332 #keV from experiment + +#name of the material in materials.cpl file +mat_name='ti7al' + +#keep at zero, cannot delete right now +misorientation_bnd=0.0 #degrees +misorientation_spacing=0.1 #degrees + +beam_stop_width=0.6#mm, assumed to be in the center of the detector + +ome_range_deg=[(0.,359.75)] #degrees + +max_tth=-1. #degrees, if a negative number is input, all peaks that will hit the detector are calculated + +#image processing +num_for_dark=250#num images to use for median data +threshold=6. #set to 7 for initial. Currently using dark image 'min' +num_erosions=2 #num iterations of images erosion, don't mess with unless you know what you're doing +num_dilations=3 #num iterations of images erosion, don't mess with unless you know what you're doing +ome_dilation_iter=1 #num iterations of 3d image stack dilations, don't mess with unless you know what you're doing + +chunk_size=500#chunksize for multiprocessing, don't mess with unless you know what you're doing + +cross_sectional_dim=1.35 #cross sectional to reconstruct (should be at least 20%-30% over sample width) +#voxel spacing for the near field reconstruction +voxel_spacing = 0.005 #in mm +##vertical (y) reconstruction voxel bounds in mm +v_bnds=[-0.085,0.085] +#v_bnds=[-0.,0.] + +#============================================================================== +# %% Set threshold values for misorientation data (ff - clouds) +#============================================================================== + +#thresholds for grains in reconstructions +comp_thresh=0.7 #only use orientations with completnesses ABOVE this threshold +chi2_thresh=1.0 #is not used; make sure value > 0 + +#============================================================================== +# %% LOAD GRAIN AND EXPERIMENT DATA +#============================================================================== + +experiment, nf_to_ff_id_map = nfutil.gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ + misorientation_spacing,ome_range_deg, num_imgs, beam_stop_width) + +#============================================================================== +# %% NEAR FIELD - MAKE MEDIAN DARK +#============================================================================== +print '>>>>>>>>>>>>>>>>>>loading images>>>>>>>>>>>>>>>>>>' +dark=nfutil.gen_nf_dark(data_folder,img_nums,num_for_dark,experiment.nrows,experiment.ncols,dark_type='median',num_digits=6) + +#============================================================================== +# %% NEAR FIELD - LOAD IMAGE DATA AND PROCESS +#============================================================================== + +image_stack=gen_nf_cleaned_image_stack(data_folder,img_nums,dark,ome_dilation_iter,threshold,experiment.nrows,experiment.ncols,num_digits=6)#,grey_bnds=(5,5),gaussian=4.5) + +#============================================================================== +# %% Load STITCHED-DATA from .npz +#============================================================================== + +print ('>>>>>>>>>>>>>>>>>>loading nf map>>>>>>>>>>>>>>>>>>') +hold = np.load(file_dir + npz_file_stem + '_grain_map_data.npz') + +grain_map = hold['grain_map'] +confidence_map = hold['confidence_map'] +Xs = hold['Xs'] +Ys = hold['Ys'] +Zs = hold['Zs'] +ori_list = hold['ori_list'] +id_remap = hold['id_remap'] + +all_grains_in_layer = np.unique(grain_map) + +#%% + +print ('.......multi process..........') +#new gen_trial_data definition +progress_handler = nfutil.progressbar_progress_observer() +save_handler=nfutil.forgetful_result_handler() + +controller = nfutil.ProcessController(save_handler, progress_handler, + ncpus=44, chunk_size=chunk_size) + +multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + +#%% +mis_all = np.zeros(grain_map.shape) +confidence_index_new = np.copy(confidence_map) +grain_map_new = np.copy(grain_map) +compiled_map = np.copy(grain_map.astype('float')) + +#%% +import scipy.ndimage.morphology as morphology + +#%% +for grain in range(1,all_grains_in_layer.shape[0]): + #GRAIN ID -- USER INPUT + grain_id = all_grains_in_layer[grain] + + print ('>>>>>>>>>>>>>>>>iteration %d>>>>>>>>>>>>>>>' % grain) + print ('>>>>>>>>>>>>>>>>>>grain %d>>>>>>>>>>>>>>>>>' % grain_id) + + grain_id_out_file= grain_id_out_dir + 'grain_id_%s.out' % (grain_id) + + #LOAD EXP_MAPS CONFIDENCE TO DECIDE THRESHOLDS + ori_out = np.loadtxt(grain_id_out_file) + ori_data = ori_out[:,3:6] + ori_comp=ori_out[:,1] + + comp_thresh=np.amax(ori_comp)*0.8 #only use orientations with completnesses ABOVE this threshold + chi2_thresh=1.0 #is not used; make sure value > 0 + + #GENERATES GRAIN MASK FROM GRAIN_MAP + grains_plot_binary_0 = np.copy(grain_map) + + grains_plot_binary_0[grains_plot_binary_0 < grain_id] = 0 + grains_plot_binary_0[grains_plot_binary_0 > grain_id] = 0 + grains_plot_binary_0[grains_plot_binary_0 == grain_id] = 1 + + grains_plot_binary = morphology.binary_dilation(grains_plot_binary_0,iterations=5).astype('int') + + # GRAIN MASK PROCESSING - CREATE GRAIN TEST GRID + test=np.where(grains_plot_binary) + + test_crd_grain=np.zeros([len(test[0]),3]) + + for ii in np.arange(len(test[0])): + test_crd_grain[ii,0]=Xs[test[0][ii],test[1][ii],test[2][ii]] + test_crd_grain[ii,1]=Ys[test[0][ii],test[1][ii],test[2][ii]] + test_crd_grain[ii,2]=Zs[test[0][ii],test[1][ii],test[2][ii]] + + print ('----number of test coordinates = %d -----' % test_crd_grain.shape[0]) + + if test_crd_grain.shape[0] == 0: + pass + + else: + # LOAD GRAIN AND EXPERIMENT DATA + experiment_g, nf_to_ff_id_map_g = gen_trial_exp_data(grain_id_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, \ + ome_range_deg, num_imgs, beam_stop_width) + + print ('----number of orientations = %d -----' % len(experiment_g.exp_maps)) + + if len(experiment_g.exp_maps) < 10: + pass + + else: + + # INSTANTIATE CONTROLLER - RUN BLOCK NO EDITING + progress_handler = nfutil.progressbar_progress_observer() + save_handler=nfutil.forgetful_result_handler() + + controller = nfutil.ProcessController(save_handler, progress_handler, + ncpus=44, chunk_size=chunk_size) + + multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + global _multiprocessing_start_method + _multiprocessing_start_method = 'fork' + #============================================================================== + # TEST ORIENTATIONS WITHIN GRAIN + #============================================================================== + try: + + raw_confidence_mis=nfutil.test_orientations(image_stack, experiment_g, test_crd_grain, + controller,multiprocessing_start_method) + + #============================================================================== + # PUT DATA BACK INTO MESH + #============================================================================== + #full mesh test_crds + print(' >>>>>>>>>>>>>>>>>>>>>putting data back in mesh>>>>>>>>>>>>>>>>>>>>>') + test_crd_all = np.vstack([Xs.flatten(), Ys.flatten(), Zs.flatten()]).T + + raw_confidence_new = np.empty([experiment_g.n_grains,len(test_crd_all[:,0])]) + + for iii in range(0,test_crd_grain.shape[0]): + grain_location = np.where((test_crd_all == test_crd_grain[iii,:]).all(axis=1)) + raw_confidence_new[:,grain_location[0][0]] = raw_confidence_mis[:,iii] + + #============================================================================== + # MASK TEST COORDINATES + #============================================================================== + + print('Compiling Confidence Map...') + confidence_map_g=np.max(raw_confidence_new,axis=0).reshape(Xs.shape) + grain_map_g=np.argmax(raw_confidence_new,axis=0).reshape(Xs.shape) + #id_remap + max_orientation_no=np.max(grain_map_g) + grain_map_copy=np.copy(grain_map_g) + print('Remapping grain ids to ff...') + for ii in np.arange(max_orientation_no): + this_orientation=np.where(grain_map_g==ii) + grain_map_copy[this_orientation]=nf_to_ff_id_map_g[ii] + grain_map_g=grain_map_copy + + #============================================================================== + # MISORIENTATION + #============================================================================== + print('calculate misorientation') + + tmp_data_avg=np.loadtxt(grain_out_file) + tmp_data_grain=np.loadtxt(grain_id_out_file) + + id_avg=tmp_data_avg[:,0] + ori_avg=tmp_data_avg[:,3:6] + id_mis=tmp_data_grain[:,0] + ori_mis=tmp_data_grain[:,3:6] + + mis = np.zeros(grain_map_g.shape) + + q_mor = rot.quatOfExpMap(ori_mis.T) + q_avg = rot.quatOfExpMap(ori_avg.T) + + material_file_loc = mat_file # hexrd material file in cpickle format + mat_name='ti7al' + + mat_list = cpl.load(open(material_file_loc, 'r')) + mat_idx = np.where([mat_list[i].name == mat_name for i in range(len(mat_list))])[0] + + # grab plane data, and useful things hanging off of it + pd = mat_list[mat_idx[0]].planeData + qsyms=sym.quatOfLaueGroup(pd.getLaueGroup()) + + for w in range(0,len(test[0])): + q2 = np.atleast_2d(q_mor[:,grain_map_g[test[0][w],test[1][w],test[2][w]]]).T + q1 = np.atleast_2d(q_avg[:,grain_id]).T + mis[test[0][w],test[1][w],test[2][w]] = rot.misorientation(q1,q2)[0]*180./np.pi + + # plt.imshow(confidence_map_g[20,:,:],cmap='gray') + # plt.hold(True) + # plt.imshow(mis[20,:,:], alpha = 0.5) + # plt.colorbar() + #============================================================================== + # put back into the master mesh + #============================================================================== + print ('put in master mesh') + #fresh array + empty_map_full = np.zeros(compiled_map.shape) + #confidence_index_local = np.copy(empty_map_full) + #grain_map_local = np.copy(empty_map_full) + mis_local = np.copy(empty_map_full) + compiled_map_local = np.copy(empty_map_full) + #place in fresh array + #confidence_index_local[test] = confidence_map_g[test] + #grain_map_local[test] = grain_map_g[test] + mis_local[test] = mis[test] + compiled_map_local[test] = grain_map[test].astype('float') + grain_map_g[test].astype('float')/100000. + + print ('saving as npz') + + save_string = 'grain_%d_nf' % grain_id + #np.savez('/nfs/chess/user/ken38/Ti7_project/nf_data_all/ti7-05-nf/grain-by-grain-nf/'+ save_string, combined_map = local_compiled_map[test], test_crds = test, local_grain_map = grain_map_g[test], local_confidence_map = confidence_map_g[test]) + np.savez(output_dir + save_string, compiled_map_local = compiled_map_local, test_crds = test, local_grain_map = grain_map_g, local_confidence_map = confidence_map_g, mis_local = mis_local) + print ('loop end') + + except: + pass + #============================================================================== + #% save grain mesh + #============================================================================== +#% +#np.savez(output_ext,grain_map_new = grain_map_new,confidence_index_new = confidence_index_new,compiled_map = compiled_map,mis_all = mis_all,Xs=Xs,Ys=Ys,Zs=Zs, grain_map = grain_map, confidence_map = confidence_map) diff --git a/scripts/ighexrd_v1/intragrain_scripts/nf_intragrainrecons.sh b/scripts/ighexrd_v1/intragrain_scripts/nf_intragrainrecons.sh new file mode 100644 index 00000000..29b80e27 --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/nf_intragrainrecons.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# -*- coding: utf-8 -*- + +cd /nfs/chess/aux/user/ken38/Ti7_project/paper-near-field-run/step_1_near_field_scripts/nf_3_final_stitched_orientations/ +source activate hexrd_0526_nftest + +echo starting final gbg nf +#python nf_gbg_final_diffvol_1.py +#python nf_gbg_final_diffvol_2.py +python nf_gbg_final_diffvol_3.py +python nf_gbg_final_diffvol_4.py +echo Done + diff --git a/scripts/ighexrd_v1/intragrain_scripts/stitch_nf_diffvols.py b/scripts/ighexrd_v1/intragrain_scripts/stitch_nf_diffvols.py new file mode 100644 index 00000000..00aef6cd --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/stitch_nf_diffvols.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Sun Aug 18 11:19:45 2019 + +@author: ken38 +""" + +import numpy as np +#import h5py +from hexrd.grainmap import nfutil +#============================================================================== +# %% Load DATA from .npz (ONLY FOR RELOADING DATA) +#============================================================================== + +voxel_spacing=0.005#in mm + +px = 271 +stack = 34 +layers = 4 +overlap_amt = 4 +stack0= 30 + +half_bnds = ((stack0*voxel_spacing*layers)+(overlap_amt*voxel_spacing))/2 +v_bnds = [-half_bnds,half_bnds] + +#%% +#full_stitch = np.empty([stack*layers,px,px]) + +full_stitch = np.empty([0,px,px]) +full_stitch_con = full_stitch +full_stitch_ori = full_stitch +full_stitch_mis = full_stitch +full_stitch_combo = full_stitch +#full_stitch_conori = full_stitch + +npz_save_dir = '/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/NEAR-FIELD/' +npz_string = 'ti7-11-stitched-initial_intragrain' + +#%% +output_dir='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/NEAR-FIELD/' + +for i in range(layers,0,-1): +#np.savez(save_dir+save_name, mask_confidence = mask_confidence,mask_grain = mask_grain, mask_misorientation = mask_misorientation, mask_combined_index=mask_combined_index, grain_avg_orientation_list=grain_avg_orientation_list) + output_stem='initial_nf_intragrain_diffvol_%d' % i + hold = np.load(output_dir+output_stem+'.npz') + grain_map = hold['mask_grain'] + misorientation_map = hold['mask_misorientation'] + confidence_map = hold['mask_confidence'] + #grain_map_ori= hold['grain_map'] + #confidence_map_ori= hold['confidence_map'] + combined_index = hold['mask_combined_index'] + + if i == layers: + mismap_red = misorientation_map + grain_map_red = grain_map + con_red = confidence_map + combo_red = combined_index + #oricon_red = confidence_map_ori + + if i < layers: + for ii in range(0,overlap_amt): + layer_idx = full_stitch_ori.shape[0]-1-ii + for iii in range(0,grain_map.shape[0]): + for iv in range(0,grain_map.shape[1]): + if full_stitch_con[layer_idx,iii,iv] < confidence_map[overlap_amt-1-ii,iii,iv]: + pass + else: + full_stitch_con[layer_idx,iii,iv] = confidence_map[overlap_amt-1-ii,iii,iv] + full_stitch_ori[layer_idx,iii,iv] = grain_map[overlap_amt-1-ii,iii,iv] + full_stitch_mis[layer_idx,iii,iv] = misorientation_map[overlap_amt-1-ii,iii,iv] + full_stitch_combo[layer_idx,iii,iv] = combined_index[overlap_amt-1-ii,iii,iv] + + + grain_map_red = grain_map[overlap_amt:,:,:] + con_red = confidence_map[overlap_amt:,:,:] + combo_red = combined_index[overlap_amt:,:,:] + mismap_red = misorientation_map[overlap_amt:,:,:] + #oricon_red = confidence_map_ori[overlap_amt:,:,:] + + full_stitch_ori = np.append(full_stitch_ori,grain_map_red,axis=0) + full_stitch_con = np.append(full_stitch_con,con_red,axis=0) + full_stitch_mis = np.append(full_stitch_mis,mismap_red,axis=0) + full_stitch_combo = np.append(full_stitch_combo,combo_red,axis=0) + #full_stitch_conori = np.append(full_stitch_conori, oricon_red, axis=0) +#%% + +test_crds, n_crds, Xs, Ys, Zs = nfutil.gen_nf_test_grid_tomo(grain_map.shape[1], grain_map.shape[2], v_bnds, voxel_spacing) + +#============================================================================== +# %% SAVE PROCESSED GRAIN MAP DATA +#============================================================================== + + +#nfutil.save_nf_data(output_dir,output_stem,full_stitch_ori,full_stitch_con,Xs,Ys,Zs, full_stitch_mis, full_stitch_conori) #ori_list,id_remap=id_remap) +np.savez(npz_save_dir + npz_string,grain_map=full_stitch_ori,confidence_map=full_stitch_con, misorientation_map=full_stitch_mis,combined_index=full_stitch_combo, Xs=Xs,Ys=Ys,Zs=Zs) + +#============================================================================== +# %% SAVE DATA AS .H5 +#============================================================================== + +hf=h5py.File(output_dir+npz_string+'_data.h5', 'w')#('data.h5','w') +#save_dir+save_stem+'_data.h5' + +g1=hf.create_group('group1') +g1.create_dataset('grain_map', data=full_stitch_ori) +g1.create_dataset('confidence_map', data=full_stitch_con) +#g1.create_dataset('original_confidence_map', data=full_stitch_conori) +g1.create_dataset('misorientation_map', data=full_stitch_mis) +g1.create_dataset('combined', data = full_stitch_combo) +g1.create_dataset('Xs', data=Xs) +g1.create_dataset('Ys', data=Ys) +g1.create_dataset('Zs', data=Zs) + + +hf.close() diff --git a/scripts/ighexrd_v1/intragrain_scripts/stitch_nf_grains.py b/scripts/ighexrd_v1/intragrain_scripts/stitch_nf_grains.py new file mode 100644 index 00000000..60513151 --- /dev/null +++ b/scripts/ighexrd_v1/intragrain_scripts/stitch_nf_grains.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Wed Jan 2 13:23:50 2019 + +@author: ken38 +""" + +#%% +import scipy.stats as stats +import numpy as np +import matplotlib.pyplot as plt + +#LOCATION OF NF DATA IN AVERAGE MAPS +file_dir='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/' +npz_file_stem = 'initial_nf_uniform_diffvol_1' + +#LOCATION OF INDIVIDUAL NF DATA NPZ FROM GOEs +npz_location = '/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/NEAR-FIELD/' + +GOE_loc='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/GOE/initial_goe' +tomo_mask_file='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/tomo_mask.npy' + +#SAVE DIRECTORY +save_dir = npz_location +#SAVE NAME +save_name = 'initial_nf_intragrain_diffvol_1.npz' + +#%% LOAD NF DATA GENERATED FROM GRAIN.OUT +hold = np.load(file_dir + npz_file_stem + '_grain_map_data.npz') + +grain_map = hold['grain_map'] +confidence_map = hold['confidence_map'] +Xs = hold['Xs'] +Ys = hold['Ys'] +Zs = hold['Zs'] +ori_list = hold['ori_list'] +id_remap = hold['id_remap'] + +all_grains_in_layer = np.unique(grain_map) + +empty_map = np.zeros(grain_map.shape) +full_map_confidence = np.copy(empty_map) +full_map_combined = np.copy(empty_map) +full_misorientation = np.copy(empty_map) +full_grain_map = np.copy(empty_map) + +#%% +for i in range(1,len(all_grains_in_layer)): + + grain = all_grains_in_layer[i] + + try: + local = np.load(npz_location + 'grain_%d_nf.npz' % grain) + local_test_crds = local['test_crds'] + local_grain_map = local['local_grain_map'] + local_confidence_map = local['local_confidence_map'] + local_combined_map = local['compiled_map_local'] + local_mis = local['mis_local'] + for ii in range (0,local_test_crds.shape[1]): + if full_map_confidence[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] > local_confidence_map[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]]: + pass + else : + full_map_confidence[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] = local_confidence_map[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] + full_map_combined[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] = local_combined_map[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] + full_misorientation[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] = local_mis[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] + full_grain_map[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] = local_grain_map[local_test_crds[0][ii],local_test_crds[1][ii],local_test_crds[2][ii]] + except: + print grain + + +#%% plot +plt.close('all') + +layer_to_plot = 30 + +plt.figure() +plt.imshow(confidence_map[layer_to_plot,:,:], vmax=1) +plt.figure() +plt.imshow(full_map_confidence[layer_to_plot,:,:], vmax=1) + +plt.figure() +plt.imshow(full_map_confidence[layer_to_plot,:,:]-confidence_map[layer_to_plot,:,:], vmin=-0.5, vmax=0.5) + +#%% +tomo_mask = np.load(tomo_mask_file) + +#%% +layer_to_plot = 15 +plt.close('all') +mask = np.where(tomo_mask == False) + +mask_confidence = np.copy(full_map_confidence) +mask_confidence[:,mask[0],mask[1]]=-.001 + +mask_grain = np.copy(np.floor(full_map_combined)) +mask_grain[:,mask[0],mask[1]] =-1 + +mask_misorientation=np.copy(full_misorientation) +mask_misorientation[:,mask[0],mask[1]]=-.001 + +mask_combined_index=np.copy(full_map_combined) +mask_combined_index[:,mask[0],mask[1]]=-1 + +mask_local_id = np.copy(full_grain_map) +mask_local_id[:,mask[0],mask[1]] = -1 + +#%% +plt.close('all') +#plt.figure() +#plt.imshow(mask_confidence[layer_to_plot,:,:], vmax=1, cmap='gray') +#plt.figure() +#plt.imshow(mask_grain[layer_to_plot,:,:]) +#plt.figure() +#plt.imshow(mask_grain[layer_to_plot,:,:])#, vmax=1)#, cmap='gray') +plt.imshow(mask_misorientation[layer_to_plot,:,:], vmax=3, alpha = 1) +plt.colorbar() +#plt.figure() +#plt.imshow(mask_combined_index[layer_to_plot,:,:]) + +#%% +grain_avg_orientation_list = np.zeros([np.unique(mask_grain).shape[0], 4]) +mask_combin_id = np.zeros([mask_confidence.shape[0],mask_confidence.shape[1],mask_confidence.shape[2]]) +all_grains_in_mask = np.unique(mask_grain) + +for iii in range(0,np.unique(mask_grain).shape[0]): + grain = all_grains_in_mask[iii] + if grain != -1: + where = np.where(mask_grain==grain) + mode_subgrain_id = stats.mode(mask_local_id[where].astype('int')) + grain_id_out = np.loadtxt(GOE_loc+'/grain_id_%d.out' % grain) + exp_map_mode = grain_id_out[mode_subgrain_id[0],3:6] + grain_avg_orientation_list[iii] = [grain, exp_map_mode[0][0], exp_map_mode[0][1], exp_map_mode[0][2]] + +#%%SAVE NEW NPZ +np.savez(save_dir+save_name, mask_confidence = mask_confidence,mask_grain = mask_grain, mask_misorientation = mask_misorientation, mask_combined_index=mask_combined_index, grain_avg_orientation_list=grain_avg_orientation_list) diff --git a/scripts/ighexrd_v1/missing_grains_scripts/mg_centroid_conver.py b/scripts/ighexrd_v1/missing_grains_scripts/mg_centroid_conver.py new file mode 100644 index 00000000..3bf9bdb2 --- /dev/null +++ b/scripts/ighexrd_v1/missing_grains_scripts/mg_centroid_conver.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Sat Feb 8 14:42:02 2020 + +@author: ken38 +""" + +import numpy as np + + +l=5 +output_dir='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/' +output_stem = 'ti7-11-initial_average_vol_%d' % l + +#global information +voxel_spacing = 0.005# in mm +overlap_in_microns = 0.020 +overlap_amt = overlap_in_microns/voxel_spacing +num_diffvols = 5 + +#diffraction_volume information +layer = 0 +hold = np.load(output_dir+output_stem+'_grain_map_data.npz') + +#============================================================================== +# %% Calculate Additional Parameters from Input (run without changing) +#============================================================================== +grain_map = hold['grain_map'] +confidence_map = hold['confidence_map'] +Xs_1 = hold['Xs'] +Ys_1 = hold['Ys'] +Zs_1 = hold['Zs'] +ori_list = hold['ori_list'] +id_remap = hold['id_remap'] + +px = grain_map.shape[1] +stack = grain_map.shape[0] + +stack0= stack-overlap_amt +half_bnds = ((stack0*voxel_spacing*num_diffvols)+(overlap_amt*voxel_spacing))/2 +v_bnds = [-half_bnds,half_bnds] + +save_file = 'centroid_diff_%i_miss_grain.npy' #FILE NAME SAVED FROM MISSING_GRAIN_CENTROIDS.PY SCRIPT + +layer_1_coordinates = np.load(save_file % 1).astype('int') +layer_2_coordinates = np.load(save_file % 2).astype('int') +layer_3_coordinates = np.load(save_file % 3).astype('int') +layer_4_coordinates = np.load(save_file % 4).astype('int') +layer_5_coordinates = np.load(save_file % 5).astype('int') +#%%% + +test_crds_1 = np.zeros([layer_1_coordinates.shape[0],3]) +for ii in np.arange(layer_1_coordinates.shape[0]): + test_crds_1[ii,0]=Xs_1[layer_1_coordinates[ii,0],layer_1_coordinates[ii,1],layer_1_coordinates[ii,2]] + test_crds_1[ii,1]=Ys_1[layer_1_coordinates[ii,0],layer_1_coordinates[ii,1],layer_1_coordinates[ii,2]] + test_crds_1[ii,2]=Zs_1[layer_1_coordinates[ii,0],layer_1_coordinates[ii,1],layer_1_coordinates[ii,2]] + +test_crds_2 = np.zeros([layer_2_coordinates.shape[0],3]) +for ii in np.arange(layer_2_coordinates.shape[0]): + test_crds_2[ii,0]=Xs_1[layer_2_coordinates[ii,0],layer_2_coordinates[ii,1],layer_2_coordinates[ii,2]] + test_crds_2[ii,1]=Ys_1[layer_2_coordinates[ii,0],layer_2_coordinates[ii,1],layer_2_coordinates[ii,2]] + test_crds_2[ii,2]=Zs_1[layer_2_coordinates[ii,0],layer_2_coordinates[ii,1],layer_2_coordinates[ii,2]] + +test_crds_3 = np.zeros([layer_3_coordinates.shape[0],3]) +for ii in np.arange(layer_3_coordinates.shape[0]): + test_crds_3[ii,0]=Xs_1[layer_3_coordinates[ii,0],layer_3_coordinates[ii,1],layer_3_coordinates[ii,2]] + test_crds_3[ii,1]=Ys_1[layer_3_coordinates[ii,0],layer_3_coordinates[ii,1],layer_3_coordinates[ii,2]] + test_crds_3[ii,2]=Zs_1[layer_3_coordinates[ii,0],layer_3_coordinates[ii,1],layer_3_coordinates[ii,2]] + +test_crds_4 = np.zeros([layer_4_coordinates.shape[0],3]) +for ii in np.arange(layer_4_coordinates.shape[0]): + test_crds_4[ii,0]=Xs_1[layer_4_coordinates[ii,0],layer_4_coordinates[ii,1],layer_4_coordinates[ii,2]] + test_crds_4[ii,1]=Ys_1[layer_4_coordinates[ii,0],layer_4_coordinates[ii,1],layer_4_coordinates[ii,2]] + test_crds_4[ii,2]=Zs_1[layer_4_coordinates[ii,0],layer_4_coordinates[ii,1],layer_4_coordinates[ii,2]] + +test_crds_5 = np.zeros([layer_5_coordinates.shape[0],3]) +for ii in np.arange(layer_5_coordinates.shape[0]): + test_crds_5[ii,0]=Xs_1[layer_5_coordinates[ii,0],layer_5_coordinates[ii,1],layer_5_coordinates[ii,2]] + test_crds_5[ii,1]=Ys_1[layer_5_coordinates[ii,0],layer_5_coordinates[ii,1],layer_5_coordinates[ii,2]] + test_crds_5[ii,2]=Zs_1[layer_5_coordinates[ii,0],layer_5_coordinates[ii,1],layer_5_coordinates[ii,2]] + +np.save('missing_coords_vol_1.npy',test_crds_1) +np.save('missing_coords_vol_2.npy',test_crds_2) +np.save('missing_coords_vol_3.npy',test_crds_3) +np.save('missing_coords_vol_4.npy',test_crds_4) +np.save('missing_coords_vol_5.npy',test_crds_5) + diff --git a/scripts/ighexrd_v1/missing_grains_scripts/mg_finder.py b/scripts/ighexrd_v1/missing_grains_scripts/mg_finder.py new file mode 100644 index 00000000..935f3cbc --- /dev/null +++ b/scripts/ighexrd_v1/missing_grains_scripts/mg_finder.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Fri Feb 7 14:13:57 2020 + +@author: ken38 +""" + +import numpy as np +import h5py +from hexrd.grainmap import nfutil +import matplotlib.pyplot as plt +#============================================================================== +# %% Input Parameters and Load DATA from .npz (FOR GENERATING OTHER PARAMETERS) +#============================================================================== +l=5 #diffraction volume - each of mine were labeled in the output stem +output_dir='/nfs/chess/aux/user/ken38/Ti7_project/ti7-11-1percent/' +output_stem = 'ti7-11-initial_average_vol_%d' % l + +#global information +voxel_spacing = 0.005# in mm +overlap_in_microns = 0.020 +overlap_amt = overlap_in_microns/voxel_spacing +num_diffvols = 5 + +#diffraction_volume information +layer = 0 +hold = np.load(output_dir+output_stem+'_grain_map_data.npz') + +#============================================================================== +# %% Calculate Additional Parameters from Input (run without changing) +#============================================================================== +grain_map = hold['grain_map'] +confidence_map = hold['confidence_map'] +Xs = hold['Xs'] +Ys = hold['Ys'] +Zs = hold['Zs'] +ori_list = hold['ori_list'] +id_remap = hold['id_remap'] + +px = grain_map.shape[1] +stack = grain_map.shape[0] + +stack0= stack-overlap_amt +half_bnds = ((stack0*voxel_spacing*num_diffvols)+(overlap_amt*voxel_spacing))/2 +v_bnds = [-half_bnds,half_bnds] + +#%% IDENTIFY LOW CONFIDENCE REGION +conf_threshold_high = 0.45 #please note, I had very poor quality data in this example so you will likely want a much higher number than this. +conf_threshold_low = 0.0 + +low_conf = np.logical_and(confidence_mapconf_threshold_low) + +#%% CHECK THAT YOUR THRESHOLD IS IDENTIFYING AREAS YOU WANT +layer_no=30 + +plt.figure('area check') +plt.imshow(confidence_map[layer_no,:,:]) +plt.hold('on') +plt.imshow(low_conf[layer_no,:,:], alpha = 0.2) + +#%% ADDITIONAL CHECK - LOOK AT HOW THE AREAS WILL SEGMENT AND MAKE SURE IT DOESNT RECOGNIZE AS ONE CONTIGUOUS BLOB +layer_no=11 + +from skimage import measure +from skimage import filters + +all_labels = measure.label(low_conf[0:32,:,:]) +blob_labels = measure.label(low_conf[0:32,:,:], background = 0) + +plt.figure('labels',figsize=(9, 3.5)) +plt.subplot(131) +plt.imshow(low_conf[layer_no,:,:], cmap='gray') +plt.axis('off') +plt.subplot(132) +plt.imshow(all_labels[layer_no,:,:], cmap='nipy_spectral') +plt.axis('off') +plt.subplot(133) +plt.imshow(blob_labels[layer_no,:,:], cmap='nipy_spectral') +plt.axis('off') + +plt.tight_layout() +plt.show() + +#%% CREATE CENTROID MAP OF LOW CONFIDENCE REGION + +from scipy import ndimage + +blob_labels_2 = ndimage.label(low_conf[0:32,:,:])[0] +centroids_2 = ndimage.measurements.center_of_mass(low_conf[0:32,:,:], blob_labels_2, np.unique(blob_labels_2)) + +centroid_point_map = np.zeros(np.shape(confidence_map)) +centroid_new = np.empty([0,3]) +for i in range(1,len(centroids_2)): + where=len(np.where(blob_labels_2==i)[0]) + if where>10: + print i + centroid_new = np.append(centroid_new,np.reshape(np.array(centroids_2[i]),[1,3]),axis=0) + centroid_point_map[np.rint(centroids_2[i][0]).astype('int'),np.rint(centroids_2[i][1]).astype('int'), np.rint(centroids_2[i][2]).astype('int')] = 10 + +#%% CAN CHECK THE CENTROIDS ARE IN LOCATIONS EXPECTED. + +layer_no=10 +plt.figure('area check') +plt.imshow(confidence_map[layer_no,:,:]) +plt.hold('on') +plt.imshow(low_conf[layer_no,:,:], alpha = 0.2) +plt.imshow(centroid_point_map[layer_no,:,:], alpha = 0.5) + +#%% SAVE FILE OF CENTROIDS + +save_file = 'centroid_diff_%d_miss_grain.npy' % l +np.save(save_file,centroid_new) + +#%% + diff --git a/scripts/ighexrd_v1/missing_grains_scripts/mg_orientation_finder.py b/scripts/ighexrd_v1/missing_grains_scripts/mg_orientation_finder.py new file mode 100644 index 00000000..b77ef851 --- /dev/null +++ b/scripts/ighexrd_v1/missing_grains_scripts/mg_orientation_finder.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Tue Aug 14 11:57:34 2018 + +@author: ken38 +""" + +#%% Necessary Dependencies +# PROCESSING NF GRAINS WITH MISORIENTATION +#============================================================================== +import numpy as np + +import matplotlib.pyplot as plt + +import multiprocessing as mp + +import os + +from hexrd.grainmap import nfutil +from hexrd.grainmap import tomoutil +from hexrd.grainmap import vtkutil + +from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd import rotations as rot +#============================================================================== +# %% FILES TO LOAD -CAN BE EDITED +#============================================================================== + +main_dir = '/nfs/chess/user/ken38/Ti7_project/ti7-11-1percent/' #working directory. + +det_file = main_dir + 'retiga.yml' #near-field camera configuration file +mat_file= main_dir + 'materials.cpl' #A materials file, is a cPickle file which contains material information like lattice +#parameters necessary for the reconstruction + +missing_grain_coordinates = 'missing_coords_vol_1.npy' #missing coordinate list identified from the find missing centroids script. +quaternion_test_list = 'quat_2.npy' #I recommend a fine discritization over the fundamental region of orientation space for your material. + +#============================================================================== +# %% OUTPUT INFO -CAN BE EDITED +#============================================================================== + +output_dir = main_dir #can change output directory. +output_stem='ti7-11-initial_average_vol_1' +new_quat_save_output = 'quats_to_add_vol_1.npy' + +#============================================================================== +# %% TOMOGRAPHY DATA FILES -CAN BE EDITED - ZERO LOAD SCAN +#============================================================================== + +#Locations of tomography bright field images +tbf_data_folder='/nfs/chess/raw/2018-1/f2/miller-774-1/ti7-11/2/nf/' + +tbf_img_start=31171 #for this rate, this is the 6th file in the folder +tbf_num_imgs=10 + +#Locations of tomography images +tomo_data_folder='/nfs/chess/raw/2018-1/f2/miller-774-1/ti7-11/3/nf/' + +tomo_img_start=31187#for this rate, this is the 6th file in the folder +tomo_num_imgs=360 + +#============================================================================== +# %% GRAINS.OUT SCAN FOR THE LOAD STEP OF INTEREST +#============================================================================== +grain_out_file = main_dir + 'ti7-11-scan-14/grains.out' + +#%% +#Locations of near field images +#Locations of near field images +data_folder='/nfs/chess/raw/2018-1/f2/miller-774-1/ti7-11/7/nf/' #layer 1 + +#img_start=31601#for 0.25 degree/steps and 5 s exposure, end up with 5 junk frames up front, this is the 6th +img_start=31602#for 0.25 degree/steps and 5 s exposure, end up with 6 junk frames up front, this is the 7th +num_imgs=1440 +img_nums=np.arange(img_start,img_start+num_imgs,1) + +#============================================================================== +# %% USER OPTIONS -CAN BE EDITED #WILL WANT THESE TO BE SET THE SAME AS INITIAL NEAR FIELD +#============================================================================== +x_ray_energy=61.332 #keV + +#name of the material for the reconstruction +mat_name='ti7al' + +#reconstruction with misorientation included, for many grains, this will quickly +#make the reconstruction size unmanagable +misorientation_bnd=0.0 #degrees +misorientation_spacing=0.25 #degrees + +beam_stop_width=0.6#mm, assumed to be in the center of the detector + +ome_range_deg=[(0.,359.75)] #degrees + +max_tth=-1. #degrees, if a negative number is input, all peaks that will hit the detector are calculated + +#image processing +num_for_dark=250#num images to use for median data +threshold=6. +num_erosions=2 #num iterations of images erosion, don't mess with unless you know what you're doing +num_dilations=3 #num iterations of images erosion, don't mess with unless you know what you're doing +ome_dilation_iter=1 #num iterations of 3d image stack dilations, don't mess with unless you know what you're doing + +chunk_size=500#chunksize for multiprocessing, don't mess with unless you know what you're doing + +#thresholds for grains in reconstructions +comp_thresh=0.3#nly use orientations from grains with completnesses ABOVE this threshold +chi2_thresh=1.0#nly use orientations from grains BELOW this chi^2 + +#tomography options +layer_row=1024 # row of layer to use to find the cross sectional specimen shape +recon_thresh=0.00025#usually varies between 0.0001 and 0.0005 +#Don't change these unless you know what you are doing, this will close small holes +#and remove noise +noise_obj_size=500 +min_hole_size=500 + +cross_sectional_dim=1.35 #cross sectional to reconstruct (should be at least 20%-30% over sample width) +#voxel spacing for the near field reconstruction +voxel_spacing = 0.005#in mm +##vertical (y) reconstruction voxel bounds in mm +v_bnds=[-0.085,0.085] +#v_bnds=[-0.,0.] +#======================= +#============================================ + + ####END USER INPUT#### + +#============================================================================== +# %% LOAD GRAIN AND EXPERIMENT DATA +#============================================================================== + +experiment, nf_to_ff_id_map = nfutil.gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ + misorientation_spacing,ome_range_deg, num_imgs, beam_stop_width) + +#============================================================================== +# %% TOMO PROCESSING - GENERATE BRIGHT FIELD +#============================================================================== + +tbf=tomoutil.gen_bright_field(tbf_data_folder,tbf_img_start,tbf_num_imgs,experiment.nrows,experiment.ncols,num_digits=6) + +#============================================================================== +# %% TOMO PROCESSING - BUILD RADIOGRAPHS +#============================================================================== + +rad_stack=tomoutil.gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,experiment.nrows,experiment.ncols,num_digits=6) + +#============================================================================== +# %% TOMO PROCESSING - INVERT SINOGRAM +#============================================================================== + +reconstruction_fbp=tomoutil.tomo_reconstruct_layer(rad_stack,cross_sectional_dim,layer_row=layer_row,\ + start_tomo_ang=ome_range_deg[0][0],end_tomo_ang=ome_range_deg[0][1],\ + tomo_num_imgs=tomo_num_imgs, center=experiment.detector_params[3]) + +#============================================================================== +# %% TOMO PROCESSING - CLEAN TOMO RECONSTRUCTION +#============================================================================== + +binary_recon=tomoutil.threshold_and_clean_tomo_layer(reconstruction_fbp,recon_thresh, noise_obj_size,min_hole_size) + +#============================================================================== +# %% TOMO PROCESSING - RESAMPLE TOMO RECONSTRUCTION +#============================================================================== + +tomo_mask=tomoutil.crop_and_rebin_tomo_layer(binary_recon,recon_thresh,voxel_spacing,experiment.pixel_size[0],cross_sectional_dim) + +#============================================================================== +# %% TOMO PROCESSING - CONSTRUCT DATA GRID +#============================================================================== + +test_crds, n_crds, Xs, Ys, Zs = nfutil.gen_nf_test_grid_tomo(tomo_mask.shape[1], tomo_mask.shape[0], v_bnds, voxel_spacing) + +#============================================================================== +# %% NEAR FIELD - MAKE MEDIAN DARK +#============================================================================== + +dark=nfutil.gen_nf_dark(data_folder,img_nums,num_for_dark,experiment.nrows,experiment.ncols,dark_type='median',num_digits=6) + +#============================================================================== +# %% NEAR FIELD - LOAD IMAGE DATA AND PROCESS +#============================================================================== + +image_stack=gen_nf_cleaned_image_stack(data_folder,img_nums,dark,ome_dilation_iter,threshold,experiment.nrows,experiment.ncols,num_digits=6,grey_bnds=(5,5),gaussian=4.5) + +#%% + +test_crds_load = np.load(output_dir + missing_grain_coordinates) +test_crds = test_crds_load[:,:] +n_crds = test_crds.shape[0] + +random_quaternions = np.load(output_dir + quaternion_test_list) + +n_grains = random_quaternions.shape[1] +rMat_c = rot.rotMatOfQuat(random_quaternions) +exp_maps = np.zeros([random_quaternions.shape[1],3]) +for i in range(0,random_quaternions.shape[1]): + phi = 2*np.arccos(random_quaternions[0,i]) + n = xfcapi.unitRowVector(random_quaternions[1:,i]) + exp_maps[i,:] = phi*n + +#%% + +experiment.n_grains = n_grains +experiment.rMat_c = rMat_c +experiment.exp_maps = exp_maps + +#============================================================================== +# %% INSTANTIATE CONTROLLER - RUN BLOCK NO EDITING +#============================================================================== + +progress_handler = nfutil.progressbar_progress_observer() +save_handler=nfutil.forgetful_result_handler() + +controller = nfutil.ProcessController(save_handler, progress_handler, + ncpus=mp.cpu_count(), chunk_size=chunk_size) + +#controller = nfutil.ProcessController(save_handler, progress_handler, +# ncpus=40, chunk_size=chunk_size) + +multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + +#============================================================================== +# %% TEST ORIENTATIONS - RUN BLOCK NO EDITING +#============================================================================== + +raw_confidence=nfutil.test_orientations(image_stack, experiment, test_crds, + controller,multiprocessing_start_method) + + +#%% +best_quaternion = np.zeros([test_crds.shape[0],4]) +for i in range(0,raw_confidence.shape[1]): + where = np.where(raw_confidence[:,i] == np.max(raw_confidence[:,i])) + best_quaternion[i,:] = random_quaternions[:,where[0][0]] + print np.max(raw_confidence[:,i]) + +#%% +np.save(output_dir + new_quat_save_output, best_quaternion) From 62c0197c917ab27495159d8410f0ecf60bc21a17 Mon Sep 17 00:00:00 2001 From: kenygren <40276151+kenygren@users.noreply.github.com> Date: Fri, 1 May 2020 23:23:53 -0400 Subject: [PATCH 230/253] Add files via upload --- hexrd/grainmap/tomoutil.py | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/hexrd/grainmap/tomoutil.py b/hexrd/grainmap/tomoutil.py index 326ce707..e068e212 100644 --- a/hexrd/grainmap/tomoutil.py +++ b/hexrd/grainmap/tomoutil.py @@ -36,15 +36,38 @@ def gen_bright_field(tbf_data_folder,tbf_img_start,tbf_num_imgs,nrows,ncols,stem tbf=np.median(tbf_stack,axis=0) return tbf + + + +def gen_median_image(data_folder,img_start,num_imgs,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): + + + img_nums=np.arange(img_start,img_start+num_imgs,1) + + + stack=np.zeros([num_imgs,nrows,ncols]) + print('Loading data for median image...') + for ii in np.arange(num_imgs): + print('Image #: ' + str(ii)) + stack[ii,:,:]=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext) + #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) + print('making median...') + + med=np.median(stack,axis=0) -def gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): + return med + +def gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,nrows,ncols,stem='nf_',num_digits=5,ext='.tif',tdf=None): #Reconstructs a single tompgrahy layer to find the extent of the sample tomo_img_nums=np.arange(tomo_img_start,tomo_img_start+tomo_num_imgs,1) + #if tdf==None: + if len(tdf) == None: + tdf=np.zeros([nrows,ncols]) rad_stack=np.zeros([tomo_num_imgs,nrows,ncols]) @@ -53,7 +76,7 @@ def gen_attenuation_rads(tomo_data_folder,tbf,tomo_img_start,tomo_num_imgs,nrows print('Image #: ' + str(ii)) tmp_img=imgio.imread(tomo_data_folder+'%s'%(stem)+str(tomo_img_nums[ii]).zfill(num_digits)+ext) - rad_stack[ii,:,:]=-np.log(tmp_img.astype(float)/tbf.astype(float)) + rad_stack[ii,:,:]=-np.log((tmp_img.astype(float)-tdf)/(tbf.astype(float)-tdf)) return rad_stack @@ -66,7 +89,7 @@ def tomo_reconstruct_layer(rad_stack,cross_sectional_dim,layer_row=1024,start_to theta = np.linspace(start_tomo_ang, end_tomo_ang, tomo_num_imgs, endpoint=False) - max_rad=int(cross_sectional_dim/pixel_size/2.*np.sqrt(2.)) + max_rad=int(cross_sectional_dim/pixel_size/2.*1.1) #10% slack to avoid edge effects if rotation_axis_pos>=0: sinogram_cut=sinogram[:,2*rotation_axis_pos:] @@ -119,7 +142,7 @@ def threshold_and_clean_tomo_layer(reconstruction_fbp,recon_thresh, noise_obj_si return binary_recon -def crop_and_rebin_tomo_layer(binary_recon,recon_thresh,voxel_spacing,pixel_size,cross_sectional_dim): +def crop_and_rebin_tomo_layer(binary_recon,recon_thresh,voxel_spacing,pixel_size,cross_sectional_dim,circular_mask_rad=None): scaling=voxel_spacing/pixel_size rows=binary_recon.shape[0] @@ -138,6 +161,15 @@ def crop_and_rebin_tomo_layer(binary_recon,recon_thresh,voxel_spacing,pixel_size cut_edge=int(np.round((binary_recon_bin.shape[0]*voxel_spacing-cross_sectional_dim)/2./voxel_spacing)) binary_recon_bin=binary_recon_bin[cut_edge:-cut_edge,cut_edge:-cut_edge] + + if circular_mask_rad is not None: + center = binary_recon_bin.shape[0]/2 + radius = np.round(circular_mask_rad/voxel_spacing) + nx,ny = binary_recon_bin.shape + y,x = np.ogrid[-center:nx-center,-center:ny-center] + mask = x*x + y*y > radius*radius + + binary_recon_bin[mask]=0 return binary_recon_bin From fa45909672b61d11d43b1ded9d448f11ecc007ff Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 7 May 2020 08:13:45 -0700 Subject: [PATCH 231/253] fix to eta edges --- hexrd/instrument.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index f4a72e50..600b5e93 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -78,14 +78,14 @@ # PARAMETERS # ============================================================================= -instrument_name_DFLT = 'GE' +instrument_name_DFLT = 'instrument' beam_energy_DFLT = 65.351 beam_vec_DFLT = ct.beam_vec eta_vec_DFLT = ct.eta_vec -panel_id_DFLT = "generic" +panel_id_DFLT = 'generic' nrows_DFLT = 2048 ncols_DFLT = 2048 pixel_size_DFLT = (0.2, 0.2) @@ -2357,8 +2357,7 @@ def __init__(self, filename, instr_cfg, grain_params, use_attr=False): self.fid = filename else: self.fid = h5py.File(filename + ".hdf5", "w") - icfg = {} - icfg.update(instr_cfg) + icfg = dict(instr_cfg) # add instrument groups and attributes self.instr_grp = self.fid.create_group('instrument') @@ -2559,10 +2558,8 @@ def __init__(self, image_series_dict, instrument, plane_data, # handle etas # WARNING: unlinke the omegas in imageseries metadata, # these are in RADIANS and represent bin centers - self._etas = etas - self._etaEdges = np.r_[ - etas - 0.5*np.radians(eta_step), - etas[-1] + 0.5*np.radians(eta_step)] + self._etaEdges = etas + self._etas = self._etaEdges[:-1] + 0.5*np.radians(eta_step) @property def dataStore(self): From ec217dfc4d0e0339d6160f8ff82a258d7e417f0a Mon Sep 17 00:00:00 2001 From: rachelelim Date: Tue, 12 May 2020 21:22:30 -0400 Subject: [PATCH 232/253] fixed grain ID handling with NF misorientation --- nfutil.py | 1153 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1153 insertions(+) create mode 100644 nfutil.py diff --git a/nfutil.py b/nfutil.py new file mode 100644 index 00000000..34a35043 --- /dev/null +++ b/nfutil.py @@ -0,0 +1,1153 @@ + + + + +#%% + +import time +import os +import logging +import numpy as np +import copy + +import numba +import argparse +import contextlib +import multiprocessing +import tempfile +import shutil + +from hexrd.xrd import transforms as xf +from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd import xrdutil + +from hexrd.xrd import rotations as rot +from hexrd import valunits + +from hexrd.xrd.transforms_CAPI import anglesToGVec, \ + makeRotMatOfExpMap, makeDetectorRotMat, makeOscillRotMat, \ + gvecToDetectorXY, detectorXYToGvec + +import yaml +import cPickle as cpl + +import scipy.ndimage as img +try: + import imageio as imgio +except(ImportError): + from skimage import io as imgio +import matplotlib.pyplot as plt + +# ============================================================================== +# %% SOME SCAFFOLDING +# ============================================================================== + +class ProcessController(object): + """This is a 'controller' that provides the necessary hooks to + track the results of the process as well as to provide clues of + the progress of the process""" + + def __init__(self, result_handler=None, progress_observer=None, ncpus = 1, + chunk_size = 100): + self.rh = result_handler + self.po = progress_observer + self.ncpus = ncpus + self.chunk_size = chunk_size + self.limits = {} + self.timing = [] + + + # progress handling -------------------------------------------------------- + + def start(self, name, count): + self.po.start(name, count) + t = time.time() + self.timing.append((name, count, t)) + + + def finish(self, name): + t = time.time() + self.po.finish() + entry = self.timing.pop() + assert name==entry[0] + total = t - entry[2] + logging.info("%s took %8.3fs (%8.6fs per item).", entry[0], total, total/entry[1]) + + + def update(self, value): + self.po.update(value) + + # result handler ----------------------------------------------------------- + + def handle_result(self, key, value): + logging.debug("handle_result (%(key)s)", locals()) + self.rh.handle_result(key, value) + + # value limitting ---------------------------------------------------------- + def set_limit(self, key, limit_function): + if key in self.limits: + logging.warn("Overwritting limit funtion for '%(key)s'", locals()) + + self.limits[key] = limit_function + + def limit(self, key, value): + try: + value = self.limits[key](value) + except KeyError: + pass + except Exception: + logging.warn("Could not apply limit to '%(key)s'", locals()) + + return value + + # configuration ----------------------------------------------------------- + def get_process_count(self): + return self.ncpus + + def get_chunk_size(self): + return self.chunk_size + + +def null_progress_observer(): + class NullProgressObserver(object): + def start(self, name, count): + pass + + def update(self, value): + pass + + def finish(self): + pass + + return NullProgressObserver() + + +def progressbar_progress_observer(): + from progressbar import ProgressBar, Percentage, Bar + + class ProgressBarProgressObserver(object): + def start(self, name, count): + self.pbar = ProgressBar(widgets=[name, Percentage(), Bar()], + maxval=count) + self.pbar.start() + + def update(self, value): + self.pbar.update(value) + + def finish(self): + self.pbar.finish() + + return ProgressBarProgressObserver() + + +def forgetful_result_handler(): + class ForgetfulResultHandler(object): + def handle_result(self, key, value): + pass # do nothing + + return ForgetfulResultHandler() + + +def saving_result_handler(filename): + """returns a result handler that saves the resulting arrays into a file + with name filename""" + class SavingResultHandler(object): + def __init__(self, file_name): + self.filename = file_name + self.arrays = {} + + def handle_result(self, key, value): + self.arrays[key] = value + + def __del__(self): + logging.debug("Writing arrays in %(filename)s", self.__dict__) + try: + np.savez_compressed(open(self.filename, "wb"), **self.arrays) + except IOError: + logging.error("Failed to write %(filename)s", self.__dict__) + + return SavingResultHandler(filename) + + +def checking_result_handler(filename): + """returns a return handler that checks the results against a + reference file. + + The Check will consider a FAIL either a result not present in the + reference file (saved as a numpy savez or savez_compressed) or a + result that differs. It will consider a PARTIAL PASS if the + reference file has a shorter result, but the existing results + match. A FULL PASS will happen when all existing results match + + """ + class CheckingResultHandler(object): + def __init__(self, reference_file): + """Checks the result against those save in 'reference_file'""" + logging.info("Loading reference results from '%s'", reference_file) + self.reference_results = np.load(open(reference_file, 'rb')) + + def handle_result(self, key, value): + if key in ['experiment', 'image_stack']: + return #ignore these + + try: + reference = self.reference_results[key] + except KeyError as e: + logging.warning("%(key)s: %(e)s", locals()) + reference = None + + if reference is None: + msg = "'{0}': No reference result." + logging.warn(msg.format(key)) + + try: + if key=="confidence": + reference = reference.T + value = value.T + + check_len = min(len(reference), len(value)) + test_passed = np.allclose(value[:check_len], reference[:check_len]) + + if not test_passed: + msg = "'{0}': FAIL" + logging.warn(msg.format(key)) + lvl = logging.WARN + elif len(value) > check_len: + msg = "'{0}': PARTIAL PASS" + lvl = logging.WARN + else: + msg = "'{0}': FULL PASS" + lvl = logging.INFO + logging.log(lvl, msg.format(key)) + except Exception as e: + msg = "%(key)s: Failure trying to check the results.\n%(e)s" + logging.error(msg, locals()) + + return CheckingResultHandler(filename) + + +# ============================================================================== +# %% OPTIMIZED BITS +# ============================================================================== + +# Some basic 3d algebra ======================================================== +@numba.njit +def _v3_dot(a, b): + return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + + +@numba.njit +def _m33_v3_multiply(m, v, dst): + v0 = v[0]; v1 = v[1]; v2 = v[2] + dst[0] = m[0, 0]*v0 + m[0, 1]*v1 + m[0, 2]*v2 + dst[1] = m[1, 0]*v0 + m[1, 1]*v1 + m[1, 2]*v2 + dst[2] = m[2, 0]*v0 + m[2, 1]*v1 + m[2, 2]*v2 + + return dst + + +@numba.njit +def _v3_normalized(src, dst): + v0 = src[0] + v1 = src[1] + v2 = src[2] + sqr_norm = v0*v0 + v1*v1 + v2*v2 + inv_norm = 1.0 if sqr_norm == 0.0 else 1./np.sqrt(sqr_norm) + + dst[0] = v0 * inv_norm + dst[1] = v1 * inv_norm + dst[2] = v2 * inv_norm + + return dst + + +@numba.njit +def _make_binary_rot_mat(src, dst): + v0 = src[0]; v1 = src[1]; v2 = src[2] + + dst[0,0] = 2.0*v0*v0 - 1.0 + dst[0,1] = 2.0*v0*v1 + dst[0,2] = 2.0*v0*v2 + dst[1,0] = 2.0*v1*v0 + dst[1,1] = 2.0*v1*v1 - 1.0 + dst[1,2] = 2.0*v1*v2 + dst[2,0] = 2.0*v2*v0 + dst[2,1] = 2.0*v2*v1 + dst[2,2] = 2.0*v2*v2 - 1.0 + + return dst + + +# code transcribed in numba from transforms module ============================= + +# This is equivalent to the transform module anglesToGVec, but written in +# numba. This should end in a module to share with other scripts +@numba.njit +def _anglesToGVec(angs, rMat_ss, rMat_c): + """From a set of angles return them in crystal space""" + result = np.empty_like(angs) + for i in range(len(angs)): + cx = np.cos(0.5*angs[i, 0]) + sx = np.sin(0.5*angs[i, 0]) + cy = np.cos(angs[i,1]) + sy = np.sin(angs[i,1]) + g0 = cx*cy + g1 = cx*sy + g2 = sx + + # with g being [cx*xy, cx*sy, sx] + # result = dot(rMat_c, dot(rMat_ss[i], g)) + t0_0 = rMat_ss[ i, 0, 0]*g0 + rMat_ss[ i, 1, 0]*g1 + rMat_ss[ i, 2, 0]*g2 + t0_1 = rMat_ss[ i, 0, 1]*g0 + rMat_ss[ i, 1, 1]*g1 + rMat_ss[ i, 2, 1]*g2 + t0_2 = rMat_ss[ i, 0, 2]*g0 + rMat_ss[ i, 1, 2]*g1 + rMat_ss[ i, 2, 2]*g2 + + result[i, 0] = rMat_c[0, 0]*t0_0 + rMat_c[ 1, 0]*t0_1 + rMat_c[ 2, 0]*t0_2 + result[i, 1] = rMat_c[0, 1]*t0_0 + rMat_c[ 1, 1]*t0_1 + rMat_c[ 2, 1]*t0_2 + result[i, 2] = rMat_c[0, 2]*t0_0 + rMat_c[ 1, 2]*t0_1 + rMat_c[ 2, 2]*t0_2 + + return result + + +# This is equivalent to the transform's module gvecToDetectorXYArray, but written in +# numba. +# As of now, it is not a good replacement as efficient allocation of the temporary +# arrays is not competitive with the stack allocation using in the C version of the +# code (WiP) + +# tC varies per coord +# gvec_cs, rSm varies per grain +# +# gvec_cs +beam = xf.bVec_ref[:, 0] +Z_l = xf.Zl[:,0] +@numba.jit() +def _gvec_to_detector_array(vG_sn, rD, rSn, rC, tD, tS, tC): + """ beamVec is the beam vector: (0, 0, -1) in this case """ + ztol = xrdutil.epsf + p3_l = np.empty((3,)) + tmp_vec = np.empty((3,)) + vG_l = np.empty((3,)) + tD_l = np.empty((3,)) + norm_vG_s = np.empty((3,)) + norm_beam = np.empty((3,)) + tZ_l = np.empty((3,)) + brMat = np.empty((3,3)) + result = np.empty((len(rSn), 2)) + + _v3_normalized(beam, norm_beam) + _m33_v3_multiply(rD, Z_l, tZ_l) + + for i in xrange(len(rSn)): + _m33_v3_multiply(rSn[i], tC, p3_l) + p3_l += tS + p3_minus_p1_l = tD - p3_l + + num = _v3_dot(tZ_l, p3_minus_p1_l) + _v3_normalized(vG_sn[i], norm_vG_s) + + _m33_v3_multiply(rC, norm_vG_s, tmp_vec) + _m33_v3_multiply(rSn[i], tmp_vec, vG_l) + + bDot = -_v3_dot(norm_beam, vG_l) + + if bDot < ztol or bDot > 1.0 - ztol: + result[i, 0] = np.nan + result[i, 1] = np.nan + continue + + _make_binary_rot_mat(vG_l, brMat) + _m33_v3_multiply(brMat, norm_beam, tD_l) + denom = _v3_dot(tZ_l, tD_l) + + if denom < ztol: + result[i, 0] = np.nan + result[i, 1] = np.nan + continue + + u = num/denom + tmp_res = u*tD_l - p3_minus_p1_l + result[i,0] = _v3_dot(tmp_res, rD[:,0]) + result[i,1] = _v3_dot(tmp_res, rD[:,1]) + + return result + + +@numba.njit +def _quant_and_clip_confidence(coords, angles, image, base, inv_deltas, clip_vals,bshw): + """quantize and clip the parametric coordinates in coords + angles + + coords - (..., 2) array: input 2d parametric coordinates + angles - (...) array: additional dimension for coordinates + base - (3,) array: base value for quantization (for each dimension) + inv_deltas - (3,) array: inverse of the quantum size (for each dimension) + clip_vals - (2,) array: clip size (only applied to coords dimensions) + bshw - (1,) half width of the beam stop in mm + + clipping is performed on ranges [0, clip_vals[0]] for x and + [0, clip_vals[1]] for y + + returns an array with the quantized coordinates, with coordinates + falling outside the clip zone filtered out. + + """ + count = len(coords) + + in_sensor = 0 + matches = 0 + for i in range(count): + xf = coords[i, 0] + yf = coords[i, 1] + + xf = np.floor((xf - base[0]) * inv_deltas[0]) + if not xf >= 0.0: + continue + if not xf < clip_vals[0]: + continue + + if not np.abs(yf)>bshw: + continue + + yf = np.floor((yf - base[1]) * inv_deltas[1]) + + + + if not yf >= 0.0: + continue + if not yf < clip_vals[1]: + continue + + zf = np.floor((angles[i] - base[2]) * inv_deltas[2]) + + in_sensor += 1 + + x, y, z = int(xf), int(yf), int(zf) + + #x_byte = x // 8 + #x_off = 7 - (x % 8) + #if image[z, y, x_byte] (1< 1: + global _multiprocessing_start_method + _multiprocessing_start_method=multiprocessing_start_method + logging.info('Running multiprocess %d processes (%s)', + ncpus, _multiprocessing_start_method) + with grand_loop_pool(ncpus=ncpus, state=(chunk_size, + image_stack, + all_angles, precomp, test_crds, + experiment)) as pool: + for rslice, rvalues in pool.imap_unordered(multiproc_inner_loop, + chunks): + count = rvalues.shape[1] + confidence[:, rslice] = rvalues + finished += count + controller.update(finished) + else: + logging.info('Running in a single process') + for chunk_start in chunks: + chunk_stop = min(n_coords, chunk_start+chunk_size) + rslice, rvalues = _grand_loop_inner(image_stack, all_angles, + precomp, test_crds, experiment, + start=chunk_start, + stop=chunk_stop) + count = rvalues.shape[1] + confidence[:, rslice] = rvalues + finished += count + controller.update(finished) + + controller.finish(subprocess) + controller.handle_result("confidence", confidence) + + del _multiprocessing_start_method + + pool.close() + + return confidence + + +def evaluate_diffraction_angles(experiment, controller=None): + """Uses simulateGVecs to generate the angles used per each grain. + returns a list containg one array per grain. + + experiment -- a bag of experiment values, including the grains specs and other + required parameters. + """ + # extract required data from experiment + exp_maps = experiment.exp_maps + plane_data = experiment.plane_data + detector_params = experiment.detector_params + pixel_size = experiment.pixel_size + ome_range = experiment.ome_range + ome_period = experiment.ome_period + + panel_dims_expanded = [(-10, -10), (10, 10)] + subprocess='evaluate diffraction angles' + pbar = controller.start(subprocess, + len(exp_maps)) + all_angles = [] + ref_gparams = np.array([0., 0., 0., 1., 1., 1., 0., 0., 0.]) + for i, exp_map in enumerate(exp_maps): + gparams = np.hstack([exp_map, ref_gparams]) + sim_results = xrdutil.simulateGVecs(plane_data, + detector_params, + gparams, + panel_dims=panel_dims_expanded, + pixel_pitch=pixel_size, + ome_range=ome_range, + ome_period=ome_period, + distortion=None) + all_angles.append(sim_results[2]) + controller.update(i+1) + pass + controller.finish(subprocess) + + return all_angles + + +def _grand_loop_inner(image_stack, angles, precomp, + coords, experiment, start=0, stop=None): + """Actual simulation code for a chunk of data. It will be used both, + in single processor and multiprocessor cases. Chunking is performed + on the coords. + + image_stack -- the image stack from the sensors + angles -- the angles (grains) to test + coords -- all the coords to test + precomp -- (gvec_cs, rmat_ss) precomputed for each grain + experiment -- bag with experiment parameters + start -- chunk start offset + stop -- chunk end offset + """ + + t = time.time() + n_coords = len(coords) + n_angles = len(angles) + + # experiment geometric layout parameters + rD = experiment.rMat_d + rCn = experiment.rMat_c + tD = experiment.tVec_d[:,0] + tS = experiment.tVec_s[:,0] + + # experiment panel related configuration + base = experiment.base + inv_deltas = experiment.inv_deltas + clip_vals = experiment.clip_vals + distortion = experiment.distortion + bshw=experiment.bsw/2. + + _to_detector = xfcapi.gvecToDetectorXYArray + #_to_detector = _gvec_to_detector_array + stop = min(stop, n_coords) if stop is not None else n_coords + + distortion_fn = None + if distortion is not None and len(distortion > 0): + distortion_fn, distortion_args = distortion + + acc_detector = 0.0 + acc_distortion = 0.0 + acc_quant_clip = 0.0 + confidence = np.zeros((n_angles, stop-start)) + grains = 0 + crds = 0 + + if distortion_fn is None: + for igrn in xrange(n_angles): + angs = angles[igrn]; rC = rCn[igrn] + gvec_cs, rMat_ss = precomp[igrn] + grains += 1 + for icrd in xrange(start, stop): + t0 = time.time() + det_xy = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) + t1 = time.time() + c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, + base, inv_deltas, clip_vals,bshw) + t2 = time.time() + acc_detector += t1 - t0 + acc_quant_clip += t2 - t1 + crds += 1 + confidence[igrn, icrd - start] = c + else: + for igrn in xrange(n_angles): + angs = angles[igrn]; rC = rCn[igrn] + gvec_cs, rMat_ss = precomp[igrn] + grains += 1 + for icrd in xrange(start, stop): + t0 = time.time() + tmp_xys = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) + t1 = time.time() + det_xy = distortion_fn(tmp_xys, distortion_args, invert=True) + t2 = time.time() + c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, + base, inv_deltas, clip_vals,bshw) + t3 = time.time() + acc_detector += t1 - t0 + acc_distortion += t2 - t1 + acc_quant_clip += t3 - t2 + crds += 1 + confidence[igrn, icrd - start] = c + + t = time.time() - t + return slice(start, stop), confidence + + +def multiproc_inner_loop(chunk): + """function to use in multiprocessing that computes the simulation over the + task's alloted chunk of data""" + + chunk_size = _mp_state[0] + n_coords = len(_mp_state[4]) + chunk_stop = min(n_coords, chunk+chunk_size) + return _grand_loop_inner(*_mp_state[1:], start=chunk, stop=chunk_stop) + + +def worker_init(id_state, id_exp): + """process initialization function. This function is only used when the + child processes are spawned (instead of forked). When using the fork model + of multiprocessing the data is just inherited in process memory.""" + import joblib + + global _mp_state + state = joblib.load(id_state) + experiment = joblib.load(id_exp) + _mp_state = state + (experiment,) + +@contextlib.contextmanager +def grand_loop_pool(ncpus, state): + """function that handles the initialization of multiprocessing. It handles + properly the use of spawned vs forked multiprocessing. The multiprocessing + can be either 'fork' or 'spawn', with 'spawn' being required in non-fork + platforms (like Windows) and 'fork' being preferred on fork platforms due + to its efficiency. + """ + # state = ( chunk_size, + # image_stack, + # angles, + # precomp, + # coords, + # experiment ) + global _multiprocessing_start_method + if _multiprocessing_start_method == 'fork': + # Use FORK multiprocessing. + + # All read-only data can be inherited in the process. So we "pass" it as + # a global that the child process will be able to see. At the end of the + # processing the global is removed. + global _mp_state + _mp_state = state + pool = multiprocessing.Pool(ncpus) + yield pool + del (_mp_state) + else: + # Use SPAWN multiprocessing. + + # As we can not inherit process data, all the required data is + # serialized into a temporary directory using joblib. The + # multiprocessing pool will have the "worker_init" as initialization + # function that takes the key for the serialized data, which will be + # used to load the parameter memory into the spawn process (also using + # joblib). In theory, joblib uses memmap for arrays if they are not + # compressed, so no compression is used for the bigger arrays. + import joblib + tmp_dir = tempfile.mkdtemp(suffix='-nf-grand-loop') + try: + # dumb dumping doesn't seem to work very well.. do something ad-hoc + logging.info('Using "%s" as temporary directory.', tmp_dir) + + id_exp = joblib.dump(state[-1], + os.path.join(tmp_dir, + 'grand-loop-experiment.gz'), + compress=True) + id_state = joblib.dump(state[:-1], + os.path.join(tmp_dir, 'grand-loop-data')) + pool = multiprocessing.Pool(ncpus, worker_init, + (id_state[0], id_exp[0])) + yield pool + finally: + logging.info('Deleting "%s".', tmp_dir) + shutil.rmtree(tmp_dir) + + + + + +#%% Loading Utilities + + +def gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ + misorientation_spacing,ome_range_deg, nframes, beam_stop_width): + + print('Loading Grain Data.....') + #gen_grain_data + ff_data=np.loadtxt(grain_out_file) + + #ff_data=np.atleast_2d(ff_data[2,:]) + + exp_maps=ff_data[:,3:6] + t_vec_ds=ff_data[:,6:9] + + + # + completeness=ff_data[:,1] + + chi2=ff_data[:,2] + + n_grains=exp_maps.shape[0] + + rMat_c = rot.rotMatOfExpMap(exp_maps.T) + + + + + cut=np.where(np.logical_and(completeness>comp_thresh,chi20.: + mat_used.planeData.tThMax = np.amax(np.radians(max_tth)) + else: + mat_used.planeData.tThMax = np.amax(pixel_tth) + + pd=mat_used.planeData + + + print('Final Assembly.....') + experiment = argparse.Namespace() + # grains related information + experiment.n_grains = n_grains # this can be derived from other values... + experiment.rMat_c = rMat_c # n_grains rotation matrices (one per grain) + experiment.exp_maps = exp_maps # n_grains exp_maps -angle * rotation axis- (one per grain) + + experiment.plane_data = pd + experiment.detector_params = detector_params + experiment.pixel_size = pixel_size + experiment.ome_range = ome_range + experiment.ome_period = ome_period + experiment.x_col_edges = x_col_edges + experiment.y_row_edges = y_row_edges + experiment.ome_edges = ome_edges + experiment.ncols = ncols + experiment.nrows = nrows + experiment.nframes = nframes# used only in simulate... + experiment.rMat_d = rMat_d + experiment.tVec_d = np.atleast_2d(detector_params[3:6]).T + experiment.chi = detector_params[6] # note this is used to compute S... why is it needed? + experiment.tVec_s = np.atleast_2d(detector_params[7:]).T + experiment.rMat_c = rMat_c + experiment.distortion = None + experiment.panel_dims = panel_dims # used only in simulate... + experiment.base = base + experiment.inv_deltas = inv_deltas + experiment.clip_vals = clip_vals + experiment.bsw = beam_stop_width + + nf_to_ff_id_map=np.tile(cut,27*mis_steps) + + return experiment, nf_to_ff_id_map + +#%% + + +def gen_nf_test_grid_tomo(x_dim_pnts, z_dim_pnts, v_bnds, voxel_spacing): + + if v_bnds[0]==v_bnds[1]: + Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),v_bnds[0],np.arange(z_dim_pnts)) + else: + Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),np.arange(v_bnds[0]+voxel_spacing/2.,v_bnds[1],voxel_spacing),np.arange(z_dim_pnts)) + #note numpy shaping of arrays is goofy, returns(length(y),length(x),length(z)) + + + Zs=(Zs-(z_dim_pnts/2))*voxel_spacing + Xs=(Xs-(x_dim_pnts/2))*voxel_spacing + + + test_crds = np.vstack([Xs.flatten(), Ys.flatten(), Zs.flatten()]).T + n_crds = len(test_crds) + + return test_crds, n_crds, Xs, Ys, Zs + + +#%% + +def gen_nf_dark(data_folder,img_nums,num_for_dark,nrows,ncols,dark_type='median',stem='nf_',num_digits=5,ext='.tif'): + + dark_stack=np.zeros([num_for_dark,nrows,ncols]) + + print('Loading data for dark generation...') + for ii in np.arange(num_for_dark): + print('Image #: ' + str(ii)) + dark_stack[ii,:,:]=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext) + #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) + + if dark_type=='median': + print('making median...') + dark=np.median(dark_stack,axis=0) + elif dark_type=='min': + print('making min...') + dark=np.min(dark_stack,axis=0) + + return dark + + +#%% +def gen_nf_image_stack(data_folder,img_nums,dark,num_erosions,num_dilations,ome_dilation_iter,threshold,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): + + + image_stack=np.zeros([img_nums.shape[0],nrows,ncols],dtype=bool) + + print('Loading and Cleaning Images...') + for ii in np.arange(img_nums.shape[0]): + print('Image #: ' + str(ii)) + tmp_img=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext)-dark + #image procesing + image_stack[ii,:,:]=img.morphology.binary_erosion(tmp_img>threshold,iterations=num_erosions) + image_stack[ii,:,:]=img.morphology.binary_dilation(image_stack[ii,:,:],iterations=num_dilations) + + #%A final dilation that includes omega + print('Final Dilation Including Omega....') + image_stack=img.morphology.binary_dilation(image_stack,iterations=ome_dilation_iter) + + return image_stack + + +#%% +def scan_detector_parm(image_stack, experiment,test_crds,controller,parm_to_opt,parm_vector,slice_shape): + #0-distance + #1-x center + #2-xtilt + #3-ytilt + #4-ztilt + + multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' + + #current detector parameters, note the value for the actively optimized parameters will be ignored + distance=experiment.detector_params[5]#mm + x_cen=experiment.detector_params[3]#mm + xtilt=experiment.detector_params[0] + ytilt=experiment.detector_params[1] + ztilt=experiment.detector_params[2] + + num_parm_pts=len(parm_vector) + + trial_data=np.zeros([num_parm_pts,slice_shape[0],slice_shape[1]]) + + tmp_td=copy.copy(experiment.tVec_d) + for jj in np.arange(num_parm_pts): + print('cycle %d of %d'%(jj+1,num_parm_pts)) + + + if parm_to_opt==0: + tmp_td[2]=parm_vector[jj] + else: + tmp_td[2]=distance + + if parm_to_opt==1: + tmp_td[0]=parm_vector[jj] + else: + tmp_td[0]=x_cen + + if parm_to_opt==2: + rMat_d_tmp=makeDetectorRotMat([parm_vector[jj],ytilt,ztilt]) + elif parm_to_opt==3: + rMat_d_tmp=makeDetectorRotMat([xtilt,parm_vector[jj],ztilt]) + elif parm_to_opt==4: + rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,parm_vector[jj]]) + else: + rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,ztilt]) + + experiment.rMat_d = rMat_d_tmp + experiment.tVec_d = tmp_td + + + + conf=test_orientations(image_stack, experiment, test_crds, + controller,multiprocessing_start_method) + + + trial_data[jj]=np.max(conf,axis=0).reshape(slice_shape) + + return trial_data + +#%% + +def extract_max_grain_map(confidence,grid_shape,binary_recon_bin=None): + if binary_recon_bin == None: + binary_recon_bin=np.ones([grid_shape[1],grid_shape[2]]) + + + conf_squeeze=np.max(confidence,axis=0).reshape(grid_shape) + grains=np.argmax(confidence,axis=0).reshape(grid_shape) + out_bounds=np.where(binary_recon_bin==0) + conf_squeeze[:,out_bounds[0],out_bounds[1]] =-0.001 + + return conf_squeeze,grains +#%% + +def process_raw_confidence(raw_confidence,vol_shape,tomo_mask=None,id_remap=None): + + print('Compiling Confidence Map...') + confidence_map=np.max(raw_confidence,axis=0).reshape(vol_shape) + grain_map=np.argmax(raw_confidence,axis=0).reshape(vol_shape) + + + if tomo_mask is not None: + print('Applying tomography mask...') + out_bounds=np.where(tomo_mask==0) + confidence_map[:,out_bounds[0],out_bounds[1]] =-0.001 + grain_map[:,out_bounds[0],out_bounds[1]] =-1 + + + if id_remap is not None: + max_grain_no=np.max(grain_map) + grain_map_copy=copy.copy(grain_map) + print('Remapping grain ids to ff...') + for ii in np.arange(max_grain_no): + this_grain=np.where(grain_map==ii) + grain_map_copy[this_grain]=id_remap[ii] + grain_map=grain_map_copy + + return grain_map, confidence_map + +#%% + +def save_raw_confidence(save_dir,save_stem,raw_confidence,id_remap=None): + print('Saving raw confidence, might take a while...') + if id_remap is not None: + np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence,id_remap=id_remap) + else: + np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence) +#%% + +def save_nf_data(save_dir,save_stem,grain_map,confidence_map,Xs,Ys,Zs,ori_list,id_remap=None): + print('Saving grain map data...') + if id_remap is not None: + np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list,id_remap=id_remap) + else: + np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list) + +#%% + +def plot_ori_map(grain_map, confidence_map, exp_maps, layer_no,id_remap=None): + + grains_plot=np.squeeze(grain_map[layer_no,:,:]) + conf_plot=np.squeeze(confidence_map[layer_no,:,:]) + n_grains=len(exp_maps) + + rgb_image=np.zeros([grains_plot.shape[0],grains_plot.shape[1],4], dtype='float32') + rgb_image[:,:,3]=1. + + for ii in np.arange(n_grains): + if id_remap is not None: + this_grain=np.where(np.squeeze(grains_plot)==id_remap[ii]) + else: + this_grain=np.where(np.squeeze(grains_plot)==ii) + if np.sum(this_grain[0])>0: + + ori=exp_maps[ii,:] + + #cubic mapping + rgb_image[this_grain[0],this_grain[1],0]=(ori[0]+(np.pi/4.))/(np.pi/2.) + rgb_image[this_grain[0],this_grain[1],1]=(ori[1]+(np.pi/4.))/(np.pi/2.) + rgb_image[this_grain[0],this_grain[1],2]=(ori[2]+(np.pi/4.))/(np.pi/2.) + + + + plt.imshow(rgb_image,interpolation='none') + plt.hold(True) + plt.imshow(conf_plot,vmin=0.0,vmax=1.,interpolation='none',cmap=plt.cm.gray,alpha=0.5) + + From 9b172e781bff09a5da4f55e61b6c0ce68ce1dbae Mon Sep 17 00:00:00 2001 From: rachelelim Date: Tue, 12 May 2020 21:24:41 -0400 Subject: [PATCH 233/253] fixing my dumb mistakes of a commit --- nfutil.py | 1153 ----------------------------------------------------- 1 file changed, 1153 deletions(-) delete mode 100644 nfutil.py diff --git a/nfutil.py b/nfutil.py deleted file mode 100644 index 34a35043..00000000 --- a/nfutil.py +++ /dev/null @@ -1,1153 +0,0 @@ - - - - -#%% - -import time -import os -import logging -import numpy as np -import copy - -import numba -import argparse -import contextlib -import multiprocessing -import tempfile -import shutil - -from hexrd.xrd import transforms as xf -from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.xrd import xrdutil - -from hexrd.xrd import rotations as rot -from hexrd import valunits - -from hexrd.xrd.transforms_CAPI import anglesToGVec, \ - makeRotMatOfExpMap, makeDetectorRotMat, makeOscillRotMat, \ - gvecToDetectorXY, detectorXYToGvec - -import yaml -import cPickle as cpl - -import scipy.ndimage as img -try: - import imageio as imgio -except(ImportError): - from skimage import io as imgio -import matplotlib.pyplot as plt - -# ============================================================================== -# %% SOME SCAFFOLDING -# ============================================================================== - -class ProcessController(object): - """This is a 'controller' that provides the necessary hooks to - track the results of the process as well as to provide clues of - the progress of the process""" - - def __init__(self, result_handler=None, progress_observer=None, ncpus = 1, - chunk_size = 100): - self.rh = result_handler - self.po = progress_observer - self.ncpus = ncpus - self.chunk_size = chunk_size - self.limits = {} - self.timing = [] - - - # progress handling -------------------------------------------------------- - - def start(self, name, count): - self.po.start(name, count) - t = time.time() - self.timing.append((name, count, t)) - - - def finish(self, name): - t = time.time() - self.po.finish() - entry = self.timing.pop() - assert name==entry[0] - total = t - entry[2] - logging.info("%s took %8.3fs (%8.6fs per item).", entry[0], total, total/entry[1]) - - - def update(self, value): - self.po.update(value) - - # result handler ----------------------------------------------------------- - - def handle_result(self, key, value): - logging.debug("handle_result (%(key)s)", locals()) - self.rh.handle_result(key, value) - - # value limitting ---------------------------------------------------------- - def set_limit(self, key, limit_function): - if key in self.limits: - logging.warn("Overwritting limit funtion for '%(key)s'", locals()) - - self.limits[key] = limit_function - - def limit(self, key, value): - try: - value = self.limits[key](value) - except KeyError: - pass - except Exception: - logging.warn("Could not apply limit to '%(key)s'", locals()) - - return value - - # configuration ----------------------------------------------------------- - def get_process_count(self): - return self.ncpus - - def get_chunk_size(self): - return self.chunk_size - - -def null_progress_observer(): - class NullProgressObserver(object): - def start(self, name, count): - pass - - def update(self, value): - pass - - def finish(self): - pass - - return NullProgressObserver() - - -def progressbar_progress_observer(): - from progressbar import ProgressBar, Percentage, Bar - - class ProgressBarProgressObserver(object): - def start(self, name, count): - self.pbar = ProgressBar(widgets=[name, Percentage(), Bar()], - maxval=count) - self.pbar.start() - - def update(self, value): - self.pbar.update(value) - - def finish(self): - self.pbar.finish() - - return ProgressBarProgressObserver() - - -def forgetful_result_handler(): - class ForgetfulResultHandler(object): - def handle_result(self, key, value): - pass # do nothing - - return ForgetfulResultHandler() - - -def saving_result_handler(filename): - """returns a result handler that saves the resulting arrays into a file - with name filename""" - class SavingResultHandler(object): - def __init__(self, file_name): - self.filename = file_name - self.arrays = {} - - def handle_result(self, key, value): - self.arrays[key] = value - - def __del__(self): - logging.debug("Writing arrays in %(filename)s", self.__dict__) - try: - np.savez_compressed(open(self.filename, "wb"), **self.arrays) - except IOError: - logging.error("Failed to write %(filename)s", self.__dict__) - - return SavingResultHandler(filename) - - -def checking_result_handler(filename): - """returns a return handler that checks the results against a - reference file. - - The Check will consider a FAIL either a result not present in the - reference file (saved as a numpy savez or savez_compressed) or a - result that differs. It will consider a PARTIAL PASS if the - reference file has a shorter result, but the existing results - match. A FULL PASS will happen when all existing results match - - """ - class CheckingResultHandler(object): - def __init__(self, reference_file): - """Checks the result against those save in 'reference_file'""" - logging.info("Loading reference results from '%s'", reference_file) - self.reference_results = np.load(open(reference_file, 'rb')) - - def handle_result(self, key, value): - if key in ['experiment', 'image_stack']: - return #ignore these - - try: - reference = self.reference_results[key] - except KeyError as e: - logging.warning("%(key)s: %(e)s", locals()) - reference = None - - if reference is None: - msg = "'{0}': No reference result." - logging.warn(msg.format(key)) - - try: - if key=="confidence": - reference = reference.T - value = value.T - - check_len = min(len(reference), len(value)) - test_passed = np.allclose(value[:check_len], reference[:check_len]) - - if not test_passed: - msg = "'{0}': FAIL" - logging.warn(msg.format(key)) - lvl = logging.WARN - elif len(value) > check_len: - msg = "'{0}': PARTIAL PASS" - lvl = logging.WARN - else: - msg = "'{0}': FULL PASS" - lvl = logging.INFO - logging.log(lvl, msg.format(key)) - except Exception as e: - msg = "%(key)s: Failure trying to check the results.\n%(e)s" - logging.error(msg, locals()) - - return CheckingResultHandler(filename) - - -# ============================================================================== -# %% OPTIMIZED BITS -# ============================================================================== - -# Some basic 3d algebra ======================================================== -@numba.njit -def _v3_dot(a, b): - return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] - - -@numba.njit -def _m33_v3_multiply(m, v, dst): - v0 = v[0]; v1 = v[1]; v2 = v[2] - dst[0] = m[0, 0]*v0 + m[0, 1]*v1 + m[0, 2]*v2 - dst[1] = m[1, 0]*v0 + m[1, 1]*v1 + m[1, 2]*v2 - dst[2] = m[2, 0]*v0 + m[2, 1]*v1 + m[2, 2]*v2 - - return dst - - -@numba.njit -def _v3_normalized(src, dst): - v0 = src[0] - v1 = src[1] - v2 = src[2] - sqr_norm = v0*v0 + v1*v1 + v2*v2 - inv_norm = 1.0 if sqr_norm == 0.0 else 1./np.sqrt(sqr_norm) - - dst[0] = v0 * inv_norm - dst[1] = v1 * inv_norm - dst[2] = v2 * inv_norm - - return dst - - -@numba.njit -def _make_binary_rot_mat(src, dst): - v0 = src[0]; v1 = src[1]; v2 = src[2] - - dst[0,0] = 2.0*v0*v0 - 1.0 - dst[0,1] = 2.0*v0*v1 - dst[0,2] = 2.0*v0*v2 - dst[1,0] = 2.0*v1*v0 - dst[1,1] = 2.0*v1*v1 - 1.0 - dst[1,2] = 2.0*v1*v2 - dst[2,0] = 2.0*v2*v0 - dst[2,1] = 2.0*v2*v1 - dst[2,2] = 2.0*v2*v2 - 1.0 - - return dst - - -# code transcribed in numba from transforms module ============================= - -# This is equivalent to the transform module anglesToGVec, but written in -# numba. This should end in a module to share with other scripts -@numba.njit -def _anglesToGVec(angs, rMat_ss, rMat_c): - """From a set of angles return them in crystal space""" - result = np.empty_like(angs) - for i in range(len(angs)): - cx = np.cos(0.5*angs[i, 0]) - sx = np.sin(0.5*angs[i, 0]) - cy = np.cos(angs[i,1]) - sy = np.sin(angs[i,1]) - g0 = cx*cy - g1 = cx*sy - g2 = sx - - # with g being [cx*xy, cx*sy, sx] - # result = dot(rMat_c, dot(rMat_ss[i], g)) - t0_0 = rMat_ss[ i, 0, 0]*g0 + rMat_ss[ i, 1, 0]*g1 + rMat_ss[ i, 2, 0]*g2 - t0_1 = rMat_ss[ i, 0, 1]*g0 + rMat_ss[ i, 1, 1]*g1 + rMat_ss[ i, 2, 1]*g2 - t0_2 = rMat_ss[ i, 0, 2]*g0 + rMat_ss[ i, 1, 2]*g1 + rMat_ss[ i, 2, 2]*g2 - - result[i, 0] = rMat_c[0, 0]*t0_0 + rMat_c[ 1, 0]*t0_1 + rMat_c[ 2, 0]*t0_2 - result[i, 1] = rMat_c[0, 1]*t0_0 + rMat_c[ 1, 1]*t0_1 + rMat_c[ 2, 1]*t0_2 - result[i, 2] = rMat_c[0, 2]*t0_0 + rMat_c[ 1, 2]*t0_1 + rMat_c[ 2, 2]*t0_2 - - return result - - -# This is equivalent to the transform's module gvecToDetectorXYArray, but written in -# numba. -# As of now, it is not a good replacement as efficient allocation of the temporary -# arrays is not competitive with the stack allocation using in the C version of the -# code (WiP) - -# tC varies per coord -# gvec_cs, rSm varies per grain -# -# gvec_cs -beam = xf.bVec_ref[:, 0] -Z_l = xf.Zl[:,0] -@numba.jit() -def _gvec_to_detector_array(vG_sn, rD, rSn, rC, tD, tS, tC): - """ beamVec is the beam vector: (0, 0, -1) in this case """ - ztol = xrdutil.epsf - p3_l = np.empty((3,)) - tmp_vec = np.empty((3,)) - vG_l = np.empty((3,)) - tD_l = np.empty((3,)) - norm_vG_s = np.empty((3,)) - norm_beam = np.empty((3,)) - tZ_l = np.empty((3,)) - brMat = np.empty((3,3)) - result = np.empty((len(rSn), 2)) - - _v3_normalized(beam, norm_beam) - _m33_v3_multiply(rD, Z_l, tZ_l) - - for i in xrange(len(rSn)): - _m33_v3_multiply(rSn[i], tC, p3_l) - p3_l += tS - p3_minus_p1_l = tD - p3_l - - num = _v3_dot(tZ_l, p3_minus_p1_l) - _v3_normalized(vG_sn[i], norm_vG_s) - - _m33_v3_multiply(rC, norm_vG_s, tmp_vec) - _m33_v3_multiply(rSn[i], tmp_vec, vG_l) - - bDot = -_v3_dot(norm_beam, vG_l) - - if bDot < ztol or bDot > 1.0 - ztol: - result[i, 0] = np.nan - result[i, 1] = np.nan - continue - - _make_binary_rot_mat(vG_l, brMat) - _m33_v3_multiply(brMat, norm_beam, tD_l) - denom = _v3_dot(tZ_l, tD_l) - - if denom < ztol: - result[i, 0] = np.nan - result[i, 1] = np.nan - continue - - u = num/denom - tmp_res = u*tD_l - p3_minus_p1_l - result[i,0] = _v3_dot(tmp_res, rD[:,0]) - result[i,1] = _v3_dot(tmp_res, rD[:,1]) - - return result - - -@numba.njit -def _quant_and_clip_confidence(coords, angles, image, base, inv_deltas, clip_vals,bshw): - """quantize and clip the parametric coordinates in coords + angles - - coords - (..., 2) array: input 2d parametric coordinates - angles - (...) array: additional dimension for coordinates - base - (3,) array: base value for quantization (for each dimension) - inv_deltas - (3,) array: inverse of the quantum size (for each dimension) - clip_vals - (2,) array: clip size (only applied to coords dimensions) - bshw - (1,) half width of the beam stop in mm - - clipping is performed on ranges [0, clip_vals[0]] for x and - [0, clip_vals[1]] for y - - returns an array with the quantized coordinates, with coordinates - falling outside the clip zone filtered out. - - """ - count = len(coords) - - in_sensor = 0 - matches = 0 - for i in range(count): - xf = coords[i, 0] - yf = coords[i, 1] - - xf = np.floor((xf - base[0]) * inv_deltas[0]) - if not xf >= 0.0: - continue - if not xf < clip_vals[0]: - continue - - if not np.abs(yf)>bshw: - continue - - yf = np.floor((yf - base[1]) * inv_deltas[1]) - - - - if not yf >= 0.0: - continue - if not yf < clip_vals[1]: - continue - - zf = np.floor((angles[i] - base[2]) * inv_deltas[2]) - - in_sensor += 1 - - x, y, z = int(xf), int(yf), int(zf) - - #x_byte = x // 8 - #x_off = 7 - (x % 8) - #if image[z, y, x_byte] (1< 1: - global _multiprocessing_start_method - _multiprocessing_start_method=multiprocessing_start_method - logging.info('Running multiprocess %d processes (%s)', - ncpus, _multiprocessing_start_method) - with grand_loop_pool(ncpus=ncpus, state=(chunk_size, - image_stack, - all_angles, precomp, test_crds, - experiment)) as pool: - for rslice, rvalues in pool.imap_unordered(multiproc_inner_loop, - chunks): - count = rvalues.shape[1] - confidence[:, rslice] = rvalues - finished += count - controller.update(finished) - else: - logging.info('Running in a single process') - for chunk_start in chunks: - chunk_stop = min(n_coords, chunk_start+chunk_size) - rslice, rvalues = _grand_loop_inner(image_stack, all_angles, - precomp, test_crds, experiment, - start=chunk_start, - stop=chunk_stop) - count = rvalues.shape[1] - confidence[:, rslice] = rvalues - finished += count - controller.update(finished) - - controller.finish(subprocess) - controller.handle_result("confidence", confidence) - - del _multiprocessing_start_method - - pool.close() - - return confidence - - -def evaluate_diffraction_angles(experiment, controller=None): - """Uses simulateGVecs to generate the angles used per each grain. - returns a list containg one array per grain. - - experiment -- a bag of experiment values, including the grains specs and other - required parameters. - """ - # extract required data from experiment - exp_maps = experiment.exp_maps - plane_data = experiment.plane_data - detector_params = experiment.detector_params - pixel_size = experiment.pixel_size - ome_range = experiment.ome_range - ome_period = experiment.ome_period - - panel_dims_expanded = [(-10, -10), (10, 10)] - subprocess='evaluate diffraction angles' - pbar = controller.start(subprocess, - len(exp_maps)) - all_angles = [] - ref_gparams = np.array([0., 0., 0., 1., 1., 1., 0., 0., 0.]) - for i, exp_map in enumerate(exp_maps): - gparams = np.hstack([exp_map, ref_gparams]) - sim_results = xrdutil.simulateGVecs(plane_data, - detector_params, - gparams, - panel_dims=panel_dims_expanded, - pixel_pitch=pixel_size, - ome_range=ome_range, - ome_period=ome_period, - distortion=None) - all_angles.append(sim_results[2]) - controller.update(i+1) - pass - controller.finish(subprocess) - - return all_angles - - -def _grand_loop_inner(image_stack, angles, precomp, - coords, experiment, start=0, stop=None): - """Actual simulation code for a chunk of data. It will be used both, - in single processor and multiprocessor cases. Chunking is performed - on the coords. - - image_stack -- the image stack from the sensors - angles -- the angles (grains) to test - coords -- all the coords to test - precomp -- (gvec_cs, rmat_ss) precomputed for each grain - experiment -- bag with experiment parameters - start -- chunk start offset - stop -- chunk end offset - """ - - t = time.time() - n_coords = len(coords) - n_angles = len(angles) - - # experiment geometric layout parameters - rD = experiment.rMat_d - rCn = experiment.rMat_c - tD = experiment.tVec_d[:,0] - tS = experiment.tVec_s[:,0] - - # experiment panel related configuration - base = experiment.base - inv_deltas = experiment.inv_deltas - clip_vals = experiment.clip_vals - distortion = experiment.distortion - bshw=experiment.bsw/2. - - _to_detector = xfcapi.gvecToDetectorXYArray - #_to_detector = _gvec_to_detector_array - stop = min(stop, n_coords) if stop is not None else n_coords - - distortion_fn = None - if distortion is not None and len(distortion > 0): - distortion_fn, distortion_args = distortion - - acc_detector = 0.0 - acc_distortion = 0.0 - acc_quant_clip = 0.0 - confidence = np.zeros((n_angles, stop-start)) - grains = 0 - crds = 0 - - if distortion_fn is None: - for igrn in xrange(n_angles): - angs = angles[igrn]; rC = rCn[igrn] - gvec_cs, rMat_ss = precomp[igrn] - grains += 1 - for icrd in xrange(start, stop): - t0 = time.time() - det_xy = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) - t1 = time.time() - c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, - base, inv_deltas, clip_vals,bshw) - t2 = time.time() - acc_detector += t1 - t0 - acc_quant_clip += t2 - t1 - crds += 1 - confidence[igrn, icrd - start] = c - else: - for igrn in xrange(n_angles): - angs = angles[igrn]; rC = rCn[igrn] - gvec_cs, rMat_ss = precomp[igrn] - grains += 1 - for icrd in xrange(start, stop): - t0 = time.time() - tmp_xys = _to_detector(gvec_cs, rD, rMat_ss, rC, tD, tS, coords[icrd]) - t1 = time.time() - det_xy = distortion_fn(tmp_xys, distortion_args, invert=True) - t2 = time.time() - c = _quant_and_clip_confidence(det_xy, angs[:,2], image_stack, - base, inv_deltas, clip_vals,bshw) - t3 = time.time() - acc_detector += t1 - t0 - acc_distortion += t2 - t1 - acc_quant_clip += t3 - t2 - crds += 1 - confidence[igrn, icrd - start] = c - - t = time.time() - t - return slice(start, stop), confidence - - -def multiproc_inner_loop(chunk): - """function to use in multiprocessing that computes the simulation over the - task's alloted chunk of data""" - - chunk_size = _mp_state[0] - n_coords = len(_mp_state[4]) - chunk_stop = min(n_coords, chunk+chunk_size) - return _grand_loop_inner(*_mp_state[1:], start=chunk, stop=chunk_stop) - - -def worker_init(id_state, id_exp): - """process initialization function. This function is only used when the - child processes are spawned (instead of forked). When using the fork model - of multiprocessing the data is just inherited in process memory.""" - import joblib - - global _mp_state - state = joblib.load(id_state) - experiment = joblib.load(id_exp) - _mp_state = state + (experiment,) - -@contextlib.contextmanager -def grand_loop_pool(ncpus, state): - """function that handles the initialization of multiprocessing. It handles - properly the use of spawned vs forked multiprocessing. The multiprocessing - can be either 'fork' or 'spawn', with 'spawn' being required in non-fork - platforms (like Windows) and 'fork' being preferred on fork platforms due - to its efficiency. - """ - # state = ( chunk_size, - # image_stack, - # angles, - # precomp, - # coords, - # experiment ) - global _multiprocessing_start_method - if _multiprocessing_start_method == 'fork': - # Use FORK multiprocessing. - - # All read-only data can be inherited in the process. So we "pass" it as - # a global that the child process will be able to see. At the end of the - # processing the global is removed. - global _mp_state - _mp_state = state - pool = multiprocessing.Pool(ncpus) - yield pool - del (_mp_state) - else: - # Use SPAWN multiprocessing. - - # As we can not inherit process data, all the required data is - # serialized into a temporary directory using joblib. The - # multiprocessing pool will have the "worker_init" as initialization - # function that takes the key for the serialized data, which will be - # used to load the parameter memory into the spawn process (also using - # joblib). In theory, joblib uses memmap for arrays if they are not - # compressed, so no compression is used for the bigger arrays. - import joblib - tmp_dir = tempfile.mkdtemp(suffix='-nf-grand-loop') - try: - # dumb dumping doesn't seem to work very well.. do something ad-hoc - logging.info('Using "%s" as temporary directory.', tmp_dir) - - id_exp = joblib.dump(state[-1], - os.path.join(tmp_dir, - 'grand-loop-experiment.gz'), - compress=True) - id_state = joblib.dump(state[:-1], - os.path.join(tmp_dir, 'grand-loop-data')) - pool = multiprocessing.Pool(ncpus, worker_init, - (id_state[0], id_exp[0])) - yield pool - finally: - logging.info('Deleting "%s".', tmp_dir) - shutil.rmtree(tmp_dir) - - - - - -#%% Loading Utilities - - -def gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ - misorientation_spacing,ome_range_deg, nframes, beam_stop_width): - - print('Loading Grain Data.....') - #gen_grain_data - ff_data=np.loadtxt(grain_out_file) - - #ff_data=np.atleast_2d(ff_data[2,:]) - - exp_maps=ff_data[:,3:6] - t_vec_ds=ff_data[:,6:9] - - - # - completeness=ff_data[:,1] - - chi2=ff_data[:,2] - - n_grains=exp_maps.shape[0] - - rMat_c = rot.rotMatOfExpMap(exp_maps.T) - - - - - cut=np.where(np.logical_and(completeness>comp_thresh,chi20.: - mat_used.planeData.tThMax = np.amax(np.radians(max_tth)) - else: - mat_used.planeData.tThMax = np.amax(pixel_tth) - - pd=mat_used.planeData - - - print('Final Assembly.....') - experiment = argparse.Namespace() - # grains related information - experiment.n_grains = n_grains # this can be derived from other values... - experiment.rMat_c = rMat_c # n_grains rotation matrices (one per grain) - experiment.exp_maps = exp_maps # n_grains exp_maps -angle * rotation axis- (one per grain) - - experiment.plane_data = pd - experiment.detector_params = detector_params - experiment.pixel_size = pixel_size - experiment.ome_range = ome_range - experiment.ome_period = ome_period - experiment.x_col_edges = x_col_edges - experiment.y_row_edges = y_row_edges - experiment.ome_edges = ome_edges - experiment.ncols = ncols - experiment.nrows = nrows - experiment.nframes = nframes# used only in simulate... - experiment.rMat_d = rMat_d - experiment.tVec_d = np.atleast_2d(detector_params[3:6]).T - experiment.chi = detector_params[6] # note this is used to compute S... why is it needed? - experiment.tVec_s = np.atleast_2d(detector_params[7:]).T - experiment.rMat_c = rMat_c - experiment.distortion = None - experiment.panel_dims = panel_dims # used only in simulate... - experiment.base = base - experiment.inv_deltas = inv_deltas - experiment.clip_vals = clip_vals - experiment.bsw = beam_stop_width - - nf_to_ff_id_map=np.tile(cut,27*mis_steps) - - return experiment, nf_to_ff_id_map - -#%% - - -def gen_nf_test_grid_tomo(x_dim_pnts, z_dim_pnts, v_bnds, voxel_spacing): - - if v_bnds[0]==v_bnds[1]: - Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),v_bnds[0],np.arange(z_dim_pnts)) - else: - Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),np.arange(v_bnds[0]+voxel_spacing/2.,v_bnds[1],voxel_spacing),np.arange(z_dim_pnts)) - #note numpy shaping of arrays is goofy, returns(length(y),length(x),length(z)) - - - Zs=(Zs-(z_dim_pnts/2))*voxel_spacing - Xs=(Xs-(x_dim_pnts/2))*voxel_spacing - - - test_crds = np.vstack([Xs.flatten(), Ys.flatten(), Zs.flatten()]).T - n_crds = len(test_crds) - - return test_crds, n_crds, Xs, Ys, Zs - - -#%% - -def gen_nf_dark(data_folder,img_nums,num_for_dark,nrows,ncols,dark_type='median',stem='nf_',num_digits=5,ext='.tif'): - - dark_stack=np.zeros([num_for_dark,nrows,ncols]) - - print('Loading data for dark generation...') - for ii in np.arange(num_for_dark): - print('Image #: ' + str(ii)) - dark_stack[ii,:,:]=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext) - #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) - - if dark_type=='median': - print('making median...') - dark=np.median(dark_stack,axis=0) - elif dark_type=='min': - print('making min...') - dark=np.min(dark_stack,axis=0) - - return dark - - -#%% -def gen_nf_image_stack(data_folder,img_nums,dark,num_erosions,num_dilations,ome_dilation_iter,threshold,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): - - - image_stack=np.zeros([img_nums.shape[0],nrows,ncols],dtype=bool) - - print('Loading and Cleaning Images...') - for ii in np.arange(img_nums.shape[0]): - print('Image #: ' + str(ii)) - tmp_img=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext)-dark - #image procesing - image_stack[ii,:,:]=img.morphology.binary_erosion(tmp_img>threshold,iterations=num_erosions) - image_stack[ii,:,:]=img.morphology.binary_dilation(image_stack[ii,:,:],iterations=num_dilations) - - #%A final dilation that includes omega - print('Final Dilation Including Omega....') - image_stack=img.morphology.binary_dilation(image_stack,iterations=ome_dilation_iter) - - return image_stack - - -#%% -def scan_detector_parm(image_stack, experiment,test_crds,controller,parm_to_opt,parm_vector,slice_shape): - #0-distance - #1-x center - #2-xtilt - #3-ytilt - #4-ztilt - - multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' - - #current detector parameters, note the value for the actively optimized parameters will be ignored - distance=experiment.detector_params[5]#mm - x_cen=experiment.detector_params[3]#mm - xtilt=experiment.detector_params[0] - ytilt=experiment.detector_params[1] - ztilt=experiment.detector_params[2] - - num_parm_pts=len(parm_vector) - - trial_data=np.zeros([num_parm_pts,slice_shape[0],slice_shape[1]]) - - tmp_td=copy.copy(experiment.tVec_d) - for jj in np.arange(num_parm_pts): - print('cycle %d of %d'%(jj+1,num_parm_pts)) - - - if parm_to_opt==0: - tmp_td[2]=parm_vector[jj] - else: - tmp_td[2]=distance - - if parm_to_opt==1: - tmp_td[0]=parm_vector[jj] - else: - tmp_td[0]=x_cen - - if parm_to_opt==2: - rMat_d_tmp=makeDetectorRotMat([parm_vector[jj],ytilt,ztilt]) - elif parm_to_opt==3: - rMat_d_tmp=makeDetectorRotMat([xtilt,parm_vector[jj],ztilt]) - elif parm_to_opt==4: - rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,parm_vector[jj]]) - else: - rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,ztilt]) - - experiment.rMat_d = rMat_d_tmp - experiment.tVec_d = tmp_td - - - - conf=test_orientations(image_stack, experiment, test_crds, - controller,multiprocessing_start_method) - - - trial_data[jj]=np.max(conf,axis=0).reshape(slice_shape) - - return trial_data - -#%% - -def extract_max_grain_map(confidence,grid_shape,binary_recon_bin=None): - if binary_recon_bin == None: - binary_recon_bin=np.ones([grid_shape[1],grid_shape[2]]) - - - conf_squeeze=np.max(confidence,axis=0).reshape(grid_shape) - grains=np.argmax(confidence,axis=0).reshape(grid_shape) - out_bounds=np.where(binary_recon_bin==0) - conf_squeeze[:,out_bounds[0],out_bounds[1]] =-0.001 - - return conf_squeeze,grains -#%% - -def process_raw_confidence(raw_confidence,vol_shape,tomo_mask=None,id_remap=None): - - print('Compiling Confidence Map...') - confidence_map=np.max(raw_confidence,axis=0).reshape(vol_shape) - grain_map=np.argmax(raw_confidence,axis=0).reshape(vol_shape) - - - if tomo_mask is not None: - print('Applying tomography mask...') - out_bounds=np.where(tomo_mask==0) - confidence_map[:,out_bounds[0],out_bounds[1]] =-0.001 - grain_map[:,out_bounds[0],out_bounds[1]] =-1 - - - if id_remap is not None: - max_grain_no=np.max(grain_map) - grain_map_copy=copy.copy(grain_map) - print('Remapping grain ids to ff...') - for ii in np.arange(max_grain_no): - this_grain=np.where(grain_map==ii) - grain_map_copy[this_grain]=id_remap[ii] - grain_map=grain_map_copy - - return grain_map, confidence_map - -#%% - -def save_raw_confidence(save_dir,save_stem,raw_confidence,id_remap=None): - print('Saving raw confidence, might take a while...') - if id_remap is not None: - np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence,id_remap=id_remap) - else: - np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence) -#%% - -def save_nf_data(save_dir,save_stem,grain_map,confidence_map,Xs,Ys,Zs,ori_list,id_remap=None): - print('Saving grain map data...') - if id_remap is not None: - np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list,id_remap=id_remap) - else: - np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list) - -#%% - -def plot_ori_map(grain_map, confidence_map, exp_maps, layer_no,id_remap=None): - - grains_plot=np.squeeze(grain_map[layer_no,:,:]) - conf_plot=np.squeeze(confidence_map[layer_no,:,:]) - n_grains=len(exp_maps) - - rgb_image=np.zeros([grains_plot.shape[0],grains_plot.shape[1],4], dtype='float32') - rgb_image[:,:,3]=1. - - for ii in np.arange(n_grains): - if id_remap is not None: - this_grain=np.where(np.squeeze(grains_plot)==id_remap[ii]) - else: - this_grain=np.where(np.squeeze(grains_plot)==ii) - if np.sum(this_grain[0])>0: - - ori=exp_maps[ii,:] - - #cubic mapping - rgb_image[this_grain[0],this_grain[1],0]=(ori[0]+(np.pi/4.))/(np.pi/2.) - rgb_image[this_grain[0],this_grain[1],1]=(ori[1]+(np.pi/4.))/(np.pi/2.) - rgb_image[this_grain[0],this_grain[1],2]=(ori[2]+(np.pi/4.))/(np.pi/2.) - - - - plt.imshow(rgb_image,interpolation='none') - plt.hold(True) - plt.imshow(conf_plot,vmin=0.0,vmax=1.,interpolation='none',cmap=plt.cm.gray,alpha=0.5) - - From eac6d5b8dc931878743a8ef4b50c8b2b48d4f5da Mon Sep 17 00:00:00 2001 From: rachelelim Date: Tue, 12 May 2020 21:25:44 -0400 Subject: [PATCH 234/253] fixed grain ID handling when reconstruction NF with misorientation --- hexrd/grainmap/nfutil.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hexrd/grainmap/nfutil.py b/hexrd/grainmap/nfutil.py index 9d8a3156..34a35043 100644 --- a/hexrd/grainmap/nfutil.py +++ b/hexrd/grainmap/nfutil.py @@ -790,6 +790,8 @@ def gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, mis_amt=misorientation_bnd*np.pi/180. spacing=misorientation_spacing*np.pi/180. + mis_steps = int(misorientation_bnd/misorientation_spacing) + ori_pts = np.arange(-mis_amt, (mis_amt+(spacing*0.999)),spacing) num_ori_grid_pts=ori_pts.shape[0]**3 num_oris=exp_maps.shape[0] @@ -933,7 +935,7 @@ def gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, experiment.clip_vals = clip_vals experiment.bsw = beam_stop_width - nf_to_ff_id_map=cut + nf_to_ff_id_map=np.tile(cut,27*mis_steps) return experiment, nf_to_ff_id_map From bba9c0d3dd1ede883c6e86f214e6dfd9c2b51437 Mon Sep 17 00:00:00 2001 From: rachelelim Date: Thu, 14 May 2020 01:54:43 -0400 Subject: [PATCH 235/253] fixed the grainID handling for no misorientation that broke when fixing handling with misorientation --- hexrd/grainmap/nfutil.py | 287 ++++++++++++++++++++------------------- 1 file changed, 144 insertions(+), 143 deletions(-) diff --git a/hexrd/grainmap/nfutil.py b/hexrd/grainmap/nfutil.py index 34a35043..3cf9d9dc 100644 --- a/hexrd/grainmap/nfutil.py +++ b/hexrd/grainmap/nfutil.py @@ -15,7 +15,7 @@ import contextlib import multiprocessing import tempfile -import shutil +import shutil from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi @@ -225,7 +225,7 @@ def handle_result(self, key, value): return CheckingResultHandler(filename) - + # ============================================================================== # %% OPTIMIZED BITS # ============================================================================== @@ -406,11 +406,11 @@ def _quant_and_clip_confidence(coords, angles, image, base, inv_deltas, clip_val if not np.abs(yf)>bshw: continue - + yf = np.floor((yf - base[1]) * inv_deltas[1]) - - + + if not yf >= 0.0: continue if not yf < clip_vals[1]: @@ -452,7 +452,7 @@ def test_orientations(image_stack, experiment, test_crds, controller,multiproces n_grains = experiment.n_grains chunk_size = controller.get_chunk_size() ncpus = controller.get_process_count() - + # generate angles ========================================================= # all_angles will be a list containing arrays for the different angles to @@ -538,11 +538,11 @@ def test_orientations(image_stack, experiment, test_crds, controller,multiproces controller.finish(subprocess) controller.handle_result("confidence", confidence) - + del _multiprocessing_start_method - + pool.close() - + return confidence @@ -735,7 +735,7 @@ def grand_loop_pool(ncpus, state): # dumb dumping doesn't seem to work very well.. do something ad-hoc logging.info('Using "%s" as temporary directory.', tmp_dir) - id_exp = joblib.dump(state[-1], + id_exp = joblib.dump(state[-1], os.path.join(tmp_dir, 'grand-loop-experiment.gz'), compress=True) @@ -751,126 +751,126 @@ def grand_loop_pool(ncpus, state): - + #%% Loading Utilities - - + + def gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, max_tth, comp_thresh, chi2_thresh, misorientation_bnd, \ misorientation_spacing,ome_range_deg, nframes, beam_stop_width): print('Loading Grain Data.....') #gen_grain_data ff_data=np.loadtxt(grain_out_file) - + #ff_data=np.atleast_2d(ff_data[2,:]) - + exp_maps=ff_data[:,3:6] t_vec_ds=ff_data[:,6:9] - - - # + + + # completeness=ff_data[:,1] - + chi2=ff_data[:,2] - + n_grains=exp_maps.shape[0] - + rMat_c = rot.rotMatOfExpMap(exp_maps.T) - - - - + + + + cut=np.where(np.logical_and(completeness>comp_thresh,chi20.: - mat_used.planeData.tThMax = np.amax(np.radians(max_tth)) + mat_used.planeData.tThMax = np.amax(np.radians(max_tth)) else: - mat_used.planeData.tThMax = np.amax(pixel_tth) - + mat_used.planeData.tThMax = np.amax(pixel_tth) + pd=mat_used.planeData - - + + print('Final Assembly.....') experiment = argparse.Namespace() # grains related information experiment.n_grains = n_grains # this can be derived from other values... experiment.rMat_c = rMat_c # n_grains rotation matrices (one per grain) experiment.exp_maps = exp_maps # n_grains exp_maps -angle * rotation axis- (one per grain) - + experiment.plane_data = pd experiment.detector_params = detector_params experiment.pixel_size = pixel_size @@ -933,19 +933,22 @@ def gen_trial_exp_data(grain_out_file,det_file,mat_file, x_ray_energy, mat_name, experiment.base = base experiment.inv_deltas = inv_deltas experiment.clip_vals = clip_vals - experiment.bsw = beam_stop_width - - nf_to_ff_id_map=np.tile(cut,27*mis_steps) - + experiment.bsw = beam_stop_width + + if mis_steps ==0: + nf_to_ff_id_map = cut + else: + nf_to_ff_id_map=np.tile(cut,27*mis_steps) + return experiment, nf_to_ff_id_map #%% - + def gen_nf_test_grid_tomo(x_dim_pnts, z_dim_pnts, v_bnds, voxel_spacing): - + if v_bnds[0]==v_bnds[1]: - Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),v_bnds[0],np.arange(z_dim_pnts)) + Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),v_bnds[0],np.arange(z_dim_pnts)) else: Xs,Ys,Zs=np.meshgrid(np.arange(x_dim_pnts),np.arange(v_bnds[0]+voxel_spacing/2.,v_bnds[1],voxel_spacing),np.arange(z_dim_pnts)) #note numpy shaping of arrays is goofy, returns(length(y),length(x),length(z)) @@ -957,14 +960,14 @@ def gen_nf_test_grid_tomo(x_dim_pnts, z_dim_pnts, v_bnds, voxel_spacing): test_crds = np.vstack([Xs.flatten(), Ys.flatten(), Zs.flatten()]).T n_crds = len(test_crds) - + return test_crds, n_crds, Xs, Ys, Zs - + #%% - + def gen_nf_dark(data_folder,img_nums,num_for_dark,nrows,ncols,dark_type='median',stem='nf_',num_digits=5,ext='.tif'): - + dark_stack=np.zeros([num_for_dark,nrows,ncols]) print('Loading data for dark generation...') @@ -972,21 +975,21 @@ def gen_nf_dark(data_folder,img_nums,num_for_dark,nrows,ncols,dark_type='median' print('Image #: ' + str(ii)) dark_stack[ii,:,:]=imgio.imread(data_folder+'%s'%(stem)+str(img_nums[ii]).zfill(num_digits)+ext) #image_stack[ii,:,:]=np.flipud(tmp_img>threshold) - - if dark_type=='median': + + if dark_type=='median': print('making median...') dark=np.median(dark_stack,axis=0) elif dark_type=='min': print('making min...') dark=np.min(dark_stack,axis=0) - + return dark -#%% +#%% def gen_nf_image_stack(data_folder,img_nums,dark,num_erosions,num_dilations,ome_dilation_iter,threshold,nrows,ncols,stem='nf_',num_digits=5,ext='.tif'): - - + + image_stack=np.zeros([img_nums.shape[0],nrows,ncols],dtype=bool) print('Loading and Cleaning Images...') @@ -996,99 +999,99 @@ def gen_nf_image_stack(data_folder,img_nums,dark,num_erosions,num_dilations,ome_ #image procesing image_stack[ii,:,:]=img.morphology.binary_erosion(tmp_img>threshold,iterations=num_erosions) image_stack[ii,:,:]=img.morphology.binary_dilation(image_stack[ii,:,:],iterations=num_dilations) - + #%A final dilation that includes omega print('Final Dilation Including Omega....') image_stack=img.morphology.binary_dilation(image_stack,iterations=ome_dilation_iter) - + return image_stack -#%% +#%% def scan_detector_parm(image_stack, experiment,test_crds,controller,parm_to_opt,parm_vector,slice_shape): #0-distance #1-x center #2-xtilt #3-ytilt #4-ztilt - + multiprocessing_start_method = 'fork' if hasattr(os, 'fork') else 'spawn' - + #current detector parameters, note the value for the actively optimized parameters will be ignored distance=experiment.detector_params[5]#mm x_cen=experiment.detector_params[3]#mm xtilt=experiment.detector_params[0] ytilt=experiment.detector_params[1] - ztilt=experiment.detector_params[2] - + ztilt=experiment.detector_params[2] + num_parm_pts=len(parm_vector) - + trial_data=np.zeros([num_parm_pts,slice_shape[0],slice_shape[1]]) - + tmp_td=copy.copy(experiment.tVec_d) for jj in np.arange(num_parm_pts): print('cycle %d of %d'%(jj+1,num_parm_pts)) - + if parm_to_opt==0: - tmp_td[2]=parm_vector[jj] + tmp_td[2]=parm_vector[jj] else: - tmp_td[2]=distance - + tmp_td[2]=distance + if parm_to_opt==1: - tmp_td[0]=parm_vector[jj] + tmp_td[0]=parm_vector[jj] else: tmp_td[0]=x_cen - + if parm_to_opt==2: - rMat_d_tmp=makeDetectorRotMat([parm_vector[jj],ytilt,ztilt]) + rMat_d_tmp=makeDetectorRotMat([parm_vector[jj],ytilt,ztilt]) elif parm_to_opt==3: rMat_d_tmp=makeDetectorRotMat([xtilt,parm_vector[jj],ztilt]) elif parm_to_opt==4: - rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,parm_vector[jj]]) + rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,parm_vector[jj]]) else: rMat_d_tmp=makeDetectorRotMat([xtilt,ytilt,ztilt]) - + experiment.rMat_d = rMat_d_tmp experiment.tVec_d = tmp_td - - + + conf=test_orientations(image_stack, experiment, test_crds, controller,multiprocessing_start_method) - - + + trial_data[jj]=np.max(conf,axis=0).reshape(slice_shape) - + return trial_data - + #%% def extract_max_grain_map(confidence,grid_shape,binary_recon_bin=None): if binary_recon_bin == None: binary_recon_bin=np.ones([grid_shape[1],grid_shape[2]]) - - + + conf_squeeze=np.max(confidence,axis=0).reshape(grid_shape) grains=np.argmax(confidence,axis=0).reshape(grid_shape) - out_bounds=np.where(binary_recon_bin==0) - conf_squeeze[:,out_bounds[0],out_bounds[1]] =-0.001 + out_bounds=np.where(binary_recon_bin==0) + conf_squeeze[:,out_bounds[0],out_bounds[1]] =-0.001 return conf_squeeze,grains #%% - + def process_raw_confidence(raw_confidence,vol_shape,tomo_mask=None,id_remap=None): - + print('Compiling Confidence Map...') confidence_map=np.max(raw_confidence,axis=0).reshape(vol_shape) grain_map=np.argmax(raw_confidence,axis=0).reshape(vol_shape) - - + + if tomo_mask is not None: print('Applying tomography mask...') - out_bounds=np.where(tomo_mask==0) - confidence_map[:,out_bounds[0],out_bounds[1]] =-0.001 - grain_map[:,out_bounds[0],out_bounds[1]] =-1 + out_bounds=np.where(tomo_mask==0) + confidence_map[:,out_bounds[0],out_bounds[1]] =-0.001 + grain_map[:,out_bounds[0],out_bounds[1]] =-1 if id_remap is not None: @@ -1101,53 +1104,51 @@ def process_raw_confidence(raw_confidence,vol_shape,tomo_mask=None,id_remap=None grain_map=grain_map_copy return grain_map, confidence_map - + #%% def save_raw_confidence(save_dir,save_stem,raw_confidence,id_remap=None): - print('Saving raw confidence, might take a while...') + print('Saving raw confidence, might take a while...') if id_remap is not None: - np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence,id_remap=id_remap) + np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence,id_remap=id_remap) else: - np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence) -#%% - + np.savez(save_dir+save_stem+'_raw_confidence.npz',raw_confidence=raw_confidence) +#%% + def save_nf_data(save_dir,save_stem,grain_map,confidence_map,Xs,Ys,Zs,ori_list,id_remap=None): print('Saving grain map data...') if id_remap is not None: - np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list,id_remap=id_remap) + np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list,id_remap=id_remap) else: np.savez(save_dir+save_stem+'_grain_map_data.npz',grain_map=grain_map,confidence_map=confidence_map,Xs=Xs,Ys=Ys,Zs=Zs,ori_list=ori_list) #%% def plot_ori_map(grain_map, confidence_map, exp_maps, layer_no,id_remap=None): - + grains_plot=np.squeeze(grain_map[layer_no,:,:]) conf_plot=np.squeeze(confidence_map[layer_no,:,:]) n_grains=len(exp_maps) - + rgb_image=np.zeros([grains_plot.shape[0],grains_plot.shape[1],4], dtype='float32') rgb_image[:,:,3]=1. - + for ii in np.arange(n_grains): if id_remap is not None: - this_grain=np.where(np.squeeze(grains_plot)==id_remap[ii]) + this_grain=np.where(np.squeeze(grains_plot)==id_remap[ii]) else: this_grain=np.where(np.squeeze(grains_plot)==ii) if np.sum(this_grain[0])>0: ori=exp_maps[ii,:] - + #cubic mapping rgb_image[this_grain[0],this_grain[1],0]=(ori[0]+(np.pi/4.))/(np.pi/2.) rgb_image[this_grain[0],this_grain[1],1]=(ori[1]+(np.pi/4.))/(np.pi/2.) rgb_image[this_grain[0],this_grain[1],2]=(ori[2]+(np.pi/4.))/(np.pi/2.) - - - + + + plt.imshow(rgb_image,interpolation='none') plt.hold(True) - plt.imshow(conf_plot,vmin=0.0,vmax=1.,interpolation='none',cmap=plt.cm.gray,alpha=0.5) - - + plt.imshow(conf_plot,vmin=0.0,vmax=1.,interpolation='none',cmap=plt.cm.gray,alpha=0.5) From c625540c45711d063a814bee36764bb3c1b5c69d Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 21 May 2020 14:40:01 -0700 Subject: [PATCH 236/253] whitespace cleanup --- hexrd/instrument.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 600b5e93..e3ad6cb7 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1516,7 +1516,7 @@ def config_dict(self, chi=0, tvec=ct.zeros_3, sat_level=None, panel_buffer=None): """ Return a dictionary of detector parameters, with optional instrument - level parameters. This is a convenience function to work with the + level parameters. This is a convenience function to work with the APIs in several functions in xrdutil. Parameters @@ -1541,7 +1541,7 @@ def config_dict(self, chi=0, tvec=ct.zeros_3, """ config_dict = {} - + # ===================================================================== # DETECTOR PARAMETERS # ===================================================================== @@ -1568,7 +1568,7 @@ def config_dict(self, chi=0, tvec=ct.zeros_3, # saturation level det_dict['saturation_level'] = sat_level - + # panel buffer # FIXME if it is an array, the write will be a mess det_dict['panel_buffer'] = panel_buffer @@ -1580,7 +1580,7 @@ def config_dict(self, chi=0, tvec=ct.zeros_3, parameters=np.r_[self.distortion[1]].tolist() ) det_dict['distortion'] = dist_d - + # ===================================================================== # SAMPLE STAGE PARAMETERS # ===================================================================== @@ -1588,7 +1588,7 @@ def config_dict(self, chi=0, tvec=ct.zeros_3, chi=chi, translation=tvec.tolist() ) - + # ===================================================================== # BEAM PARAMETERS # ===================================================================== @@ -1596,11 +1596,11 @@ def config_dict(self, chi=0, tvec=ct.zeros_3, energy=beam_energy, vector=beam_vector ) - + config_dict['detector'] = det_dict config_dict['oscillation_stage'] = stage_dict config_dict['beam'] = beam_dict - + return config_dict def pixel_angles(self, origin=ct.zeros_3): From d32c3d3050faf68bbaea462ab41d091c0556c265 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 27 May 2020 10:16:12 -0700 Subject: [PATCH 237/253] correction to bin centers --- hexrd/instrument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index f4a72e50..8c1cac9d 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -1913,7 +1913,7 @@ def make_powder_rings( axis=0) """ # !!! should be safe as eta_edges are monotonic - eta_centers = eta_edges[:-1] + del_eta + eta_centers = eta_edges[:-1] + 0.5*del_eta # !!! get chi and ome from rmat_s # chi = np.arctan2(rmat_s[2, 1], rmat_s[1, 1]) From 02e1078221301cdf0b9f158fc038fd738eba8813 Mon Sep 17 00:00:00 2001 From: rachelelim Date: Thu, 28 May 2020 16:28:19 -0400 Subject: [PATCH 238/253] fixed error when single processing NF maps --- hexrd/grainmap/nfutil.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hexrd/grainmap/nfutil.py b/hexrd/grainmap/nfutil.py index 3cf9d9dc..4c863c78 100644 --- a/hexrd/grainmap/nfutil.py +++ b/hexrd/grainmap/nfutil.py @@ -523,6 +523,9 @@ def test_orientations(image_stack, experiment, test_crds, controller,multiproces confidence[:, rslice] = rvalues finished += count controller.update(finished) + del _multiprocessing_start_method + + pool.close() else: logging.info('Running in a single process') for chunk_start in chunks: @@ -538,10 +541,9 @@ def test_orientations(image_stack, experiment, test_crds, controller,multiproces controller.finish(subprocess) controller.handle_result("confidence", confidence) + #del _multiprocessing_start_method - del _multiprocessing_start_method - - pool.close() + #pool.close() return confidence From c7292d514ed1762f276feba10191410cd5c82dee Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 28 May 2020 16:25:31 -0700 Subject: [PATCH 239/253] restore cli for find-orientations --- hexrd/config/root.py | 6 +- hexrd/findorientations.py | 534 +++++++++++++++++++------------ hexrd/instrument.py | 60 +++- hexrd/xrd/indexer.py | 645 +++++++++++++++++++++++--------------- hexrd/xrd/xrdutil.py | 27 +- 5 files changed, 781 insertions(+), 491 deletions(-) mode change 100644 => 100755 hexrd/findorientations.py diff --git a/hexrd/config/root.py b/hexrd/config/root.py index 6add97c4..b99de4b9 100644 --- a/hexrd/config/root.py +++ b/hexrd/config/root.py @@ -48,10 +48,10 @@ def material(self): @property def analysis_id(self): return '_'.join( - self.analysis_name.strip().replace(' ', '-'), - self.material.active.strip().replace(' ', '-'), + [self.analysis_name.strip().replace(' ', '-'), + self.material.active.strip().replace(' ', '-')] ) - + @property def multiprocessing(self): # determine number of processes to run in parallel diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py old mode 100644 new mode 100755 index 78f8cba7..d8cc87ef --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -1,6 +1,5 @@ from __future__ import print_function -import cPickle import logging import multiprocessing as mp import os @@ -13,16 +12,13 @@ from scipy import ndimage from skimage.feature import blob_dog, blob_log +from hexrd import constants as const from hexrd import matrixutil as mutil from hexrd.xrd import indexer from hexrd import instrument from hexrd.xrd import rotations as rot -from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi - -from hexrd.xrd import distortion as dFuncs - -from hexrd.fitgrains import get_instrument_parameters +from hexrd.xrd.xrdutil import EtaOmeMaps # just require scikit-learn? have_sklearn = False @@ -59,7 +55,7 @@ def generate_orientation_fibers(cfg, eta_ome): seed_hkl_ids = cfg.find_orientations.seed_search.hkl_seeds fiber_ndiv = cfg.find_orientations.seed_search.fiber_ndiv method_dict = cfg.find_orientations.seed_search.method - + # strip out method name and kwargs # !!! note that the config enforces that method is a dict with length 1 # TODO: put a consistency check on required kwargs, or otherwise specify @@ -68,7 +64,7 @@ def generate_orientation_fibers(cfg, eta_ome): method_kwargs = method_dict[method] logger.info('using "%s" method for fiber generation' % method) - + # seed_hkl_ids must be consistent with this... pd_hkl_ids = eta_ome.iHKLList[seed_hkl_ids] @@ -106,7 +102,7 @@ def generate_orientation_fibers(cfg, eta_ome): filt_stdev = fwhm_to_stdev * method_kwargs['filter_radius'] this_map_f = -ndimage.filters.gaussian_laplace( eta_ome.dataStore[i], filt_stdev) - + labels_t, numSpots_t = ndimage.label( this_map_f > method_kwargs['threshold'], structureNDI_label @@ -126,7 +122,7 @@ def generate_orientation_fibers(cfg, eta_ome): this_map -= np.min(this_map) scl_map = 2*this_map/np.max(this_map) - 1. - # TODO: Currently the method kwargs must be explicitly specified + # TODO: Currently the method kwargs must be explicitly specified # in the config, and there are no checks # for 'blob_log': min_sigma=0.5, max_sigma=5, # num_sigma=10, threshold=0.01, overlap=0.1 @@ -190,9 +186,9 @@ def discretefiber_reduced(params_in): """ input parameters are [hkl_id, com_ome, com_eta] """ - bMat = paramMP['bMat'] - chi = paramMP['chi'] - csym = paramMP['csym'] + bMat = paramMP['bMat'] + chi = paramMP['chi'] + csym = paramMP['csym'] fiber_ndiv = paramMP['fiber_ndiv'] hkl = params_in[:3].reshape(3, 1) @@ -215,7 +211,8 @@ def discretefiber_reduced(params_in): return tmp -def run_cluster(compl, qfib, qsym, cfg, min_samples=None, compl_thresh=None, radius=None): +def run_cluster(compl, qfib, qsym, cfg, + min_samples=None, compl_thresh=None, radius=None): """ """ algorithm = cfg.find_orientations.clustering.algorithm @@ -231,7 +228,7 @@ def run_cluster(compl, qfib, qsym, cfg, min_samples=None, compl_thresh=None, rad if radius is not None: cl_radius = radius - start = timeit.default_timer() # timeit this + start = timeit.default_timer() # timeit this num_above = sum(np.array(compl) > min_compl) if num_above == 0: @@ -244,9 +241,13 @@ def run_cluster(compl, qfib, qsym, cfg, min_samples=None, compl_thresh=None, rad else: # use compiled module for distance # just to be safe, must order qsym as C-contiguous - qsym = np.array(qsym.T, order='C').T + qsym = np.array(qsym.T, order='C').T + def quat_distance(x, y): - return xfcapi.quat_distance(np.array(x, order='C'), np.array(y, order='C'), qsym) + return xfcapi.quat_distance( + np.array(x, order='C'), np.array(y, order='C'), + qsym + ) qfib_r = qfib[:, np.array(compl) > min_compl] @@ -256,8 +257,10 @@ def quat_distance(x, y): if algorithm == 'sph-dbscan' or algorithm == 'fclusterdata': logger.info("falling back to euclidean DBSCAN") algorithm = 'ort-dbscan' - #raise RuntimeError, \ - # "Requested clustering of %d orientations, which would be too slow!" %qfib_r.shape[1] + # raise RuntimeError( + # "Requested clustering of %d orientations, " + # + "which would be too slow!" % qfib_r.shape[1] + # ) logger.info( "Feeding %d orientations above %.1f%% to clustering", @@ -270,9 +273,10 @@ def quat_distance(x, y): "sklearn >= 0.14 required for dbscan; using fclusterdata" ) - if algorithm == 'dbscan' or algorithm == 'ort-dbscan' or algorithm == 'sph-dbscan': + if algorithm in ['dbscan', 'ort-dbscan', 'sph-dbscan']: # munge min_samples according to options - if min_samples is None or cfg.find_orientations.use_quaternion_grid is not None: + if min_samples is None \ + or cfg.find_orientations.use_quaternion_grid is not None: min_samples = 1 if algorithm == 'sph-dbscan': @@ -308,10 +312,10 @@ def quat_distance(x, y): ) # extract cluster labels - cl = np.array(labels, dtype=int) # convert to array - noise_points = cl == -1 # index for marking noise - cl += 1 # move index to 1-based instead of 0 - cl[noise_points] = -1 # re-mark noise as -1 + cl = np.array(labels, dtype=int) # convert to array + noise_points = cl == -1 # index for marking noise + cl += 1 # move index to 1-based instead of 0 + cl[noise_points] = -1 # re-mark noise as -1 logger.info("dbscan found %d noise points", sum(noise_points)) elif algorithm == 'fclusterdata': logger.info("using spherical fclusetrdata") @@ -342,8 +346,7 @@ def quat_distance(x, y): pass pass - if (algorithm == 'dbscan' or algorithm == 'ort-dbscan') \ - and qbar.size/4 > 1: + if algorithm in ('dbscan', 'ort-dbscan') and qbar.size/4 > 1: logger.info("\tchecking for duplicate orientations...") cl = cluster.hierarchy.fclusterdata( qbar.T, @@ -352,8 +355,10 @@ def quat_distance(x, y): metric=quat_distance) nblobs_new = len(np.unique(cl)) if nblobs_new < nblobs: - logger.info("\tfound %d duplicates within %f degrees" \ - %(nblobs-nblobs_new, cl_radius)) + logger.info( + "\tfound %d duplicates within %f degrees", + nblobs - nblobs_new, cl_radius + ) tmp = np.zeros((4, nblobs_new)) for i in range(nblobs_new): npts = sum(cl == i + 1) @@ -378,10 +383,33 @@ def quat_distance(x, y): def load_eta_ome_maps(cfg, pd, image_series, hkls=None, clean=False): - fn = os.path.join( - cfg.working_dir, - cfg.find_orientations.orientation_maps.file - ) + """ + Load the eta-ome maps specified by the config and CLI flags. + + Parameters + ---------- + cfg : TYPE + DESCRIPTION. + pd : TYPE + DESCRIPTION. + image_series : TYPE + DESCRIPTION. + hkls : TYPE, optional + DESCRIPTION. The default is None. + clean : TYPE, optional + DESCRIPTION. The default is False. + + Returns + ------- + TYPE + DESCRIPTION. + + """ + # check maps filename + if cfg.find_orientations.orientation_maps.file is None: + maps_fname = '_'.join([cfg.analysis_id, "eta-ome_maps.npz"]) + + fn = os.path.join(cfg.working_dir, maps_fname) # ???: necessary? if fn.split('.')[-1] != 'npz': @@ -395,12 +423,19 @@ def load_eta_ome_maps(cfg, pd, image_series, hkls=None, clean=False): logger.info('loaded eta/ome orientation maps from %s', fn) hkls = [str(i) for i in available_hkls[res.iHKLList]] logger.info( - 'hkls used to generate orientation maps: %s', hkls) + 'hkls used to generate orientation maps: %s', + hkls + ) return res except (AttributeError, IOError): + logger.info("specified maps file '%s' not found " + + "and clean option specified; " + + "recomputing eta/ome orientation maps", + fn) return generate_eta_ome_maps(cfg, pd, image_series, hkls) else: - logger.info('clean option specified; recomputing eta/ome orientation maps') + logger.info('clean option specified; ' + + 'recomputing eta/ome orientation maps') return generate_eta_ome_maps(cfg, pd, image_series, hkls) @@ -408,7 +443,7 @@ def generate_eta_ome_maps(cfg, hkls=None): # extract PlaneData from config and set active hkls plane_data = cfg.material.plane_data - # handle logicl for active hkl spec + # handle logic for active hkl spec # !!!: default to all hkls defined for material, # override with # 1) hkls from config, if specified; or @@ -422,219 +457,312 @@ def generate_eta_ome_maps(cfg, hkls=None): # logging output hklseedstr = ', '.join( [str(available_hkls[i]) for i in active_hkls] - ) + ) + logger.info( "building eta_ome maps using hkls: %s", hklseedstr - ) + ) + + # grad imageseries dict from cfg + imsd = cfg.image_series + + # handle omega period + # !!! we assume all detector ims have the same ome ranges, so any will do! + oims = next(imsd.itervalues()) + ome_period = oims.omega[0, 0] + np.r_[0., 360.] + + start = timeit.default_timer() # make eta_ome maps eta_ome = instrument.GenerateEtaOmeMaps( - cfg.image_series, cfg.instrument.hedm, plane_data, + imsd, cfg.instrument.hedm, plane_data, active_hkls=active_hkls, threshold=cfg.find_orientations.orientation_maps.threshold, - ome_period=cfg.find_orientations.omega.period) + ome_period=ome_period) + + logger.info("\t\t...took %f seconds", timeit.default_timer() - start) + # save maps + # ???: should perhaps set default maps name at module level map_fname = cfg.find_orientations.orientation_maps.file \ - or '_'.join(cfg.analysis_id, 'maps.npz') + or '_'.join([cfg.analysis_id, "eta-ome_maps.npz"]) + + if not os.path.exists(cfg.working_dir): + os.mkdir(cfg.working_dir) + fn = os.path.join( cfg.working_dir, map_fname ) - fd = os.path.split(fn)[0] - if not os.path.isdir(fd): - os.makedirs(fd) + eta_ome.save(fn) + logger.info('saved eta/ome orientation maps to "%s"', fn) + return eta_ome -def find_orientations(cfg, hkls=None, clean=False, profile=False): +def find_orientations(cfg, + hkls=None, clean=False, profile=False, + use_direct_testing=False): """ - Takes a config dict as input, generally a yml document - NOTE: single cfg instance, not iterator! + + Parameters + ---------- + cfg : TYPE + DESCRIPTION. + hkls : TYPE, optional + DESCRIPTION. The default is None. + clean : TYPE, optional + DESCRIPTION. The default is False. + profile : TYPE, optional + DESCRIPTION. The default is False. + use_direct_search : TYPE, optional + DESCRIPTION. The default is False. + + Returns + ------- + None. + """ + # grab objects from config + plane_data = cfg.material.plane_data + imsd = cfg.image_series + instr = cfg.instrument.hedm + eta_ranges = cfg.find_orientations.eta.range + + # tolerances + tth_tol = plane_data.tThWidth + eta_tol = np.radians(cfg.find_orientations.eta.tolerance) + ome_tol = np.radians(cfg.find_orientations.omega.tolerance) + + # handle omega period + # !!! We assume all detector ims have the same ome ranges; + # therefore any will do for this purpose. + oims = next(imsd.itervalues()) + ome_period = oims.omega[0, 0] + np.r_[0., 360.] + ome_ranges = [ + ([i['ostart'], i['ostop']]) + for i in oims.omegawedges.wedges + ] - # ...make this an attribute in cfg? - analysis_id = '%s_%s' %( - cfg.analysis_name.strip().replace(' ', '-'), - cfg.material.active.strip().replace(' ', '-'), - ) + # for multiprocessing + ncpus = cfg.multiprocessing - # grab planeData object - matl = cPickle.load(open('materials.cpl', 'r')) - md = dict(zip([matl[i].name for i in range(len(matl))], matl)) - pd = md[cfg.material.active].planeData - - # make image_series - image_series = cfg.image_series.omegaseries - - # need instrument cfg later on down... - instr_cfg = get_instrument_parameters(cfg) - detector_params = np.hstack([ - instr_cfg['detector']['transform']['tilt_angles'], - instr_cfg['detector']['transform']['t_vec_d'], - instr_cfg['oscillation_stage']['chi'], - instr_cfg['oscillation_stage']['t_vec_s'], - ]) - rdim = cfg.instrument.detector.pixels.size[0]*cfg.instrument.detector.pixels.rows - cdim = cfg.instrument.detector.pixels.size[1]*cfg.instrument.detector.pixels.columns - panel_dims = ((-0.5*cdim, -0.5*rdim), - ( 0.5*cdim, 0.5*rdim), - ) - # UGH! hard-coded distortion... - if instr_cfg['detector']['distortion']['function_name'] == 'GE_41RT': - distortion = (dFuncs.GE_41RT, - instr_cfg['detector']['distortion']['parameters'], - ) - else: - distortion = None + # thresholds + image_threshold = cfg.find_orientations.orientation_maps.threshold + on_map_threshold = cfg.find_orientations.threshold + compl_thresh = cfg.find_orientations.clustering.completeness - min_compl = cfg.find_orientations.clustering.completeness + # clustering + cl_algorithm = cfg.find_orientations.clustering.algorithm + cl_radius = cfg.find_orientations.clustering.radius + + # ========================================================================= + # ORIENTATION SCORING + # ========================================================================= + do_grid_search = cfg.find_orientations.use_quaternion_grid is not None + + if use_direct_testing: + npdiv_DFLT = 2 + params = dict( + plane_data=plane_data, + instrument=instr, + imgser_dict=imsd, + tth_tol=tth_tol, + eta_tol=eta_tol, + ome_tol=ome_tol, + eta_ranges=np.radians(eta_ranges), + ome_period=np.radians(ome_period), + npdiv=npdiv_DFLT, + threshold=image_threshold) + + logger.info("\tusing direct search on %d processes", ncpus) + + # handle search space + if cfg.find_orientations.use_quaternion_grid is None: + # doing seeded search + logger.info( + "\tgenerating search quaternion list using %d processes", + ncpus + ) + start = timeit.default_timer() + + # need maps + eta_ome = load_eta_ome_maps(cfg, plane_data, imsd, + hkls=hkls, clean=clean) - # start logger - logger.info("beginning analysis '%s'", cfg.analysis_name) + # generate trial orientations + qfib = generate_orientation_fibers(cfg, eta_ome) - # load the eta_ome orientation maps - eta_ome = load_eta_ome_maps(cfg, pd, image_series, hkls=hkls, clean=clean) + logger.info("\t\t...took %f seconds", + timeit.default_timer() - start) + else: + # doing grid search + try: + qfib = np.load(cfg.find_orientations.use_quaternion_grid) + except(IOError): + raise RuntimeError( + "specified quaternion grid file '%s' not found!" + % cfg.find_orientations.use_quaternion_grid + ) - ome_range = ( - np.min(eta_ome.omeEdges), - np.max(eta_ome.omeEdges) + # execute direct search + pool = mp.Pool( + ncpus, + indexer.test_orientation_FF_init, + (params, ) ) - try: - # are we searching the full grid of orientation space? - qgrid_f = cfg.find_orientations.use_quaternion_grid - quats = np.load(qgrid_f) - logger.info("Using %s for full quaternion search", qgrid_f) - hkl_ids = None - except (IOError, ValueError, AttributeError): - # or doing a seeded search? - logger.info("Defaulting to seeded search") - hkl_seeds = cfg.find_orientations.seed_search.hkl_seeds - hkl_ids = [ - eta_ome.planeData.hklDataList[i]['hklID'] for i in hkl_seeds - ] - hklseedstr = ', '.join( - [str(i) for i in eta_ome.planeData.hkls.T[hkl_seeds]] - ) - logger.info( - "Seeding search using hkls from %s: %s", - cfg.find_orientations.orientation_maps.file, - hklseedstr - ) - quats = generate_orientation_fibers( - eta_ome, - detector_params[6], - cfg.find_orientations.threshold, - cfg.find_orientations.seed_search.hkl_seeds, - cfg.find_orientations.seed_search.fiber_ndiv, - ncpus=cfg.multiprocessing, + completeness = pool.map(indexer.test_orientation_FF_reduced, qfib.T) + pool.close() + else: + logger.info("\tusing map search with paintGrid on %d processes", ncpus) + + start = timeit.default_timer() + + # handle eta-ome maps + eta_ome = load_eta_ome_maps(cfg, plane_data, imsd, + hkls=hkls, clean=clean) + + # handle search space + if cfg.find_orientations.use_quaternion_grid is None: + # doing seeded search + logger.info( + "\tgenerating search quaternion list using %d processes", + ncpus ) - if save_as_ascii: - np.savetxt( - os.path.join(cfg.working_dir, 'trial_orientations.dat'), - quats.T, - fmt="%.18e", - delimiter="\t" + start = timeit.default_timer() + + qfib = generate_orientation_fibers(cfg, eta_ome) + logger.info("\t\t...took %f seconds", + timeit.default_timer() - start) + else: + # doing grid search + try: + qfib = np.load(cfg.find_orientations.use_quaternion_grid) + except(IOError): + raise RuntimeError( + "specified quaternion grid file '%s' not found!" + % cfg.find_orientations.use_quaternion_grid ) - pass - pass # close conditional on grid search + # do map-based indexing + start = timeit.default_timer() - # generate the completion maps - logger.info("Running paintgrid on %d trial orientations", quats.shape[1]) - if profile: - logger.info("Profiling mode active, forcing ncpus to 1") - ncpus = 1 - else: - ncpus = cfg.multiprocessing - logger.info( - "%d of %d available processors requested", ncpus, mp.cpu_count() + logger.info(" will test %d quaternions using %d processes", + qfib.shape[1], ncpus) + + completeness = indexer.paintGrid( + qfib, + eta_ome, + etaRange=np.radians(cfg.find_orientations.eta.range), + omeTol=np.radians(cfg.find_orientations.omega.tolerance), + etaTol=np.radians(cfg.find_orientations.eta.tolerance), + omePeriod=np.radians(cfg.find_orientations.omega.period), + threshold=on_map_threshold, + doMultiProc=ncpus > 1, + nCPUs=ncpus ) - compl = indexer.paintGrid( - quats, - eta_ome, - etaRange=np.radians(cfg.find_orientations.eta.range), - omeTol=np.radians(cfg.find_orientations.omega.tolerance), - etaTol=np.radians(cfg.find_orientations.eta.tolerance), - omePeriod=np.radians(cfg.find_orientations.omega.period), - threshold=cfg.find_orientations.threshold, - doMultiProc=ncpus > 1, - nCPUs=ncpus - ) + logger.info("\t\t...took %f seconds", + timeit.default_timer() - start) + completeness = np.array(completeness) + + logger.info("\tSaving %d scored orientations with max completeness %f%%", + qfib.shape[1], 100*np.max(completeness)) + + np.savez_compressed( + '_'.join(['scored_orientations', cfg.analysis_id]), + test_quaternions=qfib, score=completeness + ) - if save_as_ascii: - np.savetxt(os.path.join(cfg.working_dir, 'completeness.dat'), compl) + # ========================================================================= + # CLUSTERING AND GRAINS OUTPUT + # ========================================================================= + + if not os.path.exists(cfg.analysis_dir): + os.makedirs(cfg.analysis_dir) + qbar_filename = 'accepted_orientations_' + cfg.analysis_id + '.dat' + + logger.info("\trunning clustering using '%s'", cl_algorithm) + + start = timeit.default_timer() + + if do_grid_search: + min_samples = 1 + mean_rpg = 1 else: - np.save( - os.path.join( - cfg.working_dir, - 'scored_orientations_%s.npy' %analysis_id - ), - np.vstack([quats, compl]) - ) + active_hkls = cfg.find_orientations.orientation_maps.active_hkls \ + or eta_ome.iHKLList + + fiber_seeds = cfg.find_orientations.seed_search.hkl_seeds - ########################################################## - ## Simulate N random grains to get neighborhood size ## - ########################################################## - if hkl_ids is not None: + # Simulate N random grains to get neighborhood size + seed_hkl_ids = [ + plane_data.hklDataList[active_hkls[i]]['hklID'] + for i in fiber_seeds + ] + + # !!! default to use 100 grains ngrains = 100 rand_q = mutil.unitVector(np.random.randn(4, ngrains)) rand_e = np.tile(2.*np.arccos(rand_q[0, :]), (3, 1)) \ - * mutil.unitVector(rand_q[1:, :]) + * mutil.unitVector(rand_q[1:, :]) + grain_param_list = np.vstack( + [rand_e, + np.zeros((3, ngrains)), + np.tile(const.identity_6x1, (ngrains, 1)).T] + ).T + sim_results = instr.simulate_rotation_series( + plane_data, grain_param_list, + eta_ranges=np.radians(eta_ranges), + ome_ranges=np.radians(ome_ranges), + ome_period=np.radians(ome_period) + ) + refl_per_grain = np.zeros(ngrains) - num_seed_refls = np.zeros(ngrains) - print('fo: hklids = ', hkl_ids) - for i in range(ngrains): - grain_params = np.hstack([rand_e[:, i], - xf.zeroVec.flatten(), - xf.vInv_ref.flatten() - ]) - sim_results = simulateGVecs(pd, - detector_params, - grain_params, - ome_range=(ome_range,), - ome_period=(ome_range[0], ome_range[0]+2*np.pi), - eta_range=np.radians(cfg.find_orientations.eta.range), - panel_dims=panel_dims, - pixel_pitch=cfg.instrument.detector.pixels.size, - distortion=distortion, - ) - refl_per_grain[i] = len(sim_results[0]) - # lines below fix bug when sim_results[0] is empty - if refl_per_grain[i] > 0: - num_seed_refls[i] = np.sum([sum(sim_results[0] == hkl_id) for hkl_id in hkl_ids]) - else: - num_seed_refls[i] = 0 - #min_samples = 2 + seed_refl_per_grain = np.zeros(ngrains) + for sim_result in sim_results.itervalues(): + for i, refl_ids in enumerate(sim_result[0]): + refl_per_grain[i] += len(refl_ids) + seed_refl_per_grain[i] += np.sum( + [sum(refl_ids == hkl_id) for hkl_id in seed_hkl_ids] + ) + min_samples = max( - int(np.floor(0.5*min_compl*min(num_seed_refls))), + int(np.floor(0.5*compl_thresh*min(seed_refl_per_grain))), 2 - ) + ) mean_rpg = int(np.round(np.average(refl_per_grain))) - else: - min_samples = 1 - mean_rpg = 1 - logger.info("mean number of reflections per grain is %d", mean_rpg) - logger.info("neighborhood size estimate is %d points", min_samples) + logger.info("\tmean reflections per grain: %d", mean_rpg) + logger.info("\tneighborhood size: %d", min_samples) + logger.info("\tFeeding %d orientations above %.1f%% to clustering", + sum(completeness > compl_thresh), compl_thresh) - # cluster analysis to identify orientation blobs, the final output: - qbar, cl = run_cluster(compl, quats, pd.getQSym(), cfg, min_samples=min_samples) + qbar, cl = run_cluster( + completeness, qfib, plane_data.getQSym(), cfg, + min_samples=min_samples, + compl_thresh=compl_thresh, + radius=cl_radius) - analysis_id = '%s_%s' %( - cfg.analysis_name.strip().replace(' ', '-'), - cfg.material.active.strip().replace(' ', '-'), - ) + logger.info("\t\t...took %f seconds", (timeit.default_timer() - start)) + logger.info("\tfound %d grains; saved to file: '%s'", + (qbar.shape[1], qbar_filename)) + + np.savetxt(qbar_filename, qbar.T, + fmt='%.18e', delimiter='\t') - np.savetxt( - os.path.join( - cfg.working_dir, - 'accepted_orientations_%s.dat' %analysis_id - ), - qbar.T, - fmt="%.18e", - delimiter="\t") + gw = instrument.GrainDataWriter( + os.path.join(cfg.analysis_dir, 'grains.out') + ) + grain_params_list = [] + for gid, q in enumerate(qbar.T): + phi = 2*np.arccos(q[0]) + n = xfcapi.unitRowVector(q[1:]) + grain_params = np.hstack([phi*n, const.zeros_3, const.identity_6x1]) + gw.dump_grain(gid, 1., 0., grain_params) + grain_params_list.append(grain_params) + gw.close() return diff --git a/hexrd/instrument.py b/hexrd/instrument.py index b5092b40..992c1e3c 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -185,6 +185,28 @@ def centers_of_edge_vec(edges): return np.average(np.vstack([edges[:-1], edges[1:]]), axis=0) +def max_tth(instr): + """ + Return the maximum Bragg angle (in randians) subtended by the input + instrument + + Parameters + ---------- + instr : hexrd.instrument.HEDMInstrument instance + the instrument class to evalutate. + + Returns + ------- + tth_max : float + The maximum observable Bragg angle by the instrument. + """ + tth_max = 0. + for det in instr.detectors.values(): + ptth, peta = det.pixel_angles() + tth_max = max(np.max(ptth), tth_max) + return tth_max + + # ============================================================================= # CLASSES # ============================================================================= @@ -241,7 +263,7 @@ def __init__(self, instrument_config=None, # now build detector dict detectors_config = instrument_config['detectors'] det_dict = dict.fromkeys(detectors_config) - for det_id, det_info in detectors_config.iteritems(): + for det_id, det_info in detectors_config.items(): pixel_info = det_info['pixels'] saturation_level = det_info['saturation_level'] affine_info = det_info['transform'] @@ -418,7 +440,7 @@ def calibration_params(self): plist[4], plist[5], plist[6] = self.tvec ii = NP_INS - for panel in self.detectors.itervalues(): + for panel in self.detectors.values(): plist[ii:ii + NP_DET] = np.hstack([ panel.tilt.flatten(), panel.tvec.flatten(), @@ -427,7 +449,7 @@ def calibration_params(self): # FIXME: FML!!! # this assumes old style distiortion = (func, params) - for panel in self.detectors.itervalues(): + for panel in self.detectors.values(): if panel.distortion is not None: plist = np.concatenate( [plist, panel.distortion[1]] @@ -440,7 +462,7 @@ def update_from_calibration_params(self, plist): """ # check total length min_len_plist = NP_INS + NP_DET*self.num_panels - for panel in self.detectors.itervalues(): + for panel in self.detectors.values(): if panel.distortion is not None: min_len_plist += len(panel.distortion[1]) if len(plist) < min_len_plist: @@ -455,7 +477,7 @@ def update_from_calibration_params(self, plist): self.tvec = plist[4:7] ii = NP_INS - for panel in self.detectors.itervalues(): + for panel in self.detectors.values(): tilt_n_trans = plist[ii:ii + NP_DET] panel.tilt = tilt_n_trans[:3] panel.tvec = tilt_n_trans[3:] @@ -463,7 +485,7 @@ def update_from_calibration_params(self, plist): # FIXME: FML!!! # this assumes old style distiortion = (func, params) - for panel in self.detectors.itervalues(): + for panel in self.detectors.values(): if panel.distortion is not None: ldp = len(panel.distortion[1]) panel.distortion[1] = plist[ii:ii + ldp] @@ -499,7 +521,7 @@ def write_config(self, filename=None, calibration_dict={}): par_dict['oscillation_stage'] = ostage det_dict = dict.fromkeys(self.detectors) - for det_name, panel in self.detectors.iteritems(): + for det_name, panel in self.detectors.items(): det_dict[det_name] = panel.config_dict()['detector'] par_dict['detectors'] = det_dict if filename is not None: @@ -798,7 +820,7 @@ def simulate_laue_pattern(self, crystal_data, TODO: revisit output; dict, or concatenated list? """ results = dict.fromkeys(self.detectors) - for det_key, panel in self.detectors.iteritems(): + for det_key, panel in self.detectors.items(): results[det_key] = panel.simulate_laue_pattern( crystal_data, minEnergy=minEnergy, maxEnergy=maxEnergy, @@ -816,7 +838,7 @@ def simulate_rotation_series(self, plane_data, grain_param_list, TODO: revisit output; dict, or concatenated list? """ results = dict.fromkeys(self.detectors) - for det_key, panel in self.detectors.iteritems(): + for det_key, panel in self.detectors.items(): results[det_key] = panel.simulate_rotation_series( plane_data, grain_param_list, eta_ranges=eta_ranges, @@ -942,7 +964,7 @@ def pull_spots(self, plane_data, grain_params, ).T.reshape(len(patch_vertices), 1) # find vertices that all fall on the panel - det_xy, _ = xrdutil._project_on_detector_plane( + det_xy, rmats_s, on_plane = xrdutil._project_on_detector_plane( np.hstack([patch_vertices, ome_dupl]), panel.rmat, rMat_c, self.chi, panel.tvec, tVec_c, self.tvec, @@ -2062,7 +2084,7 @@ def simulate_rotation_series(self, plane_data, grain_param_list, allAngs[:, 2] = mapAngle(allAngs[:, 2], ome_period) # find points that fall on the panel - det_xy, rMat_s = xrdutil._project_on_detector_plane( + det_xy, rMat_s, on_plane = xrdutil._project_on_detector_plane( allAngs, self.rmat, rMat_c, chi, self.tvec, tVec_c, tVec_s, @@ -2070,12 +2092,20 @@ def simulate_rotation_series(self, plane_data, grain_param_list, xys_p, on_panel = self.clip_to_panel(det_xy) valid_xys.append(xys_p) + # filter angs and hkls that are on the detector plane + # !!! check this -- seems unnecessary but the results of + # _project_on_detector_plane() can have len < the input. + # the output of _project_on_detector_plane has been modified to + # hand back the index array to remedy this JVB 2020-05-27 + filtered_angs = np.atleast_2d(allAngs[on_plane, :]) + filtered_hkls = np.atleast_2d(allHKLs[on_plane, :]) + # grab hkls and gvec ids for this panel - valid_hkls.append(allHKLs[on_panel, 1:]) - valid_ids.append(allHKLs[on_panel, 0]) + valid_hkls.append(filtered_hkls[on_panel, 1:]) + valid_ids.append(filtered_hkls[on_panel, 0]) # reflection angles (voxel centers) and pixel size in (tth, eta) - valid_angs.append(allAngs[on_panel, :]) + valid_angs.append(filtered_angs[on_panel, :]) ang_pixel_size.append(self.angularPixelSize(xys_p)) return valid_ids, valid_hkls, valid_angs, valid_xys, ang_pixel_size @@ -2527,7 +2557,7 @@ def __init__(self, image_series_dict, instrument, plane_data, (len(eta_mapping), full_map.shape[0], full_map.shape[1]) ) i_p = 0 - for det_key, eta_map in eta_mapping.iteritems(): + for det_key, eta_map in eta_mapping.items(): nan_mask = ~np.isnan(eta_map[i_ring]) nan_mask_full[i_p] = nan_mask full_map[nan_mask] += eta_map[i_ring][nan_mask] diff --git a/hexrd/xrd/indexer.py b/hexrd/xrd/indexer.py index 3c3b0d45..d865ed20 100644 --- a/hexrd/xrd/indexer.py +++ b/hexrd/xrd/indexer.py @@ -25,44 +25,44 @@ # the Free Software Foundation, Inc., 59 Temple Place, Suite 330, # Boston, MA 02111-1307 USA or visit . # ============================================================================= +from __future__ import print_function + import sys import os import copy import ctypes -import tempfile -import glob import logging import time -import pdb import numpy as num -#num.seterr(invalid='ignore') +# num.seterr(invalid='ignore') import hexrd.matrixutil as mUtil - -from hexrd.xrd.grain import Grain, makeMeasuredScatteringVectors +from hexrd import constants as const +from hexrd import USE_NUMBA +from hexrd.xrd.grain import Grain, makeMeasuredScatteringVectors from hexrd.xrd.rotations import \ discreteFiber, mapAngle, \ quatOfRotMat, quatProductMatrix, \ rotMatOfExpMap, rotMatOfQuat -from hexrd.xrd.symmetry import toFundamentalRegion -from hexrd.xrd import xrdbase - -from hexrd.xrd import transforms as xf +from hexrd.xrd.symmetry import toFundamentalRegion +from hexrd.xrd import transforms as xf from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd import USE_NUMBA +from hexrd.xrd import xrdbase # FIXME: numba implementation of paintGridThis is broken if USE_NUMBA: import numba if xrdbase.haveMultiProc: - multiprocessing = xrdbase.multiprocessing # formerly import + multiprocessing = xrdbase.multiprocessing # formerly import +# ============================================================================= +# MODULE PARAMETERS +# ============================================================================= logger = logging.getLogger(__name__) - # module vars piby2 = num.pi * 0.5 r2d = 180. / num.pi @@ -74,17 +74,25 @@ fableSampCOB = num.dot(rotMatOfExpMap(piby2*Zl), rotMatOfExpMap(piby2*Yl)) + +# ============================================================================= +# CLASSES +# ============================================================================= + + class GrainSpotter: """ Interface to grain spotter, which must be in the user's path """ __execName = 'grainspotter' + def __init__(self): self.__tempFNameList = [] if (os.system('which '+self.__execName) != 0): - print >> sys.stderr, "need %s to be in the path" % (self.__execName) - raise RuntimeError, "unrecoverable error" + print("need %s to be in the path" % (self.__execName), + file=sys.stderr) + raise RuntimeError("unrecoverable error") return @@ -98,7 +106,7 @@ def __call__(self, spotsArray, **kwargs): location = self.__class__.__name__ tic = time.time() - phaseID = None + phaseID = None gVecFName = 'tmp' kwarglen = len(kwargs) @@ -111,13 +119,13 @@ def __call__(self, spotsArray, **kwargs): gVecFName = kwargs[argkeys[i]] planeData = spotsArray.getPlaneData(phaseID=phaseID) - U0 = planeData.latVecOps['U0'] - symTag = planeData.getLaueGroup() + U0 = planeData.latVecOps['U0'] + symTag = planeData.getLaueGroup() writeGVE(spotsArray, gVecFName, **kwargs) toc = time.time() - print 'in %s, setup took %g' % (location, toc-tic) + print('in %s, setup took %g' % (location, toc - tic)) tic = time.time() # tempFNameStdout = tempfile.mktemp() @@ -129,7 +137,7 @@ def __call__(self, spotsArray, **kwargs): grainSpotterCmd = '%s %s' % (self.__execName, gVecFName+'.ini') os.system(grainSpotterCmd) toc = time.time() - print 'in %s, execution took %g' % (location, toc-tic) + print('in %s, execution took %g' % (location, toc - tic)) tic = time.time() # add output files to cleanup list @@ -146,7 +154,7 @@ def __call__(self, spotsArray, **kwargs): retval = convertUToRotMat(gffData_U, U0, symTag=symTag) toc = time.time() - print 'in %s, post-processing took %g' % (location, toc-tic) + print('in %s, post-processing took %g' % (location, toc - tic)) tic = time.time() return retval @@ -195,10 +203,10 @@ def convertUToRotMat(Urows, U0, symTag='Oh', display=False): else: qout = toFundamentalRegion(qout, crysSym=symTag, sampSym=None) if display: - print "quaternions in (Fable convention):" - print qin.T - print "quaternions out (hexrd convention, symmetrically reduced)" - print qout.T + print("quaternions in (Fable convention):") + print(qin.T) + print("quaternions out (hexrd convention, symmetrically reduced)") + print(qout.T) Uout = rotMatOfQuat(qout) return Uout @@ -229,31 +237,37 @@ def convertRotMatToFableU(rMats, U0=num.eye(3), symTag='Oh', display=False): else: qout = toFundamentalRegion(qout, crysSym=symTag, sampSym=None) if display: - print "quaternions in (hexrd convention):" - print qin.T - print "quaternions out (Fable convention, symmetrically reduced)" - print qout.T + print("quaternions in (hexrd convention):") + print(qin.T) + print("quaternions out (Fable convention, symmetrically reduced)") + print(qout.T) Uout = rotMatOfQuat(qout) return Uout -###################################################################### -""" -things for doing fiberSearch with multiprocessing; -multiprocessing has a hard time pickling a function defined in the local scope -of another function, so stuck putting the function out here; -""" +# ============================================================================= +# FIBERSEARCH +# +# things for doing fiberSearch with multiprocessing; +# multiprocessing has a hard time pickling a function defined in the local +# scope of another function, so stuck putting the function out here. +# +# !!!: deprecated +# ============================================================================= + debugMultiproc = 0 if xrdbase.haveMultiProc: foundFlagShared = multiprocessing.Value(ctypes.c_bool) foundFlagShared.value = False -multiProcMode_MP = None -spotsArray_MP = None -candidate_MP = None -dspTol_MP = None +multiProcMode_MP = None +spotsArray_MP = None +candidate_MP = None +dspTol_MP = None minCompleteness_MP = None -doRefinement_MP = None -nStdDev_MP = None +doRefinement_MP = None +nStdDev_MP = None + + def testThisQ(thisQ): """ NOTES: @@ -272,19 +286,17 @@ def testThisQ(thisQ): global doRefinement_MP global nStdDev_MP # assign locals - multiProcMode = multiProcMode_MP - spotsArray = spotsArray_MP - candidate = candidate_MP - dspTol = dspTol_MP + multiProcMode = multiProcMode_MP + candidate = candidate_MP + dspTol = dspTol_MP minCompleteness = minCompleteness_MP - doRefinement = doRefinement_MP - nStdDev = nStdDev_MP - nSigmas = 2 # ... make this a settable option? + doRefinement = doRefinement_MP + nStdDev = nStdDev_MP if multiProcMode: global foundFlagShared foundGrainData = None - #print "testing %d of %d"% (iR+1, numTrials) + # print("testing %d of %d"% (iR + 1, numTrials)) thisRMat = rotMatOfQuat(thisQ) ppfx = '' @@ -297,34 +309,36 @@ def testThisQ(thisQ): but skip evaluations after an acceptable grain has been found """ if debugMultiproc > 1: - print ppfx+'skipping on '+str(thisQ) + print(ppfx + 'skipping on ' + str(thisQ)) return foundGrainData else: if debugMultiproc > 1: - print ppfx+'working on '+str(thisQ) + print(ppfx + 'working on ' + str(thisQ)) candidate.findMatches(rMat=thisRMat, strainMag=dspTol, claimingSpots=False, testClaims=True, updateSelf=True) if debugMultiproc > 1: - print ppfx+' for '+str(thisQ)+' got completeness : '\ - +str(candidate.completeness) + print(ppfx + ' for ' + str(thisQ) + ' got completeness : ' + + str(candidate.completeness)) if candidate.completeness >= minCompleteness: - ## attempt to filter out 'junk' spots here by performing full - ## refinement before claiming + ''' + Attempt to filter out 'junk' spots here by performing full + refinement before claiming + ''' fineEtaTol = candidate.etaTol fineOmeTol = candidate.omeTol if doRefinement: if multiProcMode and foundFlagShared.value: 'some other process beat this one to it' return foundGrainData - print ppfx+"testing candidate q = [%1.2e, %1.2e, %1.2e, %1.2e]"\ - %tuple(thisQ) + print(ppfx + "testing candidate q = [%1.2e, %1.2e, %1.2e, %1.2e]" + % tuple(thisQ)) # not needed # candidate.fitPrecession(display=False) - ## first fit + # first fit candidate.fit(display=False) - ## auto-tolerace based on statistics of current matches + # auto-tolerace based on statistics of current matches validRefls = candidate.grainSpots['iRefl'] > 0 fineEtaTol = nStdDev * num.std( candidate.grainSpots['diffAngles'][validRefls, 1] @@ -332,7 +346,7 @@ def testThisQ(thisQ): fineOmeTol = nStdDev * num.std( candidate.grainSpots['diffAngles'][validRefls, 2] ) - ## next fits with finer tolerances + # next fits with finer tolerances for iLoop in range(3): candidate.findMatches(etaTol=fineEtaTol, omeTol=fineOmeTol, @@ -342,7 +356,7 @@ def testThisQ(thisQ): # not needed # candidate.fitPrecession(display=False) candidate.fit(display=False) if candidate.completeness < minCompleteness: - print ppfx+"candidate failed" + print(ppfx + "candidate failed") return foundGrainData if multiProcMode and foundFlagShared.value: 'some other process beat this one to it' @@ -355,7 +369,7 @@ def testThisQ(thisQ): # not needed? # testClaims=True, # not needed? # updateSelf=True) else: - ## at least do precession correction + # at least do precession correction candidate.fitPrecession(display=False) candidate.findMatches(rMat=thisRMat, strainMag=dspTol, @@ -365,7 +379,7 @@ def testThisQ(thisQ): fineEtaTol = candidate.etaTol fineOmeTol = candidate.omeTol if candidate.completeness < minCompleteness: - print ppfx+"candidate failed" + print(ppfx + "candidate failed") return foundGrainData if multiProcMode and foundFlagShared.value: 'some other process beat this one to it' @@ -382,8 +396,11 @@ def testThisQ(thisQ): # foundGrain.strip() cInfo = quatOfRotMat(candidate.rMat).flatten().tolist() cInfo.append(candidate.completeness) - print ppfx+"Grain found at q = [%1.2e, %1.2e, %1.2e, %1.2e] "\ - "with completeness %g" % tuple(cInfo) + print( + ppfx + + "Grain found at q = [%1.2e, %1.2e, %1.2e, %1.2e] " + + "with completeness %g" % tuple(cInfo) + ) foundGrainData = candidate.getGrainData() 'tolerances not actually set in candidate, so set them manually' foundGrainData['omeTol'] = fineOmeTol @@ -453,13 +470,13 @@ def fiberSearch(spotsArray, hklList, global minCompleteness_MP global doRefinement_MP global nStdDev_MP - multiProcMode_MP = multiProcMode - spotsArray_MP = spotsArray - candidate_MP = candidate - dspTol_MP = dspTol + multiProcMode_MP = multiProcMode + spotsArray_MP = spotsArray + candidate_MP = candidate + dspTol_MP = dspTol minCompleteness_MP = minCompleteness - doRefinement_MP = doRefinement - nStdDev_MP = nStdDev + doRefinement_MP = doRefinement + nStdDev_MP = nStdDev """ set up for shared memory multiprocessing """ @@ -487,13 +504,13 @@ def fiberSearch(spotsArray, hklList, tic = time.time() for iHKL in range(n_hkls_to_search): - print "\n#####################\nProcessing hkl %d of %d\n" \ - % (iHKL+1, nHKLs) + print("\n#####################\nProcessing hkl %d of %d\n" + % (iHKL+1, nHKLs)) thisHKLID = planeData.getHKLID(hklList[iHKL]) - thisRingSpots0 = spotsArray.getHKLSpots(thisHKLID) - thisRingSpots0W = num.where(thisRingSpots0)[0] + thisRingSpots0 = spotsArray.getHKLSpots(thisHKLID) + thisRingSpots0W = num.where(thisRingSpots0)[0] unclaimedOfThese = -spotsArray.checkClaims(indices=thisRingSpots0W) - thisRingSpots = copy.deepcopy(thisRingSpots0) + thisRingSpots = copy.deepcopy(thisRingSpots0) thisRingSpots[thisRingSpots0W] = unclaimedOfThese if friedelOnly: # first, find Friedel Pairs @@ -505,9 +522,9 @@ def fiberSearch(spotsArray, hklList, ) # make some stuff for counters maxSpots = 0.5*( - sum(thisRingSpots) \ + sum(thisRingSpots) - sum(spotsArray.friedelPair[thisRingSpots] == -1) - ) + ) else: spotsIteratorI = spotsArray.getIterHKL( hklList[iHKL], unclaimedOnly=True, friedelOnly=False @@ -521,28 +538,28 @@ def fiberSearch(spotsArray, hklList, """ for iRefl, stuff in enumerate(spotsIteratorI): unclaimedOfThese = -spotsArray.checkClaims(indices=thisRingSpots0W) - thisRingSpots = copy.deepcopy(thisRingSpots0) + thisRingSpots = copy.deepcopy(thisRingSpots0) thisRingSpots[thisRingSpots0W] = unclaimedOfThese if friedelOnly: iSpot, jSpot, angs_I, angs_J = stuff - Gplus = makeMeasuredScatteringVectors(*angs_I) + Gplus = makeMeasuredScatteringVectors(*angs_I) Gminus = makeMeasuredScatteringVectors(*angs_J) Gvec = 0.5*(Gplus - Gminus) maxSpots = 0.5*( - sum(thisRingSpots) \ + sum(thisRingSpots) - sum(spotsArray.friedelPair[thisRingSpots] == -1) - ) + ) else: iSpot, angs_I = stuff - Gvec = makeMeasuredScatteringVectors(*angs_I) + Gvec = makeMeasuredScatteringVectors(*angs_I) maxSpots = sum(thisRingSpots) - print "\nProcessing reflection %d (spot %d), %d remain "\ - "unclaimed\n" % (iRefl+1, iSpot, maxSpots) + print("\nProcessing reflection %d (spot %d), %d remain " + + "unclaimed\n" % (iRefl + 1, iSpot, maxSpots)) if multiProcMode and debugMultiproc > 1: marks = spotsArray._Spots__marks[:] - print 'marks : '+str(marks) + print('marks : ' + str(marks)) # make the fiber; qfib = discreteFiber(hklList[iHKL], Gvec, B=bMat, @@ -568,10 +585,12 @@ def fiberSearch(spotsArray, hklList, if multiProcMode: foundFlagShared.value = False qfibList = map(num.array, qfib.T.tolist()) - #if debugMultiproc: - # print 'qfibList : '+str(qfibList) + # if debugMultiproc: + # print('qfibList : ' + str(qfibList)) results = num.array(pool.map(testThisQ, qfibList, chunksize=1)) - trialGrains = results[num.where(num.array(results, dtype=bool))] + trialGrains = results[ + num.where(num.array(results, dtype=bool)) + ] # for trialGrain in trialGrains: # trialGrain.restore(candidate) else: @@ -584,7 +603,7 @@ def fiberSearch(spotsArray, hklList, 'end of if multiProcMode' if len(trialGrains) == 0: - print "No grain found containing spot %d\n" % (iSpot) + print("No grain found containing spot %d\n" % (iSpot)) # import pdb;pdb.set_trace() else: asMaster = multiProcMode @@ -600,28 +619,29 @@ def fiberSearch(spotsArray, hklList, grainData=foundGrainData, claimingSpots=False ) - #check completeness before accepting - #especially important for multiproc - foundGrain.checkClaims() # updates completeness + # !!! check completeness before accepting + # especially important for multiproc + foundGrain.checkClaims() # updates completeness if debugMultiproc: - print 'final completeness of candidate is %g' \ - % (foundGrain.completeness) + print('final completeness of candidate is %g' + % (foundGrain.completeness)) if foundGrain.completeness >= minCompleteness: conflicts = foundGrain.claimSpots(asMaster=asMaster) numConfl = num.sum(conflicts) if numConfl > 0: - print 'tried to claim %d spots that are already '\ - 'claimed' % (numConfl) + print('tried to claim %d spots that are already ' + + 'claimed' % (numConfl)) grainList.append(foundGrain) nGrains += 1 numUnClaimed = num.sum(-spotsArray.checkClaims()) numClaimed = numTotal - numUnClaimed pctClaimed = num.float(numClaimed) / numTotal - print "Found %d grains so far, %f%% claimed" \ - % (nGrains,100*pctClaimed) + print("Found %d grains so far, %f%% claimed" + % (nGrains, 100*pctClaimed)) - time_to_quit = (pctClaimed > minPctClaimed) or\ - ((quit_after_ngrains > 0) and (nGrains >= quit_after_ngrains)) + time_to_quit = (pctClaimed > minPctClaimed) or \ + ((quit_after_ngrains > 0) + and (nGrains >= quit_after_ngrains)) if time_to_quit: break 'end of iRefl loop' @@ -642,7 +662,7 @@ def fiberSearch(spotsArray, hklList, if not preserveClaims: spotsArray.resetClaims() toc = time.time() - print 'fiberSearch execution took %g seconds' % (toc-tic) + print('fiberSearch execution took %g seconds' % (toc - tic)) if multiProcMode: pool.close() @@ -664,16 +684,39 @@ def fiberSearch(spotsArray, hklList, return retval + def pgRefine(x, etaOmeMaps, omegaRange, threshold): + """ + Objective function for refining orientations found with paintGrid. + + !!!: This function is flagged for removal. + + Parameters + ---------- + x : TYPE + DESCRIPTION. + etaOmeMaps : TYPE + DESCRIPTION. + omegaRange : TYPE + DESCRIPTION. + threshold : TYPE + DESCRIPTION. + + Returns + ------- + f : TYPE + DESCRIPTION. + + """ phi = sum(x*x) if phi < 1e-7: - q = [num.r_[1.,0.,0.,0.],] + q = [num.r_[1., 0., 0., 0.], ] else: phi = num.sqrt(phi) n = (1. / phi) * x.flatten() cphi2 = num.cos(0.5*phi) sphi2 = num.sin(0.5*phi) - q = [num.r_[cphi2, sphi2*n[0], sphi2*n[1], sphi2*n[2]],] + q = [num.r_[cphi2, sphi2*n[0], sphi2*n[1], sphi2*n[2]], ] c = paintGrid( q, etaOmeMaps, threshold=threshold, bMat=None, omegaRange=omegaRange, etaRange=None, debug=False @@ -681,7 +724,109 @@ def pgRefine(x, etaOmeMaps, omegaRange, threshold): f = abs(1. - c) return f -paramMP = None + +# ============================================================================= +# DIRECT SEARCH FUNCTIONS +# ============================================================================= + + +def test_orientation_FF_init(params): + """ + Broadcast the indexing parameters as globals for multiprocessing + + Parameters + ---------- + params : dict + The dictionary of indexing parameters. + + Returns + ------- + None. + + Notes + ----- + See test_orientation_FF_reduced for specification. + """ + global paramMP + paramMP = params + + +def test_orientation_FF_reduced(quat): + """ + Return the completeness score for input quaternion. + + Parameters + ---------- + quat : array_like (4,) + The unit quaternion representation for the orientation to be tested. + + Returns + ------- + float + The completeness, i.e., the ratio between the predicted and observed + Bragg reflections subject to the specified tolerances. + + + Notes + ----- + input parameters are + [plane_data, instrument, imgser_dict, + tth_tol, eta_tol, ome_tol, eta_ranges, ome_period, + npdiv, threshold] + """ + plane_data = paramMP['plane_data'] + instrument = paramMP['instrument'] + imgser_dict = paramMP['imgser_dict'] + tth_tol = paramMP['tth_tol'] + eta_tol = paramMP['eta_tol'] + ome_tol = paramMP['ome_tol'] + eta_ranges = paramMP['eta_ranges'] + ome_period = paramMP['ome_period'] + npdiv = paramMP['npdiv'] + threshold = paramMP['threshold'] + + phi = 2*num.arccos(quat[0]) + n = xfcapi.unitRowVector(quat[1:]) + grain_params = num.hstack([ + phi*n, const.zeros_3, const.identity_6x1, + ]) + + compl, scrap = instrument.pull_spots( + plane_data, grain_params, imgser_dict, + tth_tol=tth_tol, eta_tol=eta_tol, ome_tol=ome_tol, + npdiv=npdiv, threshold=threshold, + eta_ranges=eta_ranges, + ome_period=ome_period, + check_only=True) + + return sum(compl)/float(len(compl)) + + +# ============================================================================= +# PAINTGRID +# ============================================================================= + + +def paintgrid_init(params): + global paramMP + paramMP = params + + # create valid_eta_spans, valid_ome_spans from etaMin/Max and omeMin/Max + # this allows using faster checks in the code. + # TODO: build valid_eta_spans and valid_ome_spans directly in paintGrid + # instead of building etaMin/etaMax and omeMin/omeMax. It may also + # be worth handling range overlap and maybe "optimize" ranges if + # there happens to be contiguous spans. + paramMP['valid_eta_spans'] = _normalize_ranges(paramMP['etaMin'], + paramMP['etaMax'], + -num.pi) + + paramMP['valid_ome_spans'] = _normalize_ranges(paramMP['omeMin'], + paramMP['omeMax'], + min(paramMP['omePeriod'])) + return + + def paintGrid(quats, etaOmeMaps, threshold=None, bMat=None, omegaRange=None, etaRange=None, @@ -714,12 +859,12 @@ def paintGrid(quats, etaOmeMaps, planeData = etaOmeMaps.planeData - hklIDs = num.r_[etaOmeMaps.iHKLList] - hklList = num.atleast_2d(planeData.hkls[:, hklIDs].T).tolist() - nHKLS = len(hklIDs) + hklIDs = num.r_[etaOmeMaps.iHKLList] + hklList = num.atleast_2d(planeData.hkls[:, hklIDs].T).tolist() + nHKLS = len(hklIDs) - numEtas = len(etaOmeMaps.etaEdges) - 1 - numOmes = len(etaOmeMaps.omeEdges) - 1 + numEtas = len(etaOmeMaps.etaEdges) - 1 + numOmes = len(etaOmeMaps.omeEdges) - 1 if threshold is None: threshold = num.zeros(nHKLS) @@ -734,9 +879,9 @@ def paintGrid(quats, etaOmeMaps, threshold = threshold * num.ones(nHKLS) elif hasattr(threshold, '__len__'): if len(threshold) != nHKLS: - raise RuntimeError, "threshold list is wrong length!" + raise RuntimeError("threshold list is wrong length!") else: - print "INFO: using list of threshold values" + print("INFO: using list of threshold values") else: raise RuntimeError( "unknown threshold option. should be a list of numbers or None" @@ -763,15 +908,15 @@ def paintGrid(quats, etaOmeMaps, omeMin = None omeMax = None - if omegaRange is None: # this NEEDS TO BE FIXED! - omeMin = [num.min(etaOmeMaps.omeEdges),] - omeMax = [num.max(etaOmeMaps.omeEdges),] + if omegaRange is None: # this NEEDS TO BE FIXED! + omeMin = [num.min(etaOmeMaps.omeEdges), ] + omeMax = [num.max(etaOmeMaps.omeEdges), ] else: omeMin = [omegaRange[i][0] for i in range(len(omegaRange))] omeMax = [omegaRange[i][1] for i in range(len(omegaRange))] if omeMin is None: omeMin = [-num.pi, ] - omeMax = [ num.pi, ] + omeMax = [num.pi, ] omeMin = num.asarray(omeMin) omeMax = num.asarray(omeMax) @@ -782,7 +927,7 @@ def paintGrid(quats, etaOmeMaps, etaMax = [etaRange[i][1] for i in range(len(etaRange))] if etaMin is None: etaMin = [-num.pi, ] - etaMax = [ num.pi, ] + etaMax = [num.pi, ] etaMin = num.asarray(etaMin) etaMax = num.asarray(etaMax) @@ -841,10 +986,8 @@ def paintGrid(quats, etaOmeMaps, pool.close() else: # single process version. - global paramMP paintgrid_init(params) # sets paramMP retval = map(paintGridThis, quats.T) - paramMP = None # clear paramMP elapsed = (time.time() - start) logger.info("paintGrid took %.3f seconds", elapsed) @@ -866,7 +1009,6 @@ def _meshgrid2d(x, y): return (r1, r2) - def _normalize_ranges(starts, stops, offset, ccw=False): """normalize in the range [offset, 2*pi+offset[ the ranges defined by starts and stops. @@ -883,7 +1025,6 @@ def _normalize_ranges(starts, stops, offset, ccw=False): if not num.all(starts < stops): raise ValueError('Invalid angle ranges') - # If there is a range that spans more than 2*pi, # return the full range two_pi = 2 * num.pi @@ -894,8 +1035,10 @@ def _normalize_ranges(starts, stops, offset, ccw=False): stops = num.mod(stops - offset, two_pi) + offset order = num.argsort(starts) - result = num.hstack((starts[order, num.newaxis], - stops[order, num.newaxis])).ravel() + result = num.hstack( + (starts[order, num.newaxis], + stops[order, num.newaxis]) + ).ravel() # at this point, result is in its final form unless there # is wrap-around in the last segment. Handle this case: if result[-1] < result[-2]: @@ -912,26 +1055,6 @@ def _normalize_ranges(starts, stops, offset, ccw=False): return result -def paintgrid_init(params): - global paramMP - paramMP = params - - # create valid_eta_spans, valid_ome_spans from etaMin/Max and omeMin/Max - # this allows using faster checks in the code. - # TODO: build valid_eta_spans and valid_ome_spans directly in paintGrid - # instead of building etaMin/etaMax and omeMin/omeMax. It may also - # be worth handling range overlap and maybe "optimize" ranges if - # there happens to be contiguous spans. - paramMP['valid_eta_spans'] = _normalize_ranges(paramMP['etaMin'], - paramMP['etaMax'], - -num.pi) - - paramMP['valid_ome_spans'] = _normalize_ranges(paramMP['omeMin'], - paramMP['omeMax'], - min(paramMP['omePeriod'])) - return - - ############################################################################### # # paintGridThis contains the bulk of the process to perform for paintGrid for a @@ -942,6 +1065,7 @@ def paintgrid_init(params): # There is a version of PaintGridThis using numba, and another version used # when numba is not available. The numba version should be noticeably faster. + def _check_dilated(eta, ome, dpix_eta, dpix_ome, etaOmeMap, threshold): """This is part of paintGridThis: @@ -976,14 +1100,19 @@ def _check_dilated(eta, ome, dpix_eta, dpix_ome, etaOmeMap, threshold): return 0 +# ============================================================================= +# HELPER FUNCTIONS WITH SPLIT DEFS BASED ON USE_NUMBA +# ============================================================================= + + if USE_NUMBA: def paintGridThis(quat): - # Note that this version does not use omeMin/omeMax to specify the valid - # angles. It uses "valid_eta_spans" and "valid_ome_spans". These are - # precomputed and make for a faster check of ranges than + # Note that this version does not use omeMin/omeMax to specify the + # valid angles. It uses "valid_eta_spans" and "valid_ome_spans". + # These are precomputed and make for a faster check of ranges than # "validateAngleRanges" - symHKLs = paramMP['symHKLs'] # the HKLs - symHKLs_ix = paramMP['symHKLs_ix'] # index partitioning of symHKLs + symHKLs = paramMP['symHKLs'] # the HKLs + symHKLs_ix = paramMP['symHKLs_ix'] # index partitioning of symHKLs bMat = paramMP['bMat'] wavelength = paramMP['wavelength'] omeEdges = paramMP['omeEdges'] @@ -1008,7 +1137,7 @@ def paintGridThis(quat): debug = False if debug: - print( "using ome, eta dilitations of (%d, %d) pixels" \ + print("using ome, eta dilitations of (%d, %d) pixels" % (dpix_ome, dpix_eta)) # get the equivalent rotation of the quaternion in matrix form (as @@ -1026,15 +1155,15 @@ def paintGridThis(quat): etaOmeMaps, etaIndices, omeIndices, dpix_eta, dpix_ome, threshold) - @numba.jit def _find_in_range(value, spans): - """find the index in spans where value >= spans[i] and value < spans[i]. + """ + Find the index in spans where value >= spans[i] and value < spans[i]. spans is an ordered array where spans[i] <= spans[i+1] (most often < will hold). - If value is not in the range [spans[0], spans[-1][, then -2 is returned. + If value is not in the range [spans[0], spans[-1]] then -2 is returned. This is equivalent to "bisect_right" in the bisect package, in which code it is based, and it is somewhat similar to NumPy's searchsorted, @@ -1172,18 +1301,18 @@ def _filter_and_count_hits(angs_0, angs_1, symHKLs_ix, etaEdges, @numba.njit def _map_angle(angle, offset): - """Equivalent to xf.mapAngle in this context, and 'numba friendly' - """ - return num.mod(angle-offset, 2*num.pi)+offset + Equivalent to xf.mapAngle in this context, and 'numba friendly' + """ + return num.mod(angle - offset, 2*num.pi) + offset # use a jitted version of _check_dilated _check_dilated = numba.njit(_check_dilated) else: def paintGridThis(quat): # unmarshall parameters into local variables - symHKLs = paramMP['symHKLs'] # the HKLs - symHKLs_ix = paramMP['symHKLs_ix'] # index partitioning of symHKLs + symHKLs = paramMP['symHKLs'] # the HKLs + symHKLs_ix = paramMP['symHKLs_ix'] # index partitioning of symHKLs bMat = paramMP['bMat'] wavelength = paramMP['wavelength'] omeEdges = paramMP['omeEdges'] @@ -1208,7 +1337,7 @@ def paintGridThis(quat): debug = False if debug: - print( "using ome, eta dilitations of (%d, %d) pixels" \ + print("using ome, eta dilitations of (%d, %d) pixels" % (dpix_ome, dpix_eta)) # get the equivalent rotation of the quaternion in matrix form (as @@ -1225,12 +1354,15 @@ def paintGridThis(quat): valid_ome_spans, omePeriod) if len(hkl_idx > 0): - hits, predicted = _count_hits(eta_idx, ome_idx, hkl_idx, etaOmeMaps, - etaIndices, omeIndices, dpix_eta, dpix_ome, - threshold) + hits, predicted = _count_hits( + eta_idx, ome_idx, hkl_idx, etaOmeMaps, + etaIndices, omeIndices, dpix_eta, dpix_ome, + threshold + ) retval = float(hits) / float(predicted) if retval > 1: - import pdb; pdb.set_trace() + import pdb + pdb.set_trace() return retval def _normalize_angs_hkls(angs_0, angs_1, omePeriod, symHKLs_ix): @@ -1248,15 +1380,14 @@ def _normalize_angs_hkls(angs_0, angs_1, omePeriod, symHKLs_ix): symHKLs_ix = symHKLs_ix*2 hkl_idx = num.empty((symHKLs_ix[-1],), dtype=int) start = symHKLs_ix[0] - idx=0 + idx = 0 for end in symHKLs_ix[1:]: hkl_idx[start:end] = idx start = end - idx+=1 + idx += 1 return oangs, hkl_idx - def _filter_angs(angs_0, angs_1, symHKLs_ix, etaEdges, valid_eta_spans, omeEdges, valid_ome_spans, omePeriod): """ @@ -1270,9 +1401,9 @@ def _filter_angs(angs_0, angs_1, symHKLs_ix, etaEdges, valid_eta_spans, """ oangs, hkl_idx = _normalize_angs_hkls(angs_0, angs_1, omePeriod, symHKLs_ix) - # using "right" side to make sure we always get an index *past* the value - # if it happens to be equal. That is... we search the index of the first - # value that is "greater than" rather than "greater or equal" + # using "right" side to make sure we always get an index *past* the + # value if it happens to be equal. That is... we search the index of + # the first value that is "greater than" rather than "greater or equal" culled_eta_indices = num.searchsorted(etaEdges, oangs[:, 1], side='right') culled_ome_indices = num.searchsorted(omeEdges, oangs[:, 2], @@ -1282,19 +1413,23 @@ def _filter_angs(angs_0, angs_1, symHKLs_ix, etaEdges, valid_eta_spans, # The spans contains an ordered sucession of start and end angles which # form the valid angle spans. So knowing if an angle is valid is # equivalent to finding the insertion point in the spans array and - # checking if the resulting insertion index is odd or even. An odd value - # means that it falls between a start and a end point of the "valid - # span", meaning it is a hit. An even value will result in either being - # out of the range (0 or the last index, as length is even by - # construction) or that it falls between a "end" point from one span and - # the "start" point of the next one. - valid_eta = num.searchsorted(valid_eta_spans, oangs[:, 1], side='right') - valid_ome = num.searchsorted(valid_ome_spans, oangs[:, 2], side='right') + # checking if the resulting insertion index is odd or even. + # An odd value means that it falls between a start and a end point of + # the "valid span", meaning it is a hit. An even value will result in + # either being out of the range (0 or the last index, as length is even + # by construction) or that it falls between a "end" point from one span + # and the "start" point of the next one. + valid_eta = num.searchsorted( + valid_eta_spans, oangs[:, 1], side='right' + ) + valid_ome = num.searchsorted( + valid_ome_spans, oangs[:, 2], side='right' + ) # fast odd/even check valid_eta = valid_eta & 1 valid_ome = valid_ome & 1 # Create a mask of the good ones - valid = ~num.isnan(oangs[:, 0]) # tth not NaN + valid = ~num.isnan(oangs[:, 0]) # tth not NaN valid = num.logical_and(valid, valid_eta) valid = num.logical_and(valid, valid_ome) valid = num.logical_and(valid, culled_eta_indices > 0) @@ -1308,7 +1443,6 @@ def _filter_angs(angs_0, angs_1, symHKLs_ix, etaEdges, valid_eta_spans, return hkl_idx, eta_idx, ome_idx - def _count_hits(eta_idx, ome_idx, hkl_idx, etaOmeMaps, etaIndices, omeIndices, dpix_eta, dpix_ome, threshold): """ @@ -1388,19 +1522,18 @@ def writeGVE(spotsArray, fileroot, **kwargs): assert isinstance(fileroot, str) # keyword argument processing - phaseID = None - sgNum = 225 - cellString = 'P' - omeRange = num.r_[-60, 60] # in DEGREES - deltaOme = 0.25 # in DEGREES - minMeas = 24 - minCompl = 0.7 - minUniqn = 0.5 - uncertainty = [0.10, 0.25, .50] # in DEGREES - eulStep = 2 # in DEGREES - nSigmas = 2 - minFracG = 0.90 - numTrials = 100000 + phaseID = None + sgNum = 225 + cellString = 'P' + deltaOme = 0.25 # in DEGREES + minMeas = 24 + minCompl = 0.7 + minUniqn = 0.5 + uncertainty = [0.10, 0.25, .50] # in DEGREES + eulStep = 2 # in DEGREES + nSigmas = 2 + minFracG = 0.90 + numTrials = 100000 positionFit = True kwarglen = len(kwargs) @@ -1414,8 +1547,6 @@ def writeGVE(spotsArray, fileroot, **kwargs): phaseID = kwargs[argkeys[i]] elif argkeys[i] == 'cellString': cellString = kwargs[argkeys[i]] - elif argkeys[i] == 'omeRange': - omeRange = kwargs[argkeys[i]] elif argkeys[i] == 'deltaOme': deltaOme = kwargs[argkeys[i]] elif argkeys[i] == 'minMeas': @@ -1453,19 +1584,17 @@ def writeGVE(spotsArray, fileroot, **kwargs): yc_p = ncols_p - col_p zc_p = nrows_p - row_p - wd_mu = spotsArray.detectorGeom.workDist * 1e3 # in microns (Soeren) + wd_mu = spotsArray.detectorGeom.workDist * 1e3 # in microns (Soeren) osc_axis = num.dot(fableSampCOB.T, Yl).flatten() # start grabbing stuff from planeData planeData = spotsArray.getPlaneData(phaseID=phaseID) - cellp = planeData.latVecOps['dparms'] - U0 = planeData.latVecOps['U0'] - wlen = planeData.wavelength - dsp = planeData.getPlaneSpacings() - fHKLs = planeData.getSymHKLs() - tThRng = planeData.getTThRanges() - symTag = planeData.getLaueGroup() + cellp = planeData.latVecOps['dparms'] + wlen = planeData.wavelength + dsp = planeData.getPlaneSpacings() + fHKLs = planeData.getSymHKLs() + tThRng = planeData.getTThRanges() # single range should be ok since entering hkls tThMin, tThMax = (r2d*tThRng.min(), r2d*tThRng.max()) @@ -1502,13 +1631,13 @@ def writeGVE(spotsArray, fileroot, **kwargs): gvecString = '' spotsIter = spotsArray.getIterPhase(phaseID, returnBothCoordTypes=True) for iSpot, angCOM, xyoCOM in spotsIter: - sR, sC, sOme = xyoCOM # detector coords - sTTh, sEta, sOme = angCOM # angular coords (radians) - sDsp = wlen / 2. / num.sin(0.5*sTTh) # dspacing + sR, sC, sOme = xyoCOM # detector coords + sTTh, sEta, sOme = angCOM # angular coords (radians) + sDsp = wlen / 2. / num.sin(0.5*sTTh) # dspacing - # get raw y, z (Fable frame) - yraw = ncols_p - sC - zraw = nrows_p - sR + # # get raw y, z (Fable frame) + # yraw = ncols_p - sC + # zraw = nrows_p - sR # convert eta to fable frame rEta = mapAngle(90. - r2d*sEta, [0, 360], units='degrees') @@ -1535,40 +1664,42 @@ def writeGVE(spotsArray, fileroot, **kwargs): # write gve file for grainspotter fid = open(fileroot+'.gve', 'w') - print >> fid, '%1.8f %1.8f %1.8f %1.8f %1.8f %1.8f ' % tuple(cellp) \ - + cellString + '\n' \ - + '# wavelength = %1.8f\n' % (wlen) \ - + '# wedge = 0.000000\n' \ - + '# axis = %d %d %d\n' % tuple(osc_axis) \ - + '# cell__a %1.4f\n' %(cellp[0]) \ - + '# cell__b %1.4f\n' %(cellp[1]) \ - + '# cell__c %1.4f\n' %(cellp[2]) \ - + '# cell_alpha %1.4f\n' %(cellp[3]) \ - + '# cell_beta %1.4f\n' %(cellp[4]) \ - + '# cell_gamma %1.4f\n' %(cellp[5]) \ - + '# cell_lattice_[P,A,B,C,I,F,R] %s\n' %(cellString) \ - + '# chi 0.0\n' \ - + '# distance %.4f\n' %(wd_mu) \ - + '# fit_tolerance 0.5\n' \ - + '# o11 1\n' \ - + '# o12 0\n' \ - + '# o21 0\n' \ - + '# o22 -1\n' \ - + '# omegasign %1.1f\n' %(num.sign(deltaOme)) \ - + '# t_x 0\n' \ - + '# t_y 0\n' \ - + '# t_z 0\n' \ - + '# tilt_x 0.000000\n' \ - + '# tilt_y 0.000000\n' \ - + '# tilt_z 0.000000\n' \ - + '# y_center %.6f\n' %(yc_p) \ - + '# y_size %.6f\n' %(mmPerPixel*1.e3) \ - + '# z_center %.6f\n' %(zc_p) \ - + '# z_size %.6f\n' %(mmPerPixel*1.e3) \ - + '# ds h k l\n' \ - + gvecHKLString \ - + '# xr yr zr xc yc ds eta omega\n' \ - + gvecString + print( + '%1.8f %1.8f %1.8f %1.8f %1.8f %1.8f ' % tuple(cellp) + + cellString + '\n' + + '# wavelength = %1.8f\n' % (wlen) + + '# wedge = 0.000000\n' + + '# axis = %d %d %d\n' % tuple(osc_axis) + + '# cell__a %1.4f\n' % cellp[0] + + '# cell__b %1.4f\n' % cellp[1] + + '# cell__c %1.4f\n' % cellp[2] + + '# cell_alpha %1.4f\n' % cellp[3] + + '# cell_beta %1.4f\n' % cellp[4] + + '# cell_gamma %1.4f\n' % cellp[5] + + '# cell_lattice_[P,A,B,C,I,F,R] %s\n' % cellString + + '# chi 0.0\n' + + '# distance %.4f\n' % wd_mu + + '# fit_tolerance 0.5\n' + + '# o11 1\n' + + '# o12 0\n' + + '# o21 0\n' + + '# o22 -1\n' + + '# omegasign %1.1f\n' % num.sign(deltaOme) + + '# t_x 0\n' + + '# t_y 0\n' + + '# t_z 0\n' + + '# tilt_x 0.000000\n' + + '# tilt_y 0.000000\n' + + '# tilt_z 0.000000\n' + + '# y_center %.6f\n' % yc_p + + '# y_size %.6f\n' % mmPerPixel*1.e3 + + '# z_center %.6f\n' % zc_p + + '# z_size %.6f\n' % mmPerPixel*1.e3 + + '# ds h k l\n' + + gvecHKLString + + '# xr yr zr xc yc ds eta omega\n' + + gvecString, file=fid + ) fid.close() ############################################################### @@ -1587,19 +1718,23 @@ def writeGVE(spotsArray, fileroot, **kwargs): fid = open(fileroot+'_grainSpotter.ini', 'w') # self.__tempFNameList.append(fileroot) - print >> fid, 'spacegroup %d\n' % (sgNum) \ - + 'tthrange %g %g\n' % (tThMin, tThMax) \ - + 'etarange %g %g\n' % (etaMin, etaMax) \ - + 'domega %g\n' % (deltaOme) \ - + omeRangeString + \ - + 'filespecs %s.gve %s_grainSpotter.log\n' % (fileroot, fileroot) \ - + 'cuts %d %g %g\n' % (minMeas, minCompl, minUniqn) \ - + 'eulerstep %g\n' % (eulStep) \ - + 'uncertainties %g %g %g\n' \ - % (uncertainty[0], uncertainty[1], uncertainty[2]) \ - + 'nsigmas %d\n' % (nSigmas) \ - + 'minfracg %g\n' % (minFracG) \ - + randomString \ - + positionString + '\n' + print( + 'spacegroup %d\n' % (sgNum) + + 'tthrange %g %g\n' % (tThMin, tThMax) + + 'etarange %g %g\n' % (etaMin, etaMax) + + 'domega %g\n' % (deltaOme) + + omeRangeString + + 'filespecs %s.gve %s_grainSpotter.log\n' % (fileroot, fileroot) + + 'cuts %d %g %g\n' % (minMeas, minCompl, minUniqn) + + 'eulerstep %g\n' % (eulStep) + + 'uncertainties %g %g %g\n' % (uncertainty[0], + uncertainty[1], + uncertainty[2]) + + 'nsigmas %d\n' % (nSigmas) + + 'minfracg %g\n' % (minFracG) + + randomString + + positionString + + '\n', file=fid + ) fid.close() return diff --git a/hexrd/xrd/xrdutil.py b/hexrd/xrd/xrdutil.py index 4799d4cb..cf4ebd32 100644 --- a/hexrd/xrd/xrdutil.py +++ b/hexrd/xrd/xrdutil.py @@ -11,9 +11,9 @@ # # Please also see the file LICENSE. # -# This program is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License (as published by the Free Software -# Foundation) version 2.1 dated February 1999. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License (as published by the Free +# Software Foundation) version 2.1 dated February 1999. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF MERCHANTABILITY @@ -40,11 +40,10 @@ from scipy import sparse from scipy.linalg import svd from scipy import ndimage -import scipy.optimize as opt import matplotlib -from matplotlib.widgets import Slider, Button, RadioButtons -from matplotlib import cm, colors +from matplotlib.widgets import Slider +from matplotlib import cm from matplotlib import collections from hexrd import constants @@ -53,15 +52,13 @@ from hexrd import matrixutil as mutil from hexrd import pfigutil from hexrd import gridutil as gutil -from hexrd.valunits import toFloat, valWUnit +from hexrd.valunits import valWUnit from hexrd import USE_NUMBA import hexrd.orientations as ors from hexrd.xrd import crystallography from hexrd.xrd.crystallography import latticeParameters, latticeVectors, processWavelength -from hexrd.constants import keVToAngstrom - from hexrd.xrd import detector from hexrd.xrd.detector import Framer2DRC, getCMap @@ -1109,7 +1106,7 @@ def __display(self, omeEdges, etaEdges, data, nVecs, nP, opacity, rangeVV_w): 'for colorbar, make a mappable so that the range shown is correct' mappable = cm.ScalarMappable(cmap=self.cmap, norm=norm) mappable.set_array(vals) - forColorBar = mappable + #forColorBar = mappable else: pfigR = pfigutil.renderEAProj(nVecsN, vals[northern], nP) @@ -1126,7 +1123,7 @@ def __display(self, omeEdges, etaEdges, data, nVecs, nP, opacity, rangeVV_w): # if opacity is not None: # raise RuntimeError, 'not coded: opacity for non-rendered pole figure, specify integer-valued nP' conn = makeMNConn(len(omeEdges), len(etaEdges), tri=False) - nQuads = conn.shape[1] + #nQuads = conn.shape[1] #nVecsPatches = num.empty([nQuads, 4, 3]) #verts = num.empty([nQuads, 4, 2]) #vals = num.minimum(num.maximum(data[:,:].flatten(), vmin), vmax)# handled with set_clim @@ -3632,7 +3629,7 @@ def _project_on_detector_plane(allAngs, det_xy = distortion[0](det_xy, distortion[1], invert=True) - return det_xy, rMat_ss + return det_xy, rMat_ss, valid_mask def simulateGVecs(pd, detector_params, grain_params, @@ -3705,7 +3702,7 @@ def simulateGVecs(pd, detector_params, grain_params, ang_ps = [] else: #...preallocate for speed...? - det_xy, rMat_s = _project_on_detector_plane( + det_xy, rMat_s, on_plane = _project_on_detector_plane( allAngs, rMat_d, rMat_c, chi, tVec_d, tVec_c, tVec_s, @@ -4147,7 +4144,7 @@ def make_reflection_patches(instr_cfg, ]).T ) - xy_eval_vtx, _ = _project_on_detector_plane( + xy_eval_vtx, rmats_s, on_plane = _project_on_detector_plane( gVec_angs_vtx, rmat_d, rmat_c, chi, @@ -4169,7 +4166,7 @@ def make_reflection_patches(instr_cfg, num.tile(angs[2], (len(tth_eta_cen), 1))] ) - xy_eval, _ = _project_on_detector_plane( + xy_eval, rmats_s, on_plane = _project_on_detector_plane( gVec_angs, rmat_d, rmat_c, chi, From 06ae56ed370eeb5a27553a9da791924b8ad9823a Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 28 May 2020 16:45:05 -0700 Subject: [PATCH 240/253] fixed logger typo --- hexrd/findorientations.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index d8cc87ef..13bab422 100755 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -432,14 +432,17 @@ def load_eta_ome_maps(cfg, pd, image_series, hkls=None, clean=False): + "and clean option specified; " + "recomputing eta/ome orientation maps", fn) - return generate_eta_ome_maps(cfg, pd, image_series, hkls) + return generate_eta_ome_maps(cfg, hkls=hkls) else: logger.info('clean option specified; ' + 'recomputing eta/ome orientation maps') - return generate_eta_ome_maps(cfg, pd, image_series, hkls) + return generate_eta_ome_maps(cfg, hkls=hkls) def generate_eta_ome_maps(cfg, hkls=None): + """ + Generates the eta-omega maps specified in the input config. + """ # extract PlaneData from config and set active hkls plane_data = cfg.material.plane_data @@ -748,7 +751,7 @@ def find_orientations(cfg, logger.info("\t\t...took %f seconds", (timeit.default_timer() - start)) logger.info("\tfound %d grains; saved to file: '%s'", - (qbar.shape[1], qbar_filename)) + qbar.shape[1], qbar_filename) np.savetxt(qbar_filename, qbar.T, fmt='%.18e', delimiter='\t') From 3d4801cd03b0998f27bb9fd34aa7ce0e2cb3cfb0 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 1 Jun 2020 13:23:20 -0700 Subject: [PATCH 241/253] fit-grains CLI implementation --- hexrd/cli/findorientations.py | 24 +- hexrd/cli/fitgrains.py | 12 +- hexrd/config/fitgrains.py | 2 +- hexrd/findorientations.py | 37 +- hexrd/fitgrains.py | 889 ++++++++++++++++------------------ hexrd/instrument.py | 2 +- 6 files changed, 466 insertions(+), 500 deletions(-) diff --git a/hexrd/cli/findorientations.py b/hexrd/cli/findorientations.py index c528e380..b40e1d40 100644 --- a/hexrd/cli/findorientations.py +++ b/hexrd/cli/findorientations.py @@ -1,7 +1,7 @@ from __future__ import print_function, division, absolute_import -descr = 'Process diffraction data to find grain orientations' +descr = 'Process rotation image series to find grain orientations' example = """ examples: hexrd find-orientations configuration.yml @@ -79,21 +79,16 @@ def execute(args, parser): # load the configuration settings cfg = config.open(args.yml)[0] - # ...make this an attribute in cfg? - analysis_id = '%s_%s' %( - cfg.analysis_name.strip().replace(' ', '-'), - cfg.material.active.strip().replace(' ', '-'), - ) - # prepare the analysis directory quats_f = os.path.join( cfg.working_dir, - 'accepted_orientations_%s.dat' %analysis_id + 'accepted_orientations_%s.dat' % cfg.analysis_id ) if os.path.exists(quats_f) and not (args.force or args.clean): logger.error( - '%s already exists. Change yml file or specify "force" or "clean"', quats_f - ) + '%s already exists. Change yml file or specify "force" or "clean"', + quats_f + ) sys.exit() if not os.path.exists(cfg.working_dir): os.makedirs(cfg.working_dir) @@ -101,7 +96,7 @@ def execute(args, parser): # configure logging to file logfile = os.path.join( cfg.working_dir, - 'find-orientations_%s.log' %analysis_id + 'find-orientations_%s.log' % cfg.analysis_id ) fh = logging.FileHandler(logfile, mode='w') fh.setLevel(log_level) @@ -120,7 +115,12 @@ def execute(args, parser): pr.enable() # process the data - find_orientations(cfg, hkls=args.hkls, clean=args.clean, profile=args.profile) + find_orientations( + cfg, + hkls=args.hkls, + clean=args.clean, + profile=args.profile + ) if args.profile: pr.disable() diff --git a/hexrd/cli/fitgrains.py b/hexrd/cli/fitgrains.py index b011a3f0..fef1a134 100644 --- a/hexrd/cli/fitgrains.py +++ b/hexrd/cli/fitgrains.py @@ -24,11 +24,11 @@ def configure_parser(sub_parsers): ) p.add_argument( '-c', '--clean', action='store_true', - help='overwrites existing analysis, including frame cache' + help='overwrites existing analysis, uses initial orientations' ) p.add_argument( '-f', '--force', action='store_true', - help='overwrites existing analysis, exlcuding frame cache' + help='overwrites existing analysis' ) p.add_argument( '-p', '--profile', action='store_true', @@ -62,17 +62,11 @@ def execute(args, parser): cf = logging.Formatter('%(asctime)s - %(message)s', '%y-%m-%d %H:%M:%S') ch.setFormatter(cf) logger.addHandler(ch) - - # ...make this an attribute in cfg? - analysis_id = '%s_%s' %( - cfgs[0].analysis_name.strip().replace(' ', '-'), - cfgs[0].material.active.strip().replace(' ', '-'), - ) # if find-orientations has not already been run, do so: quats_f = os.path.join( cfgs[0].working_dir, - 'accepted_orientations_%s.dat' %analysis_id + 'accepted_orientations_%s.dat' % cfgs[0].analysis_id ) if not os.path.exists(quats_f): logger.info("Missing %s, running find-orientations", quats_f) diff --git a/hexrd/config/fitgrains.py b/hexrd/config/fitgrains.py index 0a3dec26..3322119c 100644 --- a/hexrd/config/fitgrains.py +++ b/hexrd/config/fitgrains.py @@ -116,7 +116,7 @@ def fit_only(self): def tth_max(self): key = 'fit_grains:tth_max' temp = self._cfg.get(key, True) - if temp in (True, False): + if isinstance(temp, bool): return temp if isinstance(temp, (int, float)): if temp > 0: diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 13bab422..53b75478 100755 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -8,6 +8,8 @@ import numpy as np # np.seterr(over='ignore', invalid='ignore') +import tqdm + import scipy.cluster as cluster from scipy import ndimage from skimage.feature import blob_dog, blob_log @@ -62,7 +64,7 @@ def generate_orientation_fibers(cfg, eta_ome): # default values for each case? They must be specified as of now. method = next(method_dict.iterkeys()) method_kwargs = method_dict[method] - logger.info('using "%s" method for fiber generation' + logger.info('\tusing "%s" method for fiber generation' % method) # seed_hkl_ids must be consistent with this... @@ -163,17 +165,32 @@ def generate_orientation_fibers(cfg, eta_ome): if ncpus > 1: # multiple process version # ???: Need a chunksize in map? + chunksize = max(1, len(input_p)//(10*ncpus)) pool = mp.Pool(ncpus, discretefiber_init, (params, )) - qfib = pool.map(discretefiber_reduced, input_p) + qfib = pool.map( + discretefiber_reduced, input_p, + chunksize=chunksize + ) + ''' + # This is an experiment... + ntotal= 10*ncpus + np.remainder(len(input_p), 10*ncpus) > 0 + for _ in tqdm.tqdm( + pool.imap_unordered( + discretefiber_reduced, input_p, chunksize=chunksize + ), total=ntotal + ): + pass + print(_.shape) + ''' pool.close() + pool.join() else: # single process version. - global paramMP discretefiber_init(params) # sets paramMP qfib = map(discretefiber_reduced, input_p) - paramMP = None # clear paramMP + discretefiber_cleanup() elapsed = (timeit.default_timer() - start) - logger.info("fiber generation took %.3f seconds", elapsed) + logger.info("\tfiber generation took %.3f seconds", elapsed) return np.hstack(qfib) @@ -181,6 +198,11 @@ def discretefiber_init(params): global paramMP paramMP = params + +def discretefiber_cleanup(): + global paramMP + del paramMP + def discretefiber_reduced(params_in): """ @@ -587,6 +609,7 @@ def find_orientations(cfg, # handle search space if cfg.find_orientations.use_quaternion_grid is None: # doing seeded search + logger.info("Will perform seeded search") logger.info( "\tgenerating search quaternion list using %d processes", ncpus @@ -653,7 +676,7 @@ def find_orientations(cfg, # do map-based indexing start = timeit.default_timer() - logger.info(" will test %d quaternions using %d processes", + logger.info("will test %d quaternions using %d processes", qfib.shape[1], ncpus) completeness = indexer.paintGrid( @@ -740,8 +763,6 @@ def find_orientations(cfg, logger.info("\tmean reflections per grain: %d", mean_rpg) logger.info("\tneighborhood size: %d", min_samples) - logger.info("\tFeeding %d orientations above %.1f%% to clustering", - sum(completeness > compl_thresh), compl_thresh) qbar, cl = run_cluster( completeness, qfib, plane_data.getQSym(), cfg, diff --git a/hexrd/fitgrains.py b/hexrd/fitgrains.py index ec1a8331..f26f6230 100644 --- a/hexrd/fitgrains.py +++ b/hexrd/fitgrains.py @@ -1,498 +1,449 @@ -from __future__ import absolute_import +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Wed Mar 22 19:04:10 2017 + +@author: bernier2 +""" +from __future__ import print_function, absolute_import -import copy -import logging -import multiprocessing as mp -from multiprocessing.queues import Empty import os +import logging +import multiprocessing +import numpy as np +import timeit import sys -import time - import yaml -import numpy as np -from scipy.sparse import coo_matrix -from scipy.linalg.matfuncs import logm +from hexrd import config +from hexrd import constants as cnst +from hexrd import instrument +from hexrd.xrd import transforms_CAPI as xfcapi -from hexrd.coreutil import ( - initialize_experiment, migrate_detector_to_instrument_config, - get_instrument_parameters, get_detector_parameters, get_detector_parameters, - get_distortion_correction, get_saturation_level, set_planedata_exclusions - ) -from hexrd.matrixutil import vecMVToSymm -from hexrd.utils.progressbar import ( - Bar, ETA, Percentage, ProgressBar, ReverseBar - ) +logger = logging.getLogger(__name__) -from hexrd.xrd import distortion as dFuncs -from hexrd.xrd.fitting import fitGrain, objFuncFitGrain -from hexrd.xrd.rotations import angleAxisOfRotMat, rotMatOfQuat -from hexrd.xrd.transforms import bVec_ref, eta_ref, mapAngle, vInv_ref, angularDifference -from hexrd.xrd.xrdutil import pullSpots -from .cacheframes import get_frames -from hexrd import USE_NUMBA -if USE_NUMBA: - import numba +# multiprocessing fit funcs -logger = logging.getLogger(__name__) +def fit_grain_FF_init(params): + """ + Broadcast the fitting parameters as globals for multiprocessing -# grain parameter refinement flags -gFlag = np.array([1, 1, 1, - 1, 1, 1, - 1, 1, 1, 1, 1, 1], dtype=bool) -# grain parameter scalings -gScl = np.array([1., 1., 1., - 1., 1., 1., - 1., 1., 1., 0.01, 0.01, 0.01]) - - -def get_job_queue(cfg, ids_to_refine=None): - job_queue = mp.JoinableQueue() - # load the queue - try: - # use an estimate of the grain parameters, if available - estimate_f = cfg.fit_grains.estimate - grain_params_list = np.atleast_2d(np.loadtxt(estimate_f)) - n_quats = len(grain_params_list) - n_jobs = 0 - for grain_params in grain_params_list: - grain_id = grain_params[0] - if ids_to_refine is None or grain_id in ids_to_refine: - job_queue.put((grain_id, grain_params[3:15])) - n_jobs += 1 - logger.info( - 'fitting grains using "%s" for the initial estimate', - estimate_f - ) - except (ValueError, IOError): - # no estimate available, use orientations and defaults - logger.info('fitting grains using default initial estimate') - - # ...make this an attribute in cfg? - analysis_id = '%s_%s' %( - cfg.analysis_name.strip().replace(' ', '-'), - cfg.material.active.strip().replace(' ', '-'), - ) - - # load quaternion file - quats = np.atleast_2d( - np.loadtxt( - os.path.join( - cfg.working_dir, - 'accepted_orientations_%s.dat' %analysis_id - ) - ) - ) - n_quats = len(quats) - n_jobs = 0 - phi, n = angleAxisOfRotMat(rotMatOfQuat(quats.T)) - for i, (phi, n) in enumerate(zip(phi, n.T)): - if ids_to_refine is None or i in ids_to_refine: - exp_map = phi*n - grain_params = np.hstack( - [exp_map, 0., 0., 0., 1., 1., 1., 0., 0., 0.] - ) - job_queue.put((i, grain_params)) - n_jobs += 1 - logger.info("fitting grains for %d of %d orientations", n_jobs, n_quats) - return job_queue, n_jobs - - -def get_data(cfg, show_progress=False, force=False, clean=False): - # TODO: this should be refactored somehow to avoid initialize_experiment - # and avoid using the old reader. Also, the detector is not used here. - pd, reader, detector = initialize_experiment(cfg) - if cfg.fit_grains.fit_only: - reader = None - else: - reader = get_frames(reader, cfg, show_progress, force, clean) - - instrument_cfg = get_instrument_parameters(cfg) - detector_params = get_detector_parameters(instrument_cfg) - saturation_level = get_saturation_level(instrument_cfg) - distortion = get_distortion_correction(instrument_cfg) - set_planedata_exclusions(cfg, detector, pd) - # HANDLE OMEGA STOP - if cfg.image_series.omega.stop is None: - assert cfg.image_series.images.stop is not None, \ - "Must specify stop point, either in omega or image" - omega_stop = cfg.image_series.omega.start + \ - cfg.image_series.omega.step*cfg.image_series.images.stop - else: - omega_stop = cfg.image_series.omega.stop - pkwargs = { - 'detector_params': detector_params, - 'distortion': distortion, - 'eta_range': np.radians(cfg.find_orientations.eta.range), - 'eta_tol': cfg.fit_grains.tolerance.eta, - 'fit_only': cfg.fit_grains.fit_only, - 'ncols': instrument_cfg['detector']['pixels']['columns'], - 'npdiv': cfg.fit_grains.npdiv, - 'nrows': instrument_cfg['detector']['pixels']['rows'], - 'omega_period': np.radians(cfg.find_orientations.omega.period), - 'omega_start': cfg.image_series.omega.start, - 'omega_step': cfg.image_series.omega.step, - 'omega_stop': omega_stop, - 'omega_tol': cfg.fit_grains.tolerance.omega, - 'overlap_table': os.path.join(cfg.analysis_dir, 'overlap_table.npz'), - 'panel_buffer': cfg.fit_grains.panel_buffer, - 'pixel_pitch': instrument_cfg['detector']['pixels']['size'], - 'plane_data': pd, - 'refit_tol': cfg.fit_grains.refit, - 'saturation_level': saturation_level, - 'spots_stem': os.path.join(cfg.analysis_dir, 'spots_%05d.out'), - 'threshold': cfg.fit_grains.threshold, - 'tth_tol': cfg.fit_grains.tolerance.tth, - } - return reader, pkwargs - - -def fit_grains(cfg, force=False, clean=False, show_progress=False, ids_to_refine=None): - # load the data - reader, pkwargs = get_data(cfg, show_progress, force, clean) - job_queue, njobs = get_job_queue(cfg, ids_to_refine) - - # log this before starting progress bar - ncpus = cfg.multiprocessing - ncpus = ncpus if ncpus < njobs else njobs - logger.info( - 'will use %d of %d processors', ncpus, mp.cpu_count() - ) - if ncpus == 1: - logger.info('multiprocessing disabled') - - # echo some of the fitting options - if cfg.fit_grains.fit_only: - logger.info('\t**fitting only; will not pull spots') - if cfg.fit_grains.refit is not None: - msg = 'will perform refit excluding spots > ' + \ - '%.2f pixels and ' %cfg.fit_grains.refit[0] + \ - '%.2f frames from expected values' %cfg.fit_grains.refit[1] - logger.info(msg) + Parameters + ---------- + params : dict + The dictionary of fitting parameters. + + Returns + ------- + None. - start = time.time() - pbar = None - if show_progress: - pbar = ProgressBar( - widgets=[Bar('>'), ' ', ETA(), ' ', ReverseBar('<')], - maxval=njobs - ).start() - - # finally start processing data - if ncpus == 1: - # no multiprocessing - results = [] - w = FitGrainsWorker( - job_queue, results, reader, copy.deepcopy(pkwargs), - progressbar=pbar - ) - w.run() - else: - # multiprocessing - manager = mp.Manager() - results = manager.list() - for i in range(ncpus): - # lets make a deep copy of the pkwargs, just in case: - w = FitGrainsWorkerMP(job_queue, results, reader, copy.deepcopy(pkwargs)) - w.daemon = True - w.start() - while True: - n_res = len(results) - if show_progress: - pbar.update(n_res) - if n_res == njobs: - break - time.sleep(0.1) - job_queue.join() - - write_grains_file(cfg, results) - - if show_progress: - pbar.finish() - elapsed = time.time() - start - logger.info('processed %d grains in %g minutes', n_res, elapsed/60) - - -def write_grains_file(cfg, results, output_name=None): - # record the results to file - if output_name is None: - f = open(os.path.join(cfg.analysis_dir, 'grains.out'), 'w') - else: - f = open(os.path.join(cfg.analysis_dir, output_name), 'w') - # going to some length to make the header line up with the data - # while also keeping the width of the lines to a minimum, settled - # on %19.12g representation. - header_items = ( - 'grain ID', 'completeness', 'chi2', - 'xi[0]', 'xi[1]', 'xi[2]', 'tVec_c[0]', 'tVec_c[1]', 'tVec_c[2]', - 'vInv_s[0]', 'vInv_s[1]', 'vInv_s[2]', 'vInv_s[4]*sqrt(2)', - 'vInv_s[5]*sqrt(2)', 'vInv_s[6]*sqrt(2)', 'ln(V[0,0])', - 'ln(V[1,1])', 'ln(V[2,2])', 'ln(V[1,2])', 'ln(V[0,2])', 'ln(V[0,1])', - ) - len_items = [] - for i in header_items[1:]: - temp = len(i) - len_items.append(temp if temp > 19 else 19) # for %19.12g - fmtstr = '#%13s ' + ' '.join(['%%%ds' % i for i in len_items]) + '\n' - f.write(fmtstr % header_items) - for (id, g_refined, compl, eMat, resd) in sorted(results): - res_items = ( - id, compl, resd, g_refined[0], g_refined[1], g_refined[2], - g_refined[3], g_refined[4], g_refined[5], g_refined[6], - g_refined[7], g_refined[8], g_refined[9], g_refined[10], - g_refined[11], eMat[0, 0], eMat[1, 1], eMat[2, 2], eMat[1, 2], - eMat[0, 2], eMat[0, 1], - ) - fmtstr = ( - '%14d ' + ' '.join(['%%%d.12g' % i for i in len_items]) + '\n' - ) - f.write(fmtstr % res_items) - - - -class FitGrainsWorker(object): - - - def __init__(self, jobs, results, reader, pkwargs, **kwargs): - self._jobs = jobs - self._results = results - self._reader = reader - # a dict containing the rest of the parameters - self._p = pkwargs - - # lets make a couple shortcuts: - self._p['bMat'] = np.ascontiguousarray( - self._p['plane_data'].latVecOps['B'] - ) # is it still necessary to re-cast? - self._p['wlen'] = self._p['plane_data'].wavelength - self._pbar = kwargs.get('progressbar', None) - - - def pull_spots(self, grain_id, grain_params, iteration): - # need to calc panel dims on the fly - xdim = self._p['pixel_pitch'][1] * self._p['ncols'] - ydim = self._p['pixel_pitch'][0] * self._p['nrows'] - panel_dims = [(-0.5*xdim, -0.5*ydim), - ( 0.5*xdim, 0.5*ydim)] - return pullSpots( - self._p['plane_data'], - self._p['detector_params'], - grain_params, - self._reader, - distortion=self._p['distortion'], - eta_range=self._p['eta_range'], - ome_period=self._p['omega_period'], - eta_tol=self._p['eta_tol'][iteration], - ome_tol=self._p['omega_tol'][iteration], - tth_tol=self._p['tth_tol'][iteration], - pixel_pitch=self._p['pixel_pitch'], - panel_dims=panel_dims, - panel_buff=self._p['panel_buffer'], - npdiv=self._p['npdiv'], - threshold=self._p['threshold'], - doClipping=False, - filename=self._p['spots_stem'] % grain_id, - ) + Notes + ----- + See fit_grain_FF_reduced for specification. + """ + global paramMP + paramMP = params + + +def fit_grain_FF_cleanup(): + """ + Tears down the global fitting parameters. + """ + global paramMP + del paramMP + + +def fit_grain_FF_reduced(grain_id): + """ + Perform non-linear least-square fit for the specified grain. + + Parameters + ---------- + grain_id : int + The grain id. + + Returns + ------- + grain_id : int + The grain id. + completeness : float + The ratio of predicted to measured (observed) Bragg reflections. + chisq: float + Figure of merit describing the sum of squared residuals for each Bragg + reflection in the form (x, y, omega) normalized by the total number of + degrees of freedom. + grain_params : array_like + The optimized grain parameters + [, ]. + + Notes + ----- + input parameters are + [plane_data, instrument, imgser_dict, + tth_tol, eta_tol, ome_tol, npdiv, threshold] + """ + grains_table = paramMP['grains_table'] + plane_data = paramMP['plane_data'] + instrument = paramMP['instrument'] + imgser_dict = paramMP['imgser_dict'] + tth_tol = paramMP['tth_tol'] + eta_tol = paramMP['eta_tol'] + ome_tol = paramMP['ome_tol'] + npdiv = paramMP['npdiv'] + refit = paramMP['refit'] + threshold = paramMP['threshold'] + eta_ranges = paramMP['eta_ranges'] + ome_period = paramMP['ome_period'] + analysis_dirname = paramMP['analysis_dirname'] + spots_filename = paramMP['spots_filename'] + + grain = grains_table[grain_id] + grain_params = grain[3:15] + + for tols in zip(tth_tol, eta_tol, ome_tol): + complvec, results = instrument.pull_spots( + plane_data, grain_params, + imgser_dict, + tth_tol=tols[0], + eta_tol=tols[1], + ome_tol=tols[2], + npdiv=npdiv, threshold=threshold, + eta_ranges=eta_ranges, + ome_period=ome_period, + dirname=analysis_dirname, filename=spots_filename % grain_id, + save_spot_list=False, + quiet=True, check_only=False, interp='nearest') + + # ======= DETERMINE VALID REFLECTIONS ======= + + culled_results = dict.fromkeys(results) + num_refl_tot = 0 + num_refl_valid = 0 + for det_key in culled_results: + panel = instrument.detectors[det_key] + + presults = results[det_key] + + valid_refl_ids = np.array([x[0] for x in presults]) >= 0 + + spot_ids = np.array([x[0] for x in presults]) + + # find unsaturated spots on this panel + if panel.saturation_level is None: + unsat_spots = np.ones(len(valid_refl_ids)) + else: + unsat_spots = \ + np.array([x[4] for x in presults]) < panel.saturation_level + idx = np.logical_and(valid_refl_ids, unsat_spots) - def fit_grains(self, grain_id, grain_params, refit_tol=None): - """ - Executes lsq fits of grains based on spot files - - REFLECTION TABLE - - Cols as follows: - 0-6: ID PID H K L sum(int) max(int) - 6-9: pred tth pred eta pred ome - 9-12: meas tth meas eta meas ome - 12-15: meas X meas Y meas ome - """ - ome_start = self._p['omega_start'] - ome_step = self._p['omega_step'] - ome_stop = self._p['omega_stop'] - refl_table = np.loadtxt(self._p['spots_stem'] % grain_id) - valid_refl_ids = refl_table[:, 0] >= 0 - unsat_spots = refl_table[:, 6] < self._p['saturation_level'] - pred_ome = refl_table[:, 9] - if angularDifference(ome_start, ome_stop, units='degrees') > 0: - # if here, incomplete have omega range and - # clip the refelctions very close to the edges to avoid - # problems with the least squares... - if np.sign(ome_step) < 0: - idx_ome = np.logical_and( - pred_ome < np.radians(ome_start + 2*ome_step), - pred_ome > np.radians(ome_stop - 2*ome_step) - ) - else: - idx_ome = np.logical_and( - pred_ome > np.radians(ome_start + 2*ome_step), - pred_ome < np.radians(ome_stop - 2*ome_step) + # if an overlap table has been written, load it and use it + overlaps = np.zeros_like(idx, dtype=bool) + try: + ot = np.load( + os.path.join( + analysis_dirname, os.path.join( + det_key, 'overlap_table.npz' + ) ) - idx = np.logical_and( - valid_refl_ids, - np.logical_and(unsat_spots, idx_ome) ) + for key in ot.keys(): + for this_table in ot[key]: + these_overlaps = np.where( + this_table[:, 0] == grain_id)[0] + if len(these_overlaps) > 0: + mark_these = np.array( + this_table[these_overlaps, 1], dtype=int + ) + otidx = [ + np.where(spot_ids == mt)[0] + for mt in mark_these + ] + overlaps[otidx] = True + idx = np.logical_and(idx, ~overlaps) + # print("found overlap table for '%s'" % det_key) + except(IOError, IndexError): + # print("no overlap table found for '%s'" % det_key) + pass + + # attach to proper dict entry + culled_results[det_key] = [presults[i] for i in np.where(idx)[0]] + num_refl_tot += len(valid_refl_ids) + num_refl_valid += sum(valid_refl_ids) + + pass # now we have culled data + + # CAVEAT: completeness from pullspots only; incl saturated and overlaps + # + completeness = num_refl_valid / float(num_refl_tot) + + # ======= DO LEASTSQ FIT ======= + + if num_refl_valid <= 12: # not enough reflections to fit... exit + return grain_id, completeness, np.inf, grain_params else: - idx = np.logical_and(valid_refl_ids, unsat_spots) - pass # end if edge case + grain_params = fitGrain( + grain_params, instrument, culled_results, + plane_data.latVecOps['B'], plane_data.wavelength + ) + # get chisq + # TODO: do this while evaluating fit??? + chisq = objFuncFitGrain( + grain_params[gFlag_ref], grain_params, gFlag_ref, + instrument, + culled_results, + plane_data.latVecOps['B'], plane_data.wavelength, + ome_period, + simOnly=False, return_value_flag=2) + pass # end conditional on fit + pass # end tolerance looping + + if refit is not None: + # first get calculated x, y, ome from previous solution + # NOTE: this result is a dict + xyo_det_fit_dict = objFuncFitGrain( + grain_params[gFlag_ref], grain_params, gFlag_ref, + instrument, + culled_results, + plane_data.latVecOps['B'], plane_data.wavelength, + ome_period, + simOnly=True, return_value_flag=2) + + # make dict to contain new culled results + culled_results_r = dict.fromkeys(culled_results) + num_refl_valid = 0 + for det_key in culled_results_r: + presults = culled_results[det_key] + + ims = imgser_dict[det_key] + ome_step = sum(np.r_[-1, 1]*ims.metadata['omega'][0, :]) + + xyo_det = np.atleast_2d( + np.vstack([np.r_[x[7], x[6][-1]] for x in presults]) + ) - # if an overlap table has been written, load it and use it - overlaps = np.zeros(len(refl_table), dtype=bool) - try: - ot = np.load(self._p['overlap_table']) - for key in ot.keys(): - for this_table in ot[key]: - these_overlaps = np.where( - this_table[:, 0] == grain_id)[0] - if len(these_overlaps) > 0: - mark_these = np.array(this_table[these_overlaps, 1], dtype=int) - overlaps[mark_these] = True - idx = np.logical_and(idx, ~overlaps) - except IOError, IndexError: - #print "no overlap table found" - pass - - # completeness from pullspots only; incl saturated and overlaps - completeness = sum(valid_refl_ids)/float(len(valid_refl_ids)) + xyo_det_fit = xyo_det_fit_dict[det_key] - # extract data from grain table - hkls = refl_table[idx, 2:5].T # must be column vectors - xyo_det = refl_table[idx, -3:] # these are the cartesian centroids + ome + xpix_tol = refit[0]*panel.pixel_size_col + ypix_tol = refit[0]*panel.pixel_size_row + fome_tol = refit[1]*ome_step - # set in parameter attribute - self._p['hkls'] = hkls - self._p['xyo_det'] = xyo_det - - if sum(idx) <= 12: # not enough reflections to fit... exit - completeness = 0. - else: - grain_params = fitGrain( - xyo_det, hkls, self._p['bMat'], self._p['wlen'], - self._p['detector_params'], - grain_params[:3], grain_params[3:6], grain_params[6:], - beamVec=bVec_ref, etaVec=eta_ref, - distortion=self._p['distortion'], - gFlag=gFlag, gScl=gScl, - omePeriod=self._p['omega_period'] + # define difference vectors for spot fits + x_diff = abs(xyo_det[:, 0] - xyo_det_fit['calc_xy'][:, 0]) + y_diff = abs(xyo_det[:, 1] - xyo_det_fit['calc_xy'][:, 1]) + ome_diff = np.degrees( + xfcapi.angularDifference(xyo_det[:, 2], + xyo_det_fit['calc_omes']) ) - if refit_tol is not None: - xpix_tol = refit_tol[0]*self._p['pixel_pitch'][1] - ypix_tol = refit_tol[0]*self._p['pixel_pitch'][0] - fome_tol = refit_tol[1]*self._p['omega_step'] - - xyo_det_fit = objFuncFitGrain( - grain_params[gFlag], grain_params, gFlag, - self._p['detector_params'], - xyo_det, hkls, self._p['bMat'], self._p['wlen'], - bVec_ref, eta_ref, - self._p['distortion'][0], self._p['distortion'][1], - self._p['omega_period'], simOnly=True - ) - # define difference vectors for spot fits - x_diff = abs(xyo_det[:, 0] - xyo_det_fit[:, 0]) - y_diff = abs(xyo_det[:, 1] - xyo_det_fit[:, 1]) - ome_diff = np.degrees( - angularDifference(xyo_det[:, 2], xyo_det_fit[:, 2]) - ) + # filter out reflections with centroids more than + # a pixel and delta omega away from predicted value + idx_new = np.logical_and( + x_diff <= xpix_tol, + np.logical_and(y_diff <= ypix_tol, + ome_diff <= fome_tol) + ) - # filter out reflections with centroids more than - # a pixel and delta omega away from predicted value - idx_1 = np.logical_and( - x_diff <= xpix_tol, - np.logical_and(y_diff <= ypix_tol, - ome_diff <= fome_tol) - ) - idx_new = np.zeros_like(idx, dtype=bool) - idx_new[np.where(idx == 1)[0][idx_1]] = True - - if sum(idx_new) > 12 and (sum(idx_new) > 0.5*sum(idx)): - # have enough reflections left - # ** the check that we have more than half of what - # we started with is a hueristic - hkls = refl_table[idx_new, 2:5].T - xyo_det = refl_table[idx_new, -3:] - - # set in parameter attribute - self._p['hkls'] = hkls - self._p['xyo_det'] = xyo_det - - # do fit - grain_params = fitGrain( - xyo_det, hkls, - self._p['bMat'], self._p['wlen'], - self._p['detector_params'], - grain_params[:3], grain_params[3:6], grain_params[6:], - beamVec=bVec_ref, etaVec=eta_ref, - distortion=self._p['distortion'], - gFlag=gFlag, gScl=gScl, - omePeriod=self._p['omega_period'] - ) - pass # end check on num of refit refls - pass # end refit loop - pass # end on num of refls - return grain_params, completeness - + # attach to proper dict entry + culled_results_r[det_key] = [ + presults[i] for i in np.where(idx_new)[0] + ] - def get_e_mat(self, grain_params): - """ - strain tensor calculation - """ - return logm(np.linalg.inv(vecMVToSymm(grain_params[6:]))) - - - def get_residuals(self, grain_params): - dFunc, dParams = self._p['distortion'] - return objFuncFitGrain( - grain_params[gFlag], grain_params, gFlag, - self._p['detector_params'], - self._p['xyo_det'], self._p['hkls'], - self._p['bMat'], self._p['wlen'], - bVec_ref, eta_ref, - dFunc, dParams, - self._p['omega_period'], - simOnly=False, return_value_flag=2) - - def loop(self): - id, grain_params = self._jobs.get(False) - iterations = (0, len(self._p['eta_tol'])) - for iteration in range(*iterations): - # pull spots if asked to, otherwise just fit - if not self._p['fit_only']: - self.pull_spots(id, grain_params, iteration) - # FITTING HERE - grain_params, compl = self.fit_grains(id, grain_params, - refit_tol=self._p['refit_tol']) - if compl == 0: - break + num_refl_valid += sum(idx_new) pass - - # final pull spots if enabled - if not self._p['fit_only']: - self.pull_spots(id, grain_params, -1) - - eMat = self.get_e_mat(grain_params) - resd = self.get_residuals(grain_params) - - self._results.append((id, grain_params, compl, eMat, resd)) - self._jobs.task_done() + # only execute fit if left with enough reflections + if num_refl_valid > 12: + grain_params = fitGrain( + grain_params, instrument, culled_results_r, + plane_data.latVecOps['B'], plane_data.wavelength + ) + # get chisq + # TODO: do this while evaluating fit??? + chisq = objFuncFitGrain( + grain_params[gFlag_ref], + grain_params, gFlag_ref, + instrument, + culled_results_r, + plane_data.latVecOps['B'], plane_data.wavelength, + ome_period, + simOnly=False, return_value_flag=2) + pass + pass # close refit conditional + return grain_id, completeness, chisq, grain_params - def run(self): - n_res = 0 - while True: - try: - self.loop() - n_res += 1 - if self._pbar is not None: - self._pbar.update(n_res) - except Empty: - break +def fit_grains(cfg, + force=False, clean=False, + show_progress=False, ids_to_refine=None): + """ + Performs optimization of grain parameters. + operates on a single HEDM config block + """ + grains_filename = os.path.join( + cfg.analysis_dir, 'grains.out' + ) + + # grab imageseries dict + imsd = cfg.image_series + + # grab instrument + instr = cfg.instrument.hedm + + # process plane data + plane_data = cfg.material.plane_data + tth_max = cfg.fit_grains.tth_max + if isinstance(tth_max, bool): + if tth_max: + max_tth = instrument.max_tth(instr) + plane_data.tThMax = max_tth + logger.info("\tsetting the maximum 2theta to instrument" + + " maximum: %.2f degrees", + np.degrees(max_tth)) + else: + logger.info("\tnot adjusting exclusions in planeData") + else: + # a value for tth max has been specified + plane_data.exclusions = None + plane_data.tThMax = np.radians(tth_max) + logger.info("\tsetting the maximum 2theta to %.2f degrees", + tth_max) + + # make output directories + if not os.path.exists(cfg.analysis_dir): + os.mkdir(cfg.analysis_dir) + for det_key in instr.detectors: + os.mkdir(os.path.join(cfg.analysis_dir, det_key)) + else: + # make sure panel dirs exist under analysis dir + for det_key in instr.detectors: + if not os.path.exists(os.path.join(cfg.analysis_dir, det_key)): + os.mkdir(os.path.join(cfg.analysis_dir, det_key)) + + # grab eta ranges and ome_period + eta_ranges = np.radians(cfg.find_orientations.eta.range) -class FitGrainsWorkerMP(FitGrainsWorker, mp.Process): + # handle omega period + # !!! we assume all detector ims have the same ome ranges, so any will do! + oims = next(imsd.itervalues()) + ome_period = np.radians(oims.omega[0, 0] + np.r_[0., 360.]) + + # number of processes + ncpus = cfg.multiprocessing + + # threshold for fitting + threshold = cfg.fit_grains.threshold + + # some conditions for arg handling + existing_analysis = os.path.exists(grains_filename) + new_with_estimate = not existing_analysis and estimate is not None + new_without_estimate = not existing_analysis and estimate is None + force_with_estimate = force and cfg.fit_grains.estimate is not None + force_without_estimate = force and cfg.fit_grains.estimate is None + + # handle args + if clean or force_without_estimate or new_without_estimate: + # need accepted orientations from indexing in this case + if clean: + logger.info( + "'clean' specified; ignoring estimate and using default" + ) + elif force_without_estimate: + logger.info( + "'force' option specified, but no initial estimate; " + + "using default" + ) + try: + qbar = np.loadtxt( + 'accepted_orientations_' + cfg.analysis_id + '.dat', + ndmin=2).T + + gw = instrument.GrainDataWriter(grains_filename) + for i_g, q in enumerate(qbar.T): + phi = 2*np.arccos(q[0]) + n = xfcapi.unitRowVector(q[1:]) + grain_params = np.hstack( + [phi*n, cnst.zeros_3, cnst.identity_6x1] + ) + gw.dump_grain(int(i_g), 1., 0., grain_params) + gw.close() + except(IOError): + raise(RuntimeError, + "indexing results '%s' not found!" + % 'accepted_orientations_' + cfg.analysis_id + '.dat') + elif force_with_estimate or new_with_estimate: + grains_filename = cfg.fit_grains.estimate + elif existing_analysis and not (clean or force): + raise(RuntimeError, + "fit results '%s' exist, but --clean or --force options not specified" + % grains_filename) + + # load grains table + grains_table = np.loadtxt(grains_filename, ndmin=2) + if ids_to_refine is not None: + grains_table = np.atleast_2d(grains_table[ids_to_refine, :]) + spots_filename = "spots_%05d.out" + params = dict( + grains_table=grains_table, + plane_data=plane_data, + instrument=instr, + imgser_dict=imsd, + tth_tol=cfg.fit_grains.tolerance.tth, + eta_tol=cfg.fit_grains.tolerance.eta, + ome_tol=cfg.fit_grains.tolerance.omega, + npdiv=cfg.fit_grains.npdiv, + refit=cfg.fit_grains.refit, + threshold=threshold, + eta_ranges=eta_ranges, + ome_period=ome_period, + analysis_dirname=cfg.analysis_dir, + spots_filename=spots_filename) + + # ===================================================================== + # EXECUTE MP FIT + # ===================================================================== + + # DO FIT! + if len(grains_table) == 1 or ncpus == 1: + logger.info("\tstarting serial fit") + start = timeit.default_timer() + fit_grain_FF_init(params) + fit_results = map( + fit_grain_FF_reduced, + np.array(grains_table[:, 0], dtype=int) + ) + fit_grain_FF_cleanup() + elapsed = timeit.default_timer() - start + else: + nproc = min(ncpus, len(grains_table)) + chunksize = max(1, len(grains_table)//ncpus) + logger.info("\tstarting fit on %d processes", nproc) + start = timeit.default_timer() + pool = multiprocessing.Pool( + nproc, + fit_grain_FF_init, + (params, ) + ) + fit_results = pool.map( + fit_grain_FF_reduced, + np.array(grains_table[:, 0], dtype=int), + chunksize=chunksize + ) + pool.close() + pool.join() + elapsed = timeit.default_timer() - start + logger.info("fitting took %f seconds", elapsed) + + # ===================================================================== + # WRITE OUTPUT + # ===================================================================== + + gw = instrument.GrainDataWriter( + os.path.join(cfg.analysis_dir, 'grains.out') + ) + for fit_result in fit_results: + gw.dump_grain(*fit_result) + pass + gw.close() - def __init__(self, *args, **kwargs): - mp.Process.__init__(self) - FitGrainsWorker.__init__(self, *args, **kwargs) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 992c1e3c..6ddb802f 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -198,7 +198,7 @@ def max_tth(instr): Returns ------- tth_max : float - The maximum observable Bragg angle by the instrument. + The maximum observable Bragg angle by the instrument in radians. """ tth_max = 0. for det in instr.detectors.values(): From 6bff7450cd2d00db4445f2bc6b53b45f70dd7b63 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 1 Jun 2020 14:02:04 -0700 Subject: [PATCH 242/253] hedm CLI restored --- hexrd/findorientations.py | 2 +- hexrd/fitgrains.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hexrd/findorientations.py b/hexrd/findorientations.py index 53b75478..93d72393 100755 --- a/hexrd/findorientations.py +++ b/hexrd/findorientations.py @@ -8,7 +8,7 @@ import numpy as np # np.seterr(over='ignore', invalid='ignore') -import tqdm +# import tqdm import scipy.cluster as cluster from scipy import ndimage diff --git a/hexrd/fitgrains.py b/hexrd/fitgrains.py index f26f6230..3e38dd9f 100644 --- a/hexrd/fitgrains.py +++ b/hexrd/fitgrains.py @@ -19,6 +19,7 @@ from hexrd import constants as cnst from hexrd import instrument from hexrd.xrd import transforms_CAPI as xfcapi +from hexrd.xrd.fitting import fitGrain, objFuncFitGrain, gFlag_ref logger = logging.getLogger(__name__) From 7b92419523cd129cbfe77f74e1f397a909f23325 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Mon, 8 Jun 2020 17:43:25 -0700 Subject: [PATCH 243/253] fix to bilinear interpolation --- hexrd/gridutil.py | 11 ++++------- hexrd/instrument.py | 35 +++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/hexrd/gridutil.py b/hexrd/gridutil.py index ac47c57e..770eedb4 100644 --- a/hexrd/gridutil.py +++ b/hexrd/gridutil.py @@ -5,7 +5,7 @@ from numpy import sum as asum from numpy.linalg import det import numpy as np -from hexrd import USE_NUMBA +from hexrd.constants import USE_NUMBA, sqrt_epsf if USE_NUMBA: import numba @@ -38,15 +38,12 @@ def cellIndices(edges, points_1d): must be mapped to the same branch cut, and abs(edges[0] - edges[-1]) = 2*pi """ - ztol = 1e-12 + ztol = sqrt_epsf assert len(edges) >= 2, "must have at least 2 edges" - points_1d = r_[points_1d].flatten() - delta = float(edges[1] - edges[0]) - - on_last_rhs = points_1d >= edges[-1] - ztol - points_1d[on_last_rhs] = points_1d[on_last_rhs] - ztol + points_1d = np.r_[points_1d].flatten() + delta = float(edges[1] - edges[0]) if delta > 0: on_last_rhs = points_1d >= edges[-1] - ztol diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 6ddb802f..da50780b 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -116,6 +116,15 @@ # ============================================================================= +def _fix_indices(idx, lo, hi): + nidx = np.array(idx) + off_lo = nidx < lo + off_hi = nidx > hi + nidx[off_lo] = lo + nidx[off_hi] = hi + return nidx + + def calc_beam_vec(azim, pola): """ Calculate unit beam propagation vector from @@ -1817,6 +1826,10 @@ def interpolate_nearest(self, xy, img, pad_with_nans=True): def interpolate_bilinear(self, xy, img, pad_with_nans=True): """ + Interpolates an image array at the specified cartesian points. + + !!! the `xy` input is in *unwarped* detector coords! + TODO: revisit normalization in here? """ is_2d = img.ndim == 2 @@ -1838,18 +1851,28 @@ def interpolate_bilinear(self, xy, img, pad_with_nans=True): ij_frac = self.cartToPixel(xy_clip) # get floors/ceils from array of pixel _centers_ + # and fix indices running off the pixel centers + # !!! notice we already clipped points to the panel! i_floor = cellIndices(self.row_pixel_vec, xy_clip[:, 1]) + i_floor_img = _fix_indices(i_floor, 0, self.rows - 1) + j_floor = cellIndices(self.col_pixel_vec, xy_clip[:, 0]) + j_floor_img = _fix_indices(j_floor, 0, self.cols - 1) + + # ceilings from floors i_ceil = i_floor + 1 + i_ceil_img = _fix_indices(i_ceil, 0, self.rows - 1) + j_ceil = j_floor + 1 + j_ceil_img = _fix_indices(j_ceil, 0, self.cols - 1) # first interpolate at top/bottom rows row_floor_int = \ - (j_ceil - ij_frac[:, 1])*img[i_floor, j_floor] \ - + (ij_frac[:, 1] - j_floor)*img[i_floor, j_ceil] + (j_ceil - ij_frac[:, 1])*img[i_floor_img, j_floor_img] \ + + (ij_frac[:, 1] - j_floor)*img[i_floor_img, j_ceil_img] row_ceil_int = \ - (j_ceil - ij_frac[:, 1])*img[i_ceil, j_floor] \ - + (ij_frac[:, 1] - j_floor)*img[i_ceil, j_ceil] + (j_ceil - ij_frac[:, 1])*img[i_ceil_img, j_floor_img] \ + + (ij_frac[:, 1] - j_floor)*img[i_ceil_img, j_ceil_img] # next interpolate across cols int_vals = \ @@ -2093,13 +2116,13 @@ def simulate_rotation_series(self, plane_data, grain_param_list, valid_xys.append(xys_p) # filter angs and hkls that are on the detector plane - # !!! check this -- seems unnecessary but the results of + # !!! check this -- seems unnecessary but the results of # _project_on_detector_plane() can have len < the input. # the output of _project_on_detector_plane has been modified to # hand back the index array to remedy this JVB 2020-05-27 filtered_angs = np.atleast_2d(allAngs[on_plane, :]) filtered_hkls = np.atleast_2d(allHKLs[on_plane, :]) - + # grab hkls and gvec ids for this panel valid_hkls.append(filtered_hkls[on_panel, 1:]) valid_ids.append(filtered_hkls[on_panel, 0]) From e3c945c4828b5a164883aace9562f58d5e1e7ea1 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 10 Jun 2020 13:31:47 -0700 Subject: [PATCH 244/253] line position cleanup --- hexrd/instrument.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index da50780b..9778864a 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -745,7 +745,7 @@ def extract_line_positions(self, plane_data, imgser_dict, # ================================================================= ring_data = [] for i_ring, these_data in enumerate(zip(pow_angs, pow_xys)): - print("working on 2theta bin (ring) %d..." % i_ring) + print("interpolating 2theta bin %d..." % i_ring) # points are already checked to fall on detector angs = these_data[0] @@ -765,13 +765,15 @@ def extract_line_positions(self, plane_data, imgser_dict, patch_data = [] for i_p, patch in enumerate(patches): # strip relevant objects out of current patch - vtx_angs, vtx_xy, conn, areas, xy_eval, ijs = patch + vtx_angs, vtx_xys, conn, areas, xys_eval, ijs = patch + + # need to reshape eval pts for interpolation + xy_eval = np.vstack([ + xys_eval[0].flatten(), + xys_eval[1].flatten()]).T + + _, on_panel = panel.clip_to_panel(xy_eval) - _, on_panel = panel.clip_to_panel( - np.vstack( - [xy_eval[0].flatten(), xy_eval[1].flatten()] - ).T - ) if np.any(~on_panel): continue @@ -781,12 +783,9 @@ def extract_line_positions(self, plane_data, imgser_dict, else: ang_data = (vtx_angs[0][0, :], angs[i_p][-1]) + prows, pcols = areas.shape area_fac = areas/float(native_area) - # need to reshape eval pts for interpolation - xy_eval = np.vstack([ - xy_eval[0].flatten(), - xy_eval[1].flatten()]).T # interpolate if not collapse_tth: @@ -826,7 +825,28 @@ def simulate_laue_pattern(self, crystal_data, minEnergy=5., maxEnergy=35., rmat_s=None, grain_params=None): """ - TODO: revisit output; dict, or concatenated list? + Simulates Laue diffraction for a list of grains. + + Parameters + ---------- + crystal_data : TYPE + DESCRIPTION. + minEnergy : TYPE, optional + DESCRIPTION. The default is 5.. + maxEnergy : TYPE, optional + DESCRIPTION. The default is 35.. + rmat_s : TYPE, optional + DESCRIPTION. The default is None. + grain_params : TYPE, optional + DESCRIPTION. The default is None. + + Returns + ------- + results : dict + results dictionary for each detector containing + [xy_det, hkls_in, angles, dspacing, energy] each a list over each + grain. + """ results = dict.fromkeys(self.detectors) for det_key, panel in self.detectors.items(): From 1d437e9b68035e4a2a18043643186f6891c55477 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 10 Jun 2020 18:09:54 -0700 Subject: [PATCH 245/253] import typo --- hexrd/gridutil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hexrd/gridutil.py b/hexrd/gridutil.py index 770eedb4..7ce487e9 100644 --- a/hexrd/gridutil.py +++ b/hexrd/gridutil.py @@ -5,7 +5,8 @@ from numpy import sum as asum from numpy.linalg import det import numpy as np -from hexrd.constants import USE_NUMBA, sqrt_epsf +from hexrd.constants import sqrt_epsf +from hexrd import USE_NUMBA if USE_NUMBA: import numba From 18ef56d5d5e21a29cf9bdc3b36af05560d5e3c65 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 11 Jun 2020 11:15:58 -0700 Subject: [PATCH 246/253] Update README.md --- README.md | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 695b6935..99efbacf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,33 @@ +HEXRD +===== + HEXRD provides a collection of resources for analysis of x-ray diffraction -data, especially high-energy x-ray diffraction. HEXRD is comprised of a -library and API for writing scripts, a command line interface, and an -interactive graphical user interface. +data, including powder diffraction, Laue diffraction, and monochromatic rotation series (i.e. 3DXRD/HEDM). +HEXRD is comprised of a library and API for writing scripts, a command line interface, and an +interactive graphical user interface (though this is not up to date in python2.7). + +Note that this is a _legacy_ repo with minimal maintenance; the canonical HEXRD repos can now be found at https://github.com/HEXRD/hexrd. + +Building +-------- +the recommended method is via `conda-build`. You can also skip this if you find a build of the desired version at my [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) page, which I update periodically. Otherwise, using conda 4.8.3 (from miniconda3 or anaconda3) the best procedure is as follows: +- go to wherever you keep your git repos, _e.g._, `cd ~/Documents/GitHub` +- clone the hexrd repo: `git clone https://github.com/joelvbernier/hexrd.git` +- `cd hexrd` +- checkout the v0.6.x branch: `git checkout v0.6.x` +- make an empty env with python2.7 and numpy: `conda create --name hexrd_0.6 python=2 numpy` +- activate your new env: `conda activate hexrd_0.6` +- install fabio from [here](https://github.com/joelvbernier/fabio.git) + - cd into wherever you keep your git repos, _e.g._, `cd ~/Documents/GitHub` + - clone repo: `git clone https://github.com/joelvbernier/fabio.git` + - `cd fabio` + - `pip install ./` +-`conda build conda.recipe/ --python=2 -c conda-forge` + +Installing +---------- +You can check [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) for prebuillt versions; if you ffind one for your platform, then simply execute +- `conda install hexrd=0.6 -c joelvbernier` + +Otherwise, you can install from a local build as follows: +- `conda install hexrd=0.6 --use-local` From bb27a1d0a49fbfebcf3957cbb1ad6fe30723e357 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 11 Jun 2020 11:23:04 -0700 Subject: [PATCH 247/253] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 99efbacf..c35a0413 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ HEXRD ===== -HEXRD provides a collection of resources for analysis of x-ray diffraction -data, including powder diffraction, Laue diffraction, and monochromatic rotation series (i.e. 3DXRD/HEDM). +HEXRD provides a collection of resources for analysis of X-ray diffraction +data, including powder diffraction, Laue diffraction, and monochromatic rotation series (_i.e._, 3DXRD/HEDM). HEXRD is comprised of a library and API for writing scripts, a command line interface, and an interactive graphical user interface (though this is not up to date in python2.7). Note that this is a _legacy_ repo with minimal maintenance; the canonical HEXRD repos can now be found at https://github.com/HEXRD/hexrd. +It is recomended that you use the conda package manager for your python environment (available from either [here](https://docs.conda.io/en/latest/miniconda.html) or [here](https://www.anaconda.com/products/individual), with the former being a smaller, more barebones install). + Building -------- the recommended method is via `conda-build`. You can also skip this if you find a build of the desired version at my [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) page, which I update periodically. Otherwise, using conda 4.8.3 (from miniconda3 or anaconda3) the best procedure is as follows: From bb6046c2792d17c324d993468acafacc039a353c Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 11 Jun 2020 11:23:45 -0700 Subject: [PATCH 248/253] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c35a0413..7b6ec235 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ the recommended method is via `conda-build`. You can also skip this if you find - clone repo: `git clone https://github.com/joelvbernier/fabio.git` - `cd fabio` - `pip install ./` --`conda build conda.recipe/ --python=2 -c conda-forge` +- `conda build conda.recipe/ --python=2 -c conda-forge` Installing ---------- From 983a617df902dc7417fb7be5aab44410fdb6d351 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Thu, 11 Jun 2020 11:27:53 -0700 Subject: [PATCH 249/253] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b6ec235..27275932 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ It is recomended that you use the conda package manager for your python environm Building -------- -the recommended method is via `conda-build`. You can also skip this if you find a build of the desired version at my [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) page, which I update periodically. Otherwise, using conda 4.8.3 (from miniconda3 or anaconda3) the best procedure is as follows: +You can skip this if you find a build of the desired version at my [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) page, which I update periodically. Otherwise, the recommended method is via `conda-build`. If you installed Miniconda, you will have to first install `conda-build` in your base env: `conda install conda-build`. Otherwise, using conda 4.8.3 (from Miniconda3 or Anaconda3) the best procedure is as follows: - go to wherever you keep your git repos, _e.g._, `cd ~/Documents/GitHub` - clone the hexrd repo: `git clone https://github.com/joelvbernier/hexrd.git` - `cd hexrd` @@ -33,3 +33,7 @@ You can check [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) for p Otherwise, you can install from a local build as follows: - `conda install hexrd=0.6 --use-local` + +Running +------- +The function libraries lend themselves to scripts for your vaired purposes, but there is a CLI for the ff-HEDM workflow, namely indexing, `hexrd find-orientations`, and grain parameter refinement, `hexrd fit-grains`. More documentation to come. From a05864f5f02502f4b63d05379664268229c80c12 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 12 Aug 2020 11:40:03 -0700 Subject: [PATCH 250/253] Update README.md Fixes to work with conda 4.8.3 --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 27275932..f6495b86 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,19 @@ Building -------- You can skip this if you find a build of the desired version at my [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) page, which I update periodically. Otherwise, the recommended method is via `conda-build`. If you installed Miniconda, you will have to first install `conda-build` in your base env: `conda install conda-build`. Otherwise, using conda 4.8.3 (from Miniconda3 or Anaconda3) the best procedure is as follows: - go to wherever you keep your git repos, _e.g._, `cd ~/Documents/GitHub` -- clone the hexrd repo: `git clone https://github.com/joelvbernier/hexrd.git` +- if you have the repo all ready, update it with a fetch and pull `fit fetch -av; git pull' +- otherwise, clone the hexrd repo: `git clone https://github.com/joelvbernier/hexrd.git` - `cd hexrd` - checkout the v0.6.x branch: `git checkout v0.6.x` -- make an empty env with python2.7 and numpy: `conda create --name hexrd_0.6 python=2 numpy` +- make an empty env with python2.7 and numpy: `conda create --name hexrd_0.6 -c anaconda -c conda-forge python=2 numpy` - activate your new env: `conda activate hexrd_0.6` - install fabio from [here](https://github.com/joelvbernier/fabio.git) - cd into wherever you keep your git repos, _e.g._, `cd ~/Documents/GitHub` - clone repo: `git clone https://github.com/joelvbernier/fabio.git` + - grab the python 2.7 compatible branch: `git checkout py27_compat` - `cd fabio` - `pip install ./` -- `conda build conda.recipe/ --python=2 -c conda-forge` +- build hexrd from the conda recipe: `conda build conda.recipe/ --python=2 -c anaconda -c conda-forge` Installing ---------- @@ -32,8 +34,18 @@ You can check [my anaconda cloud](https://anaconda.org/joelvbernier/hexrd) for p - `conda install hexrd=0.6 -c joelvbernier` Otherwise, you can install from a local build as follows: -- `conda install hexrd=0.6 --use-local` +- `conda install hexrd=0.6 --use-local -c anaconda -c conda-forge` Running ------- The function libraries lend themselves to scripts for your vaired purposes, but there is a CLI for the ff-HEDM workflow, namely indexing, `hexrd find-orientations`, and grain parameter refinement, `hexrd fit-grains`. More documentation to come. + +Additional Packages +------------------- +It is highly recommended to install the `fast-histogram` package for the indexing: + +- `pip install fast-histogram` + +And is you want spyder, the default channel is broken for python2.7. Use the following: + +- `conda install spyder=3 jupyter_client=5.3.4 -c anaconda -c conda-forge` From 99a3d0b2b0143c66220a5fdca7846290adcb4499 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 17 Mar 2021 13:08:33 -0700 Subject: [PATCH 251/253] fixed unused threshold kwarg --- hexrd/instrument.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 9778864a..9701501c 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -547,6 +547,8 @@ def extract_polar_maps(self, plane_data, imgser_dict, TODO: streamline projection code TODO: normalization + + !!!: images must be non-negative! """ if tth_tol is not None: plane_data.tThWidth = np.radians(tth_tol) @@ -671,8 +673,15 @@ def extract_polar_maps(self, plane_data, imgser_dict, ) pass pass + + # handle threshold if specified + this_det_image = np.array(imgser_dict[det_key]) + if threshold is not None: + # !!! NaNs get preserved + this_det_image[this_det_image < threshold] = 0. + # histogram intensities over eta ranges - for i_row, image in enumerate(imgser_dict[det_key]): + for i_row, image in enumerate(this_det_image): if fast_histogram: this_map[i_row, reta_idx] = histogram1d( retas, From 44473670bba0908aecf35243c20a33fc5f6cee18 Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 17 Mar 2021 15:39:04 -0700 Subject: [PATCH 252/253] moving inside loop to not blow up memory --- hexrd/instrument.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/hexrd/instrument.py b/hexrd/instrument.py index 9701501c..d246365c 100644 --- a/hexrd/instrument.py +++ b/hexrd/instrument.py @@ -674,14 +674,13 @@ def extract_polar_maps(self, plane_data, imgser_dict, pass pass - # handle threshold if specified - this_det_image = np.array(imgser_dict[det_key]) - if threshold is not None: - # !!! NaNs get preserved - this_det_image[this_det_image < threshold] = 0. - # histogram intensities over eta ranges - for i_row, image in enumerate(this_det_image): + for i_row, image in enumerate(imgser_dict[det_key]): + # handle threshold if specified + if threshold is not None: + # !!! NaNs get preserved + image = np.array(image) + image[image < threshold] = 0. if fast_histogram: this_map[i_row, reta_idx] = histogram1d( retas, From f55f78f6bd14600709b0eb0e81d604bc413d4aff Mon Sep 17 00:00:00 2001 From: Joel Bernier Date: Wed, 17 Mar 2021 16:29:28 -0700 Subject: [PATCH 253/253] use keVToAngstrom --- hexrd/xrd/crystallography.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/hexrd/xrd/crystallography.py b/hexrd/xrd/crystallography.py index 08585c25..8a08fcba 100644 --- a/hexrd/xrd/crystallography.py +++ b/hexrd/xrd/crystallography.py @@ -33,8 +33,7 @@ import csv import os -from scipy import constants as C - +from hexrd import constants from hexrd.matrixutil import sqrt, unitVector, columnNorm, sum from hexrd.xrd.rotations import rotMatOfExpMap, mapAngle from hexrd.xrd import symmetry @@ -88,21 +87,17 @@ def processWavelength(arg): if arg.isLength(): retval = arg.getVal(dUnit) elif arg.isEnergy(): - try: - speed = C.c - planck = C.h - except: - raise NotImplementedError, 'scipy does not have constants' - # speed = ... - # planck = ... - e = arg.getVal('J') - retval = valunits.valWUnit('wavelength', 'length', planck*speed/e, 'm').getVal(dUnit) + e = arg.getVal('keV') + retval = valunits.valWUnit( + 'wavelength', 'length', constants.keVToAngstrom(e), 'angstrom' + ).getVal(dUnit) else: - raise RuntimeError, 'do not know what to do with '+str(arg) + raise RuntimeError('do not know what to do with '+str(arg)) else: - keV2J = 1.e3*C.e - e = keV2J * arg - retval = valunits.valWUnit('wavelength', 'length', C.h*C.c/e, 'm').getVal(dUnit) + # !!! assuming arg is in keV + retval = valunits.valWUnit( + 'wavelength', 'length', constants.keVToAngstrom(arg), 'angstrom' + ).getVal(dUnit) return retval