class Source: """ Class for an X-ray source Args: :wavelength (float): X-ray wavelength in unit meter :focus_diameter (float): Focus diameter (characteristic transverse dimension) in unit meter :pulse_energy (float): (Statistical mean of) pulse energy in unit Joule Kwargs: :profile_model (str): Model for the spatial illumination profile (default `None`) .. note:: The (keyword) arguments ``focus_diameter`` and ``profile_model`` are passed on to the constructor of :class:`condor.utils.profile.Profile`. For more detailed information read the documentation of the initialisation function. :pulse_energy_variation (str): Statistical variation of the pulse energy (default ``None``) :pulse_energy_spread (float): Statistical spread of the pulse energy in unit Joule (default ``None``) :pulse_energy_variation_n (int): Number of samples within the specified range (default ``None``) :polarization (str): Type of polarization can be either *vertical*, *horizontal*, *unpolarized*, or *ignore* (default ``ignore``) .. note:: The keyword arguments ``pulse_energy_variation``, ``pulse_energy_spread``, and ``pulse_energy_variation_n`` are passed on to :meth:`condor.source.Source.set_pulse_energy_variation` during initialisation. For more detailed information read the documentation of the method. """ def __init__(self, wavelength, focus_diameter, pulse_energy, profile_model=None, pulse_energy_variation=None, pulse_energy_spread=None, pulse_energy_variation_n=None, polarization="ignore"): self.photon = Photon(wavelength=wavelength) self.pulse_energy_mean = pulse_energy self.set_pulse_energy_variation(pulse_energy_variation, pulse_energy_spread, pulse_energy_variation_n) self.profile = Profile(model=profile_model, focus_diameter=focus_diameter) if polarization not in [ "vertical", "horizontal", "unpolarized", "ignore" ]: log_and_raise_error( logger, "polarization = \"%s\" is an invalid input for initialization of Source instance." ) return self.polarization = polarization log_debug(logger, "Source configured") def get_conf(self): """ Get configuration in form of a dictionary. Another identically configured Source instance can be initialised by: .. code-block:: python conf = S0.get_conf() # S0: already existing Source instance S1 = condor.Source(**conf) # S1: new Source instance with the same configuration as S0 """ conf = {} conf["source"] = {} conf["source"]["wavelength"] = self.photon.get_wavelength() conf["source"]["focus_diameter"] = self.profile.focus_diameter conf["source"]["pulse_energy"] = self.pulse_energy_mean conf["source"]["profile_model"] = self.profile.get_model() pevar = self._pulse_energy_variation.get_conf() conf["source"]["pulse_energy_variation"] = pevar["mode"] conf["source"]["pulse_energy_spread"] = pevar["spread"] conf["source"]["pulse_energy_variation_n"] = pevar["n"] conf["source"]["polarization"] = self.polarization return conf def set_pulse_energy_variation(self, pulse_energy_variation=None, pulse_energy_spread=None, pulse_energy_variation_n=None): """ Set variation of the pulse energy Kwargs: :pulse_energy_variation (str): Statistical variation of the pulse energy (default ``None``) *Choose one of the following options:* - ``\'normal\'`` - random normal (Gaussian) distribution - ``\'uniform\'`` - random uniform distribution - ``\'range\'`` - equispaced pulse energies around ``pulse_energy`` - ``None`` - no variation of the pulse energy :pulse_energy_spread (float): Statistical spread of the pulse energy in unit Joule (default ``None``) :pulse_energy_variation_n (int): Number of samples within the specified range .. note:: The argument ``pulse_energy_variation_n`` takes effect only in combination with ``pulse_energy_variation=\'range\'`` """ self._pulse_energy_variation = Variation(pulse_energy_variation, pulse_energy_spread, pulse_energy_variation_n, number_of_dimensions=1) def get_intensity(self, position, unit="ph/m2", pulse_energy=None): """ Calculate the intensity at a given position in the focus Args: :position: Coordinates [*x*, *y*, *z*] of the position where the intensity shall be calculated Kwargs: :unit (str): Intensity unit (default ``\'ph/m2\'``) *Choose one of the following options:* - ``\'ph/m2\'`` - ``\'J/m2\'`` - ``\'J/um2\'`` - ``\'mJ/um2\'`` - ``\'ph/um2\'`` :pulse_energy (float): Pulse energy of that particular pulse in unit Joule. If ``None`` the mean of the pulse energy will be used (default ``None``) """ # Assuming # 1) Radially symmetric profile that is invariant along the beam axis within the sample volume # 2) The variation of intensity are on much larger scale than the dimension of the particle size (i.e. flat wavefront) r = numpy.sqrt(position[1]**2 + position[2]**2) I = (self.profile.get_radial())(r) * (pulse_energy if pulse_energy is not None else self.pulse_energy_mean) if unit == "J/m2": pass elif unit == "ph/m2": I /= self.photon.get_energy() elif unit == "J/um2": I *= 1.E-12 elif unit == "mJ/um2": I *= 1.E-9 elif unit == "ph/um2": I /= self.photon.get_energy() I *= 1.E-12 else: log_and_raise_error(logger, "%s is not a valid unit." % unit) return return I def get_next(self): """ Iterate the parameters of the Source instance and return them as a dictionary """ return { "pulse_energy": self._get_next_pulse_energy(), "wavelength": self.photon.get_wavelength(), "photon_energy": self.photon.get_energy(), "photon_energy_eV": self.photon.get_energy_eV() } def _get_next_pulse_energy(self): p = self._pulse_energy_variation.get(self.pulse_energy_mean) # Non-random if self._pulse_energy_variation._mode in [None, "range"]: if p <= 0: log_and_raise_error( logger, "Pulse energy smaller-equals zero. Change your configuration." ) else: return p # Random else: if p <= 0.: log_warning(logger, "Pulse energy smaller-equals zero. Try again.") self._get_next_pulse_energy() else: return p
class ParticleSpheroid(AbstractContinuousParticle): """ Class for a particle model *Model:* Uniformly filled spheroid particle (continuum approximation) :math:`a`: radius (*semi-diameter*) perpendicular to the rotation axis of the ellipsoid :math:`c`: radius (*semi-diameter*) along the rotation axis of the ellipsoid Before applying rotations the rotation axis is parallel to the the *y*-axis Args: :diameter (float): Sphere diameter Kwargs: :diameter_variation (str): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :diameter_spread (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :diameter_variation_n (int): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :flattening (float): (Mean) value of :math:`a/c` (default ``0.75``) :flattening_variation (str): See :meth:`condor.particle.particle_spheroid.set_flattening_variation` (default ``None``) :flattening_spread (float): See :meth:`condor.particle.particle_spheroid.set_flattening_variation` (default ``None``) :flattening_variation_n (int): See :meth:`condor.particle.particle_spheroid.set_flattening_variation` (default ``None``) :rotation_values (array): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_formalism (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_mode (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :number (float): Expectation value for the number of particles in the interaction volume. (defaukt ``1.``) :arrival (str): Arrival of particles at the interaction volume can be either ``'random'`` or ``'synchronised'``. If ``sync`` at every event the number of particles in the interaction volume equals the rounded value of ``number``. If ``'random'`` the number of particles is Poissonian and ``number`` is the expectation value. (default ``'synchronised'``) :position (array): See :class:`condor.particle.particle_abstract.AbstractParticle` (default ``None``) :position_variation (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_spread (float): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_variation_n (int): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :material_type (str): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``\'water\'``) :massdensity (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) :atomic_composition (dict): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) :electron_density (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) """ def __init__(self, diameter, diameter_variation=None, diameter_spread=None, diameter_variation_n=None, flattening=0.75, flattening_variation=None, flattening_spread=None, flattening_variation_n=None, rotation_values=None, rotation_formalism=None, rotation_mode="extrinsic", number=1., arrival="synchronised", position=None, position_variation=None, position_spread=None, position_variation_n=None, material_type='water', massdensity=None, atomic_composition=None, electron_density=None): # Initialise base class AbstractContinuousParticle.__init__( self, diameter=diameter, diameter_variation=diameter_variation, diameter_spread=diameter_spread, diameter_variation_n=diameter_variation_n, rotation_values=rotation_values, rotation_formalism=rotation_formalism, rotation_mode=rotation_mode, number=number, arrival=arrival, position=position, position_variation=position_variation, position_spread=position_spread, position_variation_n=position_variation_n, material_type=material_type, massdensity=massdensity, atomic_composition=atomic_composition, electron_density=electron_density) self.flattening_mean = flattening self.set_flattening_variation( flattening_variation=flattening_variation, flattening_spread=flattening_spread, flattening_variation_n=flattening_variation_n) def get_conf(self): """ Get configuration in form of a dictionary. Another identically configured ParticleMap instance can be initialised by: .. code-block:: python conf = P0.get_conf() # P0: already existing ParticleSpheroid instance P1 = condor.ParticleSpheroid(**conf) # P1: new ParticleSpheroid instance with the same configuration as P0 """ conf = {} conf.update(AbstractContinuousParticle.get_conf(self)) conf["flattening"] = self.flattening_mean fvar = self._flattening_variation.get_conf() conf["flattening_variation"] = fvar["mode"] conf["flattening_spread"] = fvar["spread"] conf["flattening_variation_n"] = fvar["n"] return conf def get_next(self): """ Iterate the parameters and return them as a dictionary """ O = AbstractContinuousParticle.get_next(self) O["particle_model"] = "spheroid" O["flattening"] = self._get_next_flattening() return O def set_flattening_variation(self, flattening_variation, flattening_spread, flattening_variation_n): """ Set the variation scheme of the flattening parameter Args: :flattening_variation (str): Variation of the particle flattening *Choose one of the following options:* - ``None`` - No variation - ``\'normal\'`` - Normal (*Gaussian*) variation - ``\'uniform\'`` - Uniformly distributed flattenings - ``\'range\'`` - Equidistant sequence of particle-flattening samples within the spread limits. ``flattening_variation_n`` defines the number of samples within the range :flattening_spread (float): Statistical spread of the parameter :flattening_variation_n (int): Number of particle-flattening samples within the specified range .. note:: The argument ``flattening_variation_n`` takes effect only if ``flattening_variation=\'range\'`` """ self._flattening_variation = Variation(flattening_variation, flattening_spread, flattening_variation_n) def _get_next_flattening(self): f = self._flattening_variation.get(self.flattening_mean) # Non-random if self._flattening_variation._mode in [None, "range"]: if f <= 0: log_and_raise_error( logger, "Spheroid flattening smaller-equals zero. Change your configuration." ) else: return f # Random else: if f <= 0.: log_warning( logger, "Spheroid flattening smaller-equals zero. Try again.") return self._get_next_flattening() else: return f def get_dn(self, photon_wavelength): if self.materials is None: dn = 0. else: dn = numpy.array( [m.get_dn(photon_wavelength) for m in self.materials]).sum() return dn
class Detector: """ Class for a photon area-detector .. image:: images/detector_schematic.jpg **Arguments:** :distance (float): Distance from interaction point to detector plane :pixel_size (float): Edge length of detector pixel (square shape) **Keyword arguments:** :cx (float): Horizontal beam position in unit pixel. If ``cx=None`` beam will be positioned in the center (default ``None``) :cy (float): Vertical beam position in unit pixel If ``cy=None`` beam will be positioned in the center (default ``None``) :center_variation (str): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :center_spread_x (float): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :center_spread_y (float): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :center_variation_n (int): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :noise (str): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :noise_spread (float): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :noise_filename (str): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :noise_dataset (str): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :saturation_level (float): Value at which detector pixels satutrate (default ``None``) :binning (int): Pixel binning factor, intensies are integrated over square patches that have an area of ``binning`` x ``binning`` pixels (default ``None``) :mask_CXI_bitmask (bool): If ``True`` the provided mask (``mask_dataset`` or ``mask``) is a CXI bitmask. For documentation on the implementation of CXI bitmasks see :class:`condor.utils.pixelmask.PixelMask` (default ``False``) :solid_angle_correction (bool): Whether or not solid angle correction shall be applied (default ``True``) *Choose one of the following options:* ==================== ============================================================================= ``mask_CXI_bitmask`` valid pixels ==================== ============================================================================= ``False`` ``1`` ``True`` ``(pixels & condor.utils.pixelmask.PixelMask.PIXEL_IS_IN_MASK_DEFAULT) == 0`` ==================== ============================================================================= **There are 3 alternative options to specify shape and mask of the detector** *A) Parameters* :nx (int): Number of pixels in *x* direction (not including a potential gap or hole) (default ``None``) :ny (int): Number of pixels in *y* direction (not including a potential gap or hole) (default ``None``) :x_gap_size_in_pixel (int): Size of central gap along *x* in unit pixel (default ``None``) :y_gap_size_in_pixel (int): Size of central gap along *y* in unit pixel (default ``None``) :hole_diameter_in_pixel (int): Diameter of central hole in unit pixel (default ``None``) *B) HDF5 dataset for mask* :mask_filename (str): Location of HDF5 file that contains dataset for mask (default ``None``) :mask_dataset (str): HDF5 dataset (in the file specified by the argument ``mask_filename``) that contains the mask data. Toggle the option ``mask_CXI_bitmask`` for decoding options (default ``None``) *C) Numpy array for mask* :mask (array): 2D numpy integer array that defines the mask. Toggle ``mask_CXI_bitmask`` for decoding options (default ``None``) """ def __init__(self, distance, pixel_size, x_gap_size_in_pixel=0, y_gap_size_in_pixel=0, hole_diameter_in_pixel=0, cx_hole=None, cy_hole=None, noise=None, noise_spread=None, noise_variation_n=None, noise_filename=None, noise_dataset=None, cx=None, cy=None, center_variation=None, center_spread_x=None, center_spread_y=None, center_variation_n=None, saturation_level=None, mask=None, mask_filename=None, mask_dataset=None, mask_is_cxi_bitmask=False, solid_angle_correction=True, nx=None, ny=None, binning=None): self.distance = distance self.pixel_size = float(pixel_size) self._init_mask(mask=mask, mask_is_cxi_bitmask=mask_is_cxi_bitmask, mask_filename=mask_filename, mask_dataset=mask_dataset, nx=nx, ny=ny, x_gap_size_in_pixel=x_gap_size_in_pixel, y_gap_size_in_pixel=y_gap_size_in_pixel, cx_hole=cx_hole, cy_hole=cy_hole, hole_diameter_in_pixel=hole_diameter_in_pixel) self.cx_mean = cx if cx != 'middle' else None self.cy_mean = cy if cy != 'middle' else None self.set_center_variation(center_variation=center_variation, center_spread_x=center_spread_x, center_spread_y=center_spread_y, center_variation_n=center_variation_n) self.set_noise(noise=noise, noise_spread=noise_spread, noise_variation_n=noise_variation_n, noise_filename=noise_filename, noise_dataset=noise_dataset) self.saturation_level = saturation_level self.binning = binning self.solid_angle_correction = solid_angle_correction def get_conf(self): """ Get configuration in form of a dictionary. Another identically configured Detector instance can be initialised by: .. code-block:: python conf = D0.get_conf() # D0: already existing Detector instance D1 = condor.Detector(**conf) # D1: new Detector instance with the same configuration as D0 """ conf = {} conf["detector"] = {} conf["detector"]["distance"] = self.distance conf["detector"]["pixel_size"] = self.pixel_size conf["detector"]["cx"] = self.cx_mean conf["detector"]["cy"] = self.cy_mean cvar = self._center_variation.get_conf() conf["detector"]["center_variation"] = cvar["mode"] conf["detector"]["center_spread_x"] = cvar["spread"][0] conf["detector"]["center_spread_y"] = cvar["spread"][1] conf["detector"]["center_variation_n"] = cvar["n"] noise = self._noise.get_conf() conf["detector"]["noise"] = noise["mode"] conf["detector"]["noise_spread"] = noise["spread"] conf["detector"]["noise_filename"] = self._noise_filename conf["detector"]["noise_dataset"] = self._noise_dataset conf["detector"]["saturation_level"] = self.saturation_level conf["detector"]["mask"] = self._mask.copy() conf["detector"]["mask_CXI_bitmask"] = True conf["detector"]["solid_angle_correction"] = self.solid_angle_correction return conf def set_noise(self, noise=None, noise_spread=None, noise_variation_n=None, noise_filename=None, noise_dataset=None): r""" Set detector noise type and parameters (this method is called during initialisation) Kwargs: :noise (str): Noise added to the predicted intensities (default ``None``) *Choose one of the following options:* ======================= ================================================================== ``noise`` Noise model ======================= ================================================================== ``None`` No noise ``'poisson'`` Poisson noise (*shot noise*) ``'normal'`` Normal (*Gaussian*) noise ``'uniform'`` Uniformly distributed values within spread limits ``'normal_poisson'`` Normal (*Gaussian*) noise on top of Poisson noise (*shot noise*) ``'file'`` Noise data from file ``'file_poisson'`` Noise data from file on top of Poisson noise (*shot noise*) ======================= ================================================================== :noise_spread (float): Width (full width at half maximum) of the Gaussian or uniform noise distribution (default ``None``) .. note:: The argument ``noise_spread`` takes only effect in combination with ``noise='normal'``, ``'uniform'`` or ``'normal_poisson'`` :noise_filename (str): Location of the HDF5 file that contains the noise data (default ``None``) :noise_dataset (str): HDF5 dataset (in the file specified by the argument ``noise_filename``) that contains the noise data (default ``None``) .. note:: The arguments ``noise_filename`` and ``noise_dataset`` takes effect only in combination with ``noise='file'`` or ``'file_poisson'`` """ if noise in ["file","file_poisson"]: self._noise_filename = noise_filename self._noise_dataset = noise_dataset self._noise = Variation("poisson" if noise == "file_poisson" else None, noise_spread, noise_variation_n, number_of_dimensions=1) else: self._noise_filename = None self._noise_dataset = None self._noise = Variation(noise, noise_spread, noise_variation_n, number_of_dimensions=1) def set_center_variation(self, center_variation=None, center_spread_x=None, center_spread_y=None, center_variation_n=None): """ Set the variation of the beam center position (this method is called during initialisation) Kwargs: :center_variation(str): Variation of the beam center position (default ``None``) *Choose one of the following options:* ===================== ============================================== ``center_variation`` Variation model ===================== ============================================== ``None`` No variation ``'normal'`` Normal (*Gaussian*) random distribution ``'uniform'`` Uniform random distribution ``'range'`` Equispaced grid around mean center position ===================== ============================================== :center_spread_x (float): Width of the distribution of center position in *x* [pixel] (default ``None``) :center_spread_y (float): Width of the distribution of center position in *y* [pixel] (default ``None``) .. note:: The arguments ``center_spread_y`` and ``center_spread_x`` take effect only in combination with ``center_variation='normal'``, ``'uniform'`` or ``'range'`` :center_variation_n (int): Number of samples within the specified range (default ``None``) .. note:: The argument ``center_variation_n`` takes effect only in combination with ``center_variation='range'`` """ self._center_variation = Variation(center_variation, [center_spread_x,center_spread_y], center_variation_n, number_of_dimensions=2) def _init_mask(self, mask, mask_is_cxi_bitmask, mask_filename, mask_dataset, nx, ny, x_gap_size_in_pixel, y_gap_size_in_pixel, cx_hole, cy_hole, hole_diameter_in_pixel): if mask is not None or (mask_filename is not None and mask_dataset is not None): if mask is not None: # Copy mask from array self._mask = numpy.array(mask, dtype=numpy.uint16) else: # Read mask from file import h5py with h5py.File(mask_filename,"r") as f: self._mask = numpy.array(f[mask_dataset][:,:], dtype=numpy.uint16) if not mask_is_cxi_bitmask: # Convert maskt to CXI bit format self._mask = (self._mask == 0) * PixelMask.PIXEL_IS_MISSING elif nx is not None and ny is not None: # Initialise empty mask self._mask = numpy.zeros(shape=(int(ny+y_gap_size_in_pixel), int(nx+x_gap_size_in_pixel)),dtype=numpy.uint16) else: log_and_raise_error(logger, r"Either 'mask' or 'nx' and 'ny' have to be specified.") sys.exit(1) self._nx = self._mask.shape[1] self._ny = self._mask.shape[0] # Mask out pixels in gaps if y_gap_size_in_pixel > 0: cy = int(numpy.ceil((self._ny-1)/2.)) gy = int(numpy.round(y_gap_size_in_pixel)) self._mask[cy-gy/2:cy-gy/2+gy,:] |= PixelMask.PIXEL_IS_MISSING if x_gap_size_in_pixel > 0: cx = int(numpy.ceil((self._nx-1)/2.)) gx = int(numpy.round(x_gap_size_in_pixel)) self._mask[:,cx-gx/2:cx-gx/2+gx] |= PixelMask.PIXEL_IS_MISSING # Mask out pixels in hole if hole_diameter_in_pixel > 0: if cx_hole is None: cx_hole = (self._nx-1)/2. if cy_hole is None: cy_hole = (self._ny-1)/2. Y,X = numpy.indices((self._ny,self._nx), dtype=numpy.float64) X = X-cx_hole Y = Y-cy_hole R = numpy.sqrt(X**2 + Y**2) tmp = R<=hole_diameter_in_pixel/2.0 if tmp.sum() > 0: self._mask[tmp] |= PixelMask.PIXEL_IS_MISSING def get_mask(self,intensities=None, boolmask=False): """ Return mask. The mask has information about the status of each individual detector pixel. The output can be either a CXI bitmask (default) or a boolean mask For further information and the full bitcode go to :class:`condor.utils.pixelmask.PixelMask` Kwargs: :intensities: Numpy array of photon intensities for masking saturated pixels (default ``None``) :boolmask (bool): If ``True`` the output will be a boolean array. Mask values are converted to ``True`` if no bit is set and to ``False`` otherwise """ if intensities is not None: if not condor.utils.testing.same_shape(intensities, self._mask): log_and_raise_error(logger, "Intensities and mask do not have the same shape") M = self._mask.copy() if self.saturation_level is not None and intensities is not None: M[intensities >= self.saturation_level] |= PixelMask.PIXEL_IS_SATURATED if boolmask: return numpy.array(M == 0,dtype="bool") else: return M def get_cx_mean_value(self): """ Return *x*-coordinate of the mean beam center position """ if self.cx_mean is None: return (self._nx-1) / 2. else: return self.cx_mean def get_cy_mean_value(self): """ Return *y*-coordinate of the mean beam center position """ if self.cy_mean is None: return (self._ny-1) / 2. else: return self.cy_mean def get_next(self): """ Iterate the parameters of the Detector instance and return them as a dictionary """ O = {} cx_mean = self.get_cx_mean_value() cy_mean = self.get_cy_mean_value() cx, cy = self._center_variation.get([cx_mean, cy_mean]) O["cx"] = cx O["cy"] = cy O["nx"] = self._nx O["ny"] = self._ny O["pixel_size"] = self.pixel_size O["distance"] = self.distance if self.binning is not None: O["cx_xxx"] = condor.utils.resample.downsample_pos(cx, self._nx, self.binning) O["cy_xxx"] = condor.utils.resample.downsample_pos(cy, self._ny, self.binning) return O def get_pixel_solid_angle(self, x_off=0., y_off=0.): """ Get the solid angle for a pixel at position ``x_off``, ``y_off`` with respect to the beam center Kwargs: :x_off: *x*-coordinate of the pixel position (center) in unit pixel with respect to the beam center (default 0.) :y_off: *y*-coordinate of the pixel position (center) in unit pixel with respect to the beam center (default 0.) """ r_max = numpy.sqrt(x_off**2+y_off**2) * self.pixel_size it = isinstance(r_max, collections.Iterable) if it: r_max = r_max.max() if r_max/self.distance < 0.0001: # Small angle approximation (fast) omega = self.pixel_size**2 / self.distance**2 if it: omega *= numpy.ones_like(r_max) else: # More precise formula for large angles (slow) x_alpha = numpy.arctan2((x_off+0.5)*self.pixel_size, self.distance) - numpy.arctan2((x_off-0.5)*self.pixel_size, self.distance) y_alpha = numpy.arctan2((y_off+0.5)*self.pixel_size, self.distance) - numpy.arctan2((y_off-0.5)*self.pixel_size, self.distance) omega = 4. * numpy.arcsin(numpy.sin(x_alpha/2.)*numpy.sin(y_alpha/2.)) return omega def get_all_pixel_solid_angles(self, cx, cy): """ Return the solid angles of all detector pixels assuming a beam center at position (``cx``, ``cy``). Args: :cx (float): *x*-coordinate of the center position in unit pixel :cy (float): *y*-coordinate of the center position in unit pixel """ Y, X = numpy.meshgrid(numpy.float64(numpy.arange(self._ny))-cy, numpy.float64(numpy.arange(self._nx))-cx, indexing="ij") return self.get_pixel_solid_angle(X, Y) def _get_xy_max_dist(self, cx = None, cy = None, center_variation = False): dist_max = [] for c,dc,n in [[(cx if cx is not None else self.get_cx_mean_value()),self._center_variation.get_spread()[0],self._nx], [(cy if cy is not None else self.get_cy_mean_value()),self._center_variation.get_spread()[1],self._ny]]: lim = 0 if center_variation: lim = dc lim = lim*0.5 if lim is not None else 0. cv_mode = self._center_variation.get_mode() if cv_mode is not None: if "normal" in cv_mode: lim *= 3 c_min = c - lim/2. c_max = c + lim/2. res1 = n-1-c_min res2 = -c_max dist_max.append(res1 if abs(res1) > abs(res2) else res2) return dist_max def get_p_max_dist(self, cx = None, cy = None, pos = "corner", center_variation = False): r""" Return 3D position vector of the pixel furthest away from the beam center. If each of the given center position coordinates (``cx``, ``cy``) is ``None`` the beam center is assumed to be located at its mean position Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :pos (str): Position constraint can be either ``pos='corner'`` or ``pos='edge'``. (default ``'corner'``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ x, y = self._get_xy_max_dist(cx=cx, cy=cy, center_variation=center_variation) xm = x*self.pixel_size ym = y*self.pixel_size log_debug(logger, "x = %.1f pix, y = %.1f pix" % (x, y)) p = numpy.array([0.,0.,self.distance]) if pos == "corner": p[0] = xm p[1] = ym elif pos == "edge": if abs(x) > abs(y): p[0] = xm else: p[1] = ym else: log_and_raise_error(logger, r"Invalid input: pos=%s. Input must be either 'corner' or 'edge'." % pos) return p def get_q_max(self, wavelength, cx = None, cy = None, pos = "corner", center_variation = False): """ Return q-vector of maximal lenght Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) """ p = abs(self.get_p_max_dist(cx=cx, cy=cy, pos=pos, center_variation=center_variation)) q = abs(condor.utils.scattering_vector.q_from_p(p, wavelength)) return q def _get_resolution_element(self, wavelength, cx = None, cy = None, center_variation = False, pos="edge"): res = numpy.pi / self.get_q_max(wavelength, cx=cx, cy=cy, pos=pos, center_variation=center_variation) return res def get_max_resolution(self, wavelength, cx = None, cy = None, center_variation = False): """ Return maximum resolution as a 3D vector (i.e. maximum resolution / momentum transfer in *x*, *y* and *z*) Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ return self._get_resolution_element(wavelength, cx=cx, cy=cy, center_variation=center_variation, pos="corner") def get_resolution_element_y(self, wavelength, cx = None, cy = None, center_variation = False): """ Return resolution in *y* in 1/meters Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ return self._get_resolution_element(wavelength, cx=cx, cy=cy, center_variation=center_variation, pos="corner")[1] def get_resolution_element_x(self, wavelength, cx = None, cy = None, center_variation = False): """ Return resolution in *x* in 1/meters Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ return self._get_resolution_element(wavelength, cx=cx, cy=cy, center_variation=center_variation, pos="corner")[0] def get_resolution_element_r(self, wavelength, cx = None, cy = None, center_variation = False): """ Return resolution at the furthes corner position in 1/meters Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ qmax = self.get_q_max(wavelength, cx=cx, cy=cy, pos="corner", center_variation=center_variation) res = numpy.pi / length(qmax) return res def generate_xypix(self, cx=None, cy=None): Y, X = numpy.meshgrid(numpy.float64(numpy.arange(self._ny))-(0. if cy is None else cy), numpy.float64(numpy.arange(self._nx))-(0. if cx is None else cx), indexing="ij") return X, Y def generate_qmap(self, wavelength, cx=None, cy=None, extrinsic_rotation=None, order='xyz'): X, Y = self.generate_xypix(cx, cy) return condor.utils.scattering_vector.generate_qmap(X, Y, self.pixel_size, self.distance, wavelength, extrinsic_rotation=extrinsic_rotation, order=order) def generate_qmap_3d(self, wavelength, qn=None, qmax=None, extrinsic_rotation=None, order='xyz'): if qn is None and qmax is None: qn = max([self._nx, self._ny]) qmax = self.get_q_max(wavelength, pos="edge") elif qn is not None and qmax is not None: pass else: log_and_raise_error(logger, "Either none or both optional arguments qn and qmax have to be passed to this function.") return return condor.utils.scattering_vector.generate_qmap_3d(qn=qn, qmax=qmax, extrinsic_rotation=extrinsic_rotation, order=order) #def generate_rpix_3d(self, qmax, qn, wavelength): # return condor.utils.scattering_vector.generate_rpix_3d(qn, qmax, wavelength, self.distance, self.pixel_size): def calculate_polarization_factors(self, cx=None, cy=None, polarization="ignore"): if polarization == "ignore": P = numpy.ones(shape=(self._ny, self._nx)) else: X, Y = self.generate_xypix(cx=cx, cy=cy) P = condor.utils.diffraction.polarization_factor(X*self.pixel_size, Y*self.pixel_size, self.distance, polarization=polarization) return P def detect_photons(self, I): """ Return measurement of intensities from an array of expectation values of intensities. This method also returns the mask of the pattern Args: :I (array): Intensity pattern represented as 2D array """ I_det = self._noise.get(I) if self._noise_filename is not None: import h5py with h5py.File(self._noise_filename,"r") as f: ds = f[self._noise_dataset] if len(list(ds.shape)) == 2: bg = ds[:,:] else: bg = ds[numpy.random.randint(ds.shape[0]),:,:] I_det = I_det + bg if self.saturation_level is not None: I_det = numpy.clip(I_det, -numpy.inf, self.saturation_level) if I_det.ndim == 2: M_det = self.get_mask(I_det) else: M_det = None return I_det, M_det def bin_photons(self, I_det, M_det): """ Return the tuple of binned diffraction pattern and mask. If binning has not been specified a tuple ``(None, None)`` is returned Args: :I_det (array): Intensity pattern (before binning) represented by a 2D array :M_det (array): CXI bitmask (before binning) represented by a 2D array (see also :class:`condor.utils.pixelmask.PixelMask`) """ if self.binning is not None: IXxX_det, MXxX_det = condor.utils.resample.downsample(I_det,self.binning,mode="integrate", mask2d0=M_det,bad_bits=PixelMask.PIXEL_IS_IN_MASK,min_N_pixels=1) else: IXxX_det = None MXxX_det = None return IXxX_det, MXxX_det
class AbstractParticle: r""" Base class for every derived particle class Kwargs: :rotation_values: See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_formalism (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_mode (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :number (float): Expectation value for the number of particles in the interaction volume. (defaukt ``1.``) :arrival (str): Arrival of particles at the interaction volume can be either ``'random'`` or ``'synchronised'``. If ``sync`` at every event the number of particles in the interaction volume equals the rounded value of ``number``. If ``'random'`` the number of particles is Poissonian and ``number`` is the expectation value. (default ``'synchronised'``) :position: (Mean) position vector [*x*, *y*, *z*] of the particle. If set to ``None`` the particle is placed at the origin (default ``None``) :position_variation (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_spread (float): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_variation_n (int): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) """ def __init__(self, rotation_values = None, rotation_formalism = None, rotation_mode = "extrinsic", number = 1., arrival = "synchronised", position = None, position_variation = None, position_spread = None, position_variation_n = None): self.set_alignment(rotation_values=rotation_values, rotation_formalism=rotation_formalism, rotation_mode=rotation_mode) self.set_position_variation(position_variation=position_variation, position_spread=position_spread, position_variation_n=position_variation_n) self.position_mean = position if position is not None else [0., 0., 0.] self.number = number self.arrival = arrival def get_next_number_of_particles(self): """ Iterate the number of partices """ if self.arrival == "random": return int(numpy.random.poisson(self.number)) elif self.arrival == "synchronised": return int(numpy.round(self.number)) else: log_and_raise_error(logger, "self.arrival=%s is invalid. Has to be either \'synchronised\' or \'random\'." % self.arrival) def get_next(self): """ Iterate the parameters of the Particle instance and return them as a dictionary """ O = {} O["_class_instance"] = self O["extrinsic_quaternion"] = self._get_next_extrinsic_rotation().get_as_quaternion() O["position"] = self._get_next_position() return O def get_current_rotation(self): """ Return current orientation of the particle in form of an instance of :class:`condor.utils.rotation.Rotation` """ return self._rotations.get_current_rotation() def set_alignment(self, rotation_values, rotation_formalism, rotation_mode): """ Set rotation scheme of the partice Args: :rotation_values: Array of rotation parameters. For simulating patterns of many shots this can be also a sequence of rotation parameters. Input ``None`` for no rotation and for random rotation formalisms. For more documentation see :class:`condor.utils.rotation.Rotations` (default ``None``) :rotation_mode (str): If the rotation shall be assigned to the particle choose ``\'extrinsic\'``. Choose ``\'intrinsic\'`` if the coordinate system shall be rotated (default ``\'extrinsic\'``) """ # Check input if rotation_mode not in ["extrinsic","intrinsic"]: log_and_raise_error(logger, "%s is not a valid rotation mode for alignment." % rotation_mode) sys.exit(1) self._rotation_mode = rotation_mode self._rotations = condor.utils.rotation.Rotations(values=rotation_values, formalism=rotation_formalism) def set_position_variation(self, position_variation, position_spread, position_variation_n): r""" Set position variation scheme Args: :position_variation (str): Statistical variation of the particle position (default ``None``) *Choose one of the following options:* ====================== ============================================================================================ ``position_variation`` Type of variation ====================== ============================================================================================ ``None`` No positional variation ``'normal'`` Normal (*Gaussian*) variation ``'uniform'`` Uniformly distributed positions within spread limits ``'range'`` Equidistant sequence of ``position_variation_n`` position samples within ``position_spread`` ====================== ============================================================================================ :position_spread (float): Statistical spread of the particle position :position_variation_n (int): Number of position samples within the specified range in each dimension .. note:: The argument ``position_variation_n`` takes effect only in combination with ``position_variation='range'`` """ self._position_variation = Variation(position_variation,position_spread,position_variation_n,number_of_dimensions=3) def _get_next_extrinsic_rotation(self): rotation = self._rotations.get_next_rotation() if self._rotation_mode == "intrinsic": rotation = copy.deepcopy(rotation) rotation.invert() return rotation def _get_next_position(self): return self._position_variation.get(self.position_mean) def get_conf(self): """ Get configuration in form of a dictionary """ conf = {} conf.update(self._get_conf_rotation()) conf.update(self._get_conf_position_variation()) conf["number"] = self.number conf["arrival"] = self.arrival return conf def _get_conf_alignment(self): R = self.get_current_rotation() A = { "rotation_values" : self._rotations.get_all_values(), "rotation_formalism" : self._rotations.get_formalism(), "rotation_mode" : self._rotation_mode } return A def _get_conf_position_variation(self): A = { "position_variation": self._position_variation.get_mode(), "position_variation_spread": self._position_variation.get_spread(), "position_variation_n": self._position_variation.n } return A
class AbstractContinuousParticle(AbstractParticle): """ Base class for derived particle classes that make use of the continuum approximation (density instead of discrete atoms) Args: :diameter (float): (Mean) particle diameter in unit meter Kwargs: :diameter_variation (str): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :diameter_spread (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :diameter_variation_n (int): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :rotation_values (array): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_formalism (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_mode (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :number (float): Expectation value for the number of particles in the interaction volume. (defaukt ``1.``) :arrival (str): Arrival of particles at the interaction volume can be either ``'random'`` or ``'synchronised'``. If ``sync`` at every event the number of particles in the interaction volume equals the rounded value of ``number``. If ``'random'`` the number of particles is Poissonian and ``number`` is the expectation value. (default ``'synchronised'``) :position (array): See :class:`condor.particle.particle_abstract.AbstractParticle` (default ``None``) :position_variation (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_spread (float): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_variation_n (int): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :material_type (str): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``\'water\'``) :massdensity (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) :atomic_composition (dict): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) :electron_density (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) """ def __init__(self, diameter, diameter_variation = None, diameter_spread = None, diameter_variation_n = None, rotation_values = None, rotation_formalism = None, rotation_mode = "extrinsic", number = 1., arrival = "synchronised", position = None, position_variation = None, position_spread = None, position_variation_n = None, material_type = 'water', massdensity = None, atomic_composition = None, electron_density = None): # Initialise base class AbstractParticle.__init__(self, rotation_values=rotation_values, rotation_formalism=rotation_formalism, rotation_mode=rotation_mode, number=number, arrival=arrival, position=position, position_variation=position_variation, position_spread=position_spread, position_variation_n=position_variation_n) # Diameter self.set_diameter_variation(diameter_variation=diameter_variation, diameter_spread=diameter_spread, diameter_variation_n=diameter_variation_n) self.diameter_mean = diameter # Material self.set_material(material_type=material_type, massdensity=massdensity, atomic_composition=atomic_composition, electron_density=electron_density) def get_conf(self): """ Get configuration in form of a dictionary """ conf = {} conf.update(AbstractParticle.get_conf(self)) conf["diameter"] = self.diameter_mean dvar = self._diameter_variation.get_conf() conf["diameter_variation"] = dvar["mode"] conf["diameter_spread"] = dvar["spread"] conf["diameter_variation_n"] = dvar["n"] conf.update(self._get_material_conf()) return conf def get_next(self): """ Iterate the parameters of the Particle instance and return them as a dictionary """ O = AbstractParticle.get_next(self) O["diameter"] = self._get_next_diameter() return O def set_diameter_variation(self, diameter_variation, diameter_spread, diameter_variation_n): r""" Set the variation scheme of the particle diameter Args: :diameter_variation (str): Variation of the particle diameter *Choose one of the following options:* ====================== ============================================================================================ ``diameter_variation`` Type of variation ====================== ============================================================================================ ``None`` No diameter variation ``'normal'`` Normal (*Gaussian*) variation ``'uniform'`` Uniformly distributed diameters within spread limits ``'range'`` Equidistant sequence of ``diameter_variation_n`` diameter samples within ``diameter_spread`` ====================== ============================================================================================ :diameter_spread (float): Statistical spread :diameter_variation_n (int): Number of particle-diameter samples within the specified range .. note:: The argument ``diameter_variation_n`` takes effect only if ``diameter_variation='range'`` """ self._diameter_variation = Variation(diameter_variation, diameter_spread, diameter_variation_n) def _get_next_diameter(self): d = self._diameter_variation.get(self.diameter_mean) # Non-random diameter if self._diameter_variation._mode in [None,"range"]: if d <= 0: log_and_raise_error(logger,"Sample diameter smaller-equals zero. Change your configuration.") else: return d # Random diameter else: if d <= 0.: log_warning(logger, "Sample diameter smaller-equals zero. Try again.") return self._get_next_diameter() else: return d def set_material(self, material_type, massdensity, atomic_composition, electron_density): """ Initialise and set the AtomDensityMaterial / ElectronDensityMaterial class instance of the particle Args: :material_type (str): See :class:`condor.utils.material.AtomDensityMaterial` :massdensity (float): See :class:`condor.utils.material.AtomDensityMaterial` :atomic_composition (dict): See :class:`condor.utils.material.AtomDensityMaterial` :electron_density (float): See :class:`condor.utils.material.ElectronDensityMaterial` """ if material_type is None: self.materials = None else: self.materials = [] if isinstance(material_type, list) or isinstance(massdensity, list) or isinstance(atomic_composition, list) or isinstance(electron_density, list): L = max([len(v) for v in [material_type, massdensity, atomic_composition, electron_density] if isinstance(v, list)]) material_types = material_type if material_type is not None else [None]*L massdensities = massdensity if massdensity is not None else [None]*L atomic_compositions = atomic_composition if atomic_composition is not None else [None]*L electron_densities = electron_density if electron_density is not None else [None]*L for material_type_i, massdensity_i, atomic_composition_i, electron_density_i in zip(material_types, massdensities, atomic_compositions, electron_densities): self.add_material(material_type=material_type_i, massdensity=massdensity_i, atomic_composition=atomic_composition_i, electron_density=electron_density_i) else: self.add_material(material_type=material_type, massdensity=massdensity, atomic_composition=atomic_composition, electron_density=electron_density) def add_material(self, material_type, massdensity, atomic_composition, electron_density): """ Initialise and add the AtomDensityMaterial / ElectronDensityMaterial class instance to the particle Args: :material_type (str): See :class:`condor.utils.material.AtomDensityMaterial` :massdensity (float): See :class:`condor.utils.material.AtomDensityMaterial` :atomic_composition (dict): See :class:`condor.utils.material.AtomDensityMaterial` :electron_density (float): See :class:`condor.utils.material.ElectronDensityMaterial` """ if electron_density is None: self.materials.append(AtomDensityMaterial(material_type=material_type, massdensity=massdensity, atomic_composition=atomic_composition)) else: if massdensity is not None or atomic_composition is not None: log_and_raise_error(logger, r"An electron density is defined so material_type, massdensity and atomic_composition have to be all 'None'.") return if material_type != "custom": log_and_raise_error(logger, r"An electron density is defined, so material_type must be \'custom\' but is %s." % material_type) return self.materials.append(ElectronDensityMaterial(electron_density=electron_density)) def _get_material_conf(self): conf = {} for m_i in self.materials: conf_i = m_i.get_conf() if isinstance(m_i, AtomDensityMaterial): conf_i["electron_density"] = None elif isinstance(m_i, ElectronDensityMaterial): conf_i["material_type"] = None conf_i["massdensity"] = None conf_i["atomic_composition"] = None else: log_and_raise_error(logger, "Material has the wrong class: %s" % str(m_i)) for k,v in conf_i.items(): if k not in conf: conf[k] = [] conf[k].append(conf_i[k]) return conf
class Detector: """ Class for a photon area-detector .. image:: images/detector_schematic.jpg **Arguments:** :distance (float): Distance from interaction point to detector plane :pixel_size (float): Edge length of detector pixel (square shape) **Keyword arguments:** :cx (float): Horizontal beam position in unit pixel. If ``cx=None`` beam will be positioned in the center (default ``None``) :cy (float): Vertical beam position in unit pixel If ``cy=None`` beam will be positioned in the center (default ``None``) :center_variation (str): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :center_spread_x (float): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :center_spread_y (float): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :center_variation_n (int): See :meth:`condor.detector.Detector.set_center_variation` (default ``None``) :noise (str): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :noise_spread (float): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :noise_filename (str): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :noise_dataset (str): See :meth:`condor.detector.Detector.set_noise` (default ``None``) :saturation_level (float): Value at which detector pixels satutrate (default ``None``) :binning (int): Pixel binning factor, intensies are integrated over square patches that have an area of ``binning`` x ``binning`` pixels (default ``None``) :mask_CXI_bitmask (bool): If ``True`` the provided mask (``mask_dataset`` or ``mask``) is a CXI bitmask. For documentation on the implementation of CXI bitmasks see :class:`condor.utils.pixelmask.PixelMask` (default ``False``) :solid_angle_correction (bool): Whether or not solid angle correction shall be applied (default ``True``) *Choose one of the following options:* ==================== ============================================================================= ``mask_CXI_bitmask`` valid pixels ==================== ============================================================================= ``False`` ``1`` ``True`` ``(pixels & condor.utils.pixelmask.PixelMask.PIXEL_IS_IN_MASK_DEFAULT) == 0`` ==================== ============================================================================= **There are 3 alternative options to specify shape and mask of the detector** *A) Parameters* :nx (int): Number of pixels in *x* direction (not including a potential gap or hole) (default ``None``) :ny (int): Number of pixels in *y* direction (not including a potential gap or hole) (default ``None``) :x_gap_size_in_pixel (int): Size of central gap along *x* in unit pixel (default ``None``) :y_gap_size_in_pixel (int): Size of central gap along *y* in unit pixel (default ``None``) :hole_diameter_in_pixel (int): Diameter of central hole in unit pixel (default ``None``) *B) HDF5 dataset for mask* :mask_filename (str): Location of HDF5 file that contains dataset for mask (default ``None``) :mask_dataset (str): HDF5 dataset (in the file specified by the argument ``mask_filename``) that contains the mask data. Toggle the option ``mask_CXI_bitmask`` for decoding options (default ``None``) *C) Numpy array for mask* :mask (array): 2D numpy integer array that defines the mask. Toggle ``mask_CXI_bitmask`` for decoding options (default ``None``) """ def __init__(self, distance, pixel_size, x_gap_size_in_pixel=0, y_gap_size_in_pixel=0, hole_diameter_in_pixel=0, cx_hole=None, cy_hole=None, noise=None, noise_spread=None, noise_variation_n=None, noise_filename=None, noise_dataset=None, cx=None, cy=None, center_variation=None, center_spread_x=None, center_spread_y=None, center_variation_n=None, saturation_level=None, mask=None, mask_filename=None, mask_dataset=None, mask_is_cxi_bitmask=False, solid_angle_correction=True, nx=None, ny=None, binning=None): self.distance = distance self.pixel_size = float(pixel_size) self._init_mask(mask=mask, mask_is_cxi_bitmask=mask_is_cxi_bitmask, mask_filename=mask_filename, mask_dataset=mask_dataset, nx=nx, ny=ny, x_gap_size_in_pixel=x_gap_size_in_pixel, y_gap_size_in_pixel=y_gap_size_in_pixel, cx_hole=cx_hole, cy_hole=cy_hole, hole_diameter_in_pixel=hole_diameter_in_pixel) self.cx_mean = cx if cx != 'middle' else None self.cy_mean = cy if cy != 'middle' else None self.set_center_variation(center_variation=center_variation, center_spread_x=center_spread_x, center_spread_y=center_spread_y, center_variation_n=center_variation_n) self.set_noise(noise=noise, noise_spread=noise_spread, noise_variation_n=noise_variation_n, noise_filename=noise_filename, noise_dataset=noise_dataset) self.saturation_level = saturation_level self.binning = binning self.solid_angle_correction = solid_angle_correction def get_conf(self): """ Get configuration in form of a dictionary. Another identically configured Detector instance can be initialised by: .. code-block:: python conf = D0.get_conf() # D0: already existing Detector instance D1 = condor.Detector(**conf) # D1: new Detector instance with the same configuration as D0 """ conf = {} conf["detector"] = {} conf["detector"]["distance"] = self.distance conf["detector"]["pixel_size"] = self.pixel_size conf["detector"]["cx"] = self.cx_mean conf["detector"]["cy"] = self.cy_mean cvar = self._center_variation.get_conf() conf["detector"]["center_variation"] = cvar["mode"] conf["detector"]["center_spread_x"] = cvar["spread"][0] conf["detector"]["center_spread_y"] = cvar["spread"][1] conf["detector"]["center_variation_n"] = cvar["n"] noise = self._noise.get_conf() conf["detector"]["noise"] = noise["mode"] conf["detector"]["noise_spread"] = noise["spread"] conf["detector"]["noise_filename"] = self._noise_filename conf["detector"]["noise_dataset"] = self._noise_dataset conf["detector"]["saturation_level"] = self.saturation_level conf["detector"]["mask"] = self._mask.copy() conf["detector"]["mask_CXI_bitmask"] = True conf["detector"]["solid_angle_correction"] = self.solid_angle_correction return conf def set_noise(self, noise=None, noise_spread=None, noise_variation_n=None, noise_filename=None, noise_dataset=None): r""" Set detector noise type and parameters (this method is called during initialisation) Kwargs: :noise (str): Noise added to the predicted intensities (default ``None``) *Choose one of the following options:* ======================= ================================================================== ``noise`` Noise model ======================= ================================================================== ``None`` No noise ``'poisson'`` Poisson noise (*shot noise*) ``'normal'`` Normal (*Gaussian*) noise ``'uniform'`` Uniformly distributed values within spread limits ``'normal_poisson'`` Normal (*Gaussian*) noise on top of Poisson noise (*shot noise*) ``'file'`` Noise data from file ``'file_poisson'`` Noise data from file on top of Poisson noise (*shot noise*) ======================= ================================================================== :noise_spread (float): Width (full width at half maximum) of the Gaussian or uniform noise distribution (default ``None``) .. note:: The argument ``noise_spread`` takes only effect in combination with ``noise='normal'``, ``'uniform'`` or ``'normal_poisson'`` :noise_filename (str): Location of the HDF5 file that contains the noise data (default ``None``) :noise_dataset (str): HDF5 dataset (in the file specified by the argument ``noise_filename``) that contains the noise data (default ``None``) .. note:: The arguments ``noise_filename`` and ``noise_dataset`` takes effect only in combination with ``noise='file'`` or ``'file_poisson'`` """ if noise in ["file","file_poisson"]: self._noise_filename = noise_filename self._noise_dataset = noise_dataset self._noise = Variation("poisson" if noise == "file_poisson" else None, noise_spread, noise_variation_n, number_of_dimensions=1) else: self._noise_filename = None self._noise_dataset = None self._noise = Variation(noise, noise_spread, noise_variation_n, number_of_dimensions=1) def set_center_variation(self, center_variation=None, center_spread_x=None, center_spread_y=None, center_variation_n=None): """ Set the variation of the beam center position (this method is called during initialisation) Kwargs: :center_variation(str): Variation of the beam center position (default ``None``) *Choose one of the following options:* ===================== ============================================== ``center_variation`` Variation model ===================== ============================================== ``None`` No variation ``'normal'`` Normal (*Gaussian*) random distribution ``'uniform'`` Uniform random distribution ``'range'`` Equispaced grid around mean center position ===================== ============================================== :center_spread_x (float): Width of the distribution of center position in *x* [pixel] (default ``None``) :center_spread_y (float): Width of the distribution of center position in *y* [pixel] (default ``None``) .. note:: The arguments ``center_spread_y`` and ``center_spread_x`` take effect only in combination with ``center_variation='normal'``, ``'uniform'`` or ``'range'`` :center_variation_n (int): Number of samples within the specified range (default ``None``) .. note:: The argument ``center_variation_n`` takes effect only in combination with ``center_variation='range'`` """ self._center_variation = Variation(center_variation, [center_spread_x,center_spread_y], center_variation_n, number_of_dimensions=2) def _init_mask(self, mask, mask_is_cxi_bitmask, mask_filename, mask_dataset, nx, ny, x_gap_size_in_pixel, y_gap_size_in_pixel, cx_hole, cy_hole, hole_diameter_in_pixel): if mask is not None or (mask_filename is not None and mask_dataset is not None): if mask is not None: # Copy mask from array self._mask = numpy.array(mask, dtype=numpy.uint16) else: # Read mask from file import h5py with h5py.File(mask_filename,"r") as f: self._mask = numpy.array(f[mask_dataset][:,:], dtype=numpy.uint16) if not mask_is_cxi_bitmask: # Convert maskt to CXI bit format self._mask = (self._mask == 0) * PixelMask.PIXEL_IS_MISSING elif nx is not None and ny is not None: # Initialise empty mask self._mask = numpy.zeros(shape=(int(ny+y_gap_size_in_pixel), int(nx+x_gap_size_in_pixel)),dtype=numpy.uint16) else: log_and_raise_error(logger, r"Either 'mask' or 'nx' and 'ny' have to be specified.") sys.exit(1) self._nx = self._mask.shape[1] self._ny = self._mask.shape[0] # Mask out pixels in gaps if y_gap_size_in_pixel > 0: cy = int(numpy.ceil((self._ny-1)/2.)) gy = int(numpy.round(y_gap_size_in_pixel)) self._mask[cy-gy//2:cy-gy//2+gy,:] |= PixelMask.PIXEL_IS_MISSING if x_gap_size_in_pixel > 0: cx = int(numpy.ceil((self._nx-1)/2.)) gx = int(numpy.round(x_gap_size_in_pixel)) self._mask[:,cx-gx//2:cx-gx//2+gx] |= PixelMask.PIXEL_IS_MISSING # Mask out pixels in hole if hole_diameter_in_pixel > 0: if cx_hole is None: cx_hole = (self._nx-1)/2. if cy_hole is None: cy_hole = (self._ny-1)/2. Y,X = numpy.indices((self._ny,self._nx), dtype=numpy.float64) X = X-cx_hole Y = Y-cy_hole R = numpy.sqrt(X**2 + Y**2) tmp = R<=hole_diameter_in_pixel/2.0 if tmp.sum() > 0: self._mask[tmp] |= PixelMask.PIXEL_IS_MISSING def get_mask(self,intensities=None, boolmask=False): """ Return mask. The mask has information about the status of each individual detector pixel. The output can be either a CXI bitmask (default) or a boolean mask For further information and the full bitcode go to :class:`condor.utils.pixelmask.PixelMask` Kwargs: :intensities: Numpy array of photon intensities for masking saturated pixels (default ``None``) :boolmask (bool): If ``True`` the output will be a boolean array. Mask values are converted to ``True`` if no bit is set and to ``False`` otherwise """ if intensities is not None: if not condor.utils.testing.same_shape(intensities, self._mask): log_and_raise_error(logger, "Intensities and mask do not have the same shape") M = self._mask.copy() if self.saturation_level is not None and intensities is not None: M[intensities >= self.saturation_level] |= PixelMask.PIXEL_IS_SATURATED if boolmask: return numpy.array(M == 0,dtype="bool") else: return M def get_cx_mean_value(self): """ Return *x*-coordinate of the mean beam center position """ if self.cx_mean is None: return (self._nx-1) / 2. else: return self.cx_mean def get_cy_mean_value(self): """ Return *y*-coordinate of the mean beam center position """ if self.cy_mean is None: return (self._ny-1) / 2. else: return self.cy_mean def get_next(self): """ Iterate the parameters of the Detector instance and return them as a dictionary """ O = {} cx_mean = self.get_cx_mean_value() cy_mean = self.get_cy_mean_value() cx, cy = self._center_variation.get([cx_mean, cy_mean]) O["cx"] = cx O["cy"] = cy O["nx"] = self._nx O["ny"] = self._ny O["pixel_size"] = self.pixel_size O["distance"] = self.distance if self.binning is not None: O["cx_xxx"] = condor.utils.resample.downsample_pos(cx, self._nx, self.binning) O["cy_xxx"] = condor.utils.resample.downsample_pos(cy, self._ny, self.binning) return O def get_pixel_solid_angle(self, x_off=0., y_off=0.): """ Get the solid angle for a pixel at position ``x_off``, ``y_off`` with respect to the beam center Kwargs: :x_off: *x*-coordinate of the pixel position (center) in unit pixel with respect to the beam center (default 0.) :y_off: *y*-coordinate of the pixel position (center) in unit pixel with respect to the beam center (default 0.) """ r_max = numpy.sqrt(x_off**2+y_off**2) * self.pixel_size it = isinstance(r_max, Iterable) if it: r_max = r_max.max() if r_max/self.distance < 0.0001: # Small angle approximation (fast) omega = self.pixel_size**2 / self.distance**2 if it: omega *= numpy.ones_like(r_max) else: # More precise formula for large angles (slow) x_alpha = numpy.arctan2((x_off+0.5)*self.pixel_size, self.distance) - numpy.arctan2((x_off-0.5)*self.pixel_size, self.distance) y_alpha = numpy.arctan2((y_off+0.5)*self.pixel_size, self.distance) - numpy.arctan2((y_off-0.5)*self.pixel_size, self.distance) omega = 4. * numpy.arcsin(numpy.sin(x_alpha/2.)*numpy.sin(y_alpha/2.)) return omega def get_all_pixel_solid_angles(self, cx, cy): """ Return the solid angles of all detector pixels assuming a beam center at position (``cx``, ``cy``). Args: :cx (float): *x*-coordinate of the center position in unit pixel :cy (float): *y*-coordinate of the center position in unit pixel """ Y, X = numpy.meshgrid(numpy.float64(numpy.arange(self._ny))-cy, numpy.float64(numpy.arange(self._nx))-cx, indexing="ij") return self.get_pixel_solid_angle(X, Y) def _get_xy_max_dist(self, cx = None, cy = None, center_variation = False): dist_max = [] for c,dc,n in [[(cx if cx is not None else self.get_cx_mean_value()),self._center_variation.get_spread()[0],self._nx], [(cy if cy is not None else self.get_cy_mean_value()),self._center_variation.get_spread()[1],self._ny]]: lim = 0 if center_variation: lim = dc lim = lim*0.5 if lim is not None else 0. cv_mode = self._center_variation.get_mode() if cv_mode is not None: if "normal" in cv_mode: lim *= 3 c_min = c - lim/2. c_max = c + lim/2. res1 = n-1-c_min res2 = -c_max dist_max.append(res1 if abs(res1) > abs(res2) else res2) return dist_max def get_p_max_dist(self, cx = None, cy = None, pos = "corner", center_variation = False): r""" Return 3D position vector of the pixel furthest away from the beam center. If each of the given center position coordinates (``cx``, ``cy``) is ``None`` the beam center is assumed to be located at its mean position Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :pos (str): Position constraint can be either ``pos='corner'`` or ``pos='edge'``. (default ``'corner'``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ x, y = self._get_xy_max_dist(cx=cx, cy=cy, center_variation=center_variation) xm = x*self.pixel_size ym = y*self.pixel_size log_debug(logger, "x = %.1f pix, y = %.1f pix" % (x, y)) p = numpy.array([0.,0.,self.distance]) if pos == "corner": p[0] = xm p[1] = ym elif pos == "edge": if abs(x) > abs(y): p[0] = xm else: p[1] = ym else: log_and_raise_error(logger, r"Invalid input: pos=%s. Input must be either 'corner' or 'edge'." % pos) return p def get_q_max(self, wavelength, cx = None, cy = None, pos = "corner", center_variation = False): """ Return q-vector of maximal lenght Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) """ p = abs(self.get_p_max_dist(cx=cx, cy=cy, pos=pos, center_variation=center_variation)) q = abs(condor.utils.scattering_vector.q_from_p(p, wavelength)) return q def _get_resolution_element(self, wavelength, cx = None, cy = None, center_variation = False, pos="edge"): res = numpy.pi / self.get_q_max(wavelength, cx=cx, cy=cy, pos=pos, center_variation=center_variation) return res def get_max_resolution(self, wavelength, cx = None, cy = None, center_variation = False): """ Return maximum resolution as a 3D vector (i.e. maximum resolution / momentum transfer in *x*, *y* and *z*) Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ return self._get_resolution_element(wavelength, cx=cx, cy=cy, center_variation=center_variation, pos="corner") def get_resolution_element_y(self, wavelength, cx = None, cy = None, center_variation = False): """ Return resolution in *y* in 1/meters Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ return self._get_resolution_element(wavelength, cx=cx, cy=cy, center_variation=center_variation, pos="corner")[1] def get_resolution_element_x(self, wavelength, cx = None, cy = None, center_variation = False): """ Return resolution in *x* in 1/meters Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ return self._get_resolution_element(wavelength, cx=cx, cy=cy, center_variation=center_variation, pos="corner")[0] def get_resolution_element_r(self, wavelength, cx = None, cy = None, center_variation = False): """ Return resolution at the furthes corner position in 1/meters Args: :wavelength (float): Photon wavelength in meters Kwargs: :cx (float): *x*-coordinate of the center position in unit pixel (default ``None``) :cy (float): *y*-coordinate of the center position in unit pixel (default ``None``) :center_variation (bool): If ``True`` the beam center variation is taken into account. With respect to the mean position a maximum deviation of *factor/2* times the variational spread is assumed. The *factor* is 3 for Gaussian distributed centers and 1 for others (default ``False``) """ qmax = self.get_q_max(wavelength, cx=cx, cy=cy, pos="corner", center_variation=center_variation) res = numpy.pi / length(qmax) return res def generate_xypix(self, cx=None, cy=None): Y, X = numpy.meshgrid(numpy.float64(numpy.arange(self._ny))-(0. if cy is None else cy), numpy.float64(numpy.arange(self._nx))-(0. if cx is None else cx), indexing="ij") return X, Y def generate_qmap(self, wavelength, cx=None, cy=None, extrinsic_rotation=None, order='xyz'): X, Y = self.generate_xypix(cx, cy) return condor.utils.scattering_vector.generate_qmap(X, Y, self.pixel_size, self.distance, wavelength, extrinsic_rotation=extrinsic_rotation, order=order) def generate_qmap_3d(self, wavelength, qn=None, qmax=None, extrinsic_rotation=None, order='xyz'): if qn is None and qmax is None: qn = max([self._nx, self._ny]) qmax = self.get_q_max(wavelength, pos="edge") elif qn is not None and qmax is not None: pass else: log_and_raise_error(logger, "Either none or both optional arguments qn and qmax have to be passed to this function.") return return condor.utils.scattering_vector.generate_qmap_3d(qn=qn, qmax=qmax, extrinsic_rotation=extrinsic_rotation, order=order) #def generate_rpix_3d(self, qmax, qn, wavelength): # return condor.utils.scattering_vector.generate_rpix_3d(qn, qmax, wavelength, self.distance, self.pixel_size): def calculate_polarization_factors(self, cx=None, cy=None, polarization="ignore"): if polarization == "ignore": P = numpy.ones(shape=(self._ny, self._nx)) else: X, Y = self.generate_xypix(cx=cx, cy=cy) P = condor.utils.diffraction.polarization_factor(X*self.pixel_size, Y*self.pixel_size, self.distance, polarization=polarization) return P def detect_photons(self, I): """ Return measurement of intensities from an array of expectation values of intensities. This method also returns the mask of the pattern Args: :I (array): Intensity pattern represented as 2D array """ I_det = self._noise.get(I) if self._noise_filename is not None: import h5py with h5py.File(self._noise_filename,"r") as f: ds = f[self._noise_dataset] if len(list(ds.shape)) == 2: bg = ds[:,:] else: bg = ds[numpy.random.randint(ds.shape[0]),:,:] I_det = I_det + bg if self.saturation_level is not None: I_det = numpy.clip(I_det, -numpy.inf, self.saturation_level) if I_det.ndim == 2: M_det = self.get_mask(I_det) else: M_det = None return I_det, M_det def bin_photons(self, I_det, M_det): """ Return the tuple of binned diffraction pattern and mask. If binning has not been specified a tuple ``(None, None)`` is returned Args: :I_det (array): Intensity pattern (before binning) represented by a 2D array :M_det (array): CXI bitmask (before binning) represented by a 2D array (see also :class:`condor.utils.pixelmask.PixelMask`) """ if self.binning is not None: IXxX_det, MXxX_det = condor.utils.resample.downsample(I_det,self.binning,mode="integrate", mask2d0=M_det,bad_bits=PixelMask.PIXEL_IS_IN_MASK,min_N_pixels=1) else: IXxX_det = None MXxX_det = None return IXxX_det, MXxX_det
class Source: """ Class for an X-ray source Args: :wavelength (float): X-ray wavelength in unit meter :focus_diameter (float): Focus diameter (characteristic transverse dimension) in unit meter :pulse_energy (float): (Statistical mean of) pulse energy in unit Joule Kwargs: :profile_model (str): Model for the spatial illumination profile (default `None`) .. note:: The (keyword) arguments ``focus_diameter`` and ``profile_model`` are passed on to the constructor of :class:`condor.utils.profile.Profile`. For more detailed information read the documentation of the initialisation function. :pulse_energy_variation (str): Statistical variation of the pulse energy (default ``None``) :pulse_energy_spread (float): Statistical spread of the pulse energy in unit Joule (default ``None``) :pulse_energy_variation_n (int): Number of samples within the specified range (default ``None``) :polarization (str): Type of polarization can be either *vertical*, *horizontal*, *unpolarized*, or *ignore* (default ``ignore``) .. note:: The keyword arguments ``pulse_energy_variation``, ``pulse_energy_spread``, and ``pulse_energy_variation_n`` are passed on to :meth:`condor.source.Source.set_pulse_energy_variation` during initialisation. For more detailed information read the documentation of the method. """ def __init__(self, wavelength, focus_diameter, pulse_energy, profile_model=None, pulse_energy_variation=None, pulse_energy_spread=None, pulse_energy_variation_n=None, polarization="ignore"): self.photon = Photon(wavelength=wavelength) self.pulse_energy_mean = pulse_energy self.set_pulse_energy_variation(pulse_energy_variation, pulse_energy_spread, pulse_energy_variation_n) self.profile = Profile(model=profile_model, focus_diameter=focus_diameter) if polarization not in ["vertical", "horizontal", "unpolarized", "ignore"]: log_and_raise_error(logger, "polarization = \"%s\" is an invalid input for initialization of Source instance.") return self.polarization = polarization log_debug(logger, "Source configured") def get_conf(self): """ Get configuration in form of a dictionary. Another identically configured Source instance can be initialised by: .. code-block:: python conf = S0.get_conf() # S0: already existing Source instance S1 = condor.Source(**conf) # S1: new Source instance with the same configuration as S0 """ conf = {} conf["source"] = {} conf["source"]["wavelength"] = self.photon.get_wavelength() conf["source"]["focus_diameter"] = self.profile.focus_diameter conf["source"]["pulse_energy"] = self.pulse_energy_mean conf["source"]["profile_model"] = self.profile.get_model() pevar = self._pulse_energy_variation.get_conf() conf["source"]["pulse_energy_variation"] = pevar["mode"] conf["source"]["pulse_energy_spread"] = pevar["spread"] conf["source"]["pulse_energy_variation_n"] = pevar["n"] conf["source"]["polarization"] = self.polarization return conf def set_pulse_energy_variation(self, pulse_energy_variation = None, pulse_energy_spread = None, pulse_energy_variation_n = None): """ Set variation of the pulse energy Kwargs: :pulse_energy_variation (str): Statistical variation of the pulse energy (default ``None``) *Choose one of the following options:* - ``\'normal\'`` - random normal (Gaussian) distribution - ``\'uniform\'`` - random uniform distribution - ``\'range\'`` - equispaced pulse energies around ``pulse_energy`` - ``None`` - no variation of the pulse energy :pulse_energy_spread (float): Statistical spread of the pulse energy in unit Joule (default ``None``) :pulse_energy_variation_n (int): Number of samples within the specified range .. note:: The argument ``pulse_energy_variation_n`` takes effect only in combination with ``pulse_energy_variation=\'range\'`` """ self._pulse_energy_variation = Variation(pulse_energy_variation, pulse_energy_spread, pulse_energy_variation_n, number_of_dimensions=1) def get_intensity(self, position, unit = "ph/m2", pulse_energy = None): """ Calculate the intensity at a given position in the focus Args: :position: Coordinates [*x*, *y*, *z*] of the position where the intensity shall be calculated Kwargs: :unit (str): Intensity unit (default ``\'ph/m2\'``) *Choose one of the following options:* - ``\'ph/m2\'`` - ``\'J/m2\'`` - ``\'J/um2\'`` - ``\'mJ/um2\'`` - ``\'ph/um2\'`` :pulse_energy (float): Pulse energy of that particular pulse in unit Joule. If ``None`` the mean of the pulse energy will be used (default ``None``) """ # Assuming # 1) Radially symmetric profile that is invariant along the beam axis within the sample volume # 2) The variation of intensity are on much larger scale than the dimension of the particle size (i.e. flat wavefront) r = numpy.sqrt(position[1]**2 + position[2]**2) I = (self.profile.get_radial())(r) * (pulse_energy if pulse_energy is not None else self.pulse_energy_mean) if unit == "J/m2": pass elif unit == "ph/m2": I /= self.photon.get_energy() elif unit == "J/um2": I *= 1.E-12 elif unit == "mJ/um2": I *= 1.E-9 elif unit == "ph/um2": I /= self.photon.get_energy() I *= 1.E-12 else: log_and_raise_error(logger, "%s is not a valid unit." % unit) return return I def get_next(self): """ Iterate the parameters of the Source instance and return them as a dictionary """ return {"pulse_energy":self._get_next_pulse_energy(), "wavelength":self.photon.get_wavelength(), "photon_energy":self.photon.get_energy(), "photon_energy_eV":self.photon.get_energy_eV()} def _get_next_pulse_energy(self): p = self._pulse_energy_variation.get(self.pulse_energy_mean) # Non-random if self._pulse_energy_variation._mode in [None,"range"]: if p <= 0: log_and_raise_error(logger, "Pulse energy smaller-equals zero. Change your configuration.") else: return p # Random else: if p <= 0.: log_warning(logger, "Pulse energy smaller-equals zero. Try again.") self._get_next_pulse_energy() else: return p
class ParticleSpheroid(AbstractContinuousParticle): """ Class for a particle model *Model:* Uniformly filled spheroid particle (continuum approximation) :math:`a`: radius (*semi-diameter*) perpendicular to the rotation axis of the ellipsoid :math:`c`: radius (*semi-diameter*) along the rotation axis of the ellipsoid Before applying rotations the rotation axis is parallel to the the *y*-axis Args: :diameter (float): Sphere diameter Kwargs: :diameter_variation (str): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :diameter_spread (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :diameter_variation_n (int): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_diameter_variation` (default ``None``) :flattening (float): (Mean) value of :math:`a/c` (default ``0.75``) :flattening_variation (str): See :meth:`condor.particle.particle_spheroid.set_flattening_variation` (default ``None``) :flattening_spread (float): See :meth:`condor.particle.particle_spheroid.set_flattening_variation` (default ``None``) :flattening_variation_n (int): See :meth:`condor.particle.particle_spheroid.set_flattening_variation` (default ``None``) :rotation_values (array): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_formalism (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :rotation_mode (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_alignment` (default ``None``) :number (float): Expectation value for the number of particles in the interaction volume. (defaukt ``1.``) :arrival (str): Arrival of particles at the interaction volume can be either ``'random'`` or ``'synchronised'``. If ``sync`` at every event the number of particles in the interaction volume equals the rounded value of ``number``. If ``'random'`` the number of particles is Poissonian and ``number`` is the expectation value. (default ``'synchronised'``) :position (array): See :class:`condor.particle.particle_abstract.AbstractParticle` (default ``None``) :position_variation (str): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_spread (float): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :position_variation_n (int): See :meth:`condor.particle.particle_abstract.AbstractParticle.set_position_variation` (default ``None``) :material_type (str): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``\'water\'``) :massdensity (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) :atomic_composition (dict): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) :electron_density (float): See :meth:`condor.particle.particle_abstract.AbstractContinuousParticle.set_material` (default ``None``) """ def __init__(self, diameter, diameter_variation = None, diameter_spread = None, diameter_variation_n = None, flattening = 0.75, flattening_variation = None, flattening_spread = None, flattening_variation_n = None, rotation_values = None, rotation_formalism = None, rotation_mode = "extrinsic", number = 1., arrival = "synchronised", position = None, position_variation = None, position_spread = None, position_variation_n = None, material_type = 'water', massdensity = None, atomic_composition = None, electron_density = None): # Initialise base class AbstractContinuousParticle.__init__(self, diameter=diameter, diameter_variation=diameter_variation, diameter_spread=diameter_spread, diameter_variation_n=diameter_variation_n, rotation_values=rotation_values, rotation_formalism=rotation_formalism, rotation_mode=rotation_mode, number=number, arrival=arrival, position=position, position_variation=position_variation, position_spread=position_spread, position_variation_n=position_variation_n, material_type=material_type, massdensity=massdensity, atomic_composition=atomic_composition, electron_density=electron_density) self.flattening_mean = flattening self.set_flattening_variation(flattening_variation=flattening_variation, flattening_spread=flattening_spread, flattening_variation_n=flattening_variation_n) def get_conf(self): """ Get configuration in form of a dictionary. Another identically configured ParticleMap instance can be initialised by: .. code-block:: python conf = P0.get_conf() # P0: already existing ParticleSpheroid instance P1 = condor.ParticleSpheroid(**conf) # P1: new ParticleSpheroid instance with the same configuration as P0 """ conf = {} conf.update(AbstractContinuousParticle.get_conf(self)) conf["flattening"] = self.flattening_mean fvar = self._flattening_variation.get_conf() conf["flattening_variation"] = fvar["mode"] conf["flattening_spread"] = fvar["spread"] conf["flattening_variation_n"] = fvar["n"] return conf def get_next(self): """ Iterate the parameters and return them as a dictionary """ O = AbstractContinuousParticle.get_next(self) O["particle_model"] = "spheroid" O["flattening"] = self._get_next_flattening() return O def set_flattening_variation(self, flattening_variation, flattening_spread, flattening_variation_n): """ Set the variation scheme of the flattening parameter Args: :flattening_variation (str): Variation of the particle flattening *Choose one of the following options:* - ``None`` - No variation - ``\'normal\'`` - Normal (*Gaussian*) variation - ``\'uniform\'`` - Uniformly distributed flattenings - ``\'range\'`` - Equidistant sequence of particle-flattening samples within the spread limits. ``flattening_variation_n`` defines the number of samples within the range :flattening_spread (float): Statistical spread of the parameter :flattening_variation_n (int): Number of particle-flattening samples within the specified range .. note:: The argument ``flattening_variation_n`` takes effect only if ``flattening_variation=\'range\'`` """ self._flattening_variation = Variation(flattening_variation, flattening_spread, flattening_variation_n) def _get_next_flattening(self): f = self._flattening_variation.get(self.flattening_mean) # Non-random if self._flattening_variation._mode in [None, "range"]: if f <= 0: log_and_raise_error(logger, "Spheroid flattening smaller-equals zero. Change your configuration.") else: return f # Random else: if f <= 0.: log_warning(logger, "Spheroid flattening smaller-equals zero. Try again.") return self._get_next_flattening() else: return f def get_dn(self, photon_wavelength): if self.materials is None: dn = 0. else: dn = numpy.array([m.get_dn(photon_wavelength) for m in self.materials]).sum() return dn