Source code for SimEx.Parameters.DetectorGeometry

""" :module DetectorGeometry: Module holding the DetectorGeometry class. """
##########################################################################
#                                                                        #
# Copyright (C) 2015-2020 Carsten Fortmann-Grote, Juncheng E             #
# Contact: Carsten Fortmann-Grote <carsten.grote@xfel.eu>                #
#                                                                        #
# This file is part of simex_platform.                                   #
# simex_platform is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by   #
# the Free Software Foundation, either version 3 of the License, or      #
# (at your option) any later version.                                    #
#                                                                        #
# simex_platform 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          #
# GNU General Public License for more details.                           #
#                                                                        #
# You should have received a copy of the GNU General Public License      #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.  #
#                                                                        #
##########################################################################
from SimEx.AbstractBaseClass import AbstractBaseClass
from SimEx.Parameters.AbstractCalculatorParameters import AbstractCalculatorParameters
from SimEx.Utilities.EntityChecks import checkAndSetInstance, checkAndSetNumber, checkAndSetPhysicalQuantity
from SimEx.Utilities.Units import meter, electronvolt
from cfelpyutils.crystfel_utils import load_crystfel_geometry

import numpy
import sys


[docs]class DetectorPanel(AbstractBaseClass): """:class DetectorPanel: Represents one detector panel (contiguous array of pixels, i.e. not separated by gaps). """ def __init__(self, ranges = None, pixel_size = None, energy_response = None, photon_response = None, distance_from_interaction_plane = None, distance_offset = None, fast_scan_xyz = None, slow_scan_xyz = None, corners = None, saturation_adu = None, mask = None, good_bit_mask = None, bad_bit_mask = None, saturation_map = None, badregion_flag = None, **kwargs ): """ :param ranges: The minimum and maximum values pixel numbers on the respective transverse axis. :type ranges: Dictionary ":example ranges: {"fast_scan_min : 11, "fast_scan_max" : 20, "slow_scan_min" : 1, "fast_scan_max" : 20} # First axis from 11 to 20 and second axis from 1 to 20." :param pixel_size: The physical size of the pixel (assuming quadratic shape) (SI units). :type pixel_size: PhysicalQuantity with unit meter. :param energy_response: Number of detector units (ADU) arising from one eV. :type energy_response: PhysicalQuantity with unit 1/eV (adu_per_eV) :param photon_response: Number of detector units (ADU) arising from one photon. :type photon_response: float :param distance_from_interaction_plane: Distance in z of this panel from the plane of interaction (transverse plane that contains the sample). :type distance_from_interaction_plane: PhysicalQuantity with unit meter. :param distance_offset: Offset from distance_from_interaction_plane. :type distance_offset: PhysicalQuantity with unit meter. :param fast_scan_xyz: Formula that lab frame coordinates to panel axes. :type fast_scan_xyz: str :param slow_scan_xyz: Formula that lab frame coordinates to panel axes. :type slow_scan_xyz: str :param corners: [x,y] coordinates of lower left pixel of this panel in the globale detector geometry. :type corners: dict :example corners: corners={"x" : -10, "y" : 10 } :param saturation_adu: Saturation level for this panel. :type saturation_adu: float. :param mask: Mask to apply to this panel. :type mask: numpy.array of same shape as panel data. :param good_bit_mask: Bitmask indicating the good pixels :type good_bit_mask: ??? :param bad_bit_mask: Bitmask indicating the bad pixels. :type bad_bit_mask: ??? :param saturation_map: Pixel map indicating saturated pixels. :type saturation_map: numpy.array of same shape as panel data. :param badregion_flag: Flag to indicate this panel as a bad region. :type badregion_flag: bool """ # Handle case that neither photon nor energy response is set. self.__photon_response = 1.0 self.__energy_response = None # Store on object using setters. self.ranges = ranges self.pixel_size = pixel_size self.energy_response = energy_response self.photon_response = photon_response self.distance_from_interaction_plane = distance_from_interaction_plane self.distance_offset = distance_offset self.fast_scan_xyz = fast_scan_xyz self.slow_scan_xyz = slow_scan_xyz self.corners = corners self.saturation_adu = saturation_adu self.mask = mask self.good_bit_mask = good_bit_mask self.bad_bit_mask = bad_bit_mask self.saturation_map = saturation_map self.badregion_flag = badregion_flag self.number_of_pixels_fast = int(self.ranges["fast_scan_max"] - self.ranges["fast_scan_min"]) + 1 self.number_of_pixels_slow = int(self.ranges["slow_scan_max"] - self.ranges["slow_scan_min"]) + 1 ### Accessors. # ranges @property def ranges(self): """ Query the panel ranges. """ return self.__ranges @ranges.setter def ranges(self, val): """ Set the panel ranges. """ if val is None: raise ValueError( "The parameters 'ranges' must not be None." ) self.__ranges = checkAndSetInstance( dict, val, None ) # pixel_size @property def pixel_size(self): """ Query the panel pixel_size. """ return self.__pixel_size @pixel_size.setter def pixel_size(self, val): """ Set the panel pixel_size. """ self.__pixel_size = checkAndSetPhysicalQuantity( val, 1.0e-4*meter, meter) # energy_response @property def energy_response(self): """ Query the panel energy_response. """ return self.__energy_response @energy_response.setter def energy_response(self, val): """ Set the panel energy_response. """ if val is not None: val = checkAndSetPhysicalQuantity( val, None, 1./electronvolt) self.__energy_response = val # Invalidate photon response. if val is not None and self.__photon_response is not None: self.__photon_response = None # photon_response @property def photon_response(self): """ Query the panel photon_response. """ return self.__photon_response @photon_response.setter def photon_response(self, val): """ Set the panel photon_response. """ if val is not None: val = checkAndSetInstance( float, val, None) self.__photon_response = val # Invalidate energy response. if val is not None and self.__energy_response is not None: self.__energy_response = None # Distance_from_interaction_plane @property def distance_from_interaction_plane(self): """ Query the panel distance_from_interaction_plane. """ return self.__distance_from_interaction_plane @distance_from_interaction_plane.setter def distance_from_interaction_plane(self, val): """ Set the panel distance_from_interaction_plane. """ self.__distance_from_interaction_plane = checkAndSetPhysicalQuantity( val, 0.1*meter, meter ) # distance_offset @property def distance_offset(self): """ Query the panel distance_offset. """ return self.__distance_offset @distance_offset.setter def distance_offset(self, val): """ Set the panel distance_offset. """ self.__distance_offset = checkAndSetPhysicalQuantity( val, 0.0*meter, meter) # fastscan_xyz @property def fast_scan_xyz(self): """ Query the panel fast_scan_xyz. """ return self.__fast_scan_xyz @fast_scan_xyz.setter def fast_scan_xyz(self, val): """ Set the panel fast_scan_xyz. """ self.__fast_scan_xyz = checkAndSetInstance( str, val, "1.0x" ) # slow_scan_xyz @property def slow_scan_xyz(self): """ Query the panel slow_scan_xyz. """ return self.__slow_scan_xyz @slow_scan_xyz.setter def slow_scan_xyz(self, val): """ Set the panel slow_scan_xyz. """ self.__slow_scan_xyz = checkAndSetInstance( str, val, "1.0y" ) # corners @property def corners(self): """ Query the panel cornes. """ return self.__corners @corners.setter def corners(self, val): """ Set the panel corners. """ self.__corners = checkAndSetInstance( dict, val, {"x" : 0.0, "y" : 0.0} ) # saturation_adu @property def saturation_adu(self): """ Query the panel saturation_adu. """ return self.__saturation_adu @saturation_adu.setter def saturation_adu(self, val): """ Set the panel saturation_adu. """ self.__saturation_adu = checkAndSetNumber( val, 1.0e4 ) # mask @property def mask(self): """ Query the panel mask. """ return self.__mask @mask.setter def mask(self, val): """ Set the panel mask. """ self.__mask = checkAndSetInstance( numpy.array, val, None ) # good_bit_mask @property def good_bit_mask(self): """ Query the panel good_bit_mask. """ return self.__good_bit_mask @good_bit_mask.setter def good_bit_mask(self, val): """ Set the panel good_bit_mask. """ self.__good_bit_mask = checkAndSetInstance( int, val, None ) # bad_bit_mask @property def bad_bit_mask(self): """ Query the panel bad_bit_mask. """ return self.__bad_bit_mask @bad_bit_mask.setter def bad_bit_mask(self, val): """ Set the panel bad_bit_mask. """ self.__bad_bit_mask = checkAndSetInstance( int, val, None ) # saturation_map @property def saturation_map(self): """ Query the panel saturation_map. """ return self.__saturation_map @saturation_map.setter def saturation_map(self, val): """ Set the panel saturation_map. """ self.__saturation_map = checkAndSetInstance( numpy.array, val, None ) # badregion_flag @property def badregion_flag(self): """ Query the panel badregion_flag. """ return self.__badregion_flag @badregion_flag.setter def badregion_flag(self, val): """ Set the panel badregion_flag. """ self.__badregion_flag = checkAndSetInstance( bool, val, False ) ### End accessors def _serialize(self, stream=None, panel_id=None, caller=None): """ Serialize the panel. :param stream: The stream to write the serialized panel to. :type stream: file like (default sys.stdout) """ # Check stream parameter. if stream is None: stream = sys.stdout if not hasattr(stream, "write"): raise IOError( "The given stream is not writable." ) # Check panel_id parameter. panel_id = checkAndSetInstance( int, panel_id, 0 ) # Get panel id as a string. panel_id_str = str(panel_id) # Initialize the string to be written to. serialization = ";panel %s\n" % (panel_id_str) serialization += "panel%s/min_fs = %d\n" % (panel_id_str, self.ranges["fast_scan_min"]) serialization += "panel%s/max_fs = %d\n" % (panel_id_str, self.ranges["fast_scan_max"]) serialization += "panel%s/min_ss = %d\n" % (panel_id_str, self.ranges["slow_scan_min"]) serialization += "panel%s/max_ss = %d\n" % (panel_id_str, self.ranges["slow_scan_max"]) serialization += "panel%s/corner_y = %d\n" % (panel_id_str, self.corners["y"]) serialization += "panel%s/fs = %s\n" % (panel_id_str, self.fast_scan_xyz) serialization += "panel%s/ss = %s\n" % (panel_id_str, self.slow_scan_xyz) serialization += "panel%s/clen = %8.7e\n" % (panel_id_str, self.distance_from_interaction_plane.m_as(meter)) serialization += "panel%s/res = %8.7e\n" % (panel_id_str, 1./self.pixel_size.m_as(meter)) serialization += "panel%s/coffset = %8.7e\n" % (panel_id_str, self.distance_offset.m_as(meter)) if self.energy_response is not None: serialization += "panel%s/adu_per_eV = %8.7e\n" % (panel_id_str, self.energy_response.m_as(1/electronvolt)) if self.photon_response is not None: serialization += "panel%s/adu_per_photon = %8.7e\n" % (panel_id_str, self.photon_response) if self.saturation_adu is not None: serialization += "panel%s/max_adu = %8.7e\n" % (panel_id_str, self.saturation_adu) if self.mask is not None: serialization += "panel%s/badpixmap = %s\n" % (panel_id_str, str(self.mask.magnitude)) serialization += "panel%s/badpixmap = %s\n" % (panel_id_str, str(self.mask.magnitude)) if self.good_bit_mask is not None: serialization += "panel%s/mask_good = %d\n" % (panel_id_str, self.good_bit_mask.magnitude) if self.bad_bit_mask is not None: serialization += "panel%s/mask_bad = %d\n" % (panel_id_str, self.bad_bit_mask.magnitude) if self.saturation_map is not None: serialization += "panel%s/saturation_map = %s\n" % (panel_id_str, str(self.saturation_map.magnitude) ) if 'CrystFELPhotonDiffractorParameters' not in caller.__str__(): serialization += "panel%s/px = %d\n" % (panel_id_str, self.number_of_pixels_fast) serialization += "panel%s/py = %d\n" % (panel_id_str, self.number_of_pixels_slow) serialization += "panel%s/pix_width = %8.7e\n" % (panel_id_str, self.pixel_size.m_as(meter)) serialization += "panel%s/d = %8.7e\n" % (panel_id_str, self.distance_from_interaction_plane.m_as(meter)) serialization += "panel%s/corner_x = %d\n" % (panel_id_str, self.corners["x"]) serialization += "\n" # Finally write the serialized panel. stream.write(serialization)
[docs]class DetectorGeometry(AbstractCalculatorParameters): """ Class representing the detector geometry. """ def __init__(self, panels=None, **kwargs ): """ Constructor of the DetectorGeometry class. :param panels: Single or list of detector panels that constitute the detector. :type panels: list, tuple, or instance of DetectorPanel :param kwargs: Key-value pairs to be passed to the parent class constructor. :type kwargs: dict """ super(DetectorGeometry, self).__init__(**kwargs) self.panels = panels @property def panels(self): return self.__panels @panels.setter def panels(self, val): # Check if single instance, convert to list if true. if not isinstance(val, (list, tuple)): val = [val] if not all([isinstance(v, DetectorPanel) for v in val]): raise TypeError( "Parameter 'panels' must be a list of instances, tuple of instances, or a single instance of the DetectorPanel class.") self.__panels = val def _setDefaults(self): """ Set default for required inherited parameters. """ self._AbstractCalculatorParameters__cpus_per_task_default = 1
[docs] def serialize(self, stream=None, caller=None): """ Serialize the geometry. :param stream: The stream to write the serialized geometry to (default sys.stdout). :type stream: File like object. """ # Handle default case. if stream is None: stream = sys.stdout # If this is a string, open a corresponding file. if isinstance( stream, str): with open(stream, 'w') as fstream: self._serialize(fstream, caller=caller) return if not hasattr(stream, "write"): raise IOError("The stream % is not writable." % (stream) ) # Loop over all panels and serialize each one. self._serialize(stream, caller=caller)
def _serialize(self, stream=sys.stdout, caller=None): """ Workhorse function for serialization. """ for i,panel in enumerate(self.panels): panel._serialize( stream, panel_id=i, caller=caller)
def _detectorPanelFromString( input_string, common_block=None): """ Construct a DetectorPanel instance from a serialized panel. :param input_string: The string from which to construct the panel. :param common_block: String representing a block of common parameters for a group of panels. """ # First treat common block common_dict = {} if common_block is not None: common_dict = _panelStringToDict(common_block) panel_dict = _panelStringToDict( input_string ) # Loop over common dict and fill into panel dict if not present there. for key,val in common_dict.items(): if panel_dict[key] is None: panel_dict[key] = val # Now put the data into the DetectorPanel instance. panel = DetectorPanel( ranges={"fast_scan_min" : float(panel_dict["min_fs"]), "fast_scan_max" : float(panel_dict["max_fs"]), "slow_scan_min" : float(panel_dict["min_ss"]), "slow_scan_max" : float(panel_dict["max_ss"]), }, corners={"x" : float(panel_dict["corner_x"]), "y" : float(panel_dict["corner_y"]), }, fast_scan_xyz=panel_dict["fs"], slow_scan_xyz=panel_dict["ss"], distance_from_interaction_plane=float(panel_dict["clen"])*meter, pixel_size=1.0/float(panel_dict["res"])*meter, ) if panel_dict["adu_per_photon"] is not None: panel.photon_response = float(panel_dict["adu_per_photon"]) if panel_dict["adu_per_eV"] is not None: panel.energy_response = float(panel_dict["adu_per_eV"])/electronvolt if panel_dict["coffset"] is not None: panel.distance_offset = float(panel_dict["coffset"])*meter else: panel.distance_offset=None if panel_dict["max_adu"] is not None: panel.saturation_adu = float(panel_dict["max_adu"]) else: panel.saturation_adu = None if panel_dict["mask"] is not None: panel.mask = numpy.array( eval(panel_dict["mask"]) ) else: panel.mask = None if panel_dict["mask_good"] is not None: panel.good_bit_mask = panel_dict["mask_good"] else: panel.good_bit_mask = None if panel_dict["mask_bad"] is not None: panel.bad_bit_mask = panel_dict["mask_bad"] else: panel.bad_bit_mask = None if panel_dict["saturation_map"] is not None: panel.saturation_map = panel_dict["saturation_map"] else: panel.saturation_map = None return panel def _panelStringToDict( input_string ): """ Convert a panel block string to a dictionary. """ # Get rid of panel prefix lines=input_string.split("\n") # Setup temporary dictionary. tmp_dict = { "min_fs" : None, "max_fs" : None, "min_ss" : None, "max_ss" : None, "corner_x" : None, "corner_y" : None, "fs" : None, "ss" : None, "clen" : None, "res" : None, "coffset" : None, "adu_per_eV" : None, "adu_per_photon" : None, "max_adu" : None, "mask" : None, "mask_good" : None, "mask_bad" : None, "saturation_map" : None, } # Loop over lines and extract data. for line in lines: # Bail out if not containing an assignment. if not "=" in line: continue # Get rid of white spaces. line = line.replace(" ","") line = line.replace("\t","") # Check for commented lines. if line[0] == ";": continue key_val = line.split("=") key = key_val[0] val = key_val[1] # Check for comments after assignment. if ";" in val: val=val.split(";")[0] # Get rid of panel prefix. key = key.split("/")[-1] # Store on dict. tmp_dict[key] = val return tmp_dict def _crystfel_format_vec(vec): """Convert an array of 3 numbers to CrystFEL format like "+1.0x -0.1y" from extra_geom """ s = '{:+}x {:+}y'.format(*vec[:2]) if vec[2] != 0: s += ' {:+}z'.format(vec[2]) return s def _detectorPanelFromDict(panel_dict): fs_vec = numpy.array([panel_dict['fsx'], panel_dict['fsy'], panel_dict['fsz']]) ss_vec = numpy.array([panel_dict['ssx'], panel_dict['ssy'], panel_dict['ssz']]) panel = DetectorPanel( ranges={"fast_scan_min" : float(panel_dict["min_fs"]), "fast_scan_max" : float(panel_dict["max_fs"]), "slow_scan_min" : float(panel_dict["min_ss"]), "slow_scan_max" : float(panel_dict["max_ss"]), }, corners={"x" : float(panel_dict["cnx"]), "y" : float(panel_dict["cny"]), }, fast_scan_xyz=_crystfel_format_vec(fs_vec), slow_scan_xyz=_crystfel_format_vec(ss_vec), distance_from_interaction_plane=float(panel_dict["clen"])*meter, pixel_size=1.0/float(panel_dict["res"])*meter, ) if panel_dict["adu_per_photon"] is not None: panel.photon_response = float(panel_dict["adu_per_photon"]) if panel_dict["adu_per_eV"] is not None: panel.energy_response = float(panel_dict["adu_per_eV"])/electronvolt if panel_dict["coffset"] is not None: panel.distance_offset = float(panel_dict["coffset"])*meter else: panel.distance_offset=None if panel_dict["max_adu"] is not None: panel.saturation_adu = float(panel_dict["max_adu"]) else: panel.saturation_adu = None if panel_dict["mask"] is not None: panel.mask = numpy.array( eval(panel_dict["mask"]) ) else: panel.mask = None try: panel.good_bit_mask = panel_dict["mask_good"] except KeyError: panel.good_bit_mask = None else: print("Unexpected error:", sys.exc_info()[0]) raise try: panel.bad_bit_mask = panel_dict["mask_bad"] except KeyError: panel.bad_bit_mask = None else: print("Unexpected error:", sys.exc_info()[0]) raise if panel_dict["satmap"] is not None: panel.saturation_map = panel_dict["satmap"] else: panel.saturation_map = None # saturation_map file missed here return panel
[docs]def detectorGeometryFromFile (input_file): # Create DetectorGeometry class from .geom file geometryDict = load_crystfel_geometry(input_file) panelDicts = geometryDict['panels'] panels = [_detectorPanelFromDict(panelDicts[panel]) for panel in panelDicts] return DetectorGeometry(panels=panels)
def _detectorGeometryFromString( input_string): """ Construct a DetectorGeometry instance from a serialized representation. :param input_string: The string from which to construct the panel. """ # Separate out panel blocks and common block. # Get rid of empty lines. while "\n\n" in input_string: input_string = input_string.replace("\n\n", "\n") # Get rid of white space. input_string = input_string.replace(" ", "") # Split into lines. lines = input_string.split("\n") common_block = "" panel_blocks = "" for line in lines: if line == "": continue if line[0] == ";": continue if "/" in line: panel_blocks += line+"\n" else: common_block += "COMMON_BLOCK/"+line+"\n" # If no separate panels are defined, return. if panel_blocks == "": return DetectorGeometry(panels=_detectorPanelFromString(common_block)) # Now separate the panel block into lines again and loop. lines = panel_blocks.split("\n") # Sort to make sure lines belonging to same block are adjacent. lines.sort() # Get panel identifiers. panel_identifiers = [] for line in lines: if line == "": continue panel_identifiers.append(line.split("/")[0]) # Turn into set of unique identifiers. panel_identifiers=list(set(panel_identifiers)) panel_identifiers.sort() # Gather all panel blocks into a list of strings, separated by newline to make fit for _detectorPanelFromString(). panel_blocks = ["",]*len(panel_identifiers) for line in lines: if line == "": continue # Get id. panel_identifier = line.split("/")[0] # Get index in list. block_index = panel_identifiers.index(panel_identifier) # Insert in correct position. panel_blocks[block_index] += line+"\n" # Deserialize the blocks. panels = [_detectorPanelFromString(block, common_block) for block in panel_blocks] # Return novel geometry instance. return DetectorGeometry(panels=panels)