Source code for binoculars.backends.bm25

"""
BINocular backend for beamline BM25, branch B first endstation [1] 
This backend should serve as a basic implementation of a backend based on
xrayutilities [2]. It uses the information from the edf files (motors position
and detector image) ignoring the spec file, except for using its scan numbers
to identify images belonging to the same scan.

You should use CCD file names generated with the following pattern:
filename_#n_#p_#r.edf  (n: spec-scan number, p: point number, r: image number)
Binning (2,2)

The backend is called 'EH2SCD'.

Created on 2014-10-28

[1] http://www.esrf.eu/UsersAndScience/Experiments/CRG/BM25/BeamLine/experimentalstations/Single_Crystal_Diffraction
[2] http://xrayutilities.sourceforge.net/

author: Dominik Kriegner (dominik.kriegner@gmail.com)
"""

import sys
import os
import glob
import numpy
import xrayutilities as xu

from .. import backend, errors, util


[docs] class HKLProjection(backend.ProjectionBase): # scalars: mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, wavelength # 3x3 matrix: UB
[docs] def project( self, mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, ccdtr, wavelength, UB, qconv, ): qconv.wavelength = wavelength h, k, l = qconv.area( mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, UB=UB.reshape((3, 3)) ) return (h, k, l)
[docs] def get_axis_labels(self): return "H", "K", "L"
[docs] class HKProjection(HKLProjection):
[docs] def project( self, mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, ccdtr, wavelength, UB, qconv, ): H, K, L = super().project( mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, ccdtr, wavelength, UB, qconv, ) return (H, K)
[docs] def get_axis_labels(self): return "H", "K"
[docs] class QProjection(backend.ProjectionBase):
[docs] def project( self, mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, ccdtr, wavelength, UB, qconv, ): qconv.wavelength = wavelength qx, qy, qz = qconv.area( mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, ccdtr, UB=numpy.identity(3) ) return (qx, qy, qz)
[docs] def get_axis_labels(self): return "qx", "qy", "qz"
[docs] class QinpProjection(backend.ProjectionBase):
[docs] def project( self, mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, ccdtr, wavelength, UB, qconv, ): qconv.wavelength = wavelength qx, qy, qz = qconv.area( mu, theta, phi, chi, ccdty, ccdtx, ccdtz, ccdth, ccdtr, UB=numpy.identity(3) ) return (numpy.sqrt(qx ** 2 + qy ** 2), qz)
[docs] def get_axis_labels(self): return "qinp", "qz"
[docs] class EDFInput(backend.InputBase): # OFFICIAL API
[docs] def generate_jobs(self, command): scans = util.parse_multi_range(",".join(command).replace(" ", ",")) imgs = self.list_images(scans) imgcount = len(imgs) if not len(imgs): sys.stderr.write("error: no images selected, nothing to do\n") # next(self.get_images(imgs, 0, imgcount-1, dry_run=True))# dryrun for s in util.chunk_slicer(imgcount, self.config.target_weight): yield backend.Job( images=imgs, firstimage=s.start, lastimage=s.stop - 1, weight=s.stop - s.start, )
[docs] def process_job(self, job): super().process_job(job) images = self.get_images(job.images, job.firstimage, job.lastimage) # iterator! for image in images: yield self.process_image(image)
[docs] def parse_config(self, config): super().parse_config(config) self.config.xmask = util.parse_multi_range(config.pop("xmask")) self.config.ymask = util.parse_multi_range(config.pop("ymask")) self.config.imagefile = config.pop("imagefile") self.config.UB = config.pop("ub", None) if self.config.UB: self.config.UB = util.parse_tuple(self.config.UB, length=9, type=float) self.config.sddx = float(config.pop("sddx_offset")) self.config.sddy = float(config.pop("sddy_offset")) self.config.sddz = float(config.pop("sddz_offset")) self.config.ccdth0 = float(config.pop("ccdth_offset")) self.config.pixelsize = util.parse_tuple( config.pop("pixelsize"), length=2, type=float ) self.config.centralpixel = util.parse_tuple( config.pop("centralpixel"), length=2, type=float )
[docs] def get_destination_options(self, command): if not command: return False command = ",".join(command).replace(" ", ",") scans = util.parse_multi_range(command) return dict(first=min(scans), last=max(scans), range=",".join(command))
# CONVENIENCE FUNCTIONS
[docs] @staticmethod def apply_mask(data, xmask, ymask): roi = data[ymask, :] return roi[:, xmask]
# MAIN LOGIC
[docs] def list_images(self, scannrs): pattern = self.config.imagefile imgfiles = [] # check if necessary image-files exist for nr in scannrs: try: fpattern = pattern.format(scannr=nr) except Exception as e: raise errors.ConfigError( "invalid 'imagefile' specification " f"'{self.config.imagefile}': {e!r}" ) files = glob.glob(fpattern) if len(files) == 0: raise errors.FileError( f"needed file do not exist: scannr {nr}" ) else: imgfiles += files return imgfiles
[docs] def get_images(self, imgs, first, last, dry_run=False): for i in range(first, last + 1): img = imgs[i] if dry_run: yield else: edf = xu.io.EDFFile(img) yield edf
[docs] class EH2SCD(EDFInput): monitor_counter = "C_mont" # define BM25 goniometer, SIXC geometry? with 2D detector mounted on # translation-axes # see http://www.esrf.eu/UsersAndScience/Experiments/CRG/BM25/BeamLine/experimentalstations/Single_Crystal_Diffraction # The geometry is: 4S + translations and one det. rotation # sample axis: mu, th, chi, phi # detector axis: translations + theta rotation (to make beam perpendicular # to the detector plane in symmetric arrangement) qconv = xu.experiment.QConversion( ["x+", "z+", "y+", "x+"], ["ty", "tx", "tz", "x+", "ty"], [0, 1, 0] ) # convention for coordinate system: y downstream; x in bound; z upwards # (righthanded) # QConversion will set up the goniometer geometry. So the first argument # describes the sample rotations, the second the detector rotations and the # third the primary beam direction.
[docs] def parse_config(self, config): super().parse_config(config) centralpixel = self.config.centralpixel # define detector parameters roi = ( self.config.ymask[0], self.config.ymask[-1] + 1, self.config.xmask[0], self.config.xmask[-1] + 1, ) self.qconv.init_area( "z-", "x+", cch1=centralpixel[1], cch2=centralpixel[0], Nch1=1912, Nch2=3825, pwidth1=self.config.pixelsize[1], pwidth2=self.config.pixelsize[0], distance=1e-10, roi=roi, ) print( f"{' ':>20} {'Mu':>9} {'Theta':>10}" f" {'CCD_Y':>9} {'CCD_X':>9} {'CCD_Z':>9}" )
[docs] def process_image(self, image): # motor positions mu = float(image.header["M_mu"]) th = float(image.header["M_th"]) chi = float(image.header["M_chi"]) phi = float(image.header["M_phi"]) # distance 'ctr' corresponds to distance of the detector chip from # the CCD_TH rotation axis. The rest is handled by the translations ctr = -270.0 # measured by ruler only!!! cty = float(image.header["M_CCD_Y"]) - self.config.sddy - ctr ctx = float(image.header["M_CCD_X"]) - self.config.sddx ctz = float(image.header["M_CCD_Z"]) - self.config.sddz cth = float(image.header["M_CCD_TH"]) - self.config.ccdth0 # filter correction transm = 1.0 # no filter correction! (Filters are manual on BM25!) mon = float(image.header[self.monitor_counter]) wavelength = float(image.header["WAVELENGTH"]) if self.config.UB: UB = self.config.UB else: UB = self._get_UB(image.header) # normalization data = image.data / mon / transm print( f"{os.path.split(image.filename)[-1]:>20}" f" {mu:9.4f} {th:10.4f} {cty:9.1f} {ctx:9.1f} {ctz:9.1f}" ) # masking intensity = self.apply_mask(data, self.config.xmask, self.config.ymask) return ( intensity, numpy.ones_like(intensity), ( mu, th, phi, chi, cty, ctx, ctz, cth, ctr, ## weights added to API. Treated here like before wavelength, UB, self.qconv, ), )
@staticmethod def _get_UB(header): ub = numpy.zeros(9) for i in range(9): ub[i] = float(header[f"UB{i:d}"]) return ub