Source code for dxtbx.model.beam

from __future__ import annotations

import math
from typing import Tuple

import pycbf

import libtbx.phil

try:
    from ..dxtbx_model_ext import Beam, PolychromaticBeam, Probe
except ModuleNotFoundError:
    from dxtbx_model_ext import Beam, PolychromaticBeam, Probe  # type: ignore

Vec3Float = Tuple[float, float, float]

beam_phil_scope = libtbx.phil.parse(
    """
  beam
    .expert_level = 1
    .short_caption = "Beam overrides"
  {
    type = *monochromatic polychromatic
      .type = choice
      .help = "Override the beam type"
      .short_caption = "beam_type"

    probe = *x-ray electron neutron
      .type = choice
      .help = "Override the beam probe"
      .short_caption = "beam_probe"

    wavelength = None
      .type = float
      .help = "Override the beam wavelength"

    direction = None
      .type = floats(size=3)
      .help = "Override the sample to source direction"
      .short_caption = "Sample to source direction"

    divergence = None
        .type = float
        .help = "Override the beam divergence"

    sigma_divergence = None
        .type = float
        .help = "Override the beam sigma divergence"

    polarization_normal = None
      .type = floats(size=3)
      .help = "Override the polarization normal"
      .short_caption = "Polarization normal"

    polarization_fraction = None
      .type = float(value_min=0.0, value_max=1.0)
      .help = "Override the polarization fraction"
      .short_caption = "Polarization fraction"

    transmission = None
        .type = float
        .help = "Override the transmission"
        .short_caption = "transmission"

    flux = None
        .type = float
        .help = "Override the flux"
        .short_caption = "flux"

    sample_to_source_distance = None
        .type = float
        .help = "Override the distance between sample and source (mm)"

    wavelength_range = None
        .type = floats(size=2)
        .help = "Override the wavelength range for polychromatic beams (A)"
  }
"""
)


[docs] class BeamFactory: """A factory class for beam objects, which encapsulate standard beam models. In cases where a full cbf description is available this will be used, otherwise simplified descriptions can be applied."""
[docs] @staticmethod def from_phil( params: libtbx.phil.scope_extract, reference: Beam | PolychromaticBeam | None = None, ) -> Beam | PolychromaticBeam: """ Convert the phil parameters into a beam model """ # Check the input if reference is None: beam = ( PolychromaticBeam() if params.beam.type == "polychromatic" else Beam() ) else: beam = reference # Set the parameters if params.beam.type == "monochromatic": if params.beam.wavelength is not None: beam.set_wavelength(params.beam.wavelength) elif reference is None: raise RuntimeError("No wavelength set") if params.beam.direction is not None: beam.set_direction(params.beam.direction) elif reference is None: raise RuntimeError("No beam direction set") if params.beam.divergence is not None: beam.set_divergence(params.beam.divergence) if params.beam.sigma_divergence is not None: beam.set_sigma_divergence(params.beam.sigma_divergence) if params.beam.polarization_normal is not None: beam.set_polarization_normal(params.beam.polarization_normal) if params.beam.polarization_fraction is not None: beam.set_polarization_fraction(params.beam.polarization_fraction) if params.beam.transmission is not None: beam.set_transmission(params.beam.transmission) if params.beam.flux is not None: beam.set_flux(params.beam.flux) if params.beam.sample_to_source_distance is not None: beam.set_sample_to_source_distance(params.beam.sample_to_source_distance) if params.beam.probe != "x-ray": beam.set_probe(Beam.get_probe_from_name(params.beam.probe)) if params.beam.type == "polychromatic": if params.beam.wavelength_range is not None: beam.set_wavelength_range(params.beam.wavelength_range) return beam
[docs] @staticmethod def from_dict(dict: dict, template: dict = None) -> Beam | PolychromaticBeam: """Convert the dictionary to a beam model""" if template is not None: if "__id__" in dict and "__id__" in template: assert ( dict["__id__"] == template["__id__"] ), "Beam and template dictionaries are not the same type." if dict is None and template is None: return None joint = template.copy() if template else {} joint.update(dict) # Create the model from the joint dictionary if "probe" not in joint: joint["probe"] = "x-ray" if joint.get("__id__") == "polychromatic": return PolychromaticBeam.from_dict(joint) return Beam.from_dict(joint)
[docs] @staticmethod def make_beam( sample_to_source: Vec3Float = None, wavelength: float = None, s0: Vec3Float = None, unit_s0: Vec3Float = None, divergence: float = None, sigma_divergence: float = None, ) -> Beam: if divergence is None or sigma_divergence is None: divergence = 0.0 sigma_divergence = 0.0 if sample_to_source: assert wavelength return Beam( tuple(map(float, sample_to_source)), float(wavelength), float(divergence), float(sigma_divergence), ) elif unit_s0: assert wavelength return Beam( tuple(-float(x) for x in unit_s0), float(wavelength), float(divergence), float(sigma_divergence), ) else: assert s0 return Beam(tuple(map(float, s0)))
[docs] @staticmethod def make_polychromatic_beam( direction: Vec3Float, divergence: float = 0.0, sigma_divergence: float = 0.0, polarization_normal: Vec3Float = (0.0, 1.0, 0.0), polarization_fraction: float = 0.5, flux: float = 0.0, transmission: float = 1.0, probe: Probe = Probe.xray, sample_to_source_distance: float = 0.0, deg: bool = True, wavelength_range: tuple[float, float] = (0.0, 0.0), ) -> PolychromaticBeam: return PolychromaticBeam( tuple(map(float, direction)), float(divergence), float(sigma_divergence), tuple(map(float, polarization_normal)), float(polarization_fraction), float(flux), float(transmission), probe, float(sample_to_source_distance), bool(deg), tuple(map(float, wavelength_range)), )
[docs] @staticmethod def make_polarized_beam( sample_to_source: Vec3Float = None, wavelength: float = None, s0: Vec3Float = None, unit_s0: Vec3Float = None, polarization: Vec3Float = None, polarization_fraction: float = None, divergence: float = None, sigma_divergence: float = None, flux: float = None, transmission: float = None, probe: Probe = Probe.xray, ) -> Beam: assert polarization assert 0.0 <= polarization_fraction <= 1.0 if divergence is None or sigma_divergence is None: divergence = 0.0 sigma_divergence = 0.0 if flux is None: flux = 0 if transmission is None: transmission = 1.0 if sample_to_source: assert wavelength return Beam( tuple(map(float, sample_to_source)), float(wavelength), float(divergence), float(sigma_divergence), tuple(map(float, polarization)), float(polarization_fraction), float(flux), float(transmission), probe, ) elif unit_s0: assert wavelength return Beam( tuple(-float(x) for x in unit_s0), float(wavelength), float(divergence), float(sigma_divergence), tuple(map(float, polarization)), float(polarization_fraction), float(flux), float(transmission), probe, ) else: assert s0 sum_sq_s0 = s0[0] ** 2 + s0[1] ** 2 + s0[2] ** 2 assert sum_sq_s0 > 0 wavelength = 1.0 / math.sqrt(sum_sq_s0) return Beam( tuple(map(float, s0)), wavelength, float(divergence), float(sigma_divergence), tuple(map(float, polarization)), float(polarization_fraction), float(flux), float(transmission), probe, )
[docs] @staticmethod def simple(wavelength: float) -> Beam: """Construct a beam object on the principle that the beam is aligned with the +z axis, as is quite normal. Also assume the beam has polarization fraction 0.999 and is polarized in the x-z plane, unless it has a wavelength shorter than 0.05 Å in which case we assume electron diffraction and return an unpolarized beam model.""" if wavelength > 0.05: return BeamFactory.make_beam( sample_to_source=(0.0, 0.0, 1.0), wavelength=wavelength ) else: return BeamFactory.make_polarized_beam( sample_to_source=(0.0, 0.0, 1.0), wavelength=wavelength, polarization=(0, 1, 0), polarization_fraction=0.5, )
[docs] @staticmethod def simple_directional(sample_to_source: Vec3Float, wavelength: float) -> Beam: """Construct a beam with direction and wavelength.""" if wavelength > 0.05: return BeamFactory.make_beam( sample_to_source=sample_to_source, wavelength=wavelength ) else: return BeamFactory.make_polarized_beam( sample_to_source=sample_to_source, wavelength=wavelength, polarization=(0, 1, 0), polarization_fraction=0.5, )
[docs] @staticmethod def complex( sample_to_source: Vec3Float, polarization_fraction: float, polarization_plane_normal: Vec3Float, wavelength: float, ) -> Beam: """Full access to the constructor for cases where we do know everything that we need...""" return BeamFactory.make_polarized_beam( sample_to_source=sample_to_source, wavelength=wavelength, polarization=polarization_plane_normal, polarization_fraction=polarization_fraction, )
[docs] @staticmethod def imgCIF(cif_file: str) -> Beam: """Initialize a detector model from an imgCIF file. N.B. the definition of the polarization plane is not completely helpful in this - it is the angle between the polarization plane and the +Y laboratory frame vector.""" cbf_handle = pycbf.cbf_handle_struct() cbf_handle.read_widefile(cif_file.encode(), pycbf.MSG_DIGEST) result = BeamFactory.imgCIF_H(cbf_handle) return result
[docs] @staticmethod def imgCIF_H(cbf_handle: pycbf.cbf_handle_struct) -> Beam: """Initialize a detector model from an imgCIF file. N.B. the definition of the polarization plane is not completely helpful in this - it is the angle between the polarization plane and the +Y laboratory frame vector. This example works from a cbf_handle, which is already configured.""" d2r = math.pi / 180.0 cbf_handle.find_category(b"axis") # find record with equipment = source try: cbf_handle.find_column(b"equipment") cbf_handle.find_row(b"source") # then get the vector and offset from this direction = [] for j in range(3): cbf_handle.find_column(b"vector[%d]" % (j + 1)) direction.append(cbf_handle.get_doublevalue()) except Exception as e: if str(e).split()[-1] != "CBF_NOTFOUND": raise direction = [0, 0, 1] # and the wavelength wavelength = cbf_handle.get_wavelength() # and information about the polarization - FIXME this should probably # be a rotation about the beam not about the Z axis. Should also check # to see if this is Cu K-alpha wavelength (i.e. lab source...) try: polar_fraction, polar_angle = cbf_handle.get_polarization() except Exception: polar_fraction = 0.999 polar_angle = 0.0 polar_plane_normal = ( math.sin(polar_angle * d2r), math.cos(polar_angle * d2r), 0.0, ) # and the flux if available try: cbf_handle.find_category(b"diffrn_radiation") cbf_handle.find_column(b"beam_flux") flux = cbf_handle.get_value() except Exception as e: if str(e).split()[-1] != "CBF_NOTFOUND": raise flux = None return BeamFactory.make_polarized_beam( sample_to_source=direction, wavelength=wavelength, polarization=polar_plane_normal, polarization_fraction=polar_fraction, flux=flux, )