diff --git a/reciprocalspaceship/io/crystfel.py b/reciprocalspaceship/io/crystfel.py index e40bda76..57e8db1c 100644 --- a/reciprocalspaceship/io/crystfel.py +++ b/reciprocalspaceship/io/crystfel.py @@ -1,39 +1,68 @@ +import re +from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor +from mmap import mmap +from multiprocessing import Pool, freeze_support +from os.path import getsize + import numpy as np import pandas as pd -from os.path import getsize -from reciprocalspaceship import DataSet,DataSeries -from reciprocalspaceship.utils import angle_between,eV2Angstroms -from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor -from multiprocessing import Pool,freeze_support -from reciprocalspaceship import concat -from mmap import mmap -import re +from reciprocalspaceship import DataSeries, DataSet, concat +from reciprocalspaceship.utils import angle_between, eV2Angstroms -#See Rupp Table 5-2 +# See Rupp Table 5-2 _cell_constraints = { - 'triclinic' : lambda x: x, - 'orthorhombic' : lambda x: [x[0], x[1], x[2], 90., 90., 90.], - 'monoclinic' : lambda x: [x[0], x[1], x[2], 90., x[4], 90.], - 'hexagonal' : lambda x: [0.5*(x[0] + x[1]), 0.5*(x[0] + x[1]), x[2], 90., 90., 120.], - 'rhombohedral' : lambda x: [0.5*(x[0] + x[1]), 0.5*(x[0] + x[1]), x[2], 90., 90., 120.], - 'cubic' : lambda x: [np.mean(x[:3]), np.mean(x[:3]), np.mean(x[:3]), 90., 90., 90.], - 'tetragonal' : lambda x: [0.5*(x[0] + x[1]), 0.5*(x[0] + x[1]), x[2], 90., 90., 90.], + "triclinic": lambda x: x, + "orthorhombic": lambda x: [x[0], x[1], x[2], 90.0, 90.0, 90.0], + "monoclinic": lambda x: [x[0], x[1], x[2], 90.0, x[4], 90.0], + "hexagonal": lambda x: [ + 0.5 * (x[0] + x[1]), + 0.5 * (x[0] + x[1]), + x[2], + 90.0, + 90.0, + 120.0, + ], + "rhombohedral": lambda x: [ + 0.5 * (x[0] + x[1]), + 0.5 * (x[0] + x[1]), + x[2], + 90.0, + 90.0, + 120.0, + ], + "cubic": lambda x: [ + np.mean(x[:3]), + np.mean(x[:3]), + np.mean(x[:3]), + 90.0, + 90.0, + 90.0, + ], + "tetragonal": lambda x: [ + 0.5 * (x[0] + x[1]), + 0.5 * (x[0] + x[1]), + x[2], + 90.0, + 90.0, + 90.0, + ], } # See crystFEL API reference here: https://www.desy.de/~twhite/crystfel/reference/stream_8h.html _block_markers = { - "geometry" : (r"----- Begin geometry file -----", r"----- End geometry file -----"), - "chunk" : (r"----- Begin chunk -----", r"----- End chunk -----"), - "cell" : (r"----- Begin unit cell -----", r"----- End unit cell -----"), - "peaks" : (r"Peaks from peak search", r"End of peak list"), - "crystal" : (r"--- Begin crystal", r"--- End crystal"), - "reflections" : (r"Reflections measured after indexing", r"End of reflections"), + "geometry": (r"----- Begin geometry file -----", r"----- End geometry file -----"), + "chunk": (r"----- Begin chunk -----", r"----- End chunk -----"), + "cell": (r"----- Begin unit cell -----", r"----- End unit cell -----"), + "peaks": (r"Peaks from peak search", r"End of peak list"), + "crystal": (r"--- Begin crystal", r"--- End crystal"), + "reflections": (r"Reflections measured after indexing", r"End of reflections"), } + class StreamLoader(object): """ - An object that loads stream files into rs.DataSet objects in parallel. + An object that loads stream files into rs.DataSet objects in parallel. Attributes ---------- block_regex_bytes : dict @@ -41,25 +70,26 @@ class StreamLoader(object): block_regex : dict A dictionary of compiled regular expressions that operate on byte strings """ + peak_list_columns = { - "H" : 0, - "K" : 1, - "L" : 2, - "I" : 3, - "SigI" : 4, - "XDET" : 5, - "YDET" : 6, - "s1x" : 7, - "s1y" : 8, - "s1z" : 9, - "ewald_offset" : 10, - "angular_ewald_offset" : 11, - "ewald_offset_x" : 12, - "ewald_offset_y" : 13, - "ewald_offset_z" : 14, + "H": 0, + "K": 1, + "L": 2, + "I": 3, + "SigI": 4, + "XDET": 5, + "YDET": 6, + "s1x": 7, + "s1y": 8, + "s1z": 9, + "ewald_offset": 10, + "angular_ewald_offset": 11, + "ewald_offset_x": 12, + "ewald_offset_y": 13, + "ewald_offset_z": 14, } - def __init__(self, filename : str, encoding='utf-8'): + def __init__(self, filename: str, encoding="utf-8"): self.filename = filename self.file_size = getsize(filename) self.encoding = encoding @@ -67,43 +97,55 @@ def __init__(self, filename : str, encoding='utf-8'): self.block_regex_bytes = {} # Set up all the regular expressions for finding block boundaries - for k,(beginning, ending) in _block_markers.items(): - self.block_regex[k + '_begin'] = re.compile(beginning) - self.block_regex[k + '_end'] = re.compile(ending) + for k, (beginning, ending) in _block_markers.items(): + self.block_regex[k + "_begin"] = re.compile(beginning) + self.block_regex[k + "_end"] = re.compile(ending) self.block_regex[k] = re.compile( - f"(?s){beginning}\n(?P.*?)\n{ending}") - - self.block_regex_bytes[k + '_begin'] = re.compile(beginning.encode(self.encoding)) - self.block_regex_bytes[k + '_end'] = re.compile(ending.encode(self.encoding)) + f"(?s){beginning}\n(?P.*?)\n{ending}" + ) + + self.block_regex_bytes[k + "_begin"] = re.compile( + beginning.encode(self.encoding) + ) + self.block_regex_bytes[k + "_end"] = re.compile( + ending.encode(self.encoding) + ) self.block_regex_bytes[k] = re.compile( - f"(?s){beginning}\n(?P.*?)\n{ending}".encode(self.encoding)) + f"(?s){beginning}\n(?P.*?)\n{ending}".encode( + self.encoding + ) + ) - self.re_abcstar = re.compile('[abc]star =.+\n') - self.re_photon_energy = re.compile('photon_energy_eV =.+\n') + self.re_abcstar = re.compile("[abc]star =.+\n") + self.re_photon_energy = re.compile("photon_energy_eV =.+\n") self.re_chunk_metadata = { - 'Image filename' : re.compile(r'(?<=Image filename: ).+(?=\n)'), - 'Event' : re.compile(r'(?<=Event: ).+(?=\n)'), - 'Image serial number:' : re.compile(r'(?<=Image serial number: ).+(?=\n)'), - 'indexed_by' : re.compile(r'(?<=indexed_by \= ).+(?=\n)'), - 'photon_energy_eV' : re.compile(r'(?<=photon_energy_eV \= ).+(?=\n)'), - 'beam_divergence' : re.compile(r'(?<=beam_divergence \= ).+(?=\n)'), - 'beam_bandwidth' : re.compile(r'(?<=beam_bandwidth \= ).+(?=\n)'), + "Image filename": re.compile(r"(?<=Image filename: ).+(?=\n)"), + "Event": re.compile(r"(?<=Event: ).+(?=\n)"), + "Image serial number:": re.compile(r"(?<=Image serial number: ).+(?=\n)"), + "indexed_by": re.compile(r"(?<=indexed_by \= ).+(?=\n)"), + "photon_energy_eV": re.compile(r"(?<=photon_energy_eV \= ).+(?=\n)"), + "beam_divergence": re.compile(r"(?<=beam_divergence \= ).+(?=\n)"), + "beam_bandwidth": re.compile(r"(?<=beam_bandwidth \= ).+(?=\n)"), } self.re_crystal_metadata = { - 'Cell parameters' : re.compile(r'(?<=Cell parameters).+(?=\n)'), - 'astar' : re.compile(r'(?<=astar = ).+(?=\n)'), - 'bstar' : re.compile(r'(?<=bstar = ).+(?=\n)'), - 'cstar' : re.compile(r'(?<=cstar = ).+(?=\n)'), - 'lattice_type' : re.compile(r'(?<=lattice_type = ).+(?=\n)'), - 'centering' : re.compile(r'(?<=centering = ).+(?=\n)'), - 'unique_axis' : re.compile(r'(?<=unique_axis = ).+(?=\n)'), - 'profile_radius' : re.compile(r'(?<=profile_radius = ).+(?=\n)'), - 'predict_refine/det_shift' : re.compile(r'(?<=predict_refine/det_shift ).+(?=\n)'), - 'predict_refine/R' : re.compile(r'(?<=predict_refine/R ).+(?=\n)'), - 'diffraction_resolution_limit' : re.compile(r'(?<=diffraction_resolution_limit = ).+(?=\n)'), - 'num_reflections' : re.compile(r'(?<=num_reflections = ).+(?=\n)'), + "Cell parameters": re.compile(r"(?<=Cell parameters).+(?=\n)"), + "astar": re.compile(r"(?<=astar = ).+(?=\n)"), + "bstar": re.compile(r"(?<=bstar = ).+(?=\n)"), + "cstar": re.compile(r"(?<=cstar = ).+(?=\n)"), + "lattice_type": re.compile(r"(?<=lattice_type = ).+(?=\n)"), + "centering": re.compile(r"(?<=centering = ).+(?=\n)"), + "unique_axis": re.compile(r"(?<=unique_axis = ).+(?=\n)"), + "profile_radius": re.compile(r"(?<=profile_radius = ).+(?=\n)"), + "predict_refine/det_shift": re.compile( + r"(?<=predict_refine/det_shift ).+(?=\n)" + ), + "predict_refine/R": re.compile(r"(?<=predict_refine/R ).+(?=\n)"), + "diffraction_resolution_limit": re.compile( + r"(?<=diffraction_resolution_limit = ).+(?=\n)" + ), + "num_reflections": re.compile(r"(?<=num_reflections = ).+(?=\n)"), } # TODO: replace these with the faster, non variabled length equivalents @@ -116,24 +158,24 @@ def __init__(self, filename : str, encoding='utf-8'): def extract_target_unit_cell(self) -> list | None: """ - Search the file header for target unit cell parameters. + Search the file header for target unit cell parameters. """ header = self.extract_file_header() cell = None lattice_type = None - for line in header.split('\n'): - if line.startswith('a = '): + for line in header.split("\n"): + if line.startswith("a = "): idx = 0 - elif line.startswith('b = '): + elif line.startswith("b = "): idx = 1 - elif line.startswith('c = '): + elif line.startswith("c = "): idx = 2 - elif line.startswith('al = '): + elif line.startswith("al = "): idx = 3 - elif line.startswith('be = '): + elif line.startswith("be = "): idx = 4 - elif line.startswith('ga = '): + elif line.startswith("ga = "): idx = 5 else: idx = None @@ -142,7 +184,7 @@ def extract_target_unit_cell(self) -> list | None: cell = [None] * 6 value = float(line.split()[2]) cell[idx] = value - if line.startswith('lattice_type ='): + if line.startswith("lattice_type ="): lattice_type = line.split()[-1] if lattice_type is not None: @@ -153,103 +195,111 @@ def extract_file_header(self) -> str: """ Extract all the data prior to first chunk and return it as a string. """ - with open(self.filename, 'r+') as f: + with open(self.filename, "r+") as f: memfile = mmap(f.fileno(), 0) - match = self.block_regex_bytes['chunk_begin'].search(memfile) + match = self.block_regex_bytes["chunk_begin"].search(memfile) header = memfile.read(match.start()).decode() return header @property def available_column_names(self) -> list: - """ Keys which can be passed to parallel_read_crystfel to customize the peak list output """ + """Keys which can be passed to parallel_read_crystfel to customize the peak list output""" return list(self.peak_list_columns.keys()) @property def available_chunk_metadata_keys(self) -> list: - """ Keys which can be passed to parallel_read_crystfel to customize the chunk level metadata """ + """Keys which can be passed to parallel_read_crystfel to customize the chunk level metadata""" return list(self.re_chunk_metadata.keys()) @property def available_crystal_metadata_keys(self) -> list: - """ Keys which can be passed to parallel_read_crystfel to customize the crystal level metadata """ + """Keys which can be passed to parallel_read_crystfel to customize the crystal level metadata""" return list(self.re_crystal_metadata.keys()) - def parallel_read_crystfel(self, wavelength=None, chunk_metadata_keys=None, crystal_metadata_keys=None, peak_list_columns=None) -> list: + def parallel_read_crystfel( + self, + wavelength=None, + chunk_metadata_keys=None, + crystal_metadata_keys=None, + peak_list_columns=None, + ) -> list: """ - Parse a CrystFEL stream file using multiple processors. Parallelization depends on the ray library (https://www.ray.io/). + Parse a CrystFEL stream file using multiple processors. Parallelization depends on the ray library (https://www.ray.io/). If ray is unavailable, this method falls back to serial processing on one CPU. Ray is not a dependency of reciprocalspaceship and will not be installed automatically. Users must manually install it prior to calling this method. PARAMETERS ---------- wavelength : float - Override the wavelength with this value. Wavelength is used to compute Ewald offsets. + Override the wavelength with this value. Wavelength is used to compute Ewald offsets. chunk_metadata_keys : list - A list of metadata_keys which will be returned in the resulting dictionaries under the 'chunk_metadata' entry. + A list of metadata_keys which will be returned in the resulting dictionaries under the 'chunk_metadata' entry. A list of possible keys is stored as stream_loader.available_chunk_metadata_keys crytal_metadata_keys : list - A list of metadata_keys which will be returned in the resulting dictionaries under the 'crystal_metadata' entry. + A list of metadata_keys which will be returned in the resulting dictionaries under the 'crystal_metadata' entry. A list of possible keys is stored as stream_loader.available_crystal_metadata_keys peak_list_columns : list - A list of columns to include in the peak list numpy arrays. + A list of columns to include in the peak list numpy arrays. A list of possible column names is stored as stream_loader.available_column_names. RETURNS ------- chunks : list A list of dictionaries containing the per-chunk data. The 'peak_lists' item contains a - numpy array with shape n x 14 with the following information. - h, k, l, I, SIGI, peak, background, fs/px, ss/px, s1_x, s1_y, s1_z, + numpy array with shape n x 14 with the following information. + h, k, l, I, SIGI, peak, background, fs/px, ss/px, s1_x, s1_y, s1_z, ewald_offset, angular_ewald_offset """ if peak_list_columns is not None: peak_list_columns = [self.peak_list_columns[s] for s in peak_list_columns] - with open(self.filename, 'r+') as f: + with open(self.filename, "r+") as f: memfile = mmap(f.fileno(), 0) beginnings_and_ends = zip( - self.block_regex_bytes['chunk_begin'].finditer(memfile), - self.block_regex_bytes['chunk_end'].finditer(memfile), - ) + self.block_regex_bytes["chunk_begin"].finditer(memfile), + self.block_regex_bytes["chunk_end"].finditer(memfile), + ) try: import ray + ray.init() @ray.remote - def parse_chunk(loader : StreamLoader, *args): + def parse_chunk(loader: StreamLoader, *args): return loader._parse_chunk(*args) result_ids = [] - for begin,end in beginnings_and_ends: + for begin, end in beginnings_and_ends: result_ids.append( parse_chunk.remote( self, - begin.start(), - end.end(), + begin.start(), + end.end(), wavelength, chunk_metadata_keys, crystal_metadata_keys, - peak_list_columns + peak_list_columns, ) ) - - results = ray.get(result_ids) + + results = ray.get(result_ids) ray.shutdown() except ModuleNotFoundError: import warnings + msg = "ray (https://www.ray.io/) not found, cannot load in parallel... falling back to single cpu" warnings.warn(msg) results = [] - for begin,end in beginnings_and_ends: + for begin, end in beginnings_and_ends: results.append( self._parse_chunk( - begin.start(), - end.end(), + begin.start(), + end.end(), wavelength, chunk_metadata_keys, crystal_metadata_keys, - peak_list_columns + peak_list_columns, ) ) return results @@ -274,14 +324,22 @@ def _extract_crystal_metadata(self, xtal_text, metadata_keys=None): result[k] = v return result - def _parse_chunk(self, start, end, wavelength, chunk_metadata_keys, crystal_metadata_keys, peak_list_columns): - with open(self.filename, 'r') as f: + def _parse_chunk( + self, + start, + end, + wavelength, + chunk_metadata_keys, + crystal_metadata_keys, + peak_list_columns, + ): + with open(self.filename, "r") as f: f.seek(start) data = f.read(end - start) if wavelength is None: ev_match = self.re_photon_energy.search(data) - ev_line = data[ev_match.start():ev_match.end()] + ev_line = data[ev_match.start() : ev_match.end()] photon_energy = np.float32(ev_line.split()[2]) lambda_inv = np.reciprocal(eV2Angstroms(photon_energy)) else: @@ -293,53 +351,59 @@ def _parse_chunk(self, start, end, wavelength, chunk_metadata_keys, crystal_meta crystal_metadata = [] header = None for xmatch in self.re_crystal.finditer(data): - xdata = data[xmatch.start():xmatch.end()] + xdata = data[xmatch.start() : xmatch.end()] if header is None: - header = data[:xmatch.start()] - - #crystal_metadata.append(self._extract_crystal_metadata(xdata)) - A = np.loadtxt( - self.re_abcstar.findall(xdata), - usecols = [2,3,4], - dtype='float32', - ).T / 10. + header = data[: xmatch.start()] + + # crystal_metadata.append(self._extract_crystal_metadata(xdata)) + A = ( + np.loadtxt( + self.re_abcstar.findall(xdata), + usecols=[2, 3, 4], + dtype="float32", + ).T + / 10.0 + ) a_matrices.append(A) for pmatch in self.re_refls.finditer(xdata): - pdata = xdata[pmatch.start():pmatch.end()] + pdata = xdata[pmatch.start() : pmatch.end()] crystal_metadata.append( - self._extract_crystal_metadata(xdata, crystal_metadata_keys) + self._extract_crystal_metadata(xdata, crystal_metadata_keys) ) peak_array = np.loadtxt( - pdata.split('\n')[2:-1], + pdata.split("\n")[2:-1], usecols=(0, 1, 2, 3, 4, 5, 6, 7, 8), - dtype='float32', + dtype="float32", ) - s0 = np.array([0, 0, lambda_inv], dtype='float32').T - q = (A @ peak_array[:,:3].T).T + s0 = np.array([0, 0, lambda_inv], dtype="float32").T + q = (A @ peak_array[:, :3].T).T s1 = q + s0 # This is way faster than np.linalg.norm for small dimensions - x,y,z = s1.T - s1_norm = np.sqrt(x*x + y*y + z*z) + x, y, z = s1.T + s1_norm = np.sqrt(x * x + y * y + z * z) ewald_offset = s1_norm - lambda_inv # project calculated s1 onto the ewald sphere - s1_obs = lambda_inv * s1 / s1_norm[:,None] + s1_obs = lambda_inv * s1 / s1_norm[:, None] # Compute the angular ewald offset q_obs = s1_obs - s0 qangle = np.sign(ewald_offset) * angle_between(q, q_obs) - peak_array = np.concatenate(( - peak_array, - s1, - ewald_offset[:,None], - qangle[:,None], - s1_obs - s1, #Ewald offset vector - ), axis=-1) + peak_array = np.concatenate( + ( + peak_array, + s1, + ewald_offset[:, None], + qangle[:, None], + s1_obs - s1, # Ewald offset vector + ), + axis=-1, + ) if peak_list_columns is not None: - peak_array = peak_array[:,peak_list_columns] + peak_array = peak_array[:, peak_list_columns] peak_lists.append(peak_array) if header is None: @@ -347,9 +411,9 @@ def _parse_chunk(self, start, end, wavelength, chunk_metadata_keys, crystal_meta chunk_metadata = self._extract_chunk_metadata(header, chunk_metadata_keys) result = { - 'wavelength' : wavelength, - 'A_matrices' : a_matrices, - 'peak_lists' : peak_lists, + "wavelength": wavelength, + "A_matrices": a_matrices, + "peak_lists": peak_lists, } if chunk_metadata_keys is not None: result[chunk_metadata_keys] = chunk_metadata @@ -357,12 +421,15 @@ def _parse_chunk(self, start, end, wavelength, chunk_metadata_keys, crystal_meta result[crystal_metadata_keys] = crystal_metadata return result -def read_crystfel(streamfile: str, spacegroup=None, encoding='utf-8', columns=None) -> DataSet: + +def read_crystfel( + streamfile: str, spacegroup=None, encoding="utf-8", columns=None +) -> DataSet: """ Initialize attributes and populate the DataSet object with data from a CrystFEL stream with indexed reflections. This is the output format used by CrystFEL software when processing still diffraction data. - This method is parallelized across CPUs speed up parsing. Parallelization depends on the ray library (https://www.ray.io/). + This method is parallelized across CPUs speed up parsing. Parallelization depends on the ray library (https://www.ray.io/). If ray is unavailable, this method falls back to serial processing on one CPU. Ray is not a dependency of reciprocalspaceship and will not be installed automatically. Users must manually install it prior to calling this method. @@ -373,11 +440,11 @@ def read_crystfel(streamfile: str, spacegroup=None, encoding='utf-8', columns=No spacegroup : gemmi.SpaceGroup or int or string (optional) optionally set the spacegroup of the returned DataSet. encoding : str - The type of byte-encoding (optional, 'utf-8'). + The type of byte-encoding (optional, 'utf-8'). columns : list (optional) - Optionally specify the columns of the output by a list of strings. - The default list is: - [ "H", "K", "L", "I", "SigI", "BATCH", "s1x", "s1y", "s1z", "ewald_offset", + Optionally specify the columns of the output by a list of strings. + The default list is: + [ "H", "K", "L", "I", "SigI", "BATCH", "s1x", "s1y", "s1z", "ewald_offset", "angular_ewald_offset", "XDET", "YDET" ] See `rs.io.crystfel.StreamLoader().available_column_names` for a list of available column names. @@ -391,10 +458,23 @@ def read_crystfel(streamfile: str, spacegroup=None, encoding='utf-8', columns=No # read data from stream file if columns is None: columns = [ - "H", "K", "L", "I", "SigI", "BATCH", "s1x", "s1y", "s1z", "ewald_offset", - "angular_ewald_offset", "XDET", "YDET" + "H", + "K", + "L", + "I", + "SigI", + "BATCH", + "s1x", + "s1y", + "s1z", + "ewald_offset", + "angular_ewald_offset", + "XDET", + "YDET", ] - peak_list_columns = [i for i in columns if i!='BATCH'] #BATCH is computed afterward + peak_list_columns = [ + i for i in columns if i != "BATCH" + ] # BATCH is computed afterward loader = StreamLoader(streamfile, encoding=encoding) cell = loader.extract_target_unit_cell() @@ -403,7 +483,7 @@ def read_crystfel(streamfile: str, spacegroup=None, encoding='utf-8', columns=No batch_array = [] data = [] for chunk in loader.parallel_read_crystfel(peak_list_columns=peak_list_columns): - for peak_list in chunk['peak_lists']: + for peak_list in chunk["peak_lists"]: data.append(peak_list) batch_array.append(np.ones(len(peak_list)) * batch) batch += 1 @@ -411,21 +491,20 @@ def read_crystfel(streamfile: str, spacegroup=None, encoding='utf-8', columns=No data = np.concatenate(data, axis=0) batch = np.concatenate(batch_array) - #d, cell = _parse_stream(streamfile, start, stop) - #df = pd.DataFrame.from_records(list(d.values())) + # d, cell = _parse_stream(streamfile, start, stop) + # df = pd.DataFrame.from_records(list(d.values())) mtz_dtypes = { - "H" : "H", - "K" : "H", - "L" : "H", - "I" : "J", - "SigI" : "Q", - "BATCH" : "B", + "H": "H", + "K": "H", + "L": "H", + "I": "J", + "SigI": "Q", + "BATCH": "B", } - # Start from an empty data set to minimize memory usage ds = DataSet( - spacegroup = spacegroup, + spacegroup=spacegroup, cell=cell, merged=False, ) @@ -433,15 +512,13 @@ def read_crystfel(streamfile: str, spacegroup=None, encoding='utf-8', columns=No idx = 0 for k in columns: dt = mtz_dtypes.get(k, "R") - if k != 'BATCH': - datum = data[:,idx] + if k != "BATCH": + datum = data[:, idx] idx += 1 else: datum = batch ds[k] = DataSeries(datum, dtype=dt) - ds.set_index(["H", "K", "L"], inplace=True) return ds - diff --git a/reciprocalspaceship/utils/__init__.py b/reciprocalspaceship/utils/__init__.py index cc00c365..8f75b598 100644 --- a/reciprocalspaceship/utils/__init__.py +++ b/reciprocalspaceship/utils/__init__.py @@ -52,5 +52,9 @@ phase_shift, polar_axes, ) -from reciprocalspaceship.utils.units import angstroms2ev, ev2angstroms -from reciprocalspaceship.utils.units import Angstroms2eV, eV2Angstroms +from reciprocalspaceship.utils.units import ( + Angstroms2eV, + angstroms2ev, + ev2angstroms, + eV2Angstroms, +) diff --git a/reciprocalspaceship/utils/units.py b/reciprocalspaceship/utils/units.py index c918c3f9..be22d847 100644 --- a/reciprocalspaceship/utils/units.py +++ b/reciprocalspaceship/utils/units.py @@ -1,16 +1,19 @@ -from scipy.constants import Planck, c, electron_volt import numpy as np +from scipy.constants import Planck, c, electron_volt _conversion_factor = Planck * c / 1e-10 / electron_volt + def eV2Angstroms(ev): out = np.empty_like(ev) np.divide(_conversion_factor, ev, out=out) return out + def Angstroms2eV(angstroms): return eV2Angstroms(angstroms) -#Add legacy aliases + +# Add legacy aliases ev2angstroms = eV2Angstroms angstroms2ev = Angstroms2eV