From 023bef6eceeb637c826c31dc5e89a2c47deeffec Mon Sep 17 00:00:00 2001 From: dkazanc Date: Mon, 2 Sep 2024 16:01:30 +0100 Subject: [PATCH] removing nvtx instances to allow docs to build, removing function doubleundescore duplications --- httomolibgpu/cupywrapper.py | 1 + httomolibgpu/misc/morph.py | 28 +--------- httomolibgpu/misc/rescale.py | 48 ---------------- httomolibgpu/prep/alignment.py | 50 ----------------- httomolibgpu/prep/normalize.py | 21 ------- httomolibgpu/prep/phase.py | 48 +--------------- httomolibgpu/prep/stripe.py | 53 +----------------- httomolibgpu/recon/algorithm.py | 97 +-------------------------------- httomolibgpu/recon/rotation.py | 77 ++------------------------ 9 files changed, 13 insertions(+), 410 deletions(-) diff --git a/httomolibgpu/cupywrapper.py b/httomolibgpu/cupywrapper.py index 95af7c3a..48d25e02 100644 --- a/httomolibgpu/cupywrapper.py +++ b/httomolibgpu/cupywrapper.py @@ -14,6 +14,7 @@ ) from unittest.mock import Mock import numpy as cp + cupy_run = False nvtx = Mock() diff --git a/httomolibgpu/misc/morph.py b/httomolibgpu/misc/morph.py index 76a34a01..422290f9 100644 --- a/httomolibgpu/misc/morph.py +++ b/httomolibgpu/misc/morph.py @@ -24,7 +24,6 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx cupy_run = cupywrapper.cupy_run from unittest.mock import Mock @@ -64,17 +63,6 @@ def sino_360_to_180( cp.ndarray Output 3D data. """ - if cupywrapper.cupy_run: - return __sino_360_to_180(data, overlap, rotation) - else: - print("sino_360_to_180 won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __sino_360_to_180( - data: cp.ndarray, overlap: int = 0, rotation: Literal["left", "right"] = "left" -) -> cp.ndarray: if data.ndim != 3: raise ValueError("only 3D data is supported") @@ -85,8 +73,8 @@ def __sino_360_to_180( raise ValueError("overlap must be less than data.shape[2]") if overlap < 0: raise ValueError("only positive overlaps are allowed.") - - if rotation not in ['left', 'right']: + + if rotation not in ["left", "right"]: raise ValueError('rotation parameter must be either "left" or "right"') n = dx // 2 @@ -133,18 +121,6 @@ def data_resampler( Returns: cp.ndarray: Up/Down-scaled 3D cupy array """ - if cupywrapper.cupy_run: - return __data_resampler(data, newshape, axis, interpolation) - else: - print("data_resampler won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __data_resampler( - data: cp.ndarray, newshape: list, axis: int = 1, interpolation: str = "linear" -) -> cp.ndarray: - if data.ndim != 3: raise ValueError("only 3D data is supported") diff --git a/httomolibgpu/misc/rescale.py b/httomolibgpu/misc/rescale.py index 4bf48d3f..5c4af6f4 100644 --- a/httomolibgpu/misc/rescale.py +++ b/httomolibgpu/misc/rescale.py @@ -23,7 +23,6 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx from typing import Literal, Optional, Tuple, Union @@ -70,53 +69,6 @@ def rescale_to_int( The original data, clipped to the range specified with the perc_range_min and perc_range_max, and scaled to the full range of the output integer type """ - if cupywrapper.cupy_run: - return __rescale_to_int(data, perc_range_min, perc_range_max, bits, glob_stats) - else: - print("rescale_to_int won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __rescale_to_int( - data: cp.ndarray, - perc_range_min: float = 0.0, - perc_range_max: float = 100.0, - bits: Literal[8, 16, 32] = 8, - glob_stats: Optional[Tuple[float, float, float, int]] = None, -): - """ - Rescales the data and converts it fit into the range of an unsigned integer type - with the given number of bits. - - Parameters - ---------- - data : cp.ndarray - Required input data array, on GPU - perc_range_min: float, optional - The lower cutoff point in the input data, in percent of the data range (defaults to 0). - The lower bound is computed as min + perc_range_min/100*(max-min) - perc_range_max: float, optional - The upper cutoff point in the input data, in percent of the data range (defaults to 100). - The upper bound is computed as min + perc_range_max/100*(max-min) - bits: Literal[8, 16, 32], optional - The number of bits in the output integer range (defaults to 8). - Allowed values are: - - 8 -> uint8 - - 16 -> uint16 - - 32 -> uint32 - glob_stats: tuple, optional - Global statistics of the full dataset (beyond the data passed into this call). - It's a tuple with (min, max, sum, num_items). If not given, the min/max is - computed from the given data. - - Returns - ------- - cp.ndarray - The original data, clipped to the range specified with the perc_range_min and - perc_range_max, and scaled to the full range of the output integer type - """ - if bits == 8: output_dtype: Union[type[np.uint8], type[np.uint16], type[np.uint32]] = np.uint8 elif bits == 16: diff --git a/httomolibgpu/prep/alignment.py b/httomolibgpu/prep/alignment.py index 505ba5f2..85be8cd4 100644 --- a/httomolibgpu/prep/alignment.py +++ b/httomolibgpu/prep/alignment.py @@ -24,7 +24,6 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx cupy_run = cupywrapper.cupy_run from unittest.mock import Mock @@ -82,55 +81,6 @@ def distortion_correction_proj_discorpy( cp.ndarray 3D array. Distortion-corrected array. """ - if cupywrapper.cupy_run: - return __distortion_correction_proj_discorpy( - data, metadata_path, preview, order, mode - ) - else: - print( - "distortion_correction_proj_discorpy won't be executed because CuPy is not installed" - ) - return data - - -@nvtx.annotate() -def __distortion_correction_proj_discorpy( - data: cp.ndarray, - metadata_path: str, - preview: Dict[str, List[int]], - order: int = 1, - mode: str = "reflect", -): - """Unwarp a stack of images using a backward model. - - Parameters - ---------- - data : cp.ndarray - 3D array. - - metadata_path : str - The path to the file containing the distortion coefficients for the - data. - - preview : Dict[str, List[int]] - A dict containing three key-value pairs: - - a list containing the `start` value of each dimension - - a list containing the `stop` value of each dimension - - a list containing the `step` value of each dimension - - order : int, optional. - The order of the spline interpolation. - - mode : {'reflect', 'grid-mirror', 'constant', 'grid-constant', 'nearest', - 'mirror', 'grid-wrap', 'wrap'}, optional - To determine how to handle image boundaries. - - Returns - ------- - cp.ndarray - 3D array. Distortion-corrected image(s). - """ - # Check if it's a stack of 2D images, or only a single 2D image if len(data.shape) == 2: data = cp.expand_dims(data, axis=0) diff --git a/httomolibgpu/prep/normalize.py b/httomolibgpu/prep/normalize.py index 13327f4c..08b9dde8 100644 --- a/httomolibgpu/prep/normalize.py +++ b/httomolibgpu/prep/normalize.py @@ -24,7 +24,6 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx cupy_run = cupywrapper.cupy_run from unittest.mock import Mock @@ -75,26 +74,6 @@ def normalize( cp.ndarray Normalised 3D tomographic data as a CuPy array. """ - if cupywrapper.cupy_run: - return __normalize( - data, flats, darks, cutoff, minus_log, nonnegativity, remove_nans - ) - else: - print("normalize won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __normalize( - data: cp.ndarray, - flats: cp.ndarray, - darks: cp.ndarray, - cutoff: float = 10.0, - minus_log: bool = True, - nonnegativity: bool = False, - remove_nans: bool = True, -) -> cp.ndarray: - _check_valid_input(data, flats, darks) dark0 = cp.empty(darks.shape[1:], dtype=float32) diff --git a/httomolibgpu/prep/phase.py b/httomolibgpu/prep/phase.py index b62be75b..a191bda5 100644 --- a/httomolibgpu/prep/phase.py +++ b/httomolibgpu/prep/phase.py @@ -24,10 +24,10 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx cupy_run = cupywrapper.cupy_run from unittest.mock import Mock + if cupy_run: from httomolibgpu.cuda_kernels import load_cuda_module from cupyx.scipy.fft import fft2, ifft2, fftshift @@ -98,36 +98,6 @@ def paganin_filter_savu( cp.ndarray The stack of filtered projections. """ - if cupywrapper.cupy_run: - return __paganin_filter_savu( - data, - ratio, - energy, - distance, - resolution, - pad_y, - pad_x, - pad_method, - increment, - ) - else: - print("__paganin_filter_savu won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __paganin_filter_savu( - data: cp.ndarray, - ratio: float = 250.0, - energy: float = 53.0, - distance: float = 1.0, - resolution: float = 1.28, - pad_y: int = 100, - pad_x: int = 100, - pad_method: str = "edge", - increment: float = 0.0, -) -> cp.ndarray: - # Check the input data is valid if data.ndim != 3: raise ValueError( @@ -321,22 +291,6 @@ def paganin_filter_tomopy( cp.ndarray The 3D array of Paganin phase-filtered projection images. """ - if cupywrapper.cupy_run: - return __paganin_filter_tomopy(tomo, pixel_size, dist, energy, alpha) - else: - print("paganin_filter_tomopy won't be executed because CuPy is not installed") - return tomo - - -@nvtx.annotate() -def __paganin_filter_tomopy( - tomo: cp.ndarray, - pixel_size: float = 1e-4, - dist: float = 50.0, - energy: float = 53.0, - alpha: float = 1e-3, -) -> cp.ndarray: - # Check the input data is valid if tomo.ndim != 3: raise ValueError( diff --git a/httomolibgpu/prep/stripe.py b/httomolibgpu/prep/stripe.py index b20ed040..22a838e6 100644 --- a/httomolibgpu/prep/stripe.py +++ b/httomolibgpu/prep/stripe.py @@ -24,12 +24,12 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx cupy_run = cupywrapper.cupy_run from unittest.mock import Mock + if cupy_run: - from cupyx.scipy.ndimage import median_filter, binary_dilation, uniform_filter1d + from cupyx.scipy.ndimage import median_filter, binary_dilation, uniform_filter1d else: median_filter = Mock() binary_dilation = Mock() @@ -73,21 +73,6 @@ def remove_stripe_based_sorting( Corrected 3D tomographic data as a CuPy or NumPy array. """ - if cupywrapper.cupy_run: - return __remove_stripe_based_sorting(data, size, dim) - else: - print( - "remove_stripe_based_sorting won't be executed because CuPy is not installed" - ) - return data - - -@nvtx.annotate() -def __remove_stripe_based_sorting( - data: Union[cp.ndarray, np.ndarray], - size: int = 11, - dim: int = 1, -) -> Union[cp.ndarray, np.ndarray]: if size is None: if data.shape[2] > 2000: size = 21 @@ -100,7 +85,6 @@ def __remove_stripe_based_sorting( return data -@nvtx.annotate() def _rs_sort(sinogram, size, dim): """ Remove stripes using the sorting technique. @@ -143,18 +127,6 @@ def remove_stripe_ti( ndarray 3D array of de-striped projections. """ - if cupywrapper.cupy_run: - return __remove_stripe_ti(data, beta) - else: - print("remove_stripe_ti won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __remove_stripe_ti( - data: Union[cp.ndarray, np.ndarray], - beta: float = 0.1, -) -> Union[cp.ndarray, np.ndarray]: # TODO: detector dimensions must be even otherwise error gamma = beta * ((1 - beta) / (1 + beta)) ** cp.abs( cp.fft.fftfreq(data.shape[-1]) * data.shape[-1] @@ -222,21 +194,6 @@ def remove_all_stripe( Corrected 3D tomographic data as a CuPy or NumPy array. """ - if cupywrapper.cupy_run: - return __remove_all_stripe(data, snr, la_size, sm_size, dim) - else: - print("remove_all_stripe won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __remove_all_stripe( - data: cp.ndarray, - snr: float = 3.0, - la_size: int = 61, - sm_size: int = 21, - dim: int = 1, -) -> cp.ndarray: matindex = _create_matindex(data.shape[2], data.shape[0]) for m in range(data.shape[1]): sino = data[:, m, :] @@ -247,7 +204,6 @@ def __remove_all_stripe( return data -@nvtx.annotate() def _rs_sort2(sinogram, size, matindex, dim): """ Remove stripes using the sorting technique. @@ -276,7 +232,6 @@ def _rs_sort2(sinogram, size, matindex, dim): return cp.transpose(sino_corrected) -@nvtx.annotate() def _mpolyfit(x, y): n = len(x) x_mean = cp.mean(x) @@ -290,7 +245,6 @@ def _mpolyfit(x, y): return slope, intercept -@nvtx.annotate() def _detect_stripe(listdata, snr): """ Algorithm 4 in :cite:`Vo:18`. Used to locate stripes. @@ -321,7 +275,6 @@ def _detect_stripe(listdata, snr): return listmask -@nvtx.annotate() def _rs_large(sinogram, snr, size, matindex, drop_ratio=0.1, norm=True): """ Remove large stripes. @@ -369,7 +322,6 @@ def _rs_large(sinogram, snr, size, matindex, drop_ratio=0.1, norm=True): return sinogram -@nvtx.annotate() def _rs_dead(sinogram, snr, size, matindex, norm=True): """ Remove unresponsive and fluctuating stripes. @@ -408,7 +360,6 @@ def _rs_dead(sinogram, snr, size, matindex, norm=True): return sinogram -@nvtx.annotate() def _create_matindex(nrow, ncol): """ Create a 2D array of indexes used for the sorting technique. diff --git a/httomolibgpu/recon/algorithm.py b/httomolibgpu/recon/algorithm.py index b721bba8..b254eee6 100644 --- a/httomolibgpu/recon/algorithm.py +++ b/httomolibgpu/recon/algorithm.py @@ -24,10 +24,10 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx cupy_run = cupywrapper.cupy_run from unittest.mock import Mock + if cupy_run: from tomobar.methodsDIR_CuPy import RecToolsDIRCuPy from tomobar.methodsIR_CuPy import RecToolsIRCuPy @@ -89,31 +89,6 @@ def FBP( cp.ndarray The FBP reconstructed volume as a CuPy array. """ - if cupywrapper.cupy_run: - return __FBP( - data, - angles, - center, - filter_freq_cutoff, - recon_size, - recon_mask_radius, - gpu_id, - ) - else: - print("FBP won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __FBP( - data: cp.ndarray, - angles: np.ndarray, - center: Optional[float] = None, - filter_freq_cutoff: Optional[float] = 1.1, - recon_size: Optional[int] = None, - recon_mask_radius: Optional[float] = None, - gpu_id: int = 0, -) -> cp.ndarray: RecToolsCP = _instantiate_direct_recon_class( data, angles, center, recon_size, gpu_id ) @@ -162,26 +137,6 @@ def LPRec( cp.ndarray The Log-polar Fourier reconstructed volume as a CuPy array. """ - if cupywrapper.cupy_run: - return __LPRec( - data, - angles, - center, - recon_size, - recon_mask_radius, - ) - else: - print("LPRec won't be executed because CuPy is not installed") - return data - - -def __LPRec( - data: cp.ndarray, - angles: np.ndarray, - center: Optional[float] = None, - recon_size: Optional[int] = None, - recon_mask_radius: Optional[float] = None, -) -> cp.ndarray: RecToolsCP = _instantiate_direct_recon_class(data, angles, center, recon_size, 0) reconstruction = RecToolsCP.FOURIER_INV( @@ -231,31 +186,6 @@ def SIRT( cp.ndarray The SIRT reconstructed volume as a CuPy array. """ - if cupywrapper.cupy_run: - return __SIRT( - data, - angles, - center, - recon_size, - iterations, - nonnegativity, - gpu_id, - ) - else: - print("SIRT won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __SIRT( - data: cp.ndarray, - angles: np.ndarray, - center: Optional[float] = None, - recon_size: Optional[int] = None, - iterations: Optional[int] = 300, - nonnegativity: Optional[bool] = True, - gpu_id: int = 0, -) -> cp.ndarray: RecToolsCP = _instantiate_iterative_recon_class( data, angles, center, recon_size, gpu_id, datafidelity="LS" ) @@ -311,31 +241,6 @@ def CGLS( cp.ndarray The CGLS reconstructed volume as a CuPy array. """ - if cupywrapper.cupy_run: - return __CGLS( - data, - angles, - center, - recon_size, - iterations, - nonnegativity, - gpu_id, - ) - else: - print("CGLS won't be executed because CuPy is not installed") - return data - - -@nvtx.annotate() -def __CGLS( - data: cp.ndarray, - angles: np.ndarray, - center: Optional[float] = None, - recon_size: Optional[int] = None, - iterations: Optional[int] = 20, - nonnegativity: Optional[bool] = True, - gpu_id: int = 0, -) -> cp.ndarray: RecToolsCP = _instantiate_iterative_recon_class( data, angles, center, recon_size, gpu_id, datafidelity="LS" ) diff --git a/httomolibgpu/recon/rotation.py b/httomolibgpu/recon/rotation.py index 421922ec..a88414ec 100644 --- a/httomolibgpu/recon/rotation.py +++ b/httomolibgpu/recon/rotation.py @@ -24,16 +24,16 @@ from httomolibgpu import cupywrapper cp = cupywrapper.cp -nvtx = cupywrapper.nvtx cupy_run = cupywrapper.cupy_run from unittest.mock import Mock + if cupy_run: from httomolibgpu.cuda_kernels import load_cuda_module from cupyx.scipy.ndimage import shift, gaussian_filter from skimage.registration import phase_cross_correlation from cupyx.scipy.fftpack import get_fft_plan - from cupyx.scipy.fft import rfft2 + from cupyx.scipy.fft import rfft2 else: load_cuda_module = Mock() shift = Mock() @@ -93,26 +93,6 @@ def find_center_vo( float Rotation axis location. """ - if cupywrapper.cupy_run: - return __find_center_vo(data, ind, smin, smax, srad, step, ratio, drop) - else: - print("find_center_vo won't be executed because CuPy is not installed") - return 0.0 - - -# %%%%%%%%%%%%%%%%%%%%%%%%%find_center_vo%%%%%%%%%%%%%%%%%%%%%%%%%%%% -@nvtx.annotate() -def __find_center_vo( - data: cp.ndarray, - ind: Optional[int] = None, - smin: int = -50, - smax: int = 50, - srad: float = 6.0, - step: float = 0.25, - ratio: float = 0.5, - drop: int = 20, -) -> float: - if data.ndim == 2: data = cp.expand_dims(data, 1) ind = 0 @@ -128,10 +108,8 @@ def __find_center_vo( else: _sino = data[:, ind, :] - with nvtx.annotate("gaussian_filter_1", color="green"): - _sino_cs = gaussian_filter(_sino, (3, 1), mode="reflect") - with nvtx.annotate("gaussian_filter_2", color="green"): - _sino_fs = gaussian_filter(_sino, (2, 2), mode="reflect") + _sino_cs = gaussian_filter(_sino, (3, 1), mode="reflect") + _sino_fs = gaussian_filter(_sino, (2, 2), mode="reflect") if _sino.shape[0] * _sino.shape[1] > 4e6: # data is large, so downsample it before performing search for @@ -146,7 +124,6 @@ def __find_center_vo( return cp.asnumpy(fine_cen) -@nvtx.annotate() def _search_coarse(sino, smin, smax, ratio, drop): (nrow, ncol) = sino.shape flip_sino = cp.ascontiguousarray(cp.fliplr(sino)) @@ -176,7 +153,6 @@ def _search_coarse(sino, smin, smax, ratio, drop): return cor -@nvtx.annotate() def _search_fine(sino, srad, step, init_cen, ratio, drop): (nrow, ncol) = sino.shape @@ -197,9 +173,7 @@ def _search_fine(sino, srad, step, init_cen, ratio, drop): return cor -@nvtx.annotate() def _create_mask(nrow, ncol, radius, drop): - du = 1.0 / ncol dv = (nrow - 1.0) / (nrow * 2.0 * np.pi) cen_row = int(math.ceil(nrow / 2.0) - 1) @@ -269,7 +243,6 @@ def _calculate_chunks( return stop_idx -@nvtx.annotate() def _calculate_metric(list_shift, sino1, sino2, sino3, mask, out): # this tries to simplify - if shift_col is integer, no need to spline interpolate assert list_shift.dtype == cp.float32, "shifts must be single precision floats" @@ -357,7 +330,6 @@ def _calculate_metric(list_shift, sino1, sino2, sino3, mask, out): ) -@nvtx.annotate() def _downsample(sino, level, axis): assert sino.dtype == cp.float32, "single precision floating point input required" assert sino.flags["C_CONTIGUOUS"], "list_shift must be C-contiguous" @@ -434,24 +406,6 @@ def find_center_360( Position of the window in the first image giving the best correlation metric. """ - - if cupywrapper.cupy_run: - return __find_center_360(data, ind, win_width, side, denoise, norm, use_overlap) - else: - print("find_center_360 won't be executed because CuPy is not installed") - return (0, 0, 0, 0) - - -@nvtx.annotate() -def __find_center_360( - data: cp.ndarray, - ind: Optional[int] = None, - win_width: int = 10, - side: Optional[Literal[0, 1]] = None, - denoise: bool = True, - norm: bool = False, - use_overlap: bool = False, -) -> Tuple[float, float, Optional[Literal[0, 1]], float]: if data.ndim != 3: raise ValueError("A 3D array must be provided") @@ -581,7 +535,6 @@ def _find_overlap( return overlap, side, overlap_position -@nvtx.annotate() def _search_overlap( mat1, mat2, win_width, side, denoise=True, norm=False, use_overlap=False ): @@ -621,9 +574,8 @@ def _search_overlap( if denoise is True: # note: the filtering makes the output contiguous - with nvtx.annotate("denoise_filter", color="green"): - mat1 = gaussian_filter(mat1, (2, 2), mode="reflect") - mat2 = gaussian_filter(mat2, (2, 2), mode="reflect") + mat1 = gaussian_filter(mat1, (2, 2), mode="reflect") + mat2 = gaussian_filter(mat2, (2, 2), mode="reflect") else: mat1 = cp.ascontiguousarray(mat1, dtype=cp.float32) mat2 = cp.ascontiguousarray(mat2, dtype=cp.float32) @@ -647,7 +599,6 @@ def _search_overlap( return list_metric, offset -@nvtx.annotate() def _calc_metrics(mat1, mat2, win_width, side, use_overlap, norm): assert mat1.dtype == cp.float32, "only float32 supported" assert mat2.dtype == cp.float32, "only float32 supported" @@ -691,7 +642,6 @@ def _calc_metrics(mat1, mat2, win_width, side, use_overlap, norm): return list_metric -@nvtx.annotate() def _calculate_curvature(list_metric): """ Calculate the curvature of a fitted curve going through the minimum @@ -755,21 +705,6 @@ def find_center_pc( Returns: float: Rotation axis location. """ - if cupywrapper.cupy_run: - return __find_center_pc(proj1, proj2, tol, rotc_guess) - else: - print("find_center_pc won't be executed because CuPy is not installed") - return 0 - - -@nvtx.annotate() -def __find_center_pc( - proj1: cp.ndarray, - proj2: cp.ndarray, - tol: float = 0.5, - rotc_guess: Union[float, Optional[str]] = None, -) -> float: - imgshift = 0.0 if rotc_guess is None else rotc_guess - (proj1.shape[1] - 1.0) / 2.0 proj1 = shift(proj1, [0, -imgshift], mode="constant", cval=0)