Project: s41467-024-45642-z | Function: __call__ (python)

system: You are an expert Python programmer implementing algorithms from scientific papers.
Complete the masked function based on the algorithm description.
Follow Python best practices and maintain consistent code style.
Return **only** the complete Python function code.

user: Complete this Python function:

**DESCRIPTION:** Calculate Bloch wave amplitudes for different specimen thicknesses to generate diffraction patterns for dislocation identification

**KEY TERMS:** {
  "thickness": "specimen thickness",
  "out": "complex amplitudes",
  "_bw": "Bloch wave calculations",
  "_exc": "Bloch wave excitations",
  "_tilt_samples": "tilt angles"
}

**CONTEXT CODE:**     :param q_dir_hkl: Direction of tilt axis in crystal coordinates
    :param voltage: Acceleration voltage in kV
    :param dtype: Data type of output
    :param method: Interpolation method (see scipy.interpolate.interpn)
    """
    def __init__(self, beamlist: BeamList, shape: Tuple[int, int], x_axis: LinearAxis, q_axis: LinearAxis,
                 q_dir_hkl: ArrayLike, voltage: float, dtype: DTypeLike = None, method="linear"):
        self._beamlist = beamlist

        if len(shape) != 2:
            raise ValueError("Expected *shape* to have two components.")
        self._shape = tuple(shape)

        if x_axis.unit != 'nm' or x_axis.context != 'POSITION':
            raise ValueError("Expected *x_axis* to have unit 'nm' and context 'POSITION'.")
        self._x_axis = x_axis

        if q_axis.unit != '1/nm' or q_axis.context != 'DIFFRACTION':
            raise ValueError("Expected *q_axis* to have unit '1/nm' and context 'DIFFRACTION'.")
        self._q_axis = q_axis

        q_dir_hkl = np.array(q_dir_hkl)
        if q_dir_hkl.shape != (3,):
            raise ValueError("Expected 'q_dir_hkl' to have shape (3,).")
        q_dir = self._beamlist.coord.map_rcpr_to_cartesian(q_dir_hkl)
        self._q_dir = q_dir / np.sqrt(np.sum(q_dir ** 2))

        self.dtype = np.dtype(dtype if dtype is not None else np.float32)
        self.method = method
        self.wavelength = calc_wavelength(voltage)

    @property
    def beamlist(self) -> BeamList:
        return self._beamlist

    @property
    def shape(self) -> Tuple[int, ...]:
        """Shape of the output"""
        return self._shape

    @property
    def axes(self) -> Tuple[Axis, ...]:
        """Axes of the output"""
        return (self._x_axis, self._q_axis)

    @property
    def q_dir(self) -> np.dtype:
        """Direction of Q-axis in crystal space"""
        return self._q_dir

    def __call__(self, semi_conv: float, tilt: float, source: DataSet, defocus: float = 0.0, curvature: float = 0.0) -> DataSet:
        """
        Create QX plot from 'source_data'.

        :param semi_conv: semi convergence angle (1/nm)
        :param tilt: Tilt offset (1/nm)
        :param source: Data source with axes: (beam-index, x-axis, tilt-axis)
        :param defocus: Defocus (nm)
        :param curvature: Curvature (1/nm2)
        """
        semi_conv = float(semi_conv)
        defocus = float(defocus)
        curvature = float(curvature)
        if semi_conv < 0.5 * abs(self._q_axis.scale):
            raise ValueError("Convergence angle must be larger than Q-axis scaling")

        if source.ndim != 3:
            raise ValueError("Expected 'source_data' to be 3 dimensional")
        if source.shape[0] != len(self._beamlist):
            raise ValueError("Expected dimension #0 to correspond to beam list length.")
        if source.axes[0].context != 'INDEX':
            raise ValueError("Expected axis #0 to have *context* 'INDEX'.")
        if getattr(source.axes[1], "unit", None) != "nm" or source.axes[1].context != "POSITION":
            raise ValueError("Expected axis #1 to have *unit* 'nm' and *context* 'POSITION.")
        if getattr(source.axes[2], "unit", None) != "1/nm":
            raise ValueError("Expected axis #2 to have *unit* '1/nm'.")
        if not isinstance(source.axes[1], LinearAxis) or not isinstance(source.axes[2], LinearAxis):
            raise ValueError("Expected axis #1 and #2 to be LinearAxis instances.")

        source_x_range = source.axis_range(1)
        source_t_range = -source.axis_range(2)

        if source_t_range[1] < source_t_range[0]:
            source_t_sign = -1.0
            source_t_range = -source_t_range
        else:
            source_t_sign = 1.0

        if source_x_range[1] < source_x_range[0]:
            source_x_sign = -1.0
            source_x_range = -source_x_range
        else:
            source_x_sign = 1.0

        metadata = source.metadata.copy()
        metadata['parameter']['semi_conv(1/nm)'] = semi_conv
        metadata['parameter']['tilt(1/nm)'] = tilt

        result = np.zeros(self._shape, dtype=f'c{2 * self.dtype.itemsize}')
        for n, hkl in enumerate(self.beamlist):
            g = self.beamlist.coord.map_rcpr_to_cartesian(hkl)

            g_shift = np.dot(g, self._q_dir)
            q_lo = (g_shift - semi_conv - self._q_axis.offset) / self._q_axis.scale
            q_hi = (g_shift + semi_conv - self._q_axis.offset) / self._q_axis.scale
            if self._q_axis.scale < 0:
                q_lo, q_hi = q_hi, q_lo
            q_lo = max(q_lo, 0)
            q_hi = min(q_hi, self.shape[1] - 1)
            q_ilo = int(np.floor(q_lo))
            q_ihi = int(np.ceil(q_hi))
            w_lo = 1.0 - (q_lo - q_ilo)
            w_hi = 1.0 - (q_ihi - q_hi)

            if q_ilo >= q_ihi:
                continue

            t = np.arange(q_ilo, q_ihi + 1, dtype=float) * self._q_axis.scale + self._q_axis.offset - g_shift - tilt
            x = self._x_axis.range(self._shape[0])
            x, t = np.meshgrid(x, t, indexing='ij')
            if defocus != 0.0:
                x += defocus * self.wavelength * t
            if curvature != 0.0:
                t += curvature * x

            disk = interpn((source_x_range, source_t_range), source[n],
                           (x * source_x_sign, t * source_t_sign), method=self.method, bounds_error=False,
                           fill_value=0.0)
            disk[:, 0] *= w_lo
            disk[:, -1] *= w_hi
            result[:, q_ilo:q_ihi + 1] += disk
        return DataSet(data=csquare(result), axes=self.axes, metadata=metadata)


def create_samples_from_range(amin: float, amax: float, step: float, dtype=None):
    if dtype is None:
        dtype = float
    count = (amax - amin) // step
    if abs(amin + count * step - amax) < 1e-4:
        count += 1
    return np.arange(count, dtype=dtype) * step + amin


class QXCalculation(ABC):
    @property
    @abstractmethod
    def metadata(self) -> CoreMetaData:
        pass

    @property


Complete the function implementation replacing "raise NotImplementedError".