diff --git a/cfel_crystfel.py b/cfel_crystfel.py new file mode 100644 index 0000000000000000000000000000000000000000..21951ef0ee7cc28a1f36111cc5390e1609ce8a6c --- /dev/null +++ b/cfel_crystfel.py @@ -0,0 +1,578 @@ +# This file is part of cfelpyutils. +# +# cfelpyutils 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. +# +# cfelpyutils 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 cfelpyutils. If not, see <http://www.gnu.org/licenses/>. +""" +Utilities for interoperability with the CrystFEL software package. + +This module contains reimplementation of Crystfel functions and utilities. +""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from collections import OrderedDict +from math import inf, sqrt +import re + + +def load_crystfel_geometry(filename): + + def assplode_algebraic(v): + items = [item for item in re.split('([+-])', v.strip()) if item != ''] + + if len(items) != 0 and items[0] not in ('+', '-'): + items.insert(0, '+') + + return [''.join((items[x], items[x + 1])) for x in range(0, len(items), 2)] + + def dir_conv(direction_x, direction_y, direction_z): + + direction = [direction_x, direction_y, direction_z] + + items = assplode_algebraic(value) + + if len(items) == 0: + raise RuntimeError('Invalid direction: {}.'.format(value)) + + for item in items: + axis = item[-1] + + if axis != 'x' and axis != 'y' and axis != 'z': + raise RuntimeError('Invalid Symbol: {} (must be x, y or z).'.format(axis)) + + if item[:-1] == '+': + v = '1.0' + elif item[:-1] == '-': + v = '-1.0' + else: + v = item[-1] + + if axis == 'x': + direction[0] = float(v) + elif axis == 'y': + direction[1] = float(v) + elif axis == 'z': + direction[2] = float(v) + + return direction + + def set_dim_structure_entry(k, v, pan): + + if pan['dim_structure'] is not None: + dim = pan['dim_structure'] + else: + dim = [] + + dim_index = int(k[3]) + + if dim_index > len(dim)-1: + for index in range(len(dim), dim_index + 1): + dim.append(None) + + if v == 'ss' or v == 'fs' or v == '%': + dim[dim_index] = v + else: + raise RuntimeError('Invalid dim entry: {}.'.format(v)) + + def parse_field_for_panel(k, v, pan): + + if k == 'min_fs': + pan['origin_min_fs'] = int(v) + pan['min_fs'] = int(v) + + elif k == 'max_fs': + pan['origin_max_fs'] = int(v) + pan['max_fs'] = int(v) + + elif k == 'min_ss': + pan['origin_min_ss'] = int(v) + pan['min_ss'] = int(v) + + elif k == 'max_ss': + pan['origin_max_ss'] = int(v) + pan['max_ss'] = int(v) + + elif k == 'corner_x': + pan['cnx'] = float(v) + + elif k == 'corner_y': + pan['cny'] = float(v) + + elif k == 'rail_direction': + try: + pan['rail_x'], pan['rail_y'], pan['rail_z'] = dir_conv(pan['rail_x'], + pan['rail_y'], + pan['rail_z']) + except RuntimeError as e: + raise RuntimeError('Invalid rail direction. ', e) + + elif k == 'clen_for_centering': + pan['clen_for_centering'] = float(v) + + elif k == 'adu_per_eV': + pan['adu_per_eV'] = float(v) + + elif k == 'adu_per_photon': + pan['adu_per_photon'] = float(v) + + elif k == 'rigid_group': + pan['rigid_group'] = v + + elif k == 'clen': + try: + pan['clen'] = float(v) + pan['clen_from'] = None + except ValueError: + pan['clen'] = -1 + pan['clen_from'] = v + + elif k == 'data': + if not v.startswith('/'): + raise RuntimeError('Invalid data location: {}'.format(v)) + pan['data'] = v + + elif k == 'mask': + if not v.startswith('/'): + raise RuntimeError('Invalid data location: {}'.format(v)) + pan['mask'] = v + + elif k == 'mask_file': + pan['mask_file'] = v + + elif k == 'saturation_map': + pan['saturation_map'] = v + + elif k == 'saturation_map_file': + pan['saturation_map_file'] = v + + elif k == 'coffset': + pan['coffset'] = float(v) + + elif k == 'res': + pan['res'] = float(v) + + elif k == 'max_adu': + pan['max_adu'] = v + + elif k == 'badrow_direction': + if v == 'x': + pan['badrow'] = 'f' + elif v == 'y': + pan['badrow'] = 's' + elif v == 'f': + pan['badrow'] = 'f' + elif v == 's': + pan['badrow'] = 's' + elif v == '-': + pan['badrow'] = '-' + else: + print('badrow_direction must be x, t, f, s, or \'-\'') + print('Assuming \'-\'.') + pan['badrow'] = '-' + + elif k == 'no_index': + pan['no_index'] = bool(v) + + elif k == 'fs': + try: + pan['fsx'], pan['fsy'], pan['fsz'] = dir_conv(pan['fsx'], pan['fsy'], + pan['fsz']) + + except RuntimeError as e: + raise RuntimeError('Invalid fast scan direction. ', e) + + elif k == 'ss': + try: + pan['ssx'], pan['ssy'], pan['ssz'] = dir_conv(pan['ssx'], pan['ssy'], + pan['ssz']) + + except RuntimeError as e: + raise RuntimeError('Invalid slow scan direction. ', e) + + elif k.startswith('dim'): + set_dim_structure_entry(k, v, pan) + + else: + raise RuntimeError('Unrecognised field: {}'.format(k)) + + def parse_top_level(k, v, det, b, pan): + + if k == 'mask_bad': + try: + det['mask_bad'] = int(v) + except ValueError: + det['mask_bad'] = int(v, 16) + + elif k == 'mask_good': + try: + det['mask_good'] = int(v) + except ValueError: + det['mask_good'] = int(v, 16) + + elif k == 'coffset': + pan['coffset'] = float(v) + + elif k == 'photon_energy': + if v.startswith('/'): + b['photon_energy'] = 0.0 + b['photon_energy_from'] = v + else: + b['photon_energy'] = float(v) + b['photon_energy_from'] = None + + elif k == 'photon_energy_scale': + b['photon_energy_scale'] = float(v) + + elif k == 'peak_info_location': + det['peak_info_location'] = v + + elif k.startswith('rigid_group') and not k.startswith('rigid_group_collection'): + det['rigid_groups'][k[12:]] = v.split(',') + + elif k.startswith('rigid_group_collection'): + det['rigid_group_collections'][k[23:]] = v.split(',') + + else: + parse_field_for_panel(k, v, pan) + + def check_bad_fsss(bad, is_fsss): + + if bad['is_fsss'] == 99: + bad['is_fsss'] = is_fsss + return + + if is_fsss != bad['is_fsss']: + raise RuntimeError("You can't mix x/y and fs/ss in a bad region") + + return + + def parse_field_bad(k, v, bad): + + if k == 'min_x': + bad['min_x'] = float(v) + check_bad_fsss(bad, False) + elif k == 'max_x': + bad['max_x'] = float(v) + check_bad_fsss(bad, False) + elif k == 'min_y': + bad['min_y'] = float(v) + check_bad_fsss(bad, False) + elif k == 'max_y': + bad['max_y'] = float(v) + check_bad_fsss(bad, False) + elif k == 'min_fs': + bad['min_fs'] = int(v) + check_bad_fsss(bad, True) + elif k == 'max_fs': + bad['max_fs'] = int(v) + check_bad_fsss(bad, True) + elif k == 'min_ss': + bad['min_ss'] = int(v) + check_bad_fsss(bad, True) + elif k == 'max_ss': + bad['max_ss'] = int(v) + check_bad_fsss(bad, True) + elif k == 'panel': + bad['panel'] = v + else: + raise RuntimeError('Unrecognised field: {}'.format(k)) + + return + + def check_point(n, pan, fs, ss, min_d, max_d, det): + + xs = fs * pan['fsx'] + ss * pan['ssx'] + ys = fs * pan['fsy'] + ss * pan['ssy'] + + rx = (xs + pan['cnx']) / pan['res'] + ry = (ys + pan['cny']) / pan['res'] + + dist = sqrt(rx * rx + ry * ry) + + if dist > max_d: + det['furthest_out_panel'] = n + det['furthest_out_fs'] = fs + det['furthest_out_ss'] = ss + max_d = dist + elif dist < min_d: + det['furthest_in_panel'] = n + det['furthest_in_fs'] = fs + det['furthest_in_ss'] = ss + min_d = dist + + return min_d, max_d + + def find_min_max_d(det): + + min_d = inf + max_d = 0.0 + + for n, pan in det['panels'].items(): + min_d, max_d = check_point(n, pan, 0, 0, min_d, max_d, det) + min_d, max_d = check_point(n, pan, pan['w'], 0, min_d, max_d, det) + min_d, max_d = check_point(n, pan, 0, pan['h'], min_d, max_d, det) + min_d, max_d = check_point(n, pan, pan['w'], pan['h'], min_d, max_d, det) + + fh = open(filename, 'r') + + beam = { + 'photon_energy': 0.0, + 'photon_energy_from': None, + 'photon_energy_scale': 1 + } + + detector = { + 'panels': OrderedDict(), + 'bad': OrderedDict(), + 'mask_good': 0, + 'mask_bad': 0, + 'rigid_groups': {}, + 'rigid_group_collections': {} + } + + default_panel = { + 'cnx': None, + 'cny': None, + 'clen': None, + 'coffset': 0.0, + 'res': -1.0, + 'badrow': '-', + 'no_index': False, + 'fsx': 1.0, + 'fsy': 0.0, + 'fsz': 0.0, + 'ssx': 0.0, + 'ssy': 1.0, + 'ssz': 0.0, + 'rail_x': None, + 'rail_y': None, + 'rail_z': None, + 'clen_for_centering': None, + 'adu_per_eV': None, + 'adu_per_photon': None, + 'max_adu': inf, + 'mask': None, + 'mask_file': None, + 'satmap': None, + 'satmap_file': None, + 'data': None, + 'dim_structure': None, + 'name': '' + } + + default_bad_region = { + 'min_x': None, + 'max_x': None, + 'min_y': None, + 'max_y': None, + 'min_fs': 0, + 'max_fs': 0, + 'min_ss': 0, + 'max_ss': 0, + 'is_fsss': 99, + 'name': '' + } + + default_dim = ['ss', 'fs'] + + fhlines = fh.readlines() + + for line in fhlines: + + if line.startswith(';'): + continue + + line_without_comments = line.strip().split(';')[0] + line_items = re.split('([ \t])', line_without_comments) + line_items = [item for item in line_items if item not in ('', ' ', '\t')] + + if len(line_items) < 3: + continue + + value = ''.join(line_items[2:]) + + if line_items[1] != '=': + continue + + path = re.split('(/)', line_items[0]) + path = [item for item in path if item not in '/'] + + if len(path) < 2: + parse_top_level(line_items[0], value, detector, beam, default_panel) + continue + + curr_bad = None + curr_panel = None + + if path[0].startswith('bad'): + + if path[0] in detector['bad']: + curr_bad = detector['bad'][path[0]] + else: + curr_bad = default_bad_region.copy() + detector['bad'][path[0]] = curr_bad + + else: + + if path[0] in detector['panels']: + curr_panel = detector['panels'][path[0]] + else: + curr_panel = default_panel.copy() + detector['panels'][path[0]] = curr_panel + + if curr_panel is not None: + parse_field_for_panel(path[1], value, curr_panel) + else: + parse_field_bad(path[1], value, curr_bad) + + if len(detector['panels']) == 0: + raise RuntimeError("No panel descriptions in geometry file.") + + num_placeholders_in_panels = None + + for panel in detector['panels'].values(): + + if panel['dim_structure'] is not None: + curr_num_placeholders = panel['dim_structure'].values().count('%') + else: + curr_num_placeholders = 0 + + if num_placeholders_in_panels is None: + num_placeholders_in_panels = curr_num_placeholders + else: + if curr_num_placeholders != num_placeholders_in_panels: + raise RuntimeError('All panels\' data and mask entries must have the same number of placeholders.') + + num_placeholders_in_masks = None + + for panel in detector['panels'].values(): + + if panel['mask'] is not None: + curr_num_placeholders = panel['mask'].count('%') + else: + curr_num_placeholders = 0 + + if num_placeholders_in_masks is None: + num_placeholders_in_masks = curr_num_placeholders + else: + if curr_num_placeholders != num_placeholders_in_masks: + raise RuntimeError('All panels\' data and mask entries must have the same number of placeholders.') + + if num_placeholders_in_masks > num_placeholders_in_panels: + raise RuntimeError('Number of placeholders in mask cannot be larget than for data.') + + dim_length = None + + for panel in detector['panels'].values(): + + if panel['dim_structure'] is None: + panel['dim_structure'] = default_dim.copy() + + found_ss = False + found_fs = False + found_placeholder = False + + for entry in panel['dim_structure']: + if entry is None: + raise RuntimeError('Not all dim entries are defined for all panels.') + elif entry == 'ss': + if found_ss is True: + raise RuntimeError('Only one slow scan dim coordinate is allowed.') + else: + found_ss = True + elif entry == 'fs': + if found_fs is True: + raise RuntimeError('Only one fast scan dim coordinate is allowed.') + else: + found_fs = True + elif entry == '%': + if found_placeholder is True: + raise RuntimeError('Only one placeholder dim coordinate is allowed.') + else: + found_placeholder = True + + for panel in detector['panels'].values(): + + if panel['origin_min_fs'] < 0: + raise RuntimeError('Please specify the minimum fs coordinate for panel {}.'.format(panel['name'])) + + if panel['origin_max_fs'] < 0: + raise RuntimeError('Please specify the maximum fs coordinate for panel {}.'.format(panel['name'])) + + if panel['origin_min_ss'] < 0: + raise RuntimeError('Please specify the minimum ss coordinate for panel {}.'.format(panel['name'])) + + if panel['origin_max_ss'] < 0: + raise RuntimeError('Please specify the maximum ss coordinate for panel {}.'.format(panel['name'])) + + if panel['cnx'] is None: + raise RuntimeError('Please specify the corner X coordinate for panel {}.'.format(panel['name'])) + + if panel['clen'] is None and panel['clen_from'] is None: + raise RuntimeError('Please specify the camera length for panel {}.'.format(panel['name'])) + + if panel['res'] < 0: + raise RuntimeError('Please specify the resolution or panel {}.'.format(panel['name'])) + + if panel['adu_per_eV'] is None and panel['adu_per_photon'] is None: + raise RuntimeError('Please specify either adu_per_eV or adu_per_photon for ' + 'panel {}.'.format(panel['name'])) + + if panel['clen_for_centering'] is None and panel['rail_x'] is not None: + raise RuntimeError('You must specify clen_for_centering if you specify the rail direction ' + '(panel {})'.format(panel['name'])) + + if panel['rail_x'] is None: + panel['rail_x'] = 0.0 + panel['rail_y'] = 0.0 + panel['rail_z'] = 1.0 + + if panel['clen_for_centering'] is None: + panel['clen_for_centering'] = 0.0 + + panel['w'] = panel['origin_max_fs'] - panel['origin_min_fs'] + 1 + panel['h'] = panel['origin_max_ss'] - panel['origin_min_ss'] + 1 + + for bad_region in detector['bad']: + if bad_region['is_fsss'] == 99: + raise RuntimeError('Please specify the coordinate ranges for bad region {}.'.format(bad_region['name'])) + + for group in detector['rigid_groups'].keys(): + for name in detector['rigid_groups'][group]: + if name not in detector['panels']: + raise RuntimeError('Cannot add panel to rigid_group. Panel not found: {}'.format(name)) + + for group_collection in detector['rigid_group_collections'].keys(): + for name in detector['rigid_group_collections'][group_collection]: + if name not in detector['rigid_groups']: + raise RuntimeError('Cannot add rigid_group to collection. Rigid group not found: {}'.format(name)) + + for panel in detector['panels'].values(): + + d = panel['fsx'] * panel['ssy'] - panel['ssx'] * panel['fsy'] + + if d == 0.0: + raise RuntimeError('Panel {} transformation is singluar.') + + panel['xfs'] = panel['ssy'] / d + panel['yfs'] = panel['ssx'] / d + panel['xss'] = panel['fsy'] / d + panel['yss'] = panel['fsx'] / d + + find_min_max_d(detector) + fh.close() + + return detector