From 02954eb2e31bf570aaaadc07f8e7796f7235e287 Mon Sep 17 00:00:00 2001 From: "Aaron S. Brewster" Date: Tue, 26 Sep 2023 16:00:05 -0700 Subject: [PATCH] Break the DIALS/xfel circular dependency (#627) DIALS and dxtbx have had dependencies on cctbx_project/xfel, and vice versa. This PR is one of 8 that will break the cyclic dependency. Notably for dxtbx, this specifically removes: pytest.importorskip("xfel") For more detail, see cctbx/cctbx_project#872 and dials/dials#2404 Commits: * Time code moved from xfel to serialtbx * Move in radial_average c++ code from xfel * dpack moved to serialtbx * Jiffy function iterate_detector_at_level moved from xfel to serialtbx * Utility functions and constants from cspad_tbx moved to serialtbx * basis_from_geo and basis moved into serialtbx * Use dxtbx's cbf_wrapper by adding in the only function in the xfel subclass of cbf_wrapper * Move in add_frame_specific_cbf_tables * xfel.util.jungfrau moved to serialtbx.detector * cspad constants and data reading moved to serialtbx * Rayonix toolbox methods and constants moved to serialtbx * Legacy image pickle metrology code moved to serialtbx.detector.legacy_metrology * Remove xfel import tests * Better handle if SIT_* variables are missing for psana (IE don't crash dxtbx in the import step for FormatXTC) * Fix leftover conflict * Remove xfel reference that snuck in with nxmx_writer * Use CCTBX nightly until serialtbx hits stable cctbx * Remove importer skip for xfel from test_nxmx_writer --------- Co-authored-by: Nicholas Devenish --- .azure-pipelines/ci-conda-env.txt | 2 +- newsfragments/627.feature | 1 + src/dxtbx/boost_python/ext.cpp | 42 +++++++ src/dxtbx/command_line/detector_superpose.py | 2 +- src/dxtbx/command_line/image2pickle.py | 7 +- src/dxtbx/command_line/radial_average.py | 2 +- src/dxtbx/format/FormatCBFMultiTile.py | 38 ++++++ src/dxtbx/format/FormatNexusJungfrauExt.py | 5 +- src/dxtbx/format/FormatPYmultitile.py | 18 ++- src/dxtbx/format/FormatXTC.py | 19 ++- src/dxtbx/format/FormatXTCCspad.py | 26 ++-- src/dxtbx/format/FormatXTCEpix.py | 2 +- src/dxtbx/format/FormatXTCJungfrau.py | 16 ++- src/dxtbx/format/FormatXTCRayonix.py | 29 +++-- src/dxtbx/format/cbf_writer.py | 126 ++++++++++++++++++- src/dxtbx/format/nxmx_writer.py | 4 +- tests/command_line/test_average.py | 3 - tests/nexus/test_nxmx_writer.py | 1 - tests/test_regression_images.py | 12 -- 19 files changed, 273 insertions(+), 82 deletions(-) create mode 100644 newsfragments/627.feature diff --git a/.azure-pipelines/ci-conda-env.txt b/.azure-pipelines/ci-conda-env.txt index 5cc5c4fdd..9da037df6 100644 --- a/.azure-pipelines/ci-conda-env.txt +++ b/.azure-pipelines/ci-conda-env.txt @@ -2,7 +2,7 @@ conda-forge::boost conda-forge::boost-cpp conda-forge::bzip2 conda-forge::c-compiler<1.5 -conda-forge::cctbx-base==2023.7 +cctbx-nightly::cctbx-base conda-forge::conda conda-forge::cxx-compiler<1.5 conda-forge::python-dateutil diff --git a/newsfragments/627.feature b/newsfragments/627.feature new file mode 100644 index 000000000..1f2c28178 --- /dev/null +++ b/newsfragments/627.feature @@ -0,0 +1 @@ +Remove circular dependencies between dxtbx and ``cctbx.xfel`` by using the new ``serialtbx``. diff --git a/src/dxtbx/boost_python/ext.cpp b/src/dxtbx/boost_python/ext.cpp index 70c69c06b..f95f9f96f 100644 --- a/src/dxtbx/boost_python/ext.cpp +++ b/src/dxtbx/boost_python/ext.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -193,6 +195,41 @@ namespace dxtbx { namespace boost_python { return PyBytes_FromStringAndSize(&*packed.begin(), packed.size()); } + double distance_between_points(scitbx::vec2 const& a, scitbx::vec2 const& b) { + return std::sqrt((std::pow(double(b[0]-a[0]),2)+std::pow(double(b[1]-a[1]),2))); + } + + void radial_average(scitbx::af::versa > & data, + scitbx::af::versa > & mask, + scitbx::vec2 const& beam_center, + scitbx::af::shared sums, + scitbx::af::shared sums_sq, + scitbx::af::shared counts, + double pixel_size, double distance, + scitbx::vec2 const& upper_left, + scitbx::vec2 const& lower_right) { + std::size_t extent = sums.size(); + double extent_in_mm = extent * pixel_size; + double extent_two_theta = std::atan(extent_in_mm/distance)*180/scitbx::constants::pi; + + for(std::size_t y = upper_left[1]; y < lower_right[1]; y++) { + for(std::size_t x = upper_left[0]; x < lower_right[0]; x++) { + double val = data(x,y); + if(val > 0 && mask(x,y)) { + scitbx::vec2 point((int)x,(int)y); + double d_in_mm = distance_between_points(point,beam_center) * pixel_size; + double twotheta = std::atan(d_in_mm/distance)*180/scitbx::constants::pi; + std::size_t bin = (std::size_t)std::floor(twotheta*extent/extent_two_theta); + if (bin >= extent) + continue; + sums[bin] += val; + sums_sq[bin] += val*val; + counts[bin]++; + } + } + } + } + // Python entry point to decompress Rigaku Oxford Diffractometer TY6 compression scitbx::af::flex_int uncompress_rod_TY6(const boost::python::object &data, const boost::python::object &offsets, @@ -224,6 +261,11 @@ namespace dxtbx { namespace boost_python { def("is_big_endian", is_big_endian); def("uncompress", &uncompress, (arg_("packed"), arg_("slow"), arg_("fast"))); def("compress", &compress); + def("radial_average", &radial_average, + (arg("data"), arg("beam_center"), arg("sums"), arg("sums_sq"), arg("counts"), + arg("pixel_size"), arg("distance"), + arg("upper_left"), arg("lower_right"))) + ; def("uncompress_rod_TY6", &uncompress_rod_TY6, (arg_("data"), arg_("offsets"), arg_("slow"), arg_("fast"))); diff --git a/src/dxtbx/command_line/detector_superpose.py b/src/dxtbx/command_line/detector_superpose.py index 7911e8ba8..6821d6722 100644 --- a/src/dxtbx/command_line/detector_superpose.py +++ b/src/dxtbx/command_line/detector_superpose.py @@ -8,7 +8,7 @@ from scitbx.array_family import flex from scitbx.math.superpose import least_squares_fit from scitbx.matrix import col -from xfel.command_line.cspad_detector_congruence import iterate_detector_at_level +from serialtbx.detector import iterate_detector_at_level import dials.util from dials.util.options import OptionParser diff --git a/src/dxtbx/command_line/image2pickle.py b/src/dxtbx/command_line/image2pickle.py index f9f501dbc..a3203a843 100644 --- a/src/dxtbx/command_line/image2pickle.py +++ b/src/dxtbx/command_line/image2pickle.py @@ -16,8 +16,9 @@ import libtbx.option_parser from libtbx import easy_pickle from libtbx.utils import Usage +import serialtbx.util from scitbx.array_family import flex -from xfel.cxi.cspad_ana.cspad_tbx import dpack, evt_timestamp +import serialtbx.detector.cspad import dxtbx.util @@ -265,7 +266,7 @@ def run(args=None): timestamp = None else: msec, sec = math.modf(scan.get_epochs()[0]) - timestamp = evt_timestamp((sec, msec)) + timestamp = serialtbx.util.timestamp((sec, msec)) if is_multi_image: for i in range(img.get_num_images()): @@ -329,7 +330,7 @@ def save_image( print("Skipping %s, file exists" % imgpath) return - data = dpack( + data = serialtbx.detector.cspad.dpack( data=raw_data, distance=distance, pixel_size=pixel_size, diff --git a/src/dxtbx/command_line/radial_average.py b/src/dxtbx/command_line/radial_average.py index a9acbaf08..ddf30af6c 100644 --- a/src/dxtbx/command_line/radial_average.py +++ b/src/dxtbx/command_line/radial_average.py @@ -16,7 +16,7 @@ from libtbx.utils import Sorry, Usage from scitbx.array_family import flex from scitbx.matrix import col -from xfel import radial_average +from dxtbx.ext import radial_average import dxtbx import dxtbx.util diff --git a/src/dxtbx/format/FormatCBFMultiTile.py b/src/dxtbx/format/FormatCBFMultiTile.py index e9e2c5059..a04517de6 100644 --- a/src/dxtbx/format/FormatCBFMultiTile.py +++ b/src/dxtbx/format/FormatCBFMultiTile.py @@ -9,6 +9,7 @@ import pycbf from scitbx.array_family import flex +from scitbx import matrix from dxtbx.format.FormatCBF import FormatCBF from dxtbx.format.FormatCBFFull import FormatCBFFull @@ -17,6 +18,13 @@ from dxtbx.model.detector_helpers import find_undefined_value, find_underload_value +def angle_and_axis(basis): + """Normalize a quaternion and return the angle and axis + @param params metrology object""" + q = matrix.col(basis.orientation).normalize() + return q.unit_quaternion_as_axis_and_angle(deg=True) + + class cbf_wrapper(pycbf.cbf_handle_struct): """Wrapper class that provides convenience functions for working with cbflib""" @@ -56,6 +64,36 @@ def has_sections(self): return False raise e + def add_frame_shift(self, basis, axis_settings): + """Add an axis representing a frame shift (a rotation axis with an offset)""" + angle, axis = angle_and_axis(basis) + + if angle == 0: + axis = (0, 0, 1) + + if basis.include_translation: + translation = basis.translation + else: + translation = (0, 0, 0) + + self.add_row( + [ + basis.axis_name, + "rotation", + "detector", + basis.depends_on, + str(axis[0]), + str(axis[1]), + str(axis[2]), + str(translation[0]), + str(translation[1]), + str(translation[2]), + basis.equipment_component, + ] + ) + + axis_settings.append([basis.axis_name, "FRAME1", str(angle), "0"]) + class FormatCBFMultiTile(FormatCBFFull): """An image reading class multi-tile CBF files""" diff --git a/src/dxtbx/format/FormatNexusJungfrauExt.py b/src/dxtbx/format/FormatNexusJungfrauExt.py index 15ffadd3e..11aab7d52 100644 --- a/src/dxtbx/format/FormatNexusJungfrauExt.py +++ b/src/dxtbx/format/FormatNexusJungfrauExt.py @@ -7,10 +7,7 @@ from scitbx.array_family import flex -try: - from xfel.util.jungfrau import pad_stacked_format -except ImportError: - pass +from serialtbx.detector.jungfrau import pad_stacked_format from dxtbx.format.FormatNexus import FormatNexus diff --git a/src/dxtbx/format/FormatPYmultitile.py b/src/dxtbx/format/FormatPYmultitile.py index 6f4cf4971..8e39ef549 100644 --- a/src/dxtbx/format/FormatPYmultitile.py +++ b/src/dxtbx/format/FormatPYmultitile.py @@ -8,10 +8,7 @@ from iotbx.detectors.npy import image_dict_to_unicode from scitbx.matrix import col -try: - from xfel.cftbx.detector.cspad_detector import CSPadDetector -except ImportError: - CSPadDetector = None +from serialtbx.detector.legacy_metrology.cspad_detector import CSPadDetector from dxtbx.format.FormatPY import FormatPY from dxtbx.model import Detector @@ -20,8 +17,6 @@ class FormatPYmultitile(FormatPY): @staticmethod def understand(image_file): - if not CSPadDetector: - return False try: with FormatPYmultitile.open_file(image_file, "rb") as fh: data = pickle.load(fh, encoding="bytes") @@ -60,13 +55,14 @@ def _detector(self): ASIC:s are considered, since DXTBX metrology is not concerned with hierarchies. - Merged from xfel.cftbx.detector.cspad_detector.readHeader() and - xfel.cftbx.detector.metrology.metrology_as_dxtbx_vectors(). + Merged from serialtbx.detector.legacy_metrology.cspad_detector.readHeader() + and serialtbx.detector.legacy_metrology.metrology.metrology_as_dxtbx_vectors(). """ - # XXX Introduces dependency on cctbx.xfel! Should probably be - # merged into the code here! - from xfel.cftbx.detector.metrology import _transform, get_projection_matrix + from serialtbx.detector.legacy_metrology.metrology import ( + _transform, + get_projection_matrix, + ) # Apply the detector distance to the translation of the root # detector object. diff --git a/src/dxtbx/format/FormatXTC.py b/src/dxtbx/format/FormatXTC.py index cc5d214f3..364b06c06 100644 --- a/src/dxtbx/format/FormatXTC.py +++ b/src/dxtbx/format/FormatXTC.py @@ -15,14 +15,21 @@ from dxtbx.format.FormatStill import FormatStill from dxtbx.model import Spectrum from dxtbx.util.rotate_and_average import rotate_and_average +import serialtbx.util +import serialtbx.detector.xtc try: import psana - - from xfel.cxi.cspad_ana import cspad_tbx except ImportError: psana = None - cspad_tbx = None +except TypeError: + # Check if SIT_* environment variables are set + import os + + if os.environ.get("SIT_ROOT"): + # Variables are present, so must have been another error + raise + psana = None locator_str = """ experiment = None @@ -152,7 +159,7 @@ def understand(image_file): If PSANA fails to read it, then input may not be an xtc/smd file. If success, then OK. If detector_address is not provided, a command line promp will try to get the address from the user""" - if not psana or not cspad_tbx: + if not psana: return False try: params = FormatXTC.params_from_phil(locator_scope, image_file) @@ -375,7 +382,7 @@ def get_psana_timestamp(self, index): sec = time[0] nsec = time[1] - return cspad_tbx.evt_timestamp((sec, nsec / 1e6)) + return serialtbx.util.timestamp((sec, nsec / 1e6)) def get_num_images(self): return self.n_images @@ -397,7 +404,7 @@ def _beam(self, index=None): if spectrum: wavelength = spectrum.get_weighted_wavelength() else: - wavelength = cspad_tbx.evt_wavelength( + wavelength = serialtbx.detector.xtc.evt_wavelength( evt, delta_k=self.params.wavelength_delta_k ) if wavelength is None: diff --git a/src/dxtbx/format/FormatXTCCspad.py b/src/dxtbx/format/FormatXTCCspad.py index 0538de302..a7719d375 100644 --- a/src/dxtbx/format/FormatXTCCspad.py +++ b/src/dxtbx/format/FormatXTCCspad.py @@ -8,19 +8,13 @@ from libtbx.phil import parse from scitbx.array_family import flex from scitbx.matrix import col -from xfel.cftbx.detector.cspad_cbf_tbx import read_slac_metrology -from xfel.cxi.cspad_ana.cspad_tbx import env_distance +from serialtbx.detector import cspad +from serialtbx.detector.xtc import env_distance +import serialtbx.detector.cspad from dxtbx.format.FormatXTC import FormatXTC, locator_str from dxtbx.model import Detector, ParallaxCorrectedPxMmStrategy -try: - from xfel.cftbx.detector import cspad_cbf_tbx - from xfel.cxi.cspad_ana import cspad_tbx -except ImportError: - # xfel not configured - pass - cspad_locator_str = """ cspad { detz_offset = None @@ -92,7 +86,7 @@ def get_raw_data(self, index=None): run_number = event.run() run = self._psana_runs[run_number] det = self._get_psana_detector(run) - data = cspad_cbf_tbx.get_psana_corrected_data( + data = cspad.get_psana_corrected_data( det, event, use_default=self.params.cspad.use_psana_calib, @@ -131,7 +125,7 @@ def _detector(self, index=None): run = self.get_run_from_index(index) det = self._get_psana_detector(run) geom = det.pyda.geoaccess(run.run()) - cob = read_slac_metrology(geometry=geom, include_asic_offset=True) + cob = cspad.read_slac_metrology(geometry=geom, include_asic_offset=True) distance = env_distance( self.params.detector_address[0], run.env(), self.params.cspad.detz_offset ) @@ -195,14 +189,12 @@ def _detector(self, index=None): - origin ) p.set_local_frame(fast.elems, slow.elems, origin.elems) - p.set_pixel_size( - (cspad_cbf_tbx.pixel_size, cspad_cbf_tbx.pixel_size) - ) - p.set_image_size(cspad_cbf_tbx.asic_dimension) + p.set_pixel_size((cspad.pixel_size, cspad.pixel_size)) + p.set_image_size(cspad.asic_dimension) p.set_trusted_range( ( - cspad_tbx.cspad_min_trusted_value, - cspad_tbx.cspad_saturated_value, + serialtbx.detector.cspad.cspad_min_trusted_value, + serialtbx.detector.cspad.cspad_saturated_value, ) ) p.set_name(val) diff --git a/src/dxtbx/format/FormatXTCEpix.py b/src/dxtbx/format/FormatXTCEpix.py index 705bf3eaa..bea3bcf44 100644 --- a/src/dxtbx/format/FormatXTCEpix.py +++ b/src/dxtbx/format/FormatXTCEpix.py @@ -63,7 +63,7 @@ def get_detector(self, index=None): def _detector(self, index=None): from PSCalib.SegGeometryStore import sgs - from xfel.cftbx.detector.cspad_cbf_tbx import basis_from_geo + from serialtbx.detector.xtc import basis_from_geo run = self.get_run_from_index(index) if run.run() in self._cached_detector: diff --git a/src/dxtbx/format/FormatXTCJungfrau.py b/src/dxtbx/format/FormatXTCJungfrau.py index 8f5c5a092..6e7e8b9b1 100644 --- a/src/dxtbx/format/FormatXTCJungfrau.py +++ b/src/dxtbx/format/FormatXTCJungfrau.py @@ -4,7 +4,17 @@ import sys import numpy as np -import psana + +try: + import psana +except ImportError: + psana = None +except TypeError: + # Check if SIT_* environment variables are set + if os.environ.get("SIT_ROOT"): + # Variables are present, so must have been another error + raise + psana = None from cctbx import factor_kev_angstrom from cctbx.eltbx import attenuation_coefficient @@ -54,7 +64,7 @@ def understand(image_file): return any(["jungfrau" in src.lower() for src in params.detector_address]) def get_raw_data(self, index=None): - from xfel.util import jungfrau + from serialtbx.detector.util import jungfrau if index is None: index = 0 @@ -94,7 +104,7 @@ def get_detector(self, index=None): def _detector(self, index=None): from PSCalib.SegGeometryStore import sgs - from xfel.cftbx.detector.cspad_cbf_tbx import basis_from_geo + from serialtbx.detector.xtc import basis_from_geo run = self.get_run_from_index(index) if run.run() in self._cached_detector: diff --git a/src/dxtbx/format/FormatXTCRayonix.py b/src/dxtbx/format/FormatXTCRayonix.py index afa1ed2c4..cf3c2b260 100644 --- a/src/dxtbx/format/FormatXTCRayonix.py +++ b/src/dxtbx/format/FormatXTCRayonix.py @@ -2,18 +2,25 @@ import sys -import psana +try: + import psana +except ImportError: + psana = None +except TypeError: + # Check if SIT_* environment variables are set + import os + + if os.environ.get("SIT_ROOT"): + # Variables are present, so must have been another error + raise + psana = None from libtbx.phil import parse from scitbx.array_family import flex from dxtbx.format.FormatXTC import FormatXTC, locator_str -try: - from xfel.cxi.cspad_ana import rayonix_tbx -except ImportError: - # xfel not configured - pass +from serialtbx.detector import rayonix rayonix_locator_str = """ rayonix { @@ -36,8 +43,8 @@ def __init__(self, image_file, **kwargs): bin_size = rayonix_cfg.binning_f() if self.params.rayonix.bin_size is not None: assert bin_size == self.params.rayonix.bin_size - self._pixel_size = rayonix_tbx.get_rayonix_pixel_size(bin_size) - self._image_size = rayonix_tbx.get_rayonix_detector_dimensions(self._ds.env()) + self._pixel_size = rayonix.get_rayonix_pixel_size(bin_size) + self._image_size = rayonix.get_rayonix_detector_dimensions(self._ds.env()) @staticmethod def understand(image_file): @@ -51,7 +58,7 @@ def get_raw_data(self, index=None): if index is None: index = 0 assert len(self.params.detector_address) == 1 - data = rayonix_tbx.get_data_from_psana_event( + data = rayonix.get_data_from_psana_event( self._get_event(index), self.params.detector_address[0] ) return flex.double(data) @@ -69,8 +76,8 @@ def _detector(self): pixel_size=(self._pixel_size, self._pixel_size), image_size=self._image_size, trusted_range=( - rayonix_tbx.rayonix_min_trusted_value, - rayonix_tbx.rayonix_max_trusted_value, + rayonix.rayonix_min_trusted_value, + rayonix.rayonix_max_trusted_value, ), mask=[], ) diff --git a/src/dxtbx/format/cbf_writer.py b/src/dxtbx/format/cbf_writer.py index 7489adfc6..a1a91a8ed 100644 --- a/src/dxtbx/format/cbf_writer.py +++ b/src/dxtbx/format/cbf_writer.py @@ -14,16 +14,132 @@ import pycbf +from dxtbx.format.FormatCBFMultiTile import cbf_wrapper from scitbx.array_family import flex -from xfel.cftbx.detector.cspad_cbf_tbx import ( - add_frame_specific_cbf_tables, - basis, - cbf_wrapper, -) +from serialtbx.detector import basis import dxtbx.format.Registry +def add_frame_specific_cbf_tables( + cbf, + wavelength, + timestamp, + trusted_ranges, + diffrn_id="DS1", + is_xfel=True, + gain=1.0, + flux=None, +): + """Adds tables to cbf handle that won't already exsist if the cbf file is just a header + @ param wavelength Wavelength in angstroms + @ param timestamp String formatted timestamp for the image + @ param trusted_ranges Array of trusted range tuples (min, max), one for each element""" + + """Data items in the DIFFRN_RADIATION category describe + the radiation used for measuring diffraction intensities, + its collimation and monochromatization before the sample. + + Post-sample treatment of the beam is described by data + items in the DIFFRN_DETECTOR category.""" + if flux: + cbf.add_category( + "diffrn_radiation", ["diffrn_id", "wavelength_id", "probe", "beam_flux"] + ) + cbf.add_row([diffrn_id, "WAVELENGTH1", "x-ray", "%f" % flux]) + else: + cbf.add_category("diffrn_radiation", ["diffrn_id", "wavelength_id", "probe"]) + cbf.add_row([diffrn_id, "WAVELENGTH1", "x-ray"]) + + """ Data items in the DIFFRN_RADIATION_WAVELENGTH category describe + the wavelength of the radiation used in measuring the diffraction + intensities. Items may be looped to identify and assign weights + to distinct wavelength components from a polychromatic beam.""" + cbf.add_category("diffrn_radiation_wavelength", ["id", "wavelength", "wt"]) + cbf.add_row(["WAVELENGTH1", str(wavelength), "1.0"]) + + """Data items in the DIFFRN_MEASUREMENT category record details + about the device used to orient and/or position the crystal + during data measurement and the manner in which the + diffraction data were measured.""" + cbf.add_category( + "diffrn_measurement", ["diffrn_id", "id", "number_of_axes", "method", "details"] + ) + cbf.add_row( + [ + diffrn_id, + "INJECTION" if is_xfel else "unknown", + "0", + "electrospray" + if is_xfel + else "unknown" "crystals injected by electrospray" + if is_xfel + else "unknown", + ] + ) + + """ Data items in the DIFFRN_SCAN category describe the parameters of one + or more scans, relating axis positions to frames.""" + cbf.add_category("diffrn_scan", ["id", "frame_id_start", "frame_id_end", "frames"]) + cbf.add_row(["SCAN1", "FRAME1", "FRAME1", "1"]) + + """Data items in the DIFFRN_SCAN_FRAME category describe + the relationships of particular frames to scans.""" + cbf.add_category( + "diffrn_scan_frame", + ["frame_id", "frame_number", "integration_time", "scan_id", "date"], + ) + cbf.add_row(["FRAME1", "1", "0.0", "SCAN1", timestamp]) + + """ Data items in the ARRAY_INTENSITIES category record the + information required to recover the intensity data from + the set of data values stored in the ARRAY_DATA category.""" + # More detail here: http://www.iucr.org/__data/iucr/cifdic_html/2/cif_img.dic/Carray_intensities.html + array_names = [] + cbf.find_category(b"diffrn_data_frame") + while True: + try: + cbf.find_column(b"array_id") + array_names.append(cbf.get_value().decode()) + cbf.next_row() + except Exception as e: + assert "CBF_NOTFOUND" in str(e) + break + + if not isinstance(gain, list): + gain = [gain] * len(array_names) + + cbf.add_category( + "array_intensities", + [ + "array_id", + "binary_id", + "linearity", + "gain", + "gain_esd", + "overload", + "underload", + "undefined_value", + ], + ) + for i, array_name in enumerate(array_names): + overload = trusted_ranges[i][1] + 1 + underload = trusted_ranges[i][0] + undefined = underload - 1 + cbf.add_row( + [ + array_name, + str(i + 1), + "linear", + "%f" % gain[i], + "0.0", + str(overload), + str(underload), + str(undefined), + ] + ) + + class FullCBFWriter: """Class for writing full CBF files from any dxtbx-supported format class""" diff --git a/src/dxtbx/format/nxmx_writer.py b/src/dxtbx/format/nxmx_writer.py index 58e705484..96adca876 100644 --- a/src/dxtbx/format/nxmx_writer.py +++ b/src/dxtbx/format/nxmx_writer.py @@ -21,11 +21,12 @@ from libtbx.utils import Sorry from scitbx import matrix from scitbx.array_family import flex -from xfel.cftbx.detector.cspad_cbf_tbx import angle_and_axis, basis +from serialtbx.detector import basis from dials.util.options import ArgumentParser, flatten_experiments from dxtbx import flumpy +from dxtbx.format.FormatCBFMultiTile import angle_and_axis help_message = """ Create a NeXus file from either an experiment list or a set of image files @@ -724,7 +725,6 @@ def append_frame(self, index=None, data=None): def add_scan_and_gonio(self, scan=None, gonio=None): if scan is None or gonio is None: - assert scan is None and gonio is None scan = self.scan gonio = self.goniometer diff --git a/tests/command_line/test_average.py b/tests/command_line/test_average.py index bedbc2d48..c18c8985c 100644 --- a/tests/command_line/test_average.py +++ b/tests/command_line/test_average.py @@ -10,9 +10,6 @@ @pytest.mark.parametrize("use_mpi", [True, False]) def test_average(dials_data, tmp_path, use_mpi): - # averager uses cbf handling code in the xfel module - pytest.importorskip("xfel") - # Only allow MPI tests if we've got MPI capabilities if use_mpi: pytest.importorskip("mpi4py") diff --git a/tests/nexus/test_nxmx_writer.py b/tests/nexus/test_nxmx_writer.py index fb9f60273..aa5538f86 100644 --- a/tests/nexus/test_nxmx_writer.py +++ b/tests/nexus/test_nxmx_writer.py @@ -12,7 +12,6 @@ from dxtbx.model.experiment_list import ExperimentListFactory pytest.importorskip("dials") -pytest.importorskip("xfel") from dxtbx.format.nxmx_writer import NXmxWriter, phil_scope diff --git a/tests/test_regression_images.py b/tests/test_regression_images.py index 8b3d3787a..40e560af8 100644 --- a/tests/test_regression_images.py +++ b/tests/test_regression_images.py @@ -24,13 +24,6 @@ except ModuleNotFoundError: h5py = None -try: - import xfel -except ImportError: - # Although ModuleNotFoundError is correct, cctbx may raise an ImportError - # when the xfel extension module is not found. - xfel = None - try: import cbflib_adaptbx @@ -156,9 +149,6 @@ @pytest.mark.regression @pytest.mark.parametrize("test_image", _files_with_detectorbase) def test_detectorbase(test_image, dials_regression): - if not xfel and test_image.startswith("LCLS"): - pytest.skip("could not import 'xfel'") - if not h5py and test_image.endswith((".h5", ".nxs")): pytest.skip("could not import 'h5py'") @@ -207,8 +197,6 @@ def test_detectorbase(test_image, dials_regression): @pytest.mark.parametrize("test_image", _files) def test_read_image(test_image, dials_regression): """Test that all the regression images can be read""" - if not xfel and test_image.startswith("LCLS"): - pytest.skip("could not import 'xfel'") if not h5py and test_image.endswith((".h5", ".nxs")): pytest.skip("could not import 'h5py'")