diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..59bde10c5 --- /dev/null +++ b/.clang-format @@ -0,0 +1,65 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -2 +AlignAfterOpenBracket: true +AlignEscapedNewlinesLeft: false +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakTemplateDeclarations: true +AlwaysBreakBeforeMultilineStrings: true +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BinPackParameters: false +BinPackArguments: false +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +DerivePointerAlignment: true +ExperimentalAutoDetectBinPacking: false +IndentCaseLabels: true +IndentWrappedFunctionNames: false +IndentFunctionDeclarationAfterType: false +MaxEmptyLinesToKeep: 1 +KeepEmptyLinesAtTheStartOfBlocks: false +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakString: 1000 +PenaltyBreakFirstLessLess: 120 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +SpacesBeforeTrailingComments: 2 +Cpp11BracedListStyle: true +Standard: Auto +IndentWidth: 2 +TabWidth: 8 +UseTab: Never +BreakBeforeBraces: Allman +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInAngles: false +SpaceInEmptyParentheses: false +SpacesInCStyleCastParentheses: false +SpaceAfterCStyleCast: false +SpacesInContainerLiterals: true +SpaceBeforeAssignmentOperators: true +ContinuationIndentWidth: 4 +CommentPragmas: '^ IWYU pragma:' +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +SpaceBeforeParens: ControlStatements +DisableFormat: false +... + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e54efc09c..411302b25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,37 +21,39 @@ jobs: runs-on: ubuntu-latest strategy: max-parallel: 5 + matrix: + python-version: [3.7,3.8,3.9] steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: ${{ matrix.python-version }} - name: Add conda to system path run: | # $CONDA is an environment variable pointing to the root of the miniconda directory echo $CONDA/bin >> $GITHUB_PATH - name: Install dependencies run: | - conda env update --file ptypy_core_dependencies.yml --name base + conda env update --file dependencies_core.yml --name base - name: Prepare ptypy run: | # Dry install to create ptypy/version.py python setup.py install -n -# - name: Lint with flake8 -# run: | -# conda install flake8 -# # stop the build if there are Python syntax errors or undefined names -# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics -# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide -# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Lint with flake8 + run: | + conda install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | conda install pytest conda install pytest-cov - # pytest ptypy/test --doctest-modules --junitxml=junit/test-results.xml --cov=ptypy --cov-report=xml --cov-report=html --cov-config=.coveragerc - pytest + # pytest ptypy/test -v --doctest-modules --junitxml=junit/test-results.xml --cov=ptypy --cov-report=xml --cov-report=html --cov-config=.coveragerc + pytest -v # - name: cobertura-report # if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize') # uses: 5monkeys/cobertura-action@v7 diff --git a/.gitignore b/.gitignore index 63373d719..5655be7fd 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ ptypy/version.py /env *.egg-info .DS_Store +.ipynb_checkpoints diff --git a/.travis.yml b/.travis.yml index fd5326f2e..de282d218 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ sudo: true language: python python: - 3.7 +compiler: + - gcc before_install: - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; # grab miniconda - bash miniconda.sh -b -p $HOME/miniconda # install miniconda @@ -12,6 +14,21 @@ before_install: - conda install pyyaml - conda info -a # and print the info + - CUDA=10.1.105-1 + - CUDA_SHORT=10.1 + - UBUNTU_VERSION=ubuntu1804 + - INSTALLER=cuda-repo-${UBUNTU_VERSION}_${CUDA}_amd64.deb + - wget http://developer.download.nvidia.com/compute/cuda/repos/${UBUNTU_VERSION}/x86_64/${INSTALLER} + - sudo dpkg -i ${INSTALLER} + - wget https://developer.download.nvidia.com/compute/cuda/repos/${UBUNTU_VERSION}/x86_64/7fa2af80.pub + - sudo apt-key add 7fa2af80.pub + - sudo apt update -qq + - sudo apt install -y cuda-core-${CUDA_SHORT/./-} cuda-cudart-dev-${CUDA_SHORT/./-} cuda-cufft-dev-${CUDA_SHORT/./-} cuda-curand-dev-${CUDA_SHORT/./-} + - sudo apt clean + - CUDA_HOME=/usr/local/cuda-${CUDA_SHORT} + - LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${LD_LIBRARY_PATH} + - PATH=${CUDA_HOME}/bin:${PATH} + env: - TEST_ENV_NAME=ptypy_core_dependencies - TEST_ENV_NAME=ptypy_full_dependencies @@ -27,7 +44,7 @@ script: - echo $PYTHONPATH - conda list - python setup.py install # install ptypy - - py.test ptypy/test -v --cov ptypy --cov-report term-missing # now run the tests + - py.test test -v --ignore=ptypy/test/accelerate_tests --cov ptypy --cov-report term-missing # now run the tests after_script: - coveralls diff --git a/README.rst b/README.rst index 02a9c1ec3..1083c3160 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,9 @@ To get started quickly, please find the official documentation at the project pa Features -------- -* **Difference Map** [#dm]_ algorithm engine with power bound constraint +* **Difference Map** [#dm]_ algorithm engine with power bound constraint [#power]_. * **Maximum Likelihood** [#ml]_ engine with preconditioners and regularizers. +* A few more engines (RAAR, sDR, ePIE, ...). * **Fully parallelized** (CPU only) using the Massage Passing Interface (`MPI `_). @@ -42,6 +43,8 @@ Features $ mpiexec -n [nodes] python .py +* **GPU acceleration** based on custom kernels, pycuda, and reikna. + * A **client-server** approach for visualization and control based on `ZeroMQ `_ . The reconstruction may run on a remote hpc cluster while your desktop @@ -60,11 +63,11 @@ Installation Installation should be as simple as :: - $ sudo python setup.py install + $ sudo pip install . or, as a user, :: - $ python setup.py install --user + $ pip install . --user Dependencies @@ -130,3 +133,5 @@ References .. [#dm] P.Thibault, M.Dierolf *et al.*, *Science* **321**, 7 (2009), `doi `_ .. [#ml] P.Thibault and M.Guizar-Sicairos, *New J. of Phys.* **14**, 6 (2012), `doi `_ + +.. [#power] K.Giewekemeyer *et al.*, **PNAS 108**, 2 (2007), `suppl. material `__, `doi `__ diff --git a/archive/array_based/__init__.py b/archive/array_based/__init__.py new file mode 100644 index 000000000..4221257bd --- /dev/null +++ b/archive/array_based/__init__.py @@ -0,0 +1,7 @@ +''' +A module for gpu acceleration + +''' +import numpy as np +COMPLEX_TYPE= np.complex64 +FLOAT_TYPE = np.float32 \ No newline at end of file diff --git a/archive/array_based/array_utils.py b/archive/array_based/array_utils.py new file mode 100644 index 000000000..c2d341711 --- /dev/null +++ b/archive/array_based/array_utils.py @@ -0,0 +1,87 @@ +''' +useful utilities from ptypy that should be ported to gpu. These don't ahve external dependencies +''' +import numpy as np +from scipy import ndimage as ndi + + +def dot(A, B, acc_dtype=np.float64): + assert A.dtype == B.dtype, "Input arrays must of same data type" + if np.iscomplexobj(B): + out = np.sum(np.multiply(A, B.conj()).real, dtype=acc_dtype) + else: + out = np.sum(np.multiply(A, B), dtype=acc_dtype) + return out + + +def norm2(A): + return dot(A, A) + + +def abs2(input): + ''' + + :param input. An array that we want to take the absolute value of and square. Can be inplace. Can be complex or real. + :return: The real valued abs**2 array + ''' + return np.multiply(input, input.conj()).real + +def sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype): + ''' + :param in1. An array . Can be inplace. Can be complex or real. + :param outshape. An array. + :param in1_addr. An array . Can be inplace. Can be complex or real. + :param out1_addr. An array . Can be inplace. Can be complex or real. + :return: The real valued abs**2 array + ''' + out1 = np.zeros(outshape, dtype=dtype) + inshape = in1.shape + for i1, o1 in zip(in1_addr, out1_addr): + out1[o1[0], o1[1]:(o1[1] + inshape[1]), o1[2]:(o1[2] + inshape[2])] += in1[i1[0]] + return out1 + +def norm2(input): + ''' + Input here could be a variety of 1D, 2D, 3D complex or real. all will be single precision at least. + return should be real + ''' + return np.sum(abs2(input)) + +def complex_gaussian_filter(input, mfs): + ''' + takes 2D and 3D arrays. Complex input, complex output. mfs has len 02: + raise NotImplementedError("Only batches of 2D arrays allowed!") + + if input.ndim == 3: + mfs = np.insert(mfs, 0, 0) + + return (ndi.gaussian_filter(np.real(input), mfs) +1j *ndi.gaussian_filter(np.imag(input), mfs)).astype(input.dtype) + +def mass_center(A): + ''' + Input will always be real, and 2d or 3d, single precision here + ''' + return np.array(ndi.measurements.center_of_mass(A), dtype=A.dtype) + +def interpolated_shift(c, shift, do_linear=False): + ''' + complex bicubic interpolated shift. + complex output. This shift should be applied to 2D arrays. shift should have len=c.ndims + + ''' + if not do_linear: + return ndi.interpolation.shift(np.real(c), shift, order=3, prefilter=True) + 1j*ndi.interpolation.shift(np.imag(c), shift, order=3, prefilter=True) + else: + return ndi.interpolation.shift(np.real(c), shift, order=1, mode='constant', cval=0, prefilter=False) + 1j * ndi.interpolation.shift(np.imag(c), shift, order=1, mode='constant', cval=0, prefilter=False) + + +def clip_complex_magnitudes_to_range(complex_input, clip_min, clip_max): + ''' + This takes a single precision 2D complex input, clips the absolute magnitudes to be within a range, but leaves the phase untouched. + ''' + ampl = np.abs(complex_input) + phase = np.exp(1j * np.angle(complex_input)) + ampl = np.clip(ampl, clip_min, clip_max) + complex_input[:] = ampl * phase \ No newline at end of file diff --git a/archive/array_based/base.py b/archive/array_based/base.py new file mode 100644 index 000000000..7429bbd2b --- /dev/null +++ b/archive/array_based/base.py @@ -0,0 +1,22 @@ +from collections import OrderedDict + +class Adict(object): + + def __init__(self): + pass + + +class BaseKernel(object): + + def __init__(self, queue_thread=None, verbose=False): + + self.queue = queue_thread + self.verbose = False + self.npy = Adict() + self.ocl = Adict() + self.benchmark = OrderedDict() + + + def log(self, x): + if self.verbose: + print(x) \ No newline at end of file diff --git a/archive/array_based/constraints.py b/archive/array_based/constraints.py new file mode 100644 index 000000000..b70abae3d --- /dev/null +++ b/archive/array_based/constraints.py @@ -0,0 +1,143 @@ +''' +a module to holds the constraints +''' + +import numpy as np + +from .error_metrics import log_likelihood, far_field_error, realspace_error +from .object_probe_interaction import difference_map_realspace_constraint, scan_and_multiply, difference_map_overlap_update +from .propagation import farfield_propagator +from ptypy.accelerate.array_based import array_utils as au +from ptypy.accelerate.array_based import COMPLEX_TYPE, FLOAT_TYPE + +def renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound): + renormed_f = np.zeros(f.shape, dtype=COMPLEX_TYPE) + for _pa, _oa, ea, da, ma in addr_info: + m = mask[ma[0]] + magnitudes = fmag[da[0]] + absolute_magnitudes = af[da[0]] + fourier_space_solution = f[ea[0]] + fourier_error = err_fmag[da[0]] + if pbound is None: + fm = (1 - m) + m * magnitudes / (absolute_magnitudes + 1e-10) + renormed_f[ea[0]] = np.multiply(fm, fourier_space_solution) + elif (fourier_error > pbound): + # Power bound is applied + fdev = absolute_magnitudes - magnitudes + renorm = np.sqrt(pbound / fourier_error) + fm = (1 - m) + m * (magnitudes + fdev * renorm) / (absolute_magnitudes + 1e-10) + renormed_f[ea[0]] = np.multiply(fm, fourier_space_solution) + else: + renormed_f[ea[0]] = np.zeros_like(fourier_space_solution) + return renormed_f + +def get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object): + df = np.zeros(exit_wave.shape, dtype=COMPLEX_TYPE) + for _pa, _oa, ea, da, ma in addr_info: + if (pbound is None) or (err_fmag[da[0]] > pbound): + df[ea[0]] = np.subtract(backpropagated_solution[ea[0]], probe_object[ea[0]]) + else: + df[ea[0]] = alpha * np.subtract(probe_object[ea[0]], exit_wave[ea[0]]) + return df + +def difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, postfilter, pbound=None, alpha=1.0, LL_error=True, do_realspace_error=True): + ''' + This kernel just performs the fourier renormalisation. + :param mask. The nd mask array + :param diffraction. The nd diffraction data + :param farfield_stack. The current iterant. + :param addr. The addresses of the stacks. + :return: The updated iterant + : fourier errors + ''' + + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + + # Buffer for accumulated photons + # For log likelihood error # need to double check this adp + if LL_error is True: + err_phot = log_likelihood(probe_object, mask, Idata, prefilter, postfilter, addr_info) + else: + err_phot = np.zeros(Idata.shape[0], dtype=FLOAT_TYPE) + + + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) + + f = farfield_propagator(constrained, prefilter, postfilter, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(Idata)) + af = np.sqrt(af2) + # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) + err_fmag = far_field_error(af, fmag, mask) + + vectorised_rfm = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + backpropagated_solution = farfield_propagator(vectorised_rfm, + postfilter.conj(), + prefilter.conj(), + direction='backward') + + + df = get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + + + + exit_wave += df + if do_realspace_error: + ea_first_column = np.array(ea)[:, 0] + da_first_column = np.array(da)[:, 0] + err_exit = realspace_error(df, ea_first_column, da_first_column, Idata.shape[0]) + else: + err_exit = np.zeros((Idata.shape[0])) + + if pbound is not None: + err_fmag /= pbound + + return np.array([err_fmag, err_phot, err_exit]) + + +def difference_map_iterator(diffraction, obj, object_weights, cfact_object, mask, probe, cfact_probe, probe_support, + probe_weights, exit_wave, addr, pre_fft, post_fft, pbound, overlap_max_iterations, update_object_first, + obj_smooth_std, overlap_converge_factor, probe_center_tol, probe_update_start, alpha=1, + clip_object=None, LL_error=False, num_iterations=1): + curiter = 0 + + errors = np.zeros((num_iterations, 3, len(diffraction)), dtype=FLOAT_TYPE) + for it in range(num_iterations): + if (((it+1) % 10) == 0) and (it>0): + print("iteration:%s" % (it+1)) # it's probably a good idea to print this if possible for some idea of progress + # numpy dump here for 64x64 and 4096x4096 + + errors[it] = difference_map_fourier_constraint(mask, + diffraction, + obj, + probe, + exit_wave, + addr, + prefilter=pre_fft, + postfilter=post_fft, + pbound=pbound, + alpha=alpha, + LL_error=LL_error) + + do_update_probe = (probe_update_start <= curiter) + difference_map_overlap_update(addr, + cfact_object, + cfact_probe, + do_update_probe, + exit_wave, + obj, + object_weights, + probe, + probe_support, + probe_weights, + overlap_max_iterations, + update_object_first, + obj_smooth_std, + overlap_converge_factor, + probe_center_tol, + clip_object=clip_object) + curiter += 1 + return errors \ No newline at end of file diff --git a/archive/array_based/data_utils.py b/archive/array_based/data_utils.py new file mode 100644 index 000000000..44810410c --- /dev/null +++ b/archive/array_based/data_utils.py @@ -0,0 +1,90 @@ +''' +Created on 4 Jan 2018 + +@author: clb02321 +''' + +import numpy as np +from ptypy.accelerate.array_based import FLOAT_TYPE + + +def _vectorise_array_access(diff_storage): + # Sort views according to layer in diffraction stack + views = diff_storage.views + dlayers = [view.dlayer for view in views] + views = [views[i] for i in np.argsort(dlayers)] + view_IDs = [view.ID for view in views] + + # Master pod + mpod = views[0].pod + + # Determine linked storages for probe, object and exit waves + pr = mpod.pr_view.storage + ob = mpod.ob_view.storage + ex = mpod.ex_view.storage + + poe_ID = (pr.ID, ob.ID, ex.ID) + probe_weights = [] + object_weights = [] + addr = [] + for view in views: + address = [] + for _pname, pod in view.pods.items(): + # store them for each pod + # create addresses + probe_weights.append(pod.probe_weight) + object_weights.append(pod.object_weight) + + a = np.array([ + (pod.pr_view.dlayer, pod.pr_view.dlow[0], pod.pr_view.dlow[1]), + (pod.ob_view.dlayer, pod.ob_view.dlow[0], pod.ob_view.dlow[1]), + (pod.ex_view.dlayer, pod.ex_view.dlow[0], pod.ex_view.dlow[1]), + (pod.di_view.dlayer, pod.di_view.dlow[0], pod.di_view.dlow[1]), + (pod.ma_view.dlayer, pod.ma_view.dlow[0], pod.ma_view.dlow[1])]) + addr.append(a) + + addr_out = np.array(addr).astype(np.int32) + + + return view_IDs, poe_ID, addr_out, np.array(probe_weights, dtype=np.float32), np.array(object_weights, dtype=np.float32) + +def pod_to_arrays(P, storage_id, scan_model='Full'): + ''' + :param P. A ptycho instance + :param: storage_id The storage ID for this scan. + + :return + a dictionary containing: + diffraction: The diffraction data + probe: the probe from the FIRST POD + obj: The object buffer + exit wave: The exit wave buffer + mask: The diffraction masks + meta: The meta data, containing an 'addr' array for the addresses + ''' + if scan_model == 'Full': + diffraction_storages_to_iterate = P.di.storages[storage_id] + mask_storages = P.ma.storages[storage_id] + view_IDs, poe_IDs, addr, probe_weights, object_weights = _vectorise_array_access(diffraction_storages_to_iterate) + meta = {'view_IDs': view_IDs, + 'poe_IDs': poe_IDs, + 'addr': addr} + main_pod = P.di.V[view_IDs[0]].pod # we will use this to get all the information + probe_array = main_pod.pr_view.storage.data + obj_array = main_pod.ob_view.storage.data + obj_viewcover = main_pod.ob_view.storage.get_view_coverage() + exit_wave_array = main_pod.ex_view.storage.data + mask_array = mask_storages.data # can we have booleans? + diff_array = diffraction_storages_to_iterate.data + + + return {'diffraction': diff_array, + 'probe': probe_array, + 'probe weights': probe_weights, + 'obj': obj_array, + 'object viewcover': obj_viewcover, + 'object weights': object_weights, + 'exit wave': exit_wave_array, + 'mask': mask_array, + 'meta': meta} + diff --git a/archive/array_based/error_metrics.py b/archive/array_based/error_metrics.py new file mode 100644 index 000000000..015247917 --- /dev/null +++ b/archive/array_based/error_metrics.py @@ -0,0 +1,37 @@ +''' +A module of the relevant error metrics +''' + +from .propagation import farfield_propagator +from ptypy.accelerate.array_based.array_utils import sum_to_buffer, abs2 +from ptypy.accelerate.array_based import FLOAT_TYPE, COMPLEX_TYPE +import numpy as np + + +def log_likelihood(probe_and_obj, mask, Idata, prefilter, postfilter, addr_info): + _pa, _oa, ea, da, ma = zip(*addr_info) + LLerror = np.zeros(Idata.shape[0], dtype=FLOAT_TYPE) + ft = farfield_propagator(probe_and_obj, prefilter, postfilter, direction='forward') + abs2_ft = abs2(ft) + LL = sum_to_buffer(abs2_ft, Idata.shape, ea, da, dtype=Idata.dtype) + + unq, idx = np.unique(np.array(da)[:,0], return_index=True) + for dai, mai in zip(np.array(da)[idx], np.array(ma)[idx]): + LLerror[dai[0]] = np.divide(np.sum(np.power(np.multiply(mask[mai[0]], (np.subtract(LL[dai[0]], Idata[dai[0]]))), 2) / np.add(Idata[dai[0]], 1.)), np.prod(LL[dai[0]].shape)) + return LLerror + +def far_field_error(current_solution, measured_solution, mask): + fdev = np.subtract(current_solution, measured_solution) + summed_mask = np.sum(mask, axis=(-2, -1)) + fdev2 = np.power(fdev, 2) + masked_fdev2 = np.multiply(mask, fdev2) + summed_masked_fdev2 = np.sum(masked_fdev2, axis=(-2, -1)) + err_fmag = summed_masked_fdev2 / summed_mask + return err_fmag + +def realspace_error(difference_in_exitwave, ea_first_column, da_first_column, out_length): + errors = np.mean(abs2(difference_in_exitwave), axis=(-2, -1)) + collapsed_errors = np.zeros((out_length,), dtype=FLOAT_TYPE) + for ea_idx, da_idx in zip(ea_first_column, da_first_column): + collapsed_errors[da_idx] += errors[ea_idx] + return collapsed_errors diff --git a/archive/array_based/object_probe_interaction.py b/archive/array_based/object_probe_interaction.py new file mode 100644 index 000000000..95230baad --- /dev/null +++ b/archive/array_based/object_probe_interaction.py @@ -0,0 +1,132 @@ +''' +object_probe_interaction + +Contains things pertinent to the probe and object interaction. +Should have all the engine updates +''' + +import numpy as np +from ptypy.accelerate.array_based.array_utils import norm2, complex_gaussian_filter, abs2, mass_center, interpolated_shift, clip_complex_magnitudes_to_range +from copy import deepcopy +from ptypy.accelerate.array_based import COMPLEX_TYPE + + +def difference_map_realspace_constraint(probe_and_object, exit_wave, alpha): + ''' + in theory this can just be called in ptypy instead of get_exit_wave + ''' + return (1.0 + alpha) * probe_and_object - alpha*exit_wave + + +def scan_and_multiply(probe, obj, exit_shape, addresses): + sh = exit_shape + po = np.zeros((sh[0], sh[1], sh[2]), dtype=COMPLEX_TYPE) + for pa, oa, ea, _da, _ma in addresses: + po[ea[0]] = np.multiply(probe[pa[0], pa[1]:(pa[1] + sh[1]), pa[2]:(pa[2] + sh[2])], + obj[oa[0], oa[1]:(oa[1] + sh[1]), oa[2]:(oa[2] + sh[2])]) + return po + + +def difference_map_update_object(ob, object_weights, probe, exit_wave, addr_info, cfact_object, ob_smooth_std=None, clip_object=None): + pa, oa, ea, _da, _ma = zip(*addr_info) + + if ob_smooth_std is not None: + smooth_mfs = [ob_smooth_std, ob_smooth_std] + ob[:] = cfact_object * complex_gaussian_filter(ob, smooth_mfs) + else: + ob *= cfact_object + + extract_array_from_exit_wave(exit_wave, ea, probe, pa, ob, oa, cfact_object, object_weights) + + if clip_object is not None: + clip_min, clip_max = clip_object + clip_complex_magnitudes_to_range(ob, clip_min, clip_max) + + +def difference_map_update_probe(ob, probe_weights, probe, exit_wave, addr_info, cfact_probe, probe_support=None): + pa, oa, ea, _da, _ma = zip(*addr_info) + old_probe = deepcopy(probe) + probe *= cfact_probe + extract_array_from_exit_wave(exit_wave, ea, ob, oa, probe, pa, cfact_probe, probe_weights) + if probe_support is not None: + probe *= probe_support + + change = norm2(probe - old_probe) /norm2(probe) + + return np.sqrt(change / probe.shape[0]) + + +def extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, array_to_be_updated, update_addr, cfact, weights): + ''' + :param exit_wave: The exit wave buffer + :param exit_addr: The addresses for + :param array_to_be_extracted: the array to be extracted from the exit_wave buffer. This is not updated + :param extract_addr: The addresses for the array to be extracted. + :param array_to_be_updated: This is updated in place. + :param update_addr: The addresses for the array that is to be updated + :param cfact: This is the scaling for the denominator of the updated array. Not updated. + :param weights: The weights for the extracted array + + ''' + sh = exit_wave.shape + array_to_be_updated_denominator = deepcopy(cfact) + for pa, oa, ea in zip(update_addr, extract_addr, exit_addr): + extracted_array = array_to_be_extracted[oa[0], oa[1]:(oa[1] + sh[1]), oa[2]:(oa[2] + sh[2])] + extracted_array_conj = extracted_array.conj() + array_to_be_updated[pa[0], pa[1]:(pa[1] + sh[1]), pa[2]:(pa[2] + sh[2])] += extracted_array_conj * \ + exit_wave[ea[0]] * \ + weights[pa[0]] + + array_to_be_updated_denominator[pa[0], pa[1]:(pa[1] + sh[1]), pa[2]:(pa[2] + sh[2])] += extracted_array * \ + extracted_array_conj * \ + weights[pa[0]] + + array_to_be_updated /= array_to_be_updated_denominator + + +def center_probe(probe, center_tolerance): + c1 = np.array(mass_center(abs2(probe).sum(axis=0))) + c2 = np.array(probe.shape[-2:]) // 2 + if np.sqrt(norm2(c1 - c2)) < center_tolerance: + return + offset = c2-c1 + for idx in range(probe.shape[0]): + probe[idx] = interpolated_shift(probe[idx], offset) + + +def difference_map_overlap_update(addr_info, cfact_object, cfact_probe, do_update_probe, exit_wave, ob, object_weights, + probe, probe_support, probe_weights,max_iterations, update_object_first, + obj_smooth_std, overlap_converge_factor, probe_center_tol, clip_object=None): + for inner in range(max_iterations): + + # Update object first + if update_object_first or (inner > 0): + # Update object + difference_map_update_object(ob, + object_weights, + probe, + exit_wave, + addr_info, + cfact_object, + ob_smooth_std=obj_smooth_std, + clip_object=clip_object) + + # Exit if probe should not be updated yet + if not do_update_probe: + break + # Update probe + change = difference_map_update_probe(ob, + probe_weights, + probe, + exit_wave, + addr_info, + cfact_probe, + probe_support) + + # Recenter the probe + if probe_center_tol is not None: + center_probe(probe, probe_center_tol) + + # Stop iteration if probe change is small + if change < overlap_converge_factor: + break diff --git a/archive/array_based/propagation.py b/archive/array_based/propagation.py new file mode 100644 index 000000000..413654fe2 --- /dev/null +++ b/archive/array_based/propagation.py @@ -0,0 +1,52 @@ +''' +All propagation based kernels +''' +import numpy as np +from ptypy.accelerate.array_based import COMPLEX_TYPE + +def farfield_propagator(data_to_be_transformed, prefilter=None, postfilter=None, direction='forward'): + ''' + performs a fourier transform on the nd exit wave stack. FFT shift and normalisation performed by + multiplication with prefilter and postfilter + :param data_to_be_transformed. The nd stack of the current iterant. + :param prefilter. The filter to multiply before fourier transforming. Default: None. + :param postfilter. The filter to multiply after fourier transforming. Default: None. + :param direction. The direction of the transform forward or backward. Default: Forward. + :return: The transformed stack. + ''' + + dtype = data_to_be_transformed.dtype + if direction == 'forward': + def fft(x): + output = np.zeros(x.shape, dtype=dtype) + for idx in range(output.shape[0]): + output[idx] = np.fft.fft2(x[idx]) + return output + + sc = 1.0 / np.sqrt(np.prod(data_to_be_transformed.shape[-2:])) + + elif direction == 'backward': + def fft(x): + output = np.zeros(x.shape, dtype=dtype) + for idx in range(output.shape[0]): + output[idx] = np.fft.ifft2(x[idx]) + return output + + sc = np.sqrt(np.prod(data_to_be_transformed.shape[-2:])) + + if (prefilter is None) and (postfilter is None): + return fft(data_to_be_transformed) * sc + elif (prefilter is None) and (postfilter is not None): + postfilter = postfilter.astype(dtype) + return np.multiply(postfilter, fft(data_to_be_transformed)) * sc + elif (prefilter is not None) and (postfilter is None): + prefilter = prefilter.astype(dtype) + return fft(np.multiply(data_to_be_transformed, prefilter))* sc + elif (prefilter is not None) and (postfilter is not None): + prefilter = prefilter.astype(dtype) + postfilter = postfilter.astype(dtype) + return np.multiply(postfilter, fft(np.multiply(data_to_be_transformed, prefilter))) * sc + +def sqrt_abs(diffraction): + return np.sqrt(np.abs(diffraction)) + diff --git a/archive/cuda_extension/cuda/CMakeLists.txt b/archive/cuda_extension/cuda/CMakeLists.txt new file mode 100644 index 000000000..e158c762a --- /dev/null +++ b/archive/cuda_extension/cuda/CMakeLists.txt @@ -0,0 +1,135 @@ +# version 3.8+ is needed, otherwise there's no CUDA support +cmake_minimum_required(VERSION 3.8 FATAL_ERROR) + +project(ptypy_cuda LANGUAGES CXX CUDA) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CUDA_STANDARD 11) + +option(GPU_TIMING "Run timing for the GPU code" OFF) + +############################################################################ +## Download and unpack googletest at configure time +#configure_file(CMakeLists.txt.in googletest-download/CMakeLists.txt) +#execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . +# RESULT_VARIABLE result +# WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download ) +#if(result) +# message(FATAL_ERROR "CMake step for googletest failed: ${result}") +#endif() +#execute_process(COMMAND ${CMAKE_COMMAND} --build . +# RESULT_VARIABLE result +# WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download ) +#if(result) +# message(FATAL_ERROR "Build step for googletest failed: ${result}") +#endif() + +## Prevent overriding the parent project's compiler/linker +## settings on Windows +#set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# +## Add googletest directly to our build. This defines +## the gtest and gtest_main targets. +#add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src +# ${CMAKE_BINARY_DIR}/googletest-build +# EXCLUDE_FROM_ALL) +# +## The gtest/gtest_main targets carry header search path +## dependencies automatically when using CMake 2.8.11 or +## later. Otherwise we have to add them here ourselves. +#if (CMAKE_VERSION VERSION_LESS 2.8.11) +# include_directories("${gtest_SOURCE_DIR}/include") +#endif() +############################################################################ + + +include_directories(.) + +add_library(gpu_extension STATIC + utils/Complex.h + utils/CudaFunction.h + utils/CudaFunction.cpp + utils/Errors.h + utils/Memory.h + utils/Memory.cpp + utils/ScopedTimer.h + utils/Timer.h + utils/GpuManager.h + utils/GpuManager.cu + + func/addr_info_helpers.h + func/addr_info_helpers.cpp + + func/farfield_propagator.h + func/farfield_propagator.cu + func/sqrt_abs.cu + func/scan_and_multiply.h + func/scan_and_multiply.cu + func/difference_map_realspace_constraint.h + func/difference_map_realspace_constraint.cu + func/log_likelihood.h + func/log_likelihood.cu + func/abs2.h + func/abs2.cu + func/sum_to_buffer.cu + func/sum_to_buffer.h + func/far_field_error.h + func/far_field_error.cu + func/realspace_error.h + func/realspace_error.cu + func/get_difference.h + func/get_difference.cu + func/renormalise_fourier_magnitudes.h + func/renormalise_fourier_magnitudes.cu + func/difference_map_fourier_constraint.h + func/difference_map_fourier_constraint.cu + func/norm2.h + func/norm2.cu + func/mass_center.h + func/mass_center.cu + func/clip_complex_magnitudes_to_range.h + func/clip_complex_magnitudes_to_range.cu + func/extract_array_from_exit_wave.h + func/extract_array_from_exit_wave.cu + func/interpolated_shift.cu + func/interpolated_shift.h + func/center_probe.h + func/center_probe.cu + func/difference_map_update_probe.h + func/difference_map_update_probe.cu + func/difference_map_update_object.h + func/difference_map_update_object.cu + func/difference_map_overlap_constraint.h + func/difference_map_overlap_constraint.cu + func/complex_gaussian_filter.h + func/complex_gaussian_filter.cu + func/difference_map_iterator.h + func/difference_map_iterator.cu +) + +set_target_properties(gpu_extension PROPERTIES + CUDA_RESOLVE_DEVICE_SYMBOLS OFF # no device-link symbols + CUDA_SEPARABLE_COMPILATION OFF # no device-side linking + POSITION_INDEPENDENT_CODE ON # allow linking into a dynamic lib +) + +# add "DO_GPU_TIMING" if option is true +if(GPU_TIMING) + set_target_properties(gpu_extension PROPERTIES + COMPILE_DEFINITIONS DO_GPU_TIMING + ) +endif() + +########### tests ########### + +#enable_testing() + +#macro(buildtest name) +# add_executable(${name} tests/${name}.cpp) +# target_link_libraries(${name} gtest_main) +# add_test(NAME ${name} COMMAND ${name}) +#endmacro() + +#buildtest(indexing_test) +#buildtest(gaussian_weights_test) + diff --git a/archive/cuda_extension/cuda/CMakeLists.txt.in b/archive/cuda_extension/cuda/CMakeLists.txt.in new file mode 100644 index 000000000..d60a33e9a --- /dev/null +++ b/archive/cuda_extension/cuda/CMakeLists.txt.in @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 2.8.2) + +project(googletest-download NONE) + +include(ExternalProject) +ExternalProject_Add(googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG master + SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src" + BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) \ No newline at end of file diff --git a/archive/cuda_extension/cuda/README.md b/archive/cuda_extension/cuda/README.md new file mode 100644 index 000000000..c0dbb7a1a --- /dev/null +++ b/archive/cuda_extension/cuda/README.md @@ -0,0 +1,19 @@ +# CUDA Module for Ptypy + +Builds the functions for GPU extension module, using CMake 3.8+. + +It gets built automatically together with setup.py. + +## Tests + +Some unit tests are implemented on the C++ level, using google +test. The test framework is automatically downloaded and built +during the regular cmake build. Run "make test" in the build +directory (normally build/cuda) to run the tests. + +## Naming Conventions + +- parameter_name_ == this->parameter_name +- d_parameter_name == device pointer to parameter, everything else is host + +## setup.py Build diff --git a/archive/cuda_extension/cuda/func/abs2.cu b/archive/cuda_extension/cuda/func/abs2.cu new file mode 100644 index 000000000..b1a657255 --- /dev/null +++ b/archive/cuda_extension/cuda/func/abs2.cu @@ -0,0 +1,133 @@ +#include "abs2.h" + +#include "utils/Complex.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +/************ Kernels *********************/ + +// must be real, if this template fits +template +__global__ void abs2_kernel(const T *in, T *out, size_t n) +{ + size_t ti = threadIdx.x + blockIdx.x * blockDim.x; + if (ti >= n) + return; + auto v = in[ti]; + out[ti] = v * v; +} + +// complex to real +template +__global__ void abs2_kernel(const Tc *in, T *out, size_t n) +{ + size_t ti = threadIdx.x + blockIdx.x * blockDim.x; + if (ti >= n) + return; + auto v = in[ti]; + out[ti] = v.real() * v.real() + v.imag() * v.imag(); +} + +/************ Class methods *****************/ + +template +Abs2::Abs2() : CudaFunction("abs2") +{ +} + +template +void Abs2::setParameters(size_t n) +{ + n_ = n; +} + +template +void Abs2::setDeviceBuffers(Tin *d_datain, Tout *d_dataout) +{ + d_datain_ = d_datain; + d_dataout_ = d_dataout; +} + +template +void Abs2::allocate() +{ + ScopedTimer t(this, "allocate"); + d_datain_.allocate(n_); + d_dataout_.allocate(n_); +} + +template +Tout *Abs2::getOutput() const +{ + return d_dataout_.get(); +} + +template +void Abs2::transfer_in(const Tin *datain) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_datain_.get(), datain, n_); +} + +template +void Abs2::transfer_out(Tout *dataout) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(dataout, d_dataout_.get(), n_); +} + +template +void Abs2::run() +{ + ScopedTimer t(this, "run"); + size_t block = 256; + size_t blocks = (n_ + block - 1) / block; + abs2_kernel<<>>(d_datain_.get(), d_dataout_.get(), n_); + checkLaunchErrors(); + + // sync device if timing is enabled + timing_sync(); +} + +/************** interface function *************/ + +// instantiate here to force creating all symbols +template class Abs2; +template class Abs2, float>; +template class Abs2; +template class Abs2, double>; + +template +static void entryFunc(const Tin *in, Tout *out, int n) +{ + auto abs2 = gpuManager.get_cuda_function>( + "abs2<" + getTypeName() + "," + getTypeName() + ">", n); + abs2->allocate(); + abs2->transfer_in(in); + abs2->run(); + abs2->transfer_out(out); +} + +extern "C" void abs2_c(const float *in, float *out, int n, int iisComplex) +{ + if (iisComplex != 0) + { + entryFunc(reinterpret_cast *>(in), out, n); + } + else + { + entryFunc(in, out, n); + } +} + +extern "C" void abs2d_c(const double *in, double *out, int n, int iisComplex) +{ + if (iisComplex != 0) + { + entryFunc(reinterpret_cast *>(in), out, n); + } + else + { + entryFunc(in, out, n); + } +} diff --git a/archive/cuda_extension/cuda/func/abs2.h b/archive/cuda_extension/cuda/func/abs2.h new file mode 100644 index 000000000..f5a7f3ea6 --- /dev/null +++ b/archive/cuda_extension/cuda/func/abs2.h @@ -0,0 +1,23 @@ +#pragma once + +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +template +class Abs2 : public CudaFunction +{ +public: + Abs2(); + void setParameters(size_t n); + void setDeviceBuffers(Tin *d_datain, Tout *d_dataout); + void allocate(); + Tout *getOutput() const; + void transfer_in(const Tin *datain); + void transfer_out(Tout *dataout); + void run(); + +private: + DevicePtrWrapper d_datain_; + DevicePtrWrapper d_dataout_; + size_t n_ = 0; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/addr_info_helpers.cpp b/archive/cuda_extension/cuda/func/addr_info_helpers.cpp new file mode 100644 index 000000000..9d3b217fa --- /dev/null +++ b/archive/cuda_extension/cuda/func/addr_info_helpers.cpp @@ -0,0 +1,47 @@ +#include "addr_info_helpers.h" + +#include + + +// finds which lines in the o addr array map to the same output 0 index +// --> return a map outputindex -> [addr_line_idx_0, addr_line_idx_1, ...] +static void remap_outaddr(const int *o, int N, std::map> &addr, int addr_stride) +{ + for (int i = 0; i < N; ++i) + { + int idx = i * addr_stride; + auto o_0 = o[idx]; + addr[o_0].push_back(i); + } +} + +// re-encodes the map to 3 plain vectors as follows: +// outidx contains a list of all output indices that are written to +// start holds the corresponding starting index in the indices vector for the +// mapped list indices is a flattened list with all values from the map +static void make_addrarray(const std::map> &addr, + std::vector &outidx, + std::vector &start, + std::vector &indices) +{ + for (auto &p : addr) + { + outidx.push_back(p.first); + start.push_back(int(indices.size())); + indices.insert(indices.end(), p.second.begin(), p.second.end()); + } + start.push_back(int(indices.size())); +} + + +void flatten_out_addr(const int *out_addr, + int addr_len, + int addr_stride, + std::vector &outidx, + std::vector &startidx, + std::vector &indices) +{ + std::map> addr; + remap_outaddr(out_addr, addr_len, addr, addr_stride); + make_addrarray(addr, outidx, startidx, indices); +} diff --git a/archive/cuda_extension/cuda/func/addr_info_helpers.h b/archive/cuda_extension/cuda/func/addr_info_helpers.h new file mode 100644 index 000000000..44e926370 --- /dev/null +++ b/archive/cuda_extension/cuda/func/addr_info_helpers.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +/** Creates a datastructure to get the mapping of which indices in the addr_info + * array (first dim) map to each output address (handling multiple occurrences). + * + * This function is used by sum_to_buffer, to avoid data races on output + * assignment. + * + * @param out_addr_info pointer to addr_info start location containing the + * output indices. Example, for da in full addr_info: addr_info + 9 + * @param addr_len Number of lines in addr_info + * @param addr_stride Stride to go from one line to the next (it's 15 in full + * addr_info, and 3 if data has been sliced as addr_info(:,3,:) + * @param outidx output array of indices in the output the data should be + * written to (that's the unique values of out_addr_info[0] for every line) + * @param startidx output array of starting indices into the indices array, for + * the corresponding element in outidx. The length is 1 longer than outidx, to + * accomodate start + end indices at i and i+1 + * @param indices Array of indices that map to the output index. + * + */ +void flatten_out_addr(const int *out_addr_info, + int addr_len, + int addr_stride, + std::vector &outidx, + std::vector &startidx, + std::vector &indices); diff --git a/archive/cuda_extension/cuda/func/center_probe.cu b/archive/cuda_extension/cuda/func/center_probe.cu new file mode 100644 index 000000000..54a2d58dc --- /dev/null +++ b/archive/cuda_extension/cuda/func/center_probe.cu @@ -0,0 +1,150 @@ +#include "center_probe.h" + +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +// 1 block per 2nd/3rd dim +template +__global__ void sum_abs2(const complex* in, + float* out, + int dim0, + int dim12) +{ + int ty = threadIdx.y + blockIdx.y * BlockY; + int tx = threadIdx.x; + + auto val = 0.0f; + if (ty < dim12) + { + // specific block iterates over the 1st dim, + // and has fixed x/y + in += ty; + for (int i = tx; i < dim0; i += BlockX) + { + auto cval = in[i * dim12]; + auto abs2 = cval.real() * cval.real() + cval.imag() * cval.imag(); + val += abs2; + } + } + + __shared__ float blocksums[BlockX][BlockY]; + blocksums[tx][threadIdx.y] = val; + + __syncthreads(); + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + blocksums[tx][threadIdx.y] += blocksums[c - tx - 1][threadIdx.y]; + } + __syncthreads(); + c = c - half; + } + + if (ty >= dim12) + return; + + if (tx == 0) + out[ty] = blocksums[0][threadIdx.y]; +} + +/****************** class implementation ***********/ + +CenterProbe::CenterProbe() : CudaFunction("center_probe") {} + +void CenterProbe::setParameters(int i, int m, int n) +{ + i_ = i; + m_ = m; + n_ = n; + + mass_center_ = gpuManager.get_cuda_function( + "center_probe.mass_center", m_, n_, 1); + interp_shift_ = gpuManager.get_cuda_function( + "center_probe.interpolated_shift", i_, m_, n_); +} + +void CenterProbe::setDeviceBuffers(complex* d_probe, + complex* d_out) +{ + d_probe_ = d_probe; + d_out_ = d_out; +} + +void CenterProbe::allocate() +{ + ScopedTimer t(this, "allocate"); + d_probe_.allocate(i_ * m_ * n_); + d_out_.allocate(i_ * m_ * n_); + d_buffer_.allocate(m_ * n_); + mass_center_->setDeviceBuffers(d_buffer_.get(), nullptr); + mass_center_->allocate(); + interp_shift_->setDeviceBuffers(d_probe_.get(), d_out_.get()); + interp_shift_->allocate(); +} + +void CenterProbe::transfer_in(const complex* probe) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_probe_.get(), probe, i_ * m_ * n_); +} + +void CenterProbe::transfer_out(complex* probe) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(probe, d_probe_.get(), i_ * m_ * n_); +} + +void CenterProbe::run(float center_tolerance) +{ + ScopedTimer t(this, "run"); + + dim3 threads = {32u, 32u, 1u}; + dim3 blocks = {1u, unsigned(m_ * n_ + 32 - 1) / 32u, 1u}; + sum_abs2<32, 32> + <<>>(d_probe_.get(), d_buffer_.get(), i_, m_ * n_); + checkLaunchErrors(); + + // now mass_center across the buffer + mass_center_->run(); + + // TODO: see if this can be done on the GPU directly, + // avoiding a sync point in chain of async kernels + // Note: means putting interp_shift offset as a device buffer, + // not as parameter to the run function itself + float c1[2]; + mass_center_->transfer_out(c1); + float c2[] = {float(m_ / 2), float(n_ / 2)}; + auto err_1 = c1[0] - c2[0]; + auto err_2 = c1[1] - c2[1]; + auto err = std::sqrt(err_1 * err_1 + err_2 * err_2); + + if (err < center_tolerance) + { + return; + } + + float offset[] = {c2[0] - c1[0], c2[1] - c1[1]}; + + // now interpolated_shift on the data, and back into probe array + interp_shift_->run(offset[0], offset[1], true); + gpu_memcpy_d2d(d_probe_.get(), interp_shift_->getOutput(), i_ * m_ * n_); + + timing_sync(); +} + +/************ interface functions *************/ + +extern "C" void center_probe_c( + float* f_probe, float center_tolerance, int i, int m, int n) +{ + auto probe = reinterpret_cast*>(f_probe); + auto cp = gpuManager.get_cuda_function("center_probe", i, m, n); + cp->allocate(); + cp->transfer_in(probe); + cp->run(center_tolerance); + cp->transfer_out(probe); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/center_probe.h b/archive/cuda_extension/cuda/func/center_probe.h new file mode 100644 index 000000000..9a520b2c9 --- /dev/null +++ b/archive/cuda_extension/cuda/func/center_probe.h @@ -0,0 +1,27 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include "interpolated_shift.h" +#include "mass_center.h" + +class CenterProbe : public CudaFunction +{ +public: + CenterProbe(); + void setParameters(int i, int m, int n); + void setDeviceBuffers(complex* d_probe, complex* d_out); + void allocate(); + void transfer_in(const complex* probe); + void run(float center_tolerance); + void transfer_out(complex* probe); + +private: + DevicePtrWrapper> d_probe_; + DevicePtrWrapper d_buffer_; + DevicePtrWrapper> d_out_; + int i_ = 0, m_ = 0, n_ = 0; + InterpolatedShift* interp_shift_ = nullptr; + MassCenter* mass_center_ = nullptr; +}; diff --git a/archive/cuda_extension/cuda/func/clip_complex_magnitudes_to_range.cu b/archive/cuda_extension/cuda/func/clip_complex_magnitudes_to_range.cu new file mode 100644 index 000000000..b7ab8ddd1 --- /dev/null +++ b/archive/cuda_extension/cuda/func/clip_complex_magnitudes_to_range.cu @@ -0,0 +1,90 @@ +#include "clip_complex_magnitudes_to_range.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include +#include + +/************ Kernels *********************/ + +__global__ void clip_complex_magnitudes_to_range_kernel(complex* data, + int n, + float clip_min, + float clip_max) +{ + int id = threadIdx.x + blockIdx.x * blockDim.x; + + auto v = data[id]; + + auto mag = abs(v); + auto theta = arg(v); + if (mag > clip_max) + mag = clip_max; + if (mag < clip_min) + mag = clip_min; + v = thrust::polar(mag, theta); + + data[id] = v; +} + +/************ Class implementation **********/ + +ClipComplexMagnitudesToRange::ClipComplexMagnitudesToRange() + : CudaFunction("clip_complex_magnitudes_to_range") +{ +} + +void ClipComplexMagnitudesToRange::setParameters(int n) { n_ = n; } + +void ClipComplexMagnitudesToRange::setDeviceBuffers(complex* d_data) +{ + d_data_ = d_data; +} + +void ClipComplexMagnitudesToRange::allocate() +{ + ScopedTimer t(this, "allocate"); + d_data_.allocate(n_); +} + +void ClipComplexMagnitudesToRange::transfer_in(const complex* data) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_data_.get(), data, n_); +} + +void ClipComplexMagnitudesToRange::run(float clip_min, float clip_max) +{ + ScopedTimer t(this, "run"); + + const int threadsPerBlock = 256; + int blocks = (n_ + threadsPerBlock - 1) / threadsPerBlock; + clip_complex_magnitudes_to_range_kernel<<>>( + d_data_.get(), n_, clip_min, clip_max); + + checkLaunchErrors(); + timing_sync(); +} + +void ClipComplexMagnitudesToRange::transfer_out(complex* data) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(data, d_data_.get(), n_); +} + +/************ interface ******************/ + +extern "C" void clip_complex_magnitudes_to_range_c(float* f_data, + int n, + float clip_min, + float clip_max) +{ + auto data = reinterpret_cast*>(f_data); + + auto ccmr = gpuManager.get_cuda_function( + "clip_complex_magnitudes_to_range", n); + ccmr->allocate(); + ccmr->transfer_in(data); + ccmr->run(clip_min, clip_max); + ccmr->transfer_out(data); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/clip_complex_magnitudes_to_range.h b/archive/cuda_extension/cuda/func/clip_complex_magnitudes_to_range.h new file mode 100644 index 000000000..802881f69 --- /dev/null +++ b/archive/cuda_extension/cuda/func/clip_complex_magnitudes_to_range.h @@ -0,0 +1,25 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +/** Clips the complex magnitudes to given range + * + * BEWARE - this function works on the input in-place + */ +class ClipComplexMagnitudesToRange : public CudaFunction +{ +public: + ClipComplexMagnitudesToRange(); + void setParameters(int n); + void setDeviceBuffers(complex* d_data); + void allocate(); + void transfer_in(const complex* data); + void run(float clip_min, float clip_max); + void transfer_out(complex* data); + +private: + DevicePtrWrapper> d_data_; + int n_ = 0; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/complex_gaussian_filter.cu b/archive/cuda_extension/cuda/func/complex_gaussian_filter.cu new file mode 100644 index 000000000..b29ec68ae --- /dev/null +++ b/archive/cuda_extension/cuda/func/complex_gaussian_filter.cu @@ -0,0 +1,314 @@ +#include "complex_gaussian_filter.h" +#include "utils/GaussianWeights.h" + +#include "utils/GpuManager.h" +#include "utils/Indexing.h" +#include "utils/ScopedTimer.h" + +#include +#include + +/******* kernels *****************/ + +__constant__ float c_Kernel[ComplexGaussianFilter::MAX_KERNEL_RADIUS + 1]; + +template +__global__ void convolutionRowKernel(const complex* in, + complex* out, + int height, + int width, + int kernel_radius) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int bx = blockIdx.x; + int by = blockIdx.y; + + // offset for batch + in += width * height * blockIdx.z; + out += width * height * blockIdx.z; + + extern __shared__ char shm_raw[]; + auto shm = reinterpret_cast*>(shm_raw); + + // Offset to block start of core area + int gbx = bx * BlockX; + int gby = by * BlockY; + int start = gbx * width + gby; + in += start; + out += start; + // width of shared memory + int shwidth = BlockY + 2 * kernel_radius; + + if (gbx + tx < height) + { + // main part - reflecting as needed + IndexReflect ind(-gby, width - gby); + shm[tx * shwidth + (kernel_radius + ty)] = in[tx * width + ind(ty)]; + + // left halo (kernel radius before) + for (int i = ty - kernel_radius; i < 0; i += BlockY) + { + shm[tx * shwidth + (i + kernel_radius)] = in[tx * width + ind(i)]; + } + + // right halo (kernel radius after) + for (int i = ty + BlockY; i < BlockY + kernel_radius; i += BlockY) + { + shm[tx * shwidth + (i + kernel_radius)] = in[tx * width + ind(i)]; + } + } + + __syncthreads(); + + // safe to return now, after syncing + if (gby + ty >= width || gbx + tx >= height) + return; + + // compute + auto sum = shm[tx * shwidth + (ty + kernel_radius)] * c_Kernel[0]; + for (int i = 1; i <= kernel_radius; ++i) + { + sum += (shm[tx * shwidth + (ty + i + kernel_radius)] + + shm[tx * shwidth + (ty - i + kernel_radius)]) * + c_Kernel[i]; + } + + out[tx * width + ty] = sum; +} + +template +__global__ void convolutionColumnsKernel(const complex* in, + complex* out, + int height, + int width, + int kernel_radius) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int bx = blockIdx.x; + int by = blockIdx.y; + + // offset for batch + in += width * height * blockIdx.z; + out += width * height * blockIdx.z; + + extern __shared__ char shm_raw[]; + auto shm = reinterpret_cast*>(shm_raw); + // dims: BlockX + 2*kernel_radius, BlockY + + // Offset to block start of core area + int gbx = bx * BlockX; + int gby = by * BlockY; + int start = gbx * width + gby; + in += start; + out += start; + + // only do this if column index is in range + // (need to keep threads with us, so that synchthreads below doesn't deadlock) + if (gby + ty < width) + { + // main data (center point for each thread) - reflecting if needed + IndexReflect ind(-gbx, height - gbx); + shm[(kernel_radius + tx) * BlockY + ty] = in[ind(tx) * width + ty]; + + // upper halo (kernel radius before) + for (int i = tx - kernel_radius; i < 0; i += BlockX) + { + shm[(i + kernel_radius) * BlockY + ty] = in[ind(i) * width + ty]; + } + + // lower halo (kernel radius after) + for (int i = tx + BlockX; i < BlockX + kernel_radius; i += BlockX) + { + shm[(i + kernel_radius) * BlockY + ty] = in[ind(i) * width + ty]; + } + } + __syncthreads(); + + // safe to return now, after syncing + if (gby + ty >= width || gbx + tx >= height) + return; + + // compute + auto sum = shm[(tx + kernel_radius) * BlockY + ty] * c_Kernel[0]; + for (int i = 1; i <= kernel_radius; ++i) + { + sum += (shm[(tx + i + kernel_radius) * BlockY + ty] + + shm[(tx - i + kernel_radius) * BlockY + ty]) * + c_Kernel[i]; + } + + out[tx * width + ty] = sum; +} + +/******* class implementation ********/ + +ComplexGaussianFilter::ComplexGaussianFilter() + : CudaFunction("complex_gaussian_filter") +{ +} + +int ComplexGaussianFilter::totalSize() const +{ + return std::accumulate(shape_, shape_ + ndims_, 1, std::multiplies()); +} + +std::vector ComplexGaussianFilter::calcConvolutionKernel(float stddev, + int ndevs) +{ + auto lw = int(stddev * ndevs + 0.5f); + return gaussian_kernel1d(stddev, lw); +} + +void ComplexGaussianFilter::setParameters(int ndims, + const int* shape, + const float* mfs) +{ + ndims_ = ndims; + std::copy(shape, shape + ndims, shape_); + std::copy(mfs, mfs + ndims, mfs_); +} + +void ComplexGaussianFilter::setDeviceBuffers(complex* d_input, + complex* d_output) +{ + d_input_ = d_input; + d_output_ = d_output; +} + +void ComplexGaussianFilter::allocate() +{ + ScopedTimer t(this, "allocate"); + + auto total = totalSize(); + d_input_.allocate(total); + d_output_.allocate(total); +} + +void ComplexGaussianFilter::transfer_in(const complex* input) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_input_.get(), input, totalSize()); +} + +void ComplexGaussianFilter::run() +{ + ScopedTimer t(this, "run"); + + // use separable convolution in each dim + int batches = 1; + int x = 1; + int y = 1; + float stdx = 0.0f, stdy = 0.0f; + if (ndims_ == 3) + { + batches = shape_[0]; + x = shape_[1]; + y = shape_[2]; + stdx = mfs_[0]; + stdy = mfs_[1]; + } + if (ndims_ == 2) + { + x = shape_[0]; + y = shape_[1]; + stdx = mfs_[0]; + stdy = mfs_[1]; + } + if (ndims_ == 1) + { + stdy = mfs_[0]; + stdx = 0.0f; + y = shape_[0]; + } + + if (stdx > 0.0f) + { + // construct convolution kernel in current dim + auto weights = calcConvolutionKernel(stdx, NUM_STDDEVS); + if (weights.size() - 1 > MAX_KERNEL_RADIUS) + throw GPUException("Gaussian filter length too long: " + + std::to_string(weights.size())); + + checkCudaErrors(cudaMemcpyToSymbol( + c_Kernel, weights.data(), weights.size() * sizeof(float))); + + // run colums kernel with right stride + const int bx = 16; + const int by = 4; + dim3 threads(bx, by, 1); + dim3 blocks((x + bx - 1) / bx, (y + by - 1) / by, batches); + auto kernel_radius = weights.size() - 1; + auto halos = kernel_radius * 2; + auto shared = (bx + halos) * by * sizeof(complex); + if (shared > MAX_SHARED_PER_BLOCK) + { + throw GPUException("cannot run in kernel shared memory"); + } + auto inp = d_input_.get(); + convolutionColumnsKernel<<>>( + inp, d_output_.get(), x, y, kernel_radius); + checkLaunchErrors(); + } + + // last dim is continuous, so we use a different kernel + if (stdy > 0.0) + { + // construct convolution kernel in last dim + auto weights = calcConvolutionKernel(stdy, NUM_STDDEVS); + if (weights.size() - 1 > MAX_KERNEL_RADIUS) + throw GPUException("Gaussian filter length too long: " + + std::to_string(weights.size())); + checkCudaErrors(cudaMemcpyToSymbol( + c_Kernel, weights.data(), weights.size() * sizeof(float))); + + // run y kernel + const int bx = 4; + const int by = 16; + dim3 threads(bx, by, 1); + dim3 blocks((x + bx - 1) / bx, (y + by - 1) / by, batches); + auto kernel_radius = weights.size() - 1; + auto halos = kernel_radius * 2; + auto shared = (by + halos) * bx * sizeof(complex); + auto indata = d_output_.get(); + if (stdx <= 0.0f) + { + indata = d_input_.get(); + } + convolutionRowKernel<<>>( + indata, d_output_.get(), x, y, kernel_radius); + + checkLaunchErrors(); + } + if (stdx == 0.0f && stdy == 0.0f) + { + gpu_memcpy_d2d(d_output_.get(), d_input_.get(), batches * x * y); + } + timing_sync(); +} + +void ComplexGaussianFilter::transfer_out(complex* output) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(output, d_output_.get(), totalSize()); +} + +/******* interface functions *********/ + +extern "C" void complex_gaussian_filter_c(const float* f_input, + float* f_output, + const float* mfs, + int ndims, + const int* shape) +{ + auto input = reinterpret_cast*>(f_input); + auto output = reinterpret_cast*>(f_output); + + auto cgf = gpuManager.get_cuda_function( + "complex_gaussian_filter", ndims, shape, mfs); + cgf->allocate(); + cgf->transfer_in(input); + cgf->run(); + cgf->transfer_out(output); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/complex_gaussian_filter.h b/archive/cuda_extension/cuda/func/complex_gaussian_filter.h new file mode 100644 index 000000000..a79f1deb3 --- /dev/null +++ b/archive/cuda_extension/cuda/func/complex_gaussian_filter.h @@ -0,0 +1,38 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class ComplexGaussianFilter : public CudaFunction +{ +public: + // number of standard deviations to use for the filter + static const int NUM_STDDEVS = 4; + + static const int ROWS_BLOCKDIM_X = 16; // width of tile - match kernel size? + static const int ROWS_BLOCKDIM_Y = 4; // height of tile + static const int MAX_SHARED_PER_BLOCK = + 48 * 1024 / 2; // at least 2 blocks per SM + static const int MAX_SHARED_PER_BLOCK_COMPLEX = + MAX_SHARED_PER_BLOCK / 2 * sizeof(float); + static const int MAX_KERNEL_RADIUS = + MAX_SHARED_PER_BLOCK_COMPLEX / ROWS_BLOCKDIM_X; + + ComplexGaussianFilter(); + void setParameters(int ndims, const int* shape, const float* mfs); + void setDeviceBuffers(complex* d_input, complex* d_output); + void allocate(); + void transfer_in(const complex* input); + void run(); + void transfer_out(complex* output); + +private: + int totalSize() const; + std::vector calcConvolutionKernel(float stddev, int ndevs); + + DevicePtrWrapper> d_input_; + DevicePtrWrapper> d_output_; + int ndims_ = 2; + int shape_[3]; + float mfs_[3]; +}; diff --git a/archive/cuda_extension/cuda/func/difference_map_fourier_constraint.cu b/archive/cuda_extension/cuda/func/difference_map_fourier_constraint.cu new file mode 100644 index 000000000..1d4a70b71 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_fourier_constraint.cu @@ -0,0 +1,532 @@ +#include "difference_map_fourier_constraint.h" + +#include "addr_info_helpers.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include +#include +#include + +/*************** kernels ********************/ + +template +__global__ void add_inplace_kernel(T *inout, const T *a, int n) +{ + int tid = threadIdx.x + blockIdx.x * blockDim.x; + if (tid >= n) + return; + inout[tid] += a[tid]; +} + +template +__global__ void div_inplace_kernel(T1 *inout, T2 divisor, int n) +{ + int tid = threadIdx.x + blockIdx.x * blockDim.x; + if (tid >= n) + return; + inout[tid] /= divisor; +} + +template +__global__ void sqrt_abs_kernel(const T *in, T *out, int n) +{ + int tid = threadIdx.x + blockIdx.x * blockDim.x; + if (tid >= n) + return; + using std::abs; + using std::sqrt; + out[tid] = sqrt(abs(in[tid])); +} + +template +__global__ void sqrt_kernel(const T *in, T *out, int n) +{ + int tid = threadIdx.x + blockIdx.x * blockDim.x; + if (tid >= n) + return; + using std::sqrt; + out[tid] = sqrt(in[tid]); +} + +template +__global__ void conjugate_kernel(const complex *in, complex *out, int n) +{ + int tid = threadIdx.x + blockIdx.x * blockDim.x; + if (tid >= n) + return; + out[tid] = complex(in[tid].real(), -in[tid].imag()); +} + +/*************** class implementation ***********/ + +DifferenceMapFourierConstraint::DifferenceMapFourierConstraint() + : CudaFunction("difference_map_fourier_constraint") +{ +} + +void DifferenceMapFourierConstraint::setParameters(int M, + int N, + int A, + int B, + int C, + int D, + int ob_modes, + int pr_modes, + bool do_LL_error, + bool do_realspace_error) +{ + M_ = M; + N_ = N; + A_ = A; + B_ = B; + C_ = C; + D_ = D; + pr_modes_ = pr_modes; + ob_modes_ = ob_modes; + do_LL_error_ = do_LL_error; + do_realspace_error_ = do_realspace_error; + + scan_and_multiply_ = gpuManager.get_cuda_function( + "dm_fourier_contraint.scan_and_multiply", + M_, + A_, + B_, + pr_modes_, + A_, + B_, + ob_modes_, + C_, + D_, + M_); + + log_likelihood_ = gpuManager.get_cuda_function( + "dm_fourier_contraint.log_likelihood", M_, A_, B_, M_, N_); + + difference_map_realspace_constraint_ = + gpuManager.get_cuda_function( + "dm_fourier_constraint.dm_realspace_constraint", M_, A_, B_); + + farfield_propagator_fwd_ = gpuManager.get_cuda_function( + "dm_fourier_constraint.farfield_propagator_fwd", M_, A_, B_); + + abs2_ = gpuManager.get_cuda_function, float>>( + "dm_fourier_contraint.abs2", M_ * A_ * B_); + + sum_to_buffer_ = gpuManager.get_cuda_function>( + "dm_fourier_constraint.sum2buffer", M_, A_, B_, N_, A_, B_, M_, M_); + + far_field_error_ = gpuManager.get_cuda_function( + "dm_fourier_constraint.farfield_error", N_, A_, B_); + + renormalise_fourier_magnitudes_ = + gpuManager.get_cuda_function( + "dm_fourier_constraint.renormalise_fourier_magnitudes", + M_, + N_, + A_, + B_); + + farfield_propagator_rev_ = gpuManager.get_cuda_function( + "dm_fourier_constraint.farfield_propagator_rev", M_, A_, B_); + + get_difference_ = gpuManager.get_cuda_function( + "dm_fourier_constraint.get_difference", M_, A_, B_); + + realspace_error_ = gpuManager.get_cuda_function( + "dm_fourier_constraint.realspace_error", M_, A_, B_, M_, N_); + + sum_to_buffer_->setAddrStride(15); + realspace_error_->setAddrStride(15); +} + +void DifferenceMapFourierConstraint::setDeviceBuffers( + unsigned char *d_mask, + float *d_Idata, + complex *d_obj, + complex *d_probe, + complex *d_exit_wave, + int *d_addr_info, + complex *d_prefilter, + complex *d_postfilter, + float *d_errors, + int *d_outidx, + int *d_startidx, + int *d_indices, + int outidx_size) +{ + d_mask_ = d_mask; + d_Idata_ = d_Idata; + d_obj_ = d_obj; + d_probe_ = d_probe; + d_exit_wave_ = d_exit_wave; + d_addr_info_ = d_addr_info; + d_prefilter_ = d_prefilter; + d_postfilter_ = d_postfilter; + d_errors_ = d_errors; + d_outidx_ = d_outidx; + d_startidx_ = d_startidx; + d_indices_ = d_indices; + outidx_size_ = outidx_size; +} + +int DifferenceMapFourierConstraint::calculateAddrIndices(const int *out1_addr) +{ + // calculate the indexing map + outidx_.clear(); + startidx_.clear(); + indices_.clear(); + flatten_out_addr(out1_addr, M_, 15, outidx_, startidx_, indices_); + outidx_size_ = outidx_.size(); + return outidx_size_; +} + +void DifferenceMapFourierConstraint::calculateUniqueDaIndices( + const int *da_addr) +{ + if (do_LL_error_) + { + log_likelihood_->calculateUniqueDaIndices(da_addr); + } +} + +void DifferenceMapFourierConstraint::updateErrorOutput(float *d_errors) +{ + d_errors_ = d_errors; + checkCudaErrors( + cudaMemset(d_errors_.get(), 0, N_ * 3 * sizeof(*d_errors_.get()))); + if (do_LL_error_) + { + log_likelihood_->updateErrorOutput(d_errors_.get() + N_); + } + if (do_realspace_error_) + { + realspace_error_->updateErrorOutput(d_errors_.get() + 2 * N_); + } + far_field_error_->updateErrorOutput(d_errors_.get()); + get_difference_->updateErrorInput(d_errors_.get()); + renormalise_fourier_magnitudes_->updateErrorInput(d_errors_.get()); +} + +void DifferenceMapFourierConstraint::allocate() +{ + ScopedTimer t(this, "allocate (joint)"); + + d_mask_.allocate(M_ * A_ * B_); + d_Idata_.allocate(N_ * A_ * B_); + d_obj_.allocate(ob_modes_ * C_ * D_); + d_probe_.allocate(pr_modes_ * A_ * B_); + d_exit_wave_.allocate(M_ * A_ * B_); + d_addr_info_.allocate(M_ * 3 * 5); + d_prefilter_.allocate(A_ * B_); + d_postfilter_.allocate(A_ * B_); + d_prefilter_conj_.allocate(A_ * B_); + d_postfilter_conj_.allocate(A_ * B_); + d_errors_.allocate(N_ * 3); + checkCudaErrors( + cudaMemset(d_errors_.get(), 0, N_ * 3 * sizeof(*d_errors_.get()))); + d_fmag_.allocate(M_ * A_ * B_); + if (!outidx_.empty()) + { + d_outidx_.allocate(outidx_.size()); + d_startidx_.allocate(startidx_.size()); + d_indices_.allocate(indices_.size()); + } + + // probe, obj, addr_info --> probe_obj + scan_and_multiply_->setDeviceBuffers( + d_probe_.get(), + d_obj_.get(), + d_addr_info_.get(), + nullptr // the output buffer is allocated + ); + scan_and_multiply_->allocate(); + auto d_probe_obj = scan_and_multiply_->getOutput(); + + if (do_LL_error_) + { + // probe_object, mask, Idata, prefilter, postfilter, addr --> err_phot + log_likelihood_->setDeviceBuffers(d_probe_obj, + d_mask_.get(), + d_Idata_.get(), + d_prefilter_.get(), + d_postfilter_.get(), + d_addr_info_.get(), + d_errors_.get() + N_, + d_outidx_.get(), + d_startidx_.get(), + d_indices_.get(), + outidx_size_); + log_likelihood_->allocate(); + } + + // probe_obj, exit_wave -> constrained + difference_map_realspace_constraint_->setDeviceBuffers( + d_probe_obj, + d_exit_wave_.get(), + nullptr // the output is allocated in there + ); + difference_map_realspace_constraint_->allocate(); + auto d_constrained = difference_map_realspace_constraint_->getOutput(); + + farfield_propagator_fwd_->setDeviceBuffers( + d_constrained, + d_constrained, // in-place, ok here + d_prefilter_.get(), + d_postfilter_.get()); + farfield_propagator_fwd_->allocate(); + // output is in d_constrained + auto d_f = d_constrained; + + abs2_->setDeviceBuffers(d_f, nullptr); + abs2_->allocate(); + + // abs2f, idata shape, ea, da => af2 + // giving strided access to addr_info to avoid another copy + // (constructor sets the stride to 15 instead of 3, we + // just offset it here to get to the ea and da parts) + sum_to_buffer_->setDeviceBuffers(abs2_->getOutput(), + nullptr, // output is allocated in here + d_addr_info_.get() + 6, + d_addr_info_.get() + 9, + d_outidx_.get(), + d_startidx_.get(), + d_indices_.get(), + outidx_size_); + sum_to_buffer_->allocate(); + auto d_af2 = sum_to_buffer_->getOutput(); + + // we'll run sqrt(af2) in-place + auto d_af = d_af2; + + // d_fmag = sqrt(abs(Idata)) will be run in a kernel + + // af, fmag, mask => err_fmag + far_field_error_->setDeviceBuffers( + d_af, d_fmag_.get(), d_mask_.get(), d_errors_.get()); + far_field_error_->allocate(); + auto d_err_fmag = d_errors_.get(); + + // f, af, fmag, mask, err_fmag, addr_info, pbound => vectorised_rfm + renormalise_fourier_magnitudes_->setDeviceBuffers( + d_f, + d_af, + d_fmag_.get(), + d_mask_.get(), + d_err_fmag, + d_addr_info_.get(), + nullptr // gets allocated inside + ); + renormalise_fourier_magnitudes_->allocate(); + auto d_vectorised_rfm = renormalise_fourier_magnitudes_->getOutput(); + + // vectorised_rfm, postfilter.conj, prefilter.conj, 'reverse' -> + // backpropagated_solution (flipped / conjugated post/pre filter) + farfield_propagator_rev_->setDeviceBuffers(d_vectorised_rfm, + d_vectorised_rfm, + d_postfilter_conj_.get(), + d_prefilter_conj_.get()); + farfield_propagator_rev_->allocate(); + auto d_backpropagated_solution = d_vectorised_rfm; + + // addr_info, alpha, backpropagated_solution, err_fmag, pbound, probe_object + // -> df + get_difference_->setDeviceBuffers(d_addr_info_.get(), + d_backpropagated_solution, + d_err_fmag, + d_exit_wave_.get(), + d_probe_obj, + nullptr); + get_difference_->allocate(); + auto d_df = get_difference_->getOutput(); + + // we'll add df to exit_wave in-place + + if (do_realspace_error_) + { + realspace_error_->setDeviceBuffers(d_df, + d_addr_info_.get() + 2 * 3, + d_addr_info_.get() + 3 * 3, + d_errors_.get() + 2 * N_); + realspace_error_->allocate(); + } + + // we'll div by pbound in-place for d_err_fmag +} + +void DifferenceMapFourierConstraint::transfer_in( + const unsigned char *mask, + const float *Idata, + const complex *obj, + const complex *probe, + const complex *exit_wave, + const int *addr_info, + const complex *prefilter, + const complex *postfilter) +{ + ScopedTimer t(this, "transfer in"); + + gpu_memcpy_h2d(d_mask_.get(), mask, N_ * A_ * B_); + gpu_memcpy_h2d(d_Idata_.get(), Idata, N_ * A_ * B_); + gpu_memcpy_h2d(d_obj_.get(), obj, ob_modes_ * C_ * D_); + gpu_memcpy_h2d(d_probe_.get(), probe, pr_modes_ * A_ * B_); + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, M_ * A_ * B_); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, M_ * 3 * 5); + gpu_memcpy_h2d(d_prefilter_.get(), prefilter, A_ * B_); + gpu_memcpy_h2d(d_postfilter_.get(), postfilter, A_ * B_); + // conjugate will be run first-time in the run function + + if (!outidx_.empty()) + { + gpu_memcpy_h2d(d_outidx_.get(), outidx_.data(), outidx_.size()); + gpu_memcpy_h2d(d_startidx_.get(), startidx_.data(), startidx_.size()); + gpu_memcpy_h2d(d_indices_.get(), indices_.data(), indices_.size()); + } + + calculateUniqueDaIndices(addr_info + 9); +} + +void DifferenceMapFourierConstraint::run(float pbound, + float alpha, + bool doPbound) +{ + ScopedTimer t(this, "run"); + + // conjugate the pre- and post-filters + // TODO: do this only once if called multiple times + int total = A_ * B_; + int threadsPerBlock = 256; + int blocks = (total + threadsPerBlock - 1) / threadsPerBlock; + if (d_prefilter_.get() != nullptr) + { + conjugate_kernel<<>>( + d_prefilter_.get(), d_prefilter_conj_.get(), total); + checkLaunchErrors(); + } + if (d_postfilter_.get() != nullptr) + { + conjugate_kernel<<>>( + d_postfilter_.get(), d_postfilter_conj_.get(), total); + checkLaunchErrors(); + } + + scan_and_multiply_->run(); + + if (do_LL_error_) + { + log_likelihood_->run(); + } + difference_map_realspace_constraint_->run(alpha); + farfield_propagator_fwd_->run( + d_prefilter_.get() != nullptr, d_postfilter_.get() != nullptr, true); + abs2_->run(); + sum_to_buffer_->run(); + + // sqrt(abs(Idata)) + total = N_ * A_ * B_; + threadsPerBlock = 256; + blocks = (total + threadsPerBlock - 1) / threadsPerBlock; + sqrt_abs_kernel<<>>( + d_Idata_.get(), d_fmag_.get(), total); + checkLaunchErrors(); + + // sqrt(af2), in-place + sqrt_kernel<<>>( + sum_to_buffer_->getOutput(), sum_to_buffer_->getOutput(), total); + checkLaunchErrors(); + + far_field_error_->run(); + renormalise_fourier_magnitudes_->run(pbound, doPbound); + + farfield_propagator_rev_->run( + d_prefilter_.get() != nullptr, d_postfilter_.get() != nullptr, false); + + get_difference_->run(alpha, pbound, doPbound); + + auto df = get_difference_->getOutput(); + + total = M_ * A_ * B_; + threadsPerBlock = 256; + blocks = (total + threadsPerBlock - 1) / threadsPerBlock; + + add_inplace_kernel<<>>( + d_exit_wave_.get(), df, total); + checkLaunchErrors(); + + if (do_realspace_error_) + { + realspace_error_->run(); + } + + if (doPbound) + { + auto d_err_fmag = d_errors_.get(); + total = N_; + threadsPerBlock = 256; + blocks = (total + threadsPerBlock - 1) / threadsPerBlock; + div_inplace_kernel<<>>(d_err_fmag, pbound, total); + checkLaunchErrors(); + } + + timing_sync(); +} +void DifferenceMapFourierConstraint::transfer_out(float *errors, + complex *exit_wave) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(errors, d_errors_.get(), 3 * N_); + gpu_memcpy_d2h(exit_wave, d_exit_wave_.get(), M_ * A_ * B_); +} + +/**************** interface function ***********/ + +extern "C" void difference_map_fourier_constraint_c( + const unsigned char *mask, // N x A x B + const float *Idata, // N x A x B + const float *f_obj, // ob_modes x C x D + const float *f_probe, // pr_modes x A x B + float *f_exit_wave, // M x A x B + const int *addr_info, // M x 5 x 3 + const float *f_prefilter, // A x B + const float *f_postfilter, // A x B + float pbound, + float alpha, + int do_LL_error, + int do_realspace_error, + int doPbound, + int M, + int N, + int A, + int B, + int C, + int D, + int ob_modes, + int pr_modes, + float *errors) +{ + auto obj = reinterpret_cast *>(f_obj); + auto probe = reinterpret_cast *>(f_probe); + auto exit_wave = reinterpret_cast *>(f_exit_wave); + auto prefilter = reinterpret_cast *>(f_prefilter); + auto postfilter = reinterpret_cast *>(f_postfilter); + + auto dmfc = gpuManager.get_cuda_function( + "dm_fourier_constraint", + M, + N, + A, + B, + C, + D, + ob_modes, + pr_modes, + do_LL_error != 0, + do_realspace_error != 0); + + dmfc->calculateAddrIndices(addr_info + 9); + dmfc->allocate(); + dmfc->transfer_in( + mask, Idata, obj, probe, exit_wave, addr_info, prefilter, postfilter); + dmfc->run(pbound, alpha, doPbound != 0); + dmfc->transfer_out(errors, exit_wave); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_fourier_constraint.h b/archive/cuda_extension/cuda/func/difference_map_fourier_constraint.h new file mode 100644 index 000000000..059b1fc70 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_fourier_constraint.h @@ -0,0 +1,97 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include "difference_map_realspace_constraint.h" +#include "far_field_error.h" +#include "farfield_propagator.h" +#include "get_difference.h" +#include "log_likelihood.h" +#include "realspace_error.h" +#include "renormalise_fourier_magnitudes.h" +#include "scan_and_multiply.h" +#include "sum_to_buffer.h" + +class DifferenceMapFourierConstraint : public CudaFunction +{ +public: + DifferenceMapFourierConstraint(); + void setParameters(int M, + int N, + int A, + int B, + int C, + int D, + int ob_modes, + int pr_modes, + bool do_LL_error, + bool do_realspace_error); + void setDeviceBuffers(unsigned char *d_mask, + float *d_Idata, + complex *d_obj, + complex *d_probe, + complex *d_exit_wave, + int *d_addr_info, + complex *d_prefilter, + complex *d_postfilter, + float *d_errors, + int *d_outidx, + int *d_startidx, + int *d_indices, + int outidx_size); + int calculateAddrIndices(const int *out1_addr); + void calculateUniqueDaIndices(const int *da_addr); + void allocate(); + // to be called if error point should be moved along + // in engine iterator function + void updateErrorOutput(float *d_errors); + void transfer_in(const unsigned char *mask, + const float *Idata, + const complex *obj, + const complex *probe, + const complex *exit_wave, + const int *addr_info, + const complex *prefilter, + const complex *postfilter); + void run(float pbound, float alpha, bool doPbound); + void transfer_out(float *errors, complex *exit_wave); + +private: + DevicePtrWrapper d_mask_; // N x A x B + DevicePtrWrapper d_Idata_; // N x A x B + DevicePtrWrapper> d_obj_; // ob_modes x C x D + DevicePtrWrapper> d_probe_; // pr_modes x A x B + DevicePtrWrapper> d_exit_wave_; // M x A x B + DevicePtrWrapper d_addr_info_; // M x 5 x 3 + DevicePtrWrapper> d_prefilter_; // A x B + DevicePtrWrapper> d_postfilter_; // A x B + DevicePtrWrapper d_errors_; // 3 x N + + DevicePtrWrapper d_outidx_, d_startidx_, d_indices_; + std::vector outidx_, startidx_, indices_; + int outidx_size_ = 0; + + int M_ = 0, N_ = 0, A_ = 0, B_ = 0, C_ = 0, D_ = 0, ob_modes_ = 0, + pr_modes_ = 0; + bool do_LL_error_ = 0; + bool do_realspace_error_ = 0; + // intermediate buffers + DevicePtrWrapper> d_prefilter_conj_; + DevicePtrWrapper> d_postfilter_conj_; + DevicePtrWrapper d_fmag_; + // other kernels + ScanAndMultiply *scan_and_multiply_ = nullptr; + LogLikelihood *log_likelihood_ = nullptr; + DifferenceMapRealspaceConstraint *difference_map_realspace_constraint_ = + nullptr; + FarfieldPropagator *farfield_propagator_fwd_ = nullptr; + Abs2, float> *abs2_ = nullptr; + SumToBuffer *sum_to_buffer_ = nullptr; + FarFieldError *far_field_error_ = nullptr; + RenormaliseFourierMagnitudes *renormalise_fourier_magnitudes_ = nullptr; + FarfieldPropagator *farfield_propagator_rev_ = nullptr; + GetDifference *get_difference_ = nullptr; + RealspaceError *realspace_error_ = nullptr; +}; diff --git a/archive/cuda_extension/cuda/func/difference_map_iterator.cu b/archive/cuda_extension/cuda/func/difference_map_iterator.cu new file mode 100644 index 000000000..75b08c6df --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_iterator.cu @@ -0,0 +1,381 @@ +#include "difference_map_iterator.h" + +#include "utils/Errors.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include "addr_info_helpers.h" + +/************** class implementation *********/ + +DifferenceMapIterator::DifferenceMapIterator() + : CudaFunction("difference_map_iterator") +{ +} + +void DifferenceMapIterator::setParameters(int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + int N, + int num_iterations, + float obj_smooth_std, + bool doSmoothing, + bool doClipping, + bool doCentering, + bool doPbound, + bool do_LL_error, + bool doRealspaceError, + bool doUpdateObjectFirst, + bool doProbeSupport) +{ + A_ = A; + B_ = B; + C_ = C; + D_ = D; + E_ = E; + F_ = F; + G_ = G; + H_ = H; + I_ = I; + N_ = N; + num_iterations_ = num_iterations; + doPbound_ = doPbound; + doProbeSupport_ = doProbeSupport; + + // TODO: shall we make the realspace error a parameter too? + dm_fourier_constraint_ = + gpuManager.get_cuda_function( + "dm_iterator.dm_fourier_constraint", + A, + N, + E, // E == B + F, // F == C + H, + I, + G, + D, + do_LL_error, + doRealspaceError); + dm_overlap_update_ = + gpuManager.get_cuda_function( + "dm_iterator.dm_overlap_update", + A, + B, + C, + D, + E, + F, + G, + H, + I, + obj_smooth_std, + doUpdateObjectFirst, + true, // in general, allocate mem, etc for probe update + doSmoothing, + doClipping, + doProbeSupport, + doCentering); +} + +int DifferenceMapIterator::calculateAddrIndices(const int* out1_addr) +{ + // calculate the indexing map + outidx_.clear(); + startidx_.clear(); + indices_.clear(); + flatten_out_addr(out1_addr, A_, 15, outidx_, startidx_, indices_); + outidx_size_ = outidx_.size(); + return outidx_size_; +} + +void DifferenceMapIterator::calculateUniqueDaIndices(const int* da_addr) +{ + dm_fourier_constraint_->calculateUniqueDaIndices(da_addr); +} + +void DifferenceMapIterator::setDeviceBuffers(float* d_diffraction, + complex* d_obj, + float* d_object_weights, + complex* d_cfact_object, + unsigned char* d_mask, + complex* d_probe, + complex* d_cfact_probe, + complex* d_probe_support, + float* d_probe_weights, + complex* d_exit_wave, + int* d_addr_info, + complex* d_pre_fft, + complex* d_post_fft, + float* d_errors) +{ + d_diffraction_ = d_diffraction; + d_obj_ = d_obj; + d_object_weights_ = d_object_weights; + d_cfact_object_ = d_cfact_object; + d_mask_ = d_mask; + d_probe_ = d_probe; + d_cfact_probe_ = d_cfact_probe; + d_probe_support_ = d_probe_support; + d_probe_weights_ = d_probe_weights; + d_exit_wave_ = d_exit_wave; + d_addr_info_ = d_addr_info; + d_pre_fft_ = d_pre_fft; + d_post_fft_ = d_post_fft; + d_errors_ = d_errors; +} + +void DifferenceMapIterator::allocate() +{ + ScopedTimer t(this, "allocate"); + + d_diffraction_.allocate(N_ * B_ * C_); + d_obj_.allocate(G_ * H_ * I_); + d_object_weights_.allocate(G_); + d_cfact_object_.allocate(G_ * H_ * I_); + d_mask_.allocate(N_ * B_ * C_); + d_probe_.allocate(D_ * E_ * F_); + if (doProbeSupport_) + { + d_probe_support_.allocate(D_ * E_ * F_); + } + d_probe_weights_.allocate(D_); + d_exit_wave_.allocate(A_ * B_ * C_); + d_addr_info_.allocate(A_ * 15); + d_pre_fft_.allocate(C_ * B_); + d_post_fft_.allocate(C_ * B_); + d_errors_.allocate(num_iterations_ * 3 * N_); + + if (!outidx_.empty()) + { + d_outidx_.allocate(outidx_.size()); + d_startidx_.allocate(startidx_.size()); + d_indices_.allocate(indices_.size()); + } + + dm_fourier_constraint_->setDeviceBuffers(d_mask_.get(), + d_diffraction_.get(), + d_obj_.get(), + d_probe_.get(), + d_exit_wave_.get(), + d_addr_info_.get(), + d_pre_fft_.get(), + d_post_fft_.get(), + d_errors_.get(), + d_outidx_.get(), + d_startidx_.get(), + d_indices_.get(), + outidx_size_); + dm_fourier_constraint_->allocate(); + + dm_overlap_update_->setDeviceBuffers(d_addr_info_.get(), + d_cfact_object_.get(), + d_cfact_probe_.get(), + d_exit_wave_.get(), + d_obj_.get(), + d_object_weights_.get(), + d_probe_.get(), + d_probe_support_.get(), + d_probe_weights_.get()); + dm_overlap_update_->allocate(); +} + +void DifferenceMapIterator::transfer_in(const float* diffraction, + const complex* obj, + const float* object_weights, + const complex* cfact_object, + const unsigned char* mask, + const complex* probe, + const complex* cfact_probe, + const complex* probe_support, + const float* probe_weights, + const complex* exit_wave, + const int* addr_info, + const complex* pre_fft, + const complex* post_fft) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_diffraction_.get(), diffraction, N_ * B_ * C_); + gpu_memcpy_h2d(d_obj_.get(), obj, G_ * H_ * I_); + gpu_memcpy_h2d(d_object_weights_.get(), object_weights, G_); + gpu_memcpy_h2d(d_cfact_object_.get(), cfact_object, G_ * H_ * I_); + gpu_memcpy_h2d(d_mask_.get(), mask, N_ * B_ * C_); + gpu_memcpy_h2d(d_probe_.get(), probe, D_ * E_ * F_); + gpu_memcpy_h2d(d_cfact_probe_.get(), cfact_probe, D_ * E_ * F_); + if (doProbeSupport_) + { + gpu_memcpy_h2d(d_probe_support_.get(), probe_support, D_ * E_ * F_); + } + gpu_memcpy_h2d(d_probe_weights_.get(), probe_weights, D_); + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, A_ * B_ * C_); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, A_ * 15); + gpu_memcpy_h2d(d_pre_fft_.get(), pre_fft, B_ * C_); + gpu_memcpy_h2d(d_post_fft_.get(), post_fft, B_ * C_); + + if (!outidx_.empty()) + { + gpu_memcpy_h2d(d_outidx_.get(), outidx_.data(), outidx_.size()); + gpu_memcpy_h2d(d_startidx_.get(), startidx_.data(), startidx_.size()); + gpu_memcpy_h2d(d_indices_.get(), indices_.data(), indices_.size()); + } + + calculateUniqueDaIndices(addr_info + 9); +} + +void DifferenceMapIterator::run(int overlap_max_iterations, + float overlap_converge_factor, + float probe_center_tol, + int probe_update_start, + float pbound, + float alpha, + float clip_min, + float clip_max) +{ + ScopedTimer t(this, "run"); + + for (int it = 0; it < num_iterations_; ++it) + { + if (((it + 1) % 10 == 0) && it > 0) + { + std::cout << "iteration: " << it + 1 << std::endl; + } + + dm_fourier_constraint_->updateErrorOutput(d_errors_.get() + it * 3 * N_); + dm_fourier_constraint_->run(pbound, alpha, doPbound_); + + auto do_update_probe = probe_update_start <= it; + dm_overlap_update_->run(overlap_max_iterations, + clip_min, + clip_max, + probe_center_tol, + overlap_converge_factor, + do_update_probe); + } + + timing_sync(); +} + +void DifferenceMapIterator::transfer_out(float* errors, + complex* obj, + complex* probe, + complex* exit_wave) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(errors, d_errors_.get(), num_iterations_ * 3 * N_); + gpu_memcpy_d2h(obj, d_obj_.get(), G_ * H_ * I_); + gpu_memcpy_d2h(probe, d_probe_.get(), D_ * E_ * F_); + gpu_memcpy_d2h(exit_wave, d_exit_wave_.get(), A_ * B_ * C_); +} + +/************** interface *************/ + +extern "C" void difference_map_iterator_c( + // note: E = B, F = C + const float* diffraction, // N x B x C + float* f_obj, // G x H x I + const float* object_weights, // G + const float* f_cfact_object, // G x H x I + const unsigned char* mask, // N x B x C + float* f_probe, // D x E x F + const float* f_cfact_probe, // D x E x F + const float* f_probe_support, // D x E x F + const float* probe_weights, // D + float* f_exit_wave, // A x B x C + const int* addr_info, // A x 5 x 3 + const float* f_pre_fft, // B x C + const float* f_post_fft, // B x C + float* errors, // num_iterations x 3 x N + float pbound, + int overlap_max_iterations, + int doUpdateObjectFirst, + float obj_smooth_std, + float overlap_converge_factor, + float probe_center_tol, + int probe_update_start, + float alpha, + float clip_min, + float clip_max, + int do_LL_error, + int do_realspace_error, + int num_iterations, + int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + int N, + int doSmoothing, + int doClipping, + int doCentering, + int doPbound) +{ + auto obj = reinterpret_cast*>(f_obj); + auto cfact_object = reinterpret_cast*>(f_cfact_object); + auto probe = reinterpret_cast*>(f_probe); + auto cfact_probe = reinterpret_cast*>(f_cfact_probe); + auto probe_support = reinterpret_cast*>(f_probe_support); + auto exit_wave = reinterpret_cast*>(f_exit_wave); + auto pre_fft = reinterpret_cast*>(f_pre_fft); + auto post_fft = reinterpret_cast*>(f_post_fft); + + if (E != B || F != C) + { + throw std::runtime_error("2nd/3rd dimensions of probe and mask are not consistent"); + } + + auto dmi = gpuManager.get_cuda_function( + "dm_iterator", + A, + B, + C, + D, + E, + F, + G, + H, + I, + N, + num_iterations, + obj_smooth_std, + doSmoothing != 0, + doClipping != 0, + doCentering != 0, + doPbound != 0, + do_LL_error != 0, + do_realspace_error != 0, + doUpdateObjectFirst != 0, + probe_support != nullptr); + dmi->calculateAddrIndices(addr_info + 9); + dmi->allocate(); + dmi->transfer_in(diffraction, + obj, + object_weights, + cfact_object, + mask, + probe, + cfact_probe, + probe_support, + probe_weights, + exit_wave, + addr_info, + pre_fft, + post_fft); + dmi->run(overlap_max_iterations, + overlap_converge_factor, + probe_center_tol, + probe_update_start, + pbound, + alpha, + clip_min, + clip_max); + dmi->transfer_out(errors, obj, probe, exit_wave); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_iterator.h b/archive/cuda_extension/cuda/func/difference_map_iterator.h new file mode 100644 index 000000000..e3f4eb6c9 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_iterator.h @@ -0,0 +1,104 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include "func/difference_map_fourier_constraint.h" +#include "func/difference_map_overlap_constraint.h" + +class DifferenceMapIterator : public CudaFunction +{ +public: + DifferenceMapIterator(); + void setParameters(int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + int N, + int num_iterations, + float obj_smooth_std, + bool doSmoothing, + bool doClipping, + bool doCentering, + bool doPbound, + bool do_LL_error, + bool doRealspaceError, + bool doUpdateObjectFirst, + bool doProbeSupport); + void setDeviceBuffers(float* d_diffraction, + complex* d_obj, + float* d_object_weights, + complex* d_cfact_object, + unsigned char* d_mask, + complex* d_probe, + complex* d_cfact_probe, + complex* d_probe_support, + float* d_probe_weight, + complex* d_exit_wave, + int* d_addr_info, + complex* d_pre_fft, + complex* d_post_fft, + float* d_errors); + int calculateAddrIndices(const int* out1_addr); + void calculateUniqueDaIndices(const int* da_addr); + void allocate(); + void transfer_in(const float* diffraction, + const complex* obj, + const float* object_weights, + const complex* cfact_object, + const unsigned char* mask, + const complex* probe, + const complex* cfact_probe, + const complex* probe_support, + const float* probe_weight, + const complex* exit_wave, + const int* addr_info, + const complex* pre_fft, + const complex* post_fft); + void run(int overlap_max_iterations, + float overlap_converge_factor, + float probe_center_tol, + int probe_update_start, + float pbound, + float alpha, + float clip_min, + float clip_max); + void transfer_out(float* errors, + complex* obj, + complex* probe, + complex* exit_wave); + +private: + DevicePtrWrapper d_diffraction_; // N x B x C + DevicePtrWrapper> d_obj_; // G x H x I + DevicePtrWrapper d_object_weights_; // G + DevicePtrWrapper> d_cfact_object_; // G x H x I + DevicePtrWrapper d_mask_; // N x B x C + DevicePtrWrapper> d_probe_; // D x E x F + DevicePtrWrapper> d_cfact_probe_; // D x E x F + DevicePtrWrapper> d_probe_support_; // D x E x F + DevicePtrWrapper d_probe_weights_; // D + DevicePtrWrapper> d_exit_wave_; // A x B x C + DevicePtrWrapper d_addr_info_; // A x 5 x 3 + DevicePtrWrapper> d_pre_fft_; // B x C + DevicePtrWrapper> d_post_fft_; // B x C + DevicePtrWrapper d_errors_; // num_iterations x 3 x N + + DevicePtrWrapper d_outidx_, d_startidx_, d_indices_; + std::vector outidx_, startidx_, indices_; + int outidx_size_ = 0; + + int A_ = 0, B_ = 0, C_ = 0, D_ = 0, E_ = 0, F_ = 0, G_ = 0, H_ = 0, I_ = 0, + N_ = 0; + int num_iterations_ = 0; + bool doPbound_ = false; + bool doProbeSupport_ = false; + + DifferenceMapFourierConstraint* dm_fourier_constraint_ = nullptr; + DifferenceMapOverlapConstraint* dm_overlap_update_ = nullptr; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_overlap_constraint.cu b/archive/cuda_extension/cuda/func/difference_map_overlap_constraint.cu new file mode 100644 index 000000000..01a0120ec --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_overlap_constraint.cu @@ -0,0 +1,303 @@ +#include "difference_map_overlap_constraint.h" + +#include "utils/Complex.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +/********* class implementation ************/ + +DifferenceMapOverlapConstraint::DifferenceMapOverlapConstraint() + : CudaFunction("difference_map_overlap_constraint") +{ +} + +void DifferenceMapOverlapConstraint::setParameters(int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + float obj_smooth_std, + bool doUpdateObjectFirst, + bool doUpdateProbe, + bool doSmoothing, + bool doClipping, + bool withProbeSupport, + bool doCentering) +{ + A_ = A; + B_ = B; + C_ = C; + D_ = D; + E_ = E; + F_ = F; + G_ = G; + H_ = H; + I_ = I; + doUpdateObjectFirst_ = doUpdateObjectFirst; + doUpdateProbe_ = doUpdateProbe; + doSmoothing_ = doSmoothing; + doClipping_ = doClipping; + withProbeSupport_ = withProbeSupport; + doCentering_ = doCentering; + + dm_update_object_ = gpuManager.get_cuda_function( + "dm_overlap_constraint.dm_update_object", + A, + B, + C, + D, + E, + F, + G, + H, + I, + obj_smooth_std, + doSmoothing, + doClipping); + if (doUpdateProbe_) + { + /* size parameter translation: + D=>G,E=>H,F=>I, + G=>D,H=>E,I=>F + */ + dm_update_probe_ = gpuManager.get_cuda_function( + "dm_overlap_constraint.dm_update_probe", + A, + B, + C, + G, + H, + I, + D, + E, + F, + withProbeSupport); + } + + if (doCentering_) + { + center_probe_ = gpuManager.get_cuda_function( + "dm_overlap_constraint.center_probe", D_, E_, F_); + } +} + +void DifferenceMapOverlapConstraint::setDeviceBuffers( + int* d_addr_info, + complex* d_cfact_object, + complex* d_cfact_probe, + complex* d_exit_wave, + complex* d_obj, + float* d_obj_weigths, + complex* d_probe, + complex* d_probe_support, + float* d_probe_weights) +{ + d_addr_info_ = d_addr_info; + d_cfact_object_ = d_cfact_object; + d_cfact_probe_ = d_cfact_probe; + d_exit_wave_ = d_exit_wave; + d_obj_ = d_obj; + d_obj_weights_ = d_obj_weigths; + d_probe_ = d_probe; + d_probe_support_ = d_probe_support; + d_probe_weights_ = d_probe_weights; +} + +void DifferenceMapOverlapConstraint::allocate() +{ + ScopedTimer t(this, "allocate"); + + d_addr_info_.allocate(A_ * 15); + d_cfact_object_.allocate(G_ * H_ * I_); + d_cfact_probe_.allocate(D_ * E_ * F_); + d_exit_wave_.allocate(A_ * B_ * C_); + d_obj_.allocate(G_ * H_ * I_); + d_obj_weights_.allocate(G_); + d_probe_.allocate(D_ * E_ * F_); + if (withProbeSupport_) + { + d_probe_support_.allocate(D_ * E_ * F_); + } + d_probe_weights_.allocate(D_); + + dm_update_object_->setDeviceBuffers(d_obj_.get(), + d_obj_weights_.get(), + d_probe_.get(), + d_exit_wave_.get(), + d_addr_info_.get(), + d_cfact_object_.get()); + dm_update_object_->allocate(); + + if (doUpdateProbe_) + { + dm_update_probe_->setDeviceBuffers(d_obj_.get(), + d_probe_weights_.get(), + d_probe_.get(), + d_exit_wave_.get(), + d_addr_info_.get(), + d_cfact_probe_.get(), + d_probe_support_.get()); + dm_update_probe_->allocate(); + } + if (doCentering_) + { + center_probe_->setDeviceBuffers(d_probe_.get(), nullptr); + center_probe_->allocate(); + } +} + +void DifferenceMapOverlapConstraint::transfer_in( + const int* addr_info, + const complex* cfact_object, + const complex* cfact_probe, + const complex* exit_wave, + const complex* obj, + const float* obj_weigths, + const complex* probe, + const complex* probe_support, + const float* probe_weights) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, A_ * 15); + gpu_memcpy_h2d(d_cfact_object_.get(), cfact_object, G_ * H_ * I_); + gpu_memcpy_h2d(d_cfact_probe_.get(), cfact_probe, D_ * E_ * F_); + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, A_ * B_ * C_); + gpu_memcpy_h2d(d_obj_.get(), obj, G_ * H_ * I_); + gpu_memcpy_h2d(d_obj_weights_.get(), obj_weigths, G_); + gpu_memcpy_h2d(d_probe_.get(), probe, D_ * E_ * F_); + if (withProbeSupport_) + { + gpu_memcpy_h2d(d_probe_support_.get(), probe_support, D_ * E_ * F_); + } + gpu_memcpy_h2d(d_probe_weights_.get(), probe_weights, D_); +} + +void DifferenceMapOverlapConstraint::run(int max_iterations, + float clip_min, + float clip_max, + float probe_center_tol, + float overlap_converge_factor, + bool do_update_probe) +{ + ScopedTimer t(this, "run"); + + bool doUpdateProbeCombined = doUpdateProbe_ && do_update_probe; + + for (int inner = 0; inner < max_iterations; ++inner) + { + if (doUpdateObjectFirst_ || inner > 0) + { + dm_update_object_->run(clip_min, clip_max); + } + + // exit if probe should not be updated yet + if (!doUpdateProbeCombined) + { + break; + } + + dm_update_probe_->run(); + float change = 0.0f; + dm_update_probe_->transfer_out(nullptr, &change); + + // recenter the probe + if (doCentering_) + { + center_probe_->run(probe_center_tol); + } + + // stop iteration if probe change is small + if (change < overlap_converge_factor) + break; + } + + timing_sync(); +} + +void DifferenceMapOverlapConstraint::transfer_out(complex* probe, + complex* obj) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(probe, d_probe_.get(), D_ * E_ * F_); + gpu_memcpy_d2h(obj, d_obj_.get(), G_ * H_ * I_); +} + +/********* interface functions *************/ + +extern "C" void difference_map_overlap_constraint_c( + const int* addr_info, // A x 5 x 3 + const float* f_cfact_object, // G x H x I + const float* f_cfact_probe, // D x E x F + const float* f_exit_wave, // A x B x C + float* f_obj, // G x H x I + const float* object_weights, // G + float* f_probe, // D x E x F + const float* f_probe_support, // D x E x F, can be null + const float* probe_weights, // D + float obj_smooth_std, + float clip_min, + float clip_max, + float probe_center_tol, + float overlap_converge_factor, + int max_iterations, + int doUpdateObjectFirst, + int doUpdateProbe, + int doSmoothing, + int doClipping, + int doCentering, + int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I) +{ + auto obj = reinterpret_cast*>(f_obj); + auto probe = reinterpret_cast*>(f_probe); + auto probe_support = reinterpret_cast*>(f_probe_support); + auto cfact_object = reinterpret_cast*>(f_cfact_object); + auto cfact_probe = reinterpret_cast*>(f_cfact_probe); + auto exit_wave = reinterpret_cast*>(f_exit_wave); + + auto dmoc = gpuManager.get_cuda_function( + "dm_overlap_constraint", + A, + B, + C, + D, + E, + F, + G, + H, + I, + obj_smooth_std, + doUpdateObjectFirst != 0, + doUpdateProbe != 0, + doSmoothing != 0, + doClipping != 0, + probe_support != 0, + doCentering != 0); + dmoc->allocate(); + dmoc->transfer_in(addr_info, + cfact_object, + cfact_probe, + exit_wave, + obj, + object_weights, + probe, + probe_support, + probe_weights); + dmoc->run(max_iterations, + clip_min, + clip_max, + probe_center_tol, + overlap_converge_factor); + dmoc->transfer_out(probe, obj); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_overlap_constraint.h b/archive/cuda_extension/cuda/func/difference_map_overlap_constraint.h new file mode 100644 index 000000000..75e315959 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_overlap_constraint.h @@ -0,0 +1,82 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include "center_probe.h" +#include "difference_map_update_object.h" +#include "difference_map_update_probe.h" + +class DifferenceMapOverlapConstraint : public CudaFunction +{ +public: + DifferenceMapOverlapConstraint(); + void setParameters( + int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + float obj_smooth_std, + bool doUpdateObjectFirst, + bool doUpdateProbe, // in general (alloc space / ops for it) + bool doSmoothing, + bool doClipping, + bool withProbeSupport, + bool doCentering); + void setDeviceBuffers(int* d_addr_info, + complex* d_cfact_object, + complex* d_cfact_probe, + complex* d_exit_wave, + complex* d_obj, + float* d_obj_weigths, + complex* d_probe, + complex* d_probe_support, + float* d_probe_weights); + void allocate(); + void transfer_in(const int* addr_info, + const complex* cfact_object, + const complex* cfact_probe, + const complex* exit_wave, + const complex* obj, + const float* obj_weigths, + const complex* probe, + const complex* probe_support, + const float* probe_weights); + void run( + int max_iterations, + float clip_min, + float clip_max, + float probe_center_tol, + float overlap_converge_factor, + bool do_update_probe = true // update it in this call? (logical and with + // the general setParameters one is done) + ); + void transfer_out(complex* probe, complex* obj); + +private: + DevicePtrWrapper d_addr_info_; // A x 5 x 3 + DevicePtrWrapper> d_cfact_object_; // G x H x I + DevicePtrWrapper> d_cfact_probe_; // D x E x F + DevicePtrWrapper> d_exit_wave_; // A x B x C + DevicePtrWrapper> d_obj_; // G x H x I + DevicePtrWrapper d_obj_weights_; // G + DevicePtrWrapper> d_probe_; // D x E x F + DevicePtrWrapper> d_probe_support_; // D x E x F + DevicePtrWrapper d_probe_weights_; // D + bool doUpdateObjectFirst_ = false; + bool doUpdateProbe_ = true; + bool doSmoothing_ = false; + bool doClipping_ = true; + bool withProbeSupport_ = true; + bool doCentering_ = true; + int A_ = 0, B_ = 0, C_ = 0, D_ = 0, E_ = 0, F_ = 0, G_ = 0, H_ = 0, I_ = 0; + + DifferenceMapUpdateProbe* dm_update_probe_ = nullptr; + DifferenceMapUpdateObject* dm_update_object_ = nullptr; + CenterProbe* center_probe_ = nullptr; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_realspace_constraint.cu b/archive/cuda_extension/cuda/func/difference_map_realspace_constraint.cu new file mode 100644 index 000000000..6c881cf50 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_realspace_constraint.cu @@ -0,0 +1,112 @@ +#include "difference_map_realspace_constraint.h" + +#include "utils/Complex.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +/************ Kernels ******************/ + +__global__ void difference_map_realspace_constraint_kernel( + const complex *obj_and_probe, + const complex *exit_wave, + float alpha, + complex *out, + size_t total) +{ + size_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= total) + return; + auto pno = obj_and_probe[idx]; + auto ex = exit_wave[idx]; + auto val = (1.0f + alpha) * pno - alpha * ex; + out[idx] = val; +} + +/***************** Class implementation ***********/ + +DifferenceMapRealspaceConstraint::DifferenceMapRealspaceConstraint() + : CudaFunction("difference_map_realspace_constraint") +{ +} + +void DifferenceMapRealspaceConstraint::setParameters(int i, int m, int n) +{ + i_ = i; + m_ = m; + n_ = n; +} + +void DifferenceMapRealspaceConstraint::setDeviceBuffers( + complex *d_obj_and_probe, + complex *d_exit_wave, + complex *d_out) +{ + d_obj_and_probe_ = d_obj_and_probe; + d_exit_wave_ = d_exit_wave; + d_out_ = d_out; +} + +void DifferenceMapRealspaceConstraint::allocate() +{ + ScopedTimer t(this, "allocate"); + d_obj_and_probe_.allocate(i_ * m_ * n_); + d_exit_wave_.allocate(i_ * m_ * n_); + d_out_.allocate(i_ * m_ * n_); +} + +complex *DifferenceMapRealspaceConstraint::getOutput() const +{ + return d_out_.get(); +} + +void DifferenceMapRealspaceConstraint::transfer_in( + const complex *obj_and_probe, const complex *exit_wave) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_obj_and_probe_.get(), obj_and_probe, i_ * m_ * n_); + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, i_ * m_ * n_); +} + +void DifferenceMapRealspaceConstraint::transfer_out(complex *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), i_ * m_ * n_); +} + +void DifferenceMapRealspaceConstraint::run(float alpha) +{ + ScopedTimer t(this, "run"); + size_t total = size_t(m_) * size_t(n_) * size_t(i_); + size_t block = 256; + size_t blocks = (total + block - 1) / block; + + difference_map_realspace_constraint_kernel<<>>( + d_obj_and_probe_.get(), d_exit_wave_.get(), alpha, d_out_.get(), total); + checkLaunchErrors(); + + // sync device if timing is enabled + timing_sync(); +} + +/**************** interface function ************/ + +extern "C" void difference_map_realspace_constraint_c( + const float *fobj_and_probe, + const float *f_exit_wave, + float alpha, + int i, + int m, + int n, + float *fout) +{ + auto obj_and_probe = reinterpret_cast *>(fobj_and_probe); + auto exit_wave = reinterpret_cast *>(f_exit_wave); + auto out = reinterpret_cast *>(fout); + + auto dmc = gpuManager.get_cuda_function( + "dm_realspace_constraint", i, m, n); + dmc->allocate(); + dmc->transfer_in(obj_and_probe, exit_wave); + dmc->run(alpha); + dmc->transfer_out(out); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_realspace_constraint.h b/archive/cuda_extension/cuda/func/difference_map_realspace_constraint.h new file mode 100644 index 000000000..0d80f2a71 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_realspace_constraint.h @@ -0,0 +1,27 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class DifferenceMapRealspaceConstraint : public CudaFunction +{ +public: + DifferenceMapRealspaceConstraint(); + void setParameters(int i, int m, int n); + void setDeviceBuffers(complex *d_obj_and_probe, + complex *d_exit_wave, + complex *d_out); + void allocate(); + complex *getOutput() const; + void transfer_in(const complex *obj_and_probe, + const complex *exit_wave); + void transfer_out(complex *out); + void run(float alpha); + +private: + DevicePtrWrapper> d_obj_and_probe_; + DevicePtrWrapper> d_exit_wave_; + DevicePtrWrapper> d_out_; + int i_ = 0, m_ = 0, n_ = 0; +}; diff --git a/archive/cuda_extension/cuda/func/difference_map_update_object.cu b/archive/cuda_extension/cuda/func/difference_map_update_object.cu new file mode 100644 index 000000000..ab424380f --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_update_object.cu @@ -0,0 +1,226 @@ +#include "difference_map_update_object.h" +#include "utils/Complex.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +/********* kernels *****************/ + +static __global__ void multiply_kernel(const complex* in1, + const complex* in2, + complex* out, + int n) +{ + int gid = threadIdx.x + blockIdx.x * blockDim.x; + if (gid >= n) + return; + out[gid] = in1[gid] * in2[gid]; +} + +/********** Class implementation **********/ + +DifferenceMapUpdateObject::DifferenceMapUpdateObject() + : CudaFunction("difference_map_update_object") +{ +} + +void DifferenceMapUpdateObject::setParameters(int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + float obj_smooth_std, + bool doSmoothing, + bool doClipping) +{ + A_ = A; + B_ = B; + C_ = C; + D_ = D; + E_ = E; + F_ = F; + G_ = G; + H_ = H; + I_ = I; + doSmoothing_ = doSmoothing; + doClipping_ = doClipping; + + extract_array_from_exit_wave_ = + gpuManager.get_cuda_function( + "dm_update_object.extract_array_from_exit_wave", + A_, + B_, + C_, + D_, + E_, + F_, + G_, + H_, + I_); + extract_array_from_exit_wave_->setAddrStride(15); + + if (doClipping_) + { + clip_complex_magnitudes_to_range_ = + gpuManager.get_cuda_function( + "dm_update_object.clip_complex_magnitudes_to_range", G_ * H_ * I_); + } + if (doSmoothing_) + { + int dims[] = {G_, H_, I_}; + float mfs[] = {obj_smooth_std, obj_smooth_std}; + gaussian_filter_ = gpuManager.get_cuda_function( + "dm_update_object.gaussian_filter", 3, dims, mfs); + } +} + +void DifferenceMapUpdateObject::setDeviceBuffers(complex* d_obj, + float* d_object_weights, + complex* d_probe, + complex* d_exit_wave, + int* d_addr_info, + complex* d_cfact) +{ + d_obj_ = d_obj; + d_object_weights_ = d_object_weights; + d_probe_ = d_probe; + d_exit_wave_ = d_exit_wave; + d_addr_info_ = d_addr_info; + d_cfact_ = d_cfact; +} + +void DifferenceMapUpdateObject::allocate() +{ + ScopedTimer t(this, "allocate"); + d_obj_.allocate(G_ * H_ * I_); + d_object_weights_.allocate(G_); + d_probe_.allocate(D_ * E_ * F_); + d_exit_wave_.allocate(A_ * B_ * C_); + d_addr_info_.allocate(A_ * 15); + d_cfact_.allocate(G_ * H_ * I_); + + extract_array_from_exit_wave_->setDeviceBuffers(d_exit_wave_.get(), + d_addr_info_.get() + 6, + d_probe_.get(), + d_addr_info_.get(), + d_obj_.get(), + d_addr_info_.get() + 3, + d_object_weights_.get(), + d_cfact_.get(), + nullptr); + + extract_array_from_exit_wave_->allocate(); + + if (doClipping_) + { + clip_complex_magnitudes_to_range_->setDeviceBuffers(d_obj_.get()); + clip_complex_magnitudes_to_range_->allocate(); + } + + if (doSmoothing_) + { + gaussian_filter_->setDeviceBuffers(d_obj_.get(), d_obj_.get()); + gaussian_filter_->allocate(); + } +} + +void DifferenceMapUpdateObject::transfer_in(const complex* obj, + const float* object_weigths, + const complex* probe, + const complex* exit_wave, + const int* addr_info, + const complex* cfact) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_obj_.get(), obj, G_ * H_ * I_); + gpu_memcpy_h2d(d_object_weights_.get(), object_weigths, G_); + gpu_memcpy_h2d(d_probe_.get(), probe, D_ * E_ * F_); + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, A_ * B_ * C_); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, A_ * 15); + gpu_memcpy_h2d(d_cfact_.get(), cfact, G_ * H_ * I_); +} + +void DifferenceMapUpdateObject::run(float clip_min, float clip_max) +{ + ScopedTimer t(this, "run"); + + if (doSmoothing_) + { + gaussian_filter_->run(); + } + + int total = G_ * H_ * I_; + int threadsPerBlock = 256; + int blocks = (total + threadsPerBlock - 1) / threadsPerBlock; + multiply_kernel<<>>( + d_obj_.get(), d_cfact_.get(), d_obj_.get(), G_ * H_ * I_); + checkLaunchErrors(); + + extract_array_from_exit_wave_->run(); + + if (doClipping_) + { + clip_complex_magnitudes_to_range_->run(clip_min, clip_max); + } + + timing_sync(); +} + +void DifferenceMapUpdateObject::transfer_out(complex* obj) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(obj, d_obj_.get(), G_ * H_ * I_); +} + +/********** Interface functions ******/ + +extern "C" void difference_map_update_object_c( + float* f_obj, // G x H x I + const float* object_weights, // G + const float* f_probe, // D x E x F + const float* f_exit_wave, // A x B x C + const int* addr_info, // A x 5 x 3 + const float* f_cfact_object, // G x H x I + float ob_smooth_std, // scalar + float clip_min, // scalar + float clip_max, // scalar + int doSmoothing, // boolean if smoothing should be done + int doClipping, // boolean if clipping should be done + int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I) +{ + auto obj = reinterpret_cast*>(f_obj); + auto probe = reinterpret_cast*>(f_probe); + auto exit_wave = reinterpret_cast*>(f_exit_wave); + auto cfact = reinterpret_cast*>(f_cfact_object); + + auto dmuo = gpuManager.get_cuda_function( + "dm_update_object", + A, + B, + C, + D, + E, + F, + G, + H, + I, + ob_smooth_std, + doSmoothing != 0, + doClipping != 0); + + dmuo->allocate(); + dmuo->transfer_in(obj, object_weights, probe, exit_wave, addr_info, cfact); + dmuo->run(clip_min, clip_max); + dmuo->transfer_out(obj); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_update_object.h b/archive/cuda_extension/cuda/func/difference_map_update_object.h new file mode 100644 index 000000000..a23d9b7aa --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_update_object.h @@ -0,0 +1,57 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include "complex_gaussian_filter.h" +#include "extract_array_from_exit_wave.h" +#include "clip_complex_magnitudes_to_range.h" + +class DifferenceMapUpdateObject : public CudaFunction +{ +public: + DifferenceMapUpdateObject(); + void setParameters(int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + float obj_smooth_std, + bool doSmoothing, + bool doClipping); + void setDeviceBuffers(complex* d_obj, + float* d_object_weights, + complex* d_probe, + complex* d_exit_wave, + int* d_addr_info, + complex* d_cfact); + void allocate(); + void transfer_in(const complex* obj, + const float* object_weigths, + const complex* probe, + const complex* exit_wave, + const int* addr_info, + const complex* cfact); + void run(float clip_min, float clip_max); + void transfer_out(complex* obj); + +private: + DevicePtrWrapper> d_obj_; // G x H x I + DevicePtrWrapper d_object_weights_; // G + DevicePtrWrapper> d_probe_; // D x E x F + DevicePtrWrapper> d_exit_wave_; // A x B x C + DevicePtrWrapper d_addr_info_; // A x 5 x 3 + DevicePtrWrapper> d_cfact_; // G x H x I + bool doSmoothing_ = false, doClipping_ = false; + int A_ = 0, B_ = 0, C_ = 0, D_ = 0, E_ = 0, F_ = 0, G_ = 0, H_ = 0, I_ = 0; + + // child kernels + ExtractArrayFromExitWave* extract_array_from_exit_wave_ = nullptr; + ClipComplexMagnitudesToRange* clip_complex_magnitudes_to_range_ = nullptr; + ComplexGaussianFilter* gaussian_filter_ = nullptr; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_update_probe.cu b/archive/cuda_extension/cuda/func/difference_map_update_probe.cu new file mode 100644 index 000000000..8d3d11933 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_update_probe.cu @@ -0,0 +1,242 @@ +#include "difference_map_update_probe.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include + +/********* kernels *****************/ + +static __global__ void multiply_kernel(const complex* in1, + const complex* in2, + complex* out, + int n) +{ + int gid = threadIdx.x + blockIdx.x * blockDim.x; + if (gid >= n) + return; + out[gid] = in1[gid] * in2[gid]; +} + +static __global__ void diff_kernel(const complex* a, + const complex* b, + complex* res, + int n) +{ + int gid = threadIdx.x + blockIdx.x * blockDim.x; + if (gid >= n) + return; + res[gid] = a[gid] - b[gid]; +} + +/********* class implementation *********/ + +DifferenceMapUpdateProbe::DifferenceMapUpdateProbe() + : CudaFunction("difference_map_update_probe") +{ +} + +void DifferenceMapUpdateProbe::setParameters(int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + bool withProbeSupport) +{ + A_ = A; + B_ = B; + C_ = C; + D_ = D; + E_ = E; + F_ = F; + G_ = G; + H_ = H; + I_ = I; + withProbeSupport_ = withProbeSupport; + + extract_array_from_exit_wave_ = + gpuManager.get_cuda_function( + "dm_update_probe.extract_array_from_exit_wave", + A_, + B_, + C_, + D_, + E_, + F_, + G_, + H_, + I_); + extract_array_from_exit_wave_->setAddrStride(15); + norm2_probe_ = gpuManager.get_cuda_function>>( + "dm_update_probe.norm2_probe", G_ * H_ * I_); + norm2_diff_ = gpuManager.get_cuda_function>>( + "dm_update_probe.norm2_diff", G_ * H_ * I_); +} + +void DifferenceMapUpdateProbe::setDeviceBuffers(complex* d_obj, + float* d_probe_weights, + complex* d_probe, + complex* d_exit_wave, + int* d_addr_info, + complex* d_cfact_probe, + complex* d_probe_support) +{ + d_obj_ = d_obj; + d_probe_weights_ = d_probe_weights; + d_probe_ = d_probe; + d_exit_wave_ = d_exit_wave; + d_addr_info_ = d_addr_info; + d_cfact_probe_ = d_cfact_probe; + d_probe_support_ = d_probe_support; +} + +void DifferenceMapUpdateProbe::allocate() +{ + ScopedTimer t(this, "allocate"); + + if (withProbeSupport_) + { + d_probe_support_.allocate(G_ * H_ * I_); + } + + d_obj_.allocate(D_ * E_ * F_); + d_probe_weights_.allocate(G_); + d_probe_.allocate(G_ * H_ * I_); + d_buffer_.allocate(G_ * H_ * I_); + d_exit_wave_.allocate(A_ * B_ * C_); + d_addr_info_.allocate(A_ * 5 * 3); + d_cfact_probe_.allocate(G_ * H_ * I_); + + + + extract_array_from_exit_wave_->setDeviceBuffers(d_exit_wave_.get(), + d_addr_info_.get() + 6, + d_obj_.get(), + d_addr_info_.get() + 3, + d_buffer_.get(), + d_addr_info_.get(), + d_probe_weights_.get(), + d_cfact_probe_.get(), + nullptr); + extract_array_from_exit_wave_->allocate(); + + norm2_probe_->setDeviceBuffers(d_buffer_.get(), + nullptr // just size 1, allocated internally + ); + norm2_probe_->allocate(); + + // here, we'll run d_probe = d_buffer_ - d_probe + norm2_diff_->setDeviceBuffers(d_probe_.get(), + nullptr // just size 1, allocated internally + ); + norm2_diff_->allocate(); + + // and we'll copy d_buffer_ to d_probe_ in the end +} + +void DifferenceMapUpdateProbe::transfer_in(const complex* obj, + const float* probe_weights, + const complex* probe, + const complex* exit_wave, + const int* addr_info, + const complex* cfact_probe, + const complex* probe_support) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_obj_.get(), obj, D_ * E_ * F_); + gpu_memcpy_h2d(d_probe_weights_.get(), probe_weights, G_); + gpu_memcpy_h2d(d_probe_.get(), probe, G_ * H_ * I_); + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, A_ * B_ * C_); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, A_ * 3 * 5); + gpu_memcpy_h2d(d_cfact_probe_.get(), cfact_probe, G_ * H_ * I_); + if (withProbeSupport_) + { + gpu_memcpy_h2d(d_probe_support_.get(), probe_support, G_ * H_ * I_); + } +} + +void DifferenceMapUpdateProbe::run() +{ + ScopedTimer t(this, "run"); + + int total = G_ * H_ * I_; + int threadsPerBlock = 256; + int blocks = (total + threadsPerBlock - 1) / threadsPerBlock; + multiply_kernel<<>>( + d_probe_.get(), d_cfact_probe_.get(), d_buffer_.get(), G_ * H_ * I_); + checkLaunchErrors(); + + extract_array_from_exit_wave_->run(); + + if (withProbeSupport_) + { + multiply_kernel<<>>( + d_buffer_.get(), d_probe_support_.get(), d_buffer_.get(), G_ * H_ * I_); + checkLaunchErrors(); + } + + // d_probe = d_buffer_ - d_probe + diff_kernel<<>>( + d_buffer_.get(), d_probe_.get(), d_probe_.get(), G_ * H_ * I_); + checkLaunchErrors(); + + norm2_probe_->run(); + norm2_diff_->run(); + + gpu_memcpy_d2d(d_probe_.get(), d_buffer_.get(), G_ * H_ * I_); + + timing_sync(); +} + +void DifferenceMapUpdateProbe::transfer_out(complex* probe, + float* change) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(probe, d_probe_.get(), G_ * H_ * I_); + + float norm2diff, norm2probe; + gpu_memcpy_d2h(&norm2diff, norm2_diff_->getOutput(), 1); + gpu_memcpy_d2h(&norm2probe, norm2_probe_->getOutput(), 1); + + *change = std::sqrt(norm2diff / norm2probe / G_); +} + +/********* interface functions *********/ + +extern "C" float difference_map_update_probe_c( + const float* f_obj, // D x E x F + const float* probe_weights, // G + float* f_probe, // G x H x I + const float* f_exit_wave, // A x B x C + const int* addr_info, // A x 5 x 3 + const float* f_cfact_probe, // G x H x I + const float* f_probe_support, // G x H x I - can be null + int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I) +{ + auto obj = reinterpret_cast*>(f_obj); + auto probe = reinterpret_cast*>(f_probe); + auto exit_wave = reinterpret_cast*>(f_exit_wave); + auto cfact = reinterpret_cast*>(f_cfact_probe); + auto probe_support = reinterpret_cast*>(f_probe_support); + + auto dmup = gpuManager.get_cuda_function( + "dm_update_probe", A, B, C, D, E, F, G, H, I, f_probe_support != nullptr); + dmup->allocate(); + dmup->transfer_in( + obj, probe_weights, probe, exit_wave, addr_info, cfact, probe_support); + dmup->run(); + float change; + dmup->transfer_out(probe, &change); + return change; +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/difference_map_update_probe.h b/archive/cuda_extension/cuda/func/difference_map_update_probe.h new file mode 100644 index 000000000..a222ca616 --- /dev/null +++ b/archive/cuda_extension/cuda/func/difference_map_update_probe.h @@ -0,0 +1,60 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include "extract_array_from_exit_wave.h" +#include "norm2.h" + +class DifferenceMapUpdateProbe : public CudaFunction +{ +public: + DifferenceMapUpdateProbe(); + void setParameters(int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + bool withProbeSupport); + void setDeviceBuffers(complex* d_obj, + float* d_probe_weights, + complex* d_probe, + complex* d_exit_wave, + int* d_addr_info, + complex* d_cfact_probe, + complex* d_probe_support); + void allocate(); + void transfer_in(const complex* obj, + const float* probe_weights, + const complex* probe, + const complex* exit_wave, + const int* addr_info, + const complex* cfact_probe, + const complex* probe_support); + void run(); + void transfer_out(complex* probe, float* change); + +private: + DevicePtrWrapper> d_obj_; + DevicePtrWrapper d_probe_weights_; + DevicePtrWrapper> d_probe_; + DevicePtrWrapper> d_exit_wave_; + DevicePtrWrapper d_addr_info_; + DevicePtrWrapper> d_cfact_probe_; + DevicePtrWrapper> d_probe_support_; + int A_ = 0, B_ = 0, C_ = 0, D_ = 0, E_ = 0, F_ = 0, G_ = 0, H_ = 0, I_ = 0; + bool withProbeSupport_ = true; + + // temporary buffers + DevicePtrWrapper> d_buffer_; // size = probe + + // child kernels + ExtractArrayFromExitWave* extract_array_from_exit_wave_ = nullptr; + Norm2>* norm2_probe_; + Norm2>* norm2_diff_; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/extract_array_from_exit_wave.cu b/archive/cuda_extension/cuda/func/extract_array_from_exit_wave.cu new file mode 100644 index 000000000..c5a34a0a3 --- /dev/null +++ b/archive/cuda_extension/cuda/func/extract_array_from_exit_wave.cu @@ -0,0 +1,255 @@ +#include "addr_info_helpers.h" +#include "extract_array_from_exit_wave.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include +#include +#include + +/*********** kernels ************************/ + +__device__ inline void atomicAdd(complex* x, complex y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, y.real()); + atomicAdd(xf + 1, y.imag()); +} + +template +__global__ void extract_array_from_exit_wave_kernel( + const complex* exit_wave, + int A, + int B, + int C, + const int* exit_addr, + const complex* array_to_be_extracted, + int D, + int E, + int F, + const int* extract_addr, + complex* array_to_be_updated, + int G, + int H, + int I, + const int* update_addr, + const float* weights, + complex* denominator, + int addr_stride) +{ + // one block per addr instance + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + + auto pa = update_addr + bid * addr_stride; + auto oa = extract_addr + bid * addr_stride; + auto ea = exit_addr + bid * addr_stride; + + array_to_be_extracted += oa[0] * E * F + oa[1] * F + oa[2]; + array_to_be_updated += pa[0] * H * I + pa[1] * I + pa[2]; + denominator += pa[0] * H * I + pa[1] * I + pa[2]; + + assert(pa[0] * H * I + pa[1] * I + pa[2] + (B - 1) * I + C - 1 < G * H * I); + + auto weight = weights[pa[0]]; + exit_wave += ea[0] * B * C; + + for (int b = tx; b < B; b += blockDim.x) + { + for (int c = ty; c < C; c += blockDim.y) + { + auto extracted_array = array_to_be_extracted[b * F + c]; + auto extracted_array_conj = conj(extracted_array); + atomicAdd(&array_to_be_updated[b * I + c], + extracted_array_conj * exit_wave[b * C + c] * weight); + atomicAdd(&denominator[b * I + c], + extracted_array * extracted_array_conj * weight); + } + } +} + +template +__global__ void div_by_denominator(complex* array_to_be_updated, + const complex* denominator, + int n) +{ + int gid = threadIdx.x + blockIdx.x * blockDim.x; + if (gid >= n) + return; + array_to_be_updated[gid] /= denominator[gid]; +} + +/*********** class implementation *********/ + +ExtractArrayFromExitWave::ExtractArrayFromExitWave() + : CudaFunction("extract_array_from_exit_wave") + +{ +} + +void ExtractArrayFromExitWave::setParameters( + int A, int B, int C, int D, int E, int F, int G, int H, int I) +{ + A_ = A; + B_ = B; + C_ = C; + D_ = D; + E_ = E; + F_ = F; + G_ = G; + H_ = H; + I_ = I; +} + +void ExtractArrayFromExitWave::setDeviceBuffers( + complex* d_exit_wave, + int* d_exit_addr, + complex* d_array_to_be_extracted, + int* d_extract_addr, + complex* d_array_to_be_updated, + int* d_update_addr, + float* d_weights, + complex* d_cfact, + complex* d_denominator) +{ + d_exit_wave_ = d_exit_wave; + d_exit_addr_ = d_exit_addr; + d_array_to_be_extracted_ = d_array_to_be_extracted; + d_extract_addr_ = d_extract_addr; + d_array_to_be_updated_ = d_array_to_be_updated; + d_update_addr_ = d_update_addr; + d_weights_ = d_weights; + d_cfact_ = d_cfact; + d_denominator_ = d_denominator; +} + +void ExtractArrayFromExitWave::allocate() +{ + ScopedTimer t(this, "allocate"); + + d_exit_wave_.allocate(A_ * B_ * C_); + d_exit_addr_.allocate(A_ * addr_stride_); + d_array_to_be_extracted_.allocate(D_ * E_ * F_); + d_extract_addr_.allocate(A_ * addr_stride_); + d_array_to_be_updated_.allocate(G_ * H_ * I_); + d_update_addr_.allocate(A_ * addr_stride_); + d_weights_.allocate(G_); + d_cfact_.allocate(G_ * H_ * I_); + d_denominator_.allocate(G_ * H_ * I_); +} + +void ExtractArrayFromExitWave::transfer_in( + const complex* exit_wave, + const int* exit_addr, + const complex* array_to_be_extracted, + const int* extract_addr, + const complex* array_to_be_updated, + const int* update_addr, + const float* weights, + const complex* cfact) +{ + ScopedTimer t(this, "transfer in"); + + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, A_ * B_ * C_); + gpu_memcpy_h2d(d_exit_addr_.get(), exit_addr, A_ * addr_stride_); + gpu_memcpy_h2d( + d_array_to_be_extracted_.get(), array_to_be_extracted, D_ * E_ * F_); + gpu_memcpy_h2d(d_extract_addr_.get(), extract_addr, A_ * addr_stride_); + gpu_memcpy_h2d( + d_array_to_be_updated_.get(), array_to_be_updated, G_ * H_ * I_); + gpu_memcpy_h2d(d_update_addr_.get(), update_addr, A_ * addr_stride_); + gpu_memcpy_h2d(d_weights_.get(), weights, G_); + gpu_memcpy_h2d(d_cfact_.get(), cfact, G_ * H_ * I_); +} + +void ExtractArrayFromExitWave::run() +{ + ScopedTimer t(this, "run"); + + gpu_memcpy_d2d(d_denominator_.get(), d_cfact_.get(), G_ * H_ * I_); + + // we used one block per updateidx + dim3 threadsPerBlock = {32u, 32u, 1u}; + dim3 blocks = {unsigned(A_), 1u, 1u}; + extract_array_from_exit_wave_kernel<32, 32> + <<>>(d_exit_wave_.get(), + A_, + B_, + C_, + d_exit_addr_.get(), + d_array_to_be_extracted_.get(), + D_, + E_, + F_, + d_extract_addr_.get(), + d_array_to_be_updated_.get(), + G_, + H_, + I_, + d_update_addr_.get(), + d_weights_.get(), + d_denominator_.get(), + addr_stride_); + checkLaunchErrors(); + + int total = G_ * H_ * I_; + int blocks2 = (total + 255) / 256; + div_by_denominator<256><<>>( + d_array_to_be_updated_.get(), d_denominator_.get(), total); + + checkLaunchErrors(); + timing_sync(); +} + +void ExtractArrayFromExitWave::transfer_out(complex* array_to_be_updated) +{ + ScopedTimer t(this, "transfer out"); + + gpu_memcpy_d2h( + array_to_be_updated, d_array_to_be_updated_.get(), G_ * H_ * I_); +} + +/************ interface *******************/ + +extern "C" void extract_array_from_exit_wave_c( + const float* f_exit_wave, // complex + int A, + int B, + int C, + const int* exit_addr, // A x 3 - int + const float* f_array_to_be_extracted, // complex + int D, + int E, + int F, + const int* extract_addr, // A x 3 - int + float* f_array_to_be_updated, // complex + int G, + int H, + int I, + const int* update_addr, // A x 3 - int + const float* weights, // G - real + const float* f_cfact // G, H, I - complex +) +{ + auto exit_wave = reinterpret_cast*>(f_exit_wave); + auto array_to_be_extracted = + reinterpret_cast*>(f_array_to_be_extracted); + auto array_to_be_updated = + reinterpret_cast*>(f_array_to_be_updated); + auto cfact = reinterpret_cast*>(f_cfact); + + auto ex = gpuManager.get_cuda_function( + "extract_array_from_exit_wave", A, B, C, D, E, F, G, H, I); + ex->allocate(); + ex->transfer_in(exit_wave, + exit_addr, + array_to_be_extracted, + extract_addr, + array_to_be_updated, + update_addr, + weights, + cfact); + ex->run(); + ex->transfer_out(array_to_be_updated); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/extract_array_from_exit_wave.h b/archive/cuda_extension/cuda/func/extract_array_from_exit_wave.h new file mode 100644 index 000000000..c5c24f7cf --- /dev/null +++ b/archive/cuda_extension/cuda/func/extract_array_from_exit_wave.h @@ -0,0 +1,46 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class ExtractArrayFromExitWave : public CudaFunction +{ +public: + ExtractArrayFromExitWave(); + void setParameters( + int A, int B, int C, int D, int E, int F, int G, int H, int I); + void setDeviceBuffers(complex* d_exit_wave, + int* d_exit_addr, + complex* d_array_to_be_extracted, + int* d_extract_addr, + complex* d_array_to_be_updated, + int* d_update_addr, + float* d_weights, + complex* d_cfact, + complex* d_denominator); + void setAddrStride(int stride) { addr_stride_ = stride; } + void allocate(); + void transfer_in(const complex* exit_wave, + const int* exit_addr, + const complex* array_to_be_extracted, + const int* extract_addr, + const complex* array_to_be_updated, + const int* update_addr, + const float* weights, + const complex* cfact); + void run(); + void transfer_out(complex* array_to_be_updated); + +private: + DevicePtrWrapper> d_exit_wave_; // A x B x C + DevicePtrWrapper d_exit_addr_; // A x 3 + DevicePtrWrapper> d_array_to_be_extracted_; // D x E x F + DevicePtrWrapper d_extract_addr_; // A x 3 + DevicePtrWrapper> d_array_to_be_updated_; // G x H x I + DevicePtrWrapper d_update_addr_; // A x 3 + DevicePtrWrapper d_weights_; // G + DevicePtrWrapper> d_cfact_; // G x H x I + DevicePtrWrapper> d_denominator_; // G x H x I + int A_ = 0, B_ = 0, C_ = 0, D_ = 0, E_ = 0, F_ = 0, G_ = 0, H_ = 0, I_ = 0; + int addr_stride_ = 3; // default is 3, but if full addr_info is used, it's 15 +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/far_field_error.cu b/archive/cuda_extension/cuda/func/far_field_error.cu new file mode 100644 index 000000000..9fca007f6 --- /dev/null +++ b/archive/cuda_extension/cuda/func/far_field_error.cu @@ -0,0 +1,154 @@ +#include "far_field_error.h" + +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +/************* Kernels **********************/ + +template +__global__ void far_field_error_kernel(const float *current, + const float *measured, + const unsigned char *mask, + float *out, + int m, + int n) +{ + int batch = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + + extern __shared__ float sum_v[]; + auto sum_mask = (int *)(sum_v + BlockX * BlockY); + auto shidx = tx * BlockY + ty; + sum_v[shidx] = 0.0; + sum_mask[shidx] = 0.0; + + auto offset = batch * m * n; +#pragma unroll(2) + for (int i = tx; i < m; i += BlockX) + { +#pragma unroll(1) + for (int j = ty; j < n; j += BlockY) + { + auto idx = offset + i * n + j; + if (mask[idx]) + { + auto fdev = current[idx] - measured[idx]; + auto fdev2 = fdev * fdev; + sum_v[shidx] += fdev2; + sum_mask[shidx] += 1; + } + } + } + + // now sum up the data in shared memory, tree type reduction + __syncthreads(); + int nt = BlockX * BlockY; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (shidx < half) + { + sum_v[shidx] += sum_v[c - shidx - 1]; + sum_mask[shidx] += sum_mask[c - shidx - 1]; + } + __syncthreads(); + c = c - half; + } + + if (shidx == 0) + { + out[batch] = sum_v[0] / float(sum_mask[0]); + } +} + +/************* class implementation ********************/ + +FarFieldError::FarFieldError() : CudaFunction("far_field_error") {} + +void FarFieldError::setParameters(int i, int m, int n) +{ + i_ = i; + m_ = m; + n_ = n; +} + +void FarFieldError::setDeviceBuffers(float *d_current, + float *d_measured, + unsigned char *d_mask, + float *d_out) +{ + d_current_ = d_current; + d_measured_ = d_measured; + d_mask_ = d_mask; + d_out_ = d_out; +} + +void FarFieldError::allocate() +{ + ScopedTimer t(this, "allocate"); + d_current_.allocate(i_ * m_ * n_); + d_measured_.allocate(i_ * m_ * n_); + d_mask_.allocate(i_ * m_ * n_); + d_out_.allocate(i_); +} + +void FarFieldError::updateErrorOutput(float *d_out) { d_out_ = d_out; } + +float *FarFieldError::getOutput() const { return d_out_.get(); } + +void FarFieldError::transfer_in(const float *current, + const float *measured, + const unsigned char *mask) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_current_.get(), current, i_ * m_ * n_); + gpu_memcpy_h2d(d_measured_.get(), measured, i_ * m_ * n_); + gpu_memcpy_h2d(d_mask_.get(), mask, i_ * m_ * n_); +} + +void FarFieldError::run() +{ + ScopedTimer t(this, "run"); + + // always use a 32x32 block of threads + dim3 threadsPerBlock = {32u, 32u, 1u}; + dim3 blocks = {unsigned(i_), 1u, 1u}; + + far_field_error_kernel<32, 32> + <<>>( + d_current_.get(), + d_measured_.get(), + d_mask_.get(), + d_out_.get(), + m_, + n_); + checkLaunchErrors(); + + timing_sync(); +} + +void FarFieldError::transfer_out(float *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), i_); +} + +/************* interface function **********************/ + +extern "C" void far_field_error_c(const float *current, + const float *measured, + const unsigned char *mask, + float *out, + int i, + int m, + int n) +{ + auto ffe = + gpuManager.get_cuda_function("farfield_error", i, m, n); + ffe->allocate(); + ffe->transfer_in(current, measured, mask); + ffe->run(); + ffe->transfer_out(out); +} diff --git a/archive/cuda_extension/cuda/func/far_field_error.h b/archive/cuda_extension/cuda/func/far_field_error.h new file mode 100644 index 000000000..c7f332a32 --- /dev/null +++ b/archive/cuda_extension/cuda/func/far_field_error.h @@ -0,0 +1,29 @@ +#pragma once +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class FarFieldError : public CudaFunction +{ +public: + FarFieldError(); + void setParameters(int i, int m, int n); + void setDeviceBuffers(float *d_current, + float *d_measured, + unsigned char *d_mask, + float *d_out); + void allocate(); + void updateErrorOutput(float *d_out); + float *getOutput() const; + void transfer_in(const float *current, + const float *measured, + const unsigned char *mask); + void run(); + void transfer_out(float *out); + +private: + int i_ = 0, m_ = 0, n_ = 0; + DevicePtrWrapper d_mask_; + DevicePtrWrapper d_current_; + DevicePtrWrapper d_measured_; + DevicePtrWrapper d_out_; +}; diff --git a/archive/cuda_extension/cuda/func/farfield_propagator.cu b/archive/cuda_extension/cuda/func/farfield_propagator.cu new file mode 100644 index 000000000..2c9fa6cc8 --- /dev/null +++ b/archive/cuda_extension/cuda/func/farfield_propagator.cu @@ -0,0 +1,231 @@ +#include "farfield_propagator.h" + +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include + +/**************** Kernels ***************/ + +// can't do in-place modification of inputs +__global__ void applyPrefilter(const complex *datain, + complex *dataout, + const complex *__restrict__ filter, + size_t batchsize, + size_t size) +{ + size_t offset = threadIdx.x + blockIdx.x * blockDim.x; + if (offset >= batchsize * size) + return; + dataout[offset] = datain[offset] * filter[offset % size]; +} + +// this can always be done in-place +__global__ void applyPostfilter(complex *data, + const complex *__restrict__ filter, + float sc, + size_t batchsize, + size_t size) +{ + size_t offset = threadIdx.x + blockIdx.x * blockDim.x; + size_t total = batchsize * size; + if (offset >= total) + return; + auto val = data[offset]; + if (filter) + { + val = filter[offset % size] * val; + } + val *= sc; + data[offset] = val; +} + +/*************** Class implementation ********************/ + +cufftHandle FFTPlanManager::get_or_create_plan(int i, int m, int n) +{ + key_t key{i, m, n}; + if (plans_.find(key) == plans_.end()) + { + cufftHandle plan; + checkCudaErrors(cufftCreate(&plan)); + plans_[key] = plan; + + int dims[] = {m, n}; + size_t workSize; + checkCudaErrors(cufftMakePlanMany( + plan, 2, dims, 0, 0, 0, 0, 0, 0, CUFFT_C2C, i, &workSize)); +#ifndef NDEBUG + debug_addMemory((void *)long(plan), workSize); + std::cout << "Made FFT Plan for " << m << "x" << n << ", batch=" << i + << std::endl; + std::cout << "Allocated " << (void *)long(plan) + << ", total: " << double(debug_getMemory()) << std::endl; +#endif + } + + return plans_[key]; +} + +void FFTPlanManager::clearCache() +{ + for (auto &item : plans_) + { + cufftDestroy(item.second); +#ifndef NDEBUG + std::cout << "Freeing for FFT plan " << (void *)long(item.second) + << std::endl; + debug_freeMemory((void *)long(item.second)); + std::cout << "Total allocated: " << double(debug_getMemory()) << std::endl; +#endif + } + plans_.clear(); +} + +FFTPlanManager::~FFTPlanManager() { clearCache(); } + +/******************************/ + +FFTPlanManager FarfieldPropagator::planManager_; + +FarfieldPropagator::FarfieldPropagator() : CudaFunction("farfield_propagator") +{ +} + +void FarfieldPropagator::setParameters(size_t batch_size, size_t m, size_t n) +{ + batch_size_ = batch_size; + m_ = m; + n_ = n; + sc_ = + 1.0f / std::sqrt(float(m * n)); // with cuFFT, we need to scale both ways +} + +void FarfieldPropagator::setDeviceBuffers(complex *d_datain, + complex *d_dataout, + complex *d_prefilter, + complex *d_postfilter) +{ + d_datain_ = d_datain; + d_dataout_ = d_dataout; + d_pre_ = d_prefilter; + d_post_ = d_postfilter; +} + +void FarfieldPropagator::allocate() +{ + { + ScopedTimer t(this, "allocate"); + + d_datain_.allocate(batch_size_ * m_ * n_); + d_dataout_.allocate(batch_size_ * m_ * n_); + d_pre_.allocate(m_ * n_); + d_post_.allocate(m_ * n_); + } + + { + ScopedTimer t(this, "plan create"); + plan_ = FarfieldPropagator::planManager_.get_or_create_plan( + batch_size_, m_, n_); + } +} + +void FarfieldPropagator::transfer_in( + const complex *data_to_be_transformed, + const complex *prefilter, + const complex *postfilter) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d( + d_datain_.get(), data_to_be_transformed, batch_size_ * m_ * n_); + gpu_memcpy_h2d(d_pre_.get(), prefilter, m_ * n_); + gpu_memcpy_h2d(d_post_.get(), postfilter, m_ * n_); +} + +void FarfieldPropagator::transfer_out(complex *out) +{ + ScopedTimer t(this, "transfer out"); + if (out) + { + gpu_memcpy_d2h(out, d_dataout_.get(), batch_size_ * m_ * n_); + } +} + +void FarfieldPropagator::run(bool doPreFilter, + bool doPostFilter, + bool isForward) +{ + ScopedTimer t(this, "run"); + + size_t block = 256; + size_t total = batch_size_ * m_ * n_; + size_t blocks = (total + block - 1) / block; + auto indata = d_datain_.get(); + if (doPreFilter) + { + applyPrefilter<<>>( + d_datain_.get(), d_dataout_.get(), d_pre_.get(), batch_size_, m_ * n_); + checkLaunchErrors(); + indata = d_dataout_.get(); + } + + if (isForward) + { + checkCudaErrors( + cufftExecC2C(plan_, + reinterpret_cast(indata), + reinterpret_cast(d_dataout_.get()), + CUFFT_FORWARD)); + } + else + { + checkCudaErrors( + cufftExecC2C(plan_, + reinterpret_cast(indata), + reinterpret_cast(d_dataout_.get()), + CUFFT_INVERSE)); + } + + if (doPostFilter) + { + applyPostfilter<<>>( + d_dataout_.get(), d_post_.get(), sc_, batch_size_, m_ * n_); + checkLaunchErrors(); + } + else + { + applyPostfilter<<>>( + d_dataout_.get(), nullptr, sc_, batch_size_, m_ * n_); + checkLaunchErrors(); + } + + // sync device if timing is enabled + timing_sync(); +} + +/************* Interface function ************/ + +extern "C" void farfield_propagator_c(const float *fdata_to_be_transformed, + const float *fprefilter, + const float *fpostfilter, + float *fout, + int b, + int m, + int n, + int iisForward) +{ + auto data_to_be_transformed = + reinterpret_cast *>(fdata_to_be_transformed); + // pre- and post-filter are 2D, applied in every batch item + auto prefilter = reinterpret_cast *>(fprefilter); + auto postfilter = reinterpret_cast *>(fpostfilter); + auto out = reinterpret_cast *>(fout); + auto isForward = iisForward != 0; + + auto prop = gpuManager.get_cuda_function( + "farfield_propagator", b, m, n); + prop->allocate(); + prop->transfer_in(data_to_be_transformed, prefilter, postfilter); + prop->run(prefilter != nullptr, postfilter != nullptr, isForward); + prop->transfer_out(out); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/farfield_propagator.h b/archive/cuda_extension/cuda/func/farfield_propagator.h new file mode 100644 index 000000000..6e1e3fa47 --- /dev/null +++ b/archive/cuda_extension/cuda/func/farfield_propagator.h @@ -0,0 +1,83 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include +#include +#include + +/** Class to manage FFT plans - re-using plans for same + * dimensions. + * + * Intended as static member of farfield_propagator + * + * Note that plans will stay allocated for the duration of the process. + * To avoid that, this class could be included into the GpuManager, and + * cleared together with the resetFunctionCache method. + */ +class FFTPlanManager +{ +public: + cufftHandle get_or_create_plan(int i, int m, int n); + void clearCache(); + ~FFTPlanManager(); + +private: + /// map key type + struct key_t + { + int i, m, n; + }; + + struct comp_t + { + bool operator()(const key_t &a, const key_t &b) const + { + if (a.i != b.i) + return a.i < b.i; + if (a.m != b.m) + return a.m < b.m; + return a.n < b.n; + } + }; + + /// map type + typedef std::map map_t; + + /// our stored plans + map_t plans_; +}; + +class FarfieldPropagator : public CudaFunction +{ +public: + FarfieldPropagator(); + void setParameters(size_t batch_size, size_t m, size_t n); + + // for setting external memory to be used + // (can be null if internal should be used) + void setDeviceBuffers(complex *d_datain, + complex *d_dataout, + complex *d_prefilter, + complex *d_postfilter); + + void allocate(); + void transfer_in(const complex *data_to_be_transformed, + const complex *prefilter, + const complex *postfilter); + void transfer_out(complex *out); + void run(bool doPreFilter, bool doPostFilter, bool isForward); + static void clearPlanCache() { planManager_.clearCache(); } + +private: + size_t batch_size_ = 0, m_ = 0, n_ = 0; + float sc_ = 1.0f; + DevicePtrWrapper> d_datain_; + DevicePtrWrapper> d_dataout_; + DevicePtrWrapper> d_pre_; + DevicePtrWrapper> d_post_; + cufftHandle plan_ = 0; + static FFTPlanManager planManager_; +}; diff --git a/archive/cuda_extension/cuda/func/get_difference.cu b/archive/cuda_extension/cuda/func/get_difference.cu new file mode 100644 index 000000000..715eb6dca --- /dev/null +++ b/archive/cuda_extension/cuda/func/get_difference.cu @@ -0,0 +1,190 @@ +#include "get_difference.h" + +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +/************* kernels ******************************/ + +template +__global__ void get_difference_kernel( + const int *addr_info, + float alpha, + const complex *backpropagated_solution, + const float *err_fmag, + const complex *exit_wave, + float pbound, + const complex *probe_obj, + complex *out, + int m, + int n) +{ + int batch = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + + // each of these are 3-d arrays with indices + auto ea = addr_info + batch * 3 * 5 + 6; + auto da = ea + 3; + + // these are the start indices for the batch item + auto offset = ea[0] * m * n; + + auto da_0 = da[0]; + +#pragma unroll(2) + for (int i = tx; i < m; i += BlockX) + { +#pragma unroll(1) // to make sure the compiler doesn't unroll + for (int j = ty; j < n; j += BlockY) + { + auto outidx = offset + i * n + j; + if (!usePbound || err_fmag[da_0] > pbound) + { + out[outidx] = backpropagated_solution[outidx] - probe_obj[outidx]; + } + else + { + out[outidx] = alpha * (probe_obj[outidx] - exit_wave[outidx]); + } + } + } +} + +/************* class implementation *****************/ + +GetDifference::GetDifference() : CudaFunction("get_difference") {} + +void GetDifference::setParameters(int i, int m, int n) +{ + i_ = i; + m_ = m; + n_ = n; +} + +void GetDifference::setDeviceBuffers(int *d_addr_info, + complex *d_backpropagated_solution, + float *d_err_fmag, + complex *d_exit_wave, + complex *d_probe_obj, + complex *d_out) +{ + d_addr_info_ = d_addr_info; + d_backpropagated_solution_ = d_backpropagated_solution; + d_err_fmag_ = d_err_fmag; + d_exit_wave_ = d_exit_wave; + d_probe_obj_ = d_probe_obj; + d_out_ = d_out; +} +void GetDifference::allocate() +{ + ScopedTimer t(this, "allocate"); + d_addr_info_.allocate(i_ * 5 * 3); + d_backpropagated_solution_.allocate(i_ * m_ * n_); + d_err_fmag_.allocate(i_); + d_exit_wave_.allocate(i_ * m_ * n_); + d_probe_obj_.allocate(i_ * m_ * n_); + d_out_.allocate(i_ * m_ * n_); +} + +void GetDifference::updateErrorInput(float *d_err_fmag) +{ + d_err_fmag_ = d_err_fmag; +} + +complex *GetDifference::getOutput() const { return d_out_.get(); } + +void GetDifference::transfer_in(const int *addr_info, + const complex *backpropagated_solution, + const float *err_fmag, + const complex *exit_wave, + const complex *probe_obj) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, i_ * 5 * 3); + gpu_memcpy_h2d( + d_backpropagated_solution_.get(), backpropagated_solution, i_ * m_ * n_); + gpu_memcpy_h2d(d_err_fmag_.get(), err_fmag, i_); + gpu_memcpy_h2d(d_exit_wave_.get(), exit_wave, i_ * m_ * n_); + gpu_memcpy_h2d(d_probe_obj_.get(), probe_obj, i_ * m_ * n_); +} + +void GetDifference::run(float alpha, float pbound, bool usePbound) +{ + ScopedTimer t(this, "run"); + + // TODO: is this really needed? + checkCudaErrors( + cudaMemset(d_out_.get(), 0, i_ * m_ * n_ * sizeof(*d_out_.get()))); + + // always use a 32x32 block of threads + dim3 threadsPerBlock = {32u, 32u, 1u}; + dim3 blocks = {unsigned(i_), 1u, 1u}; + if (usePbound) + { + get_difference_kernel + <<>>(d_addr_info_.get(), + alpha, + d_backpropagated_solution_.get(), + d_err_fmag_.get(), + d_exit_wave_.get(), + pbound, + d_probe_obj_.get(), + d_out_.get(), + m_, + n_); + checkLaunchErrors(); + } + else + { + get_difference_kernel + <<>>(d_addr_info_.get(), + alpha, + d_backpropagated_solution_.get(), + d_err_fmag_.get(), + d_exit_wave_.get(), + pbound, + d_probe_obj_.get(), + d_out_.get(), + m_, + n_); + checkLaunchErrors(); + } + + timing_sync(); +} + +void GetDifference::transfer_out(complex *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), i_ * m_ * n_); +} + +/************* interface function *******************/ + +extern "C" void get_difference_c(const int *addr_info, + float alpha, + const float *fbackpropagated_solution, + const float *err_fmag, + const float *fexit_wave, + float pbound, + const float *fprobe_obj, + float *fout, + int i, + int m, + int n, + int usePbound) +{ + auto backpropagated_solution = + reinterpret_cast *>(fbackpropagated_solution); + auto exit_wave = reinterpret_cast *>(fexit_wave); + auto probe_obj = reinterpret_cast *>(fprobe_obj); + auto out = reinterpret_cast *>(fout); + + auto gd = + gpuManager.get_cuda_function("get_difference", i, m, n); + gd->allocate(); + gd->transfer_in( + addr_info, backpropagated_solution, err_fmag, exit_wave, probe_obj); + gd->run(alpha, pbound, usePbound != 0); + gd->transfer_out(out); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/get_difference.h b/archive/cuda_extension/cuda/func/get_difference.h new file mode 100644 index 000000000..1dede5750 --- /dev/null +++ b/archive/cuda_extension/cuda/func/get_difference.h @@ -0,0 +1,36 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class GetDifference : public CudaFunction +{ +public: + GetDifference(); + void setParameters(int i, int m, int n); + void setDeviceBuffers(int *d_addr_info, + complex *d_backpropagated_solution, + float *d_err_fmag, + complex *d_exit_wave, + complex *d_probe_obj, + complex *d_out); + void allocate(); + void updateErrorInput(float *d_err_fmag); + complex *getOutput() const; + void transfer_in(const int *addr_info, + const complex *backpropagated_solution, + const float *err_fmag, + const complex *exit_wave, + const complex *probe_obj); + void run(float alpha, float pbound = 0.0f, bool usePbound = false); + void transfer_out(complex *out); + +private: + DevicePtrWrapper d_addr_info_; + DevicePtrWrapper> d_backpropagated_solution_; + DevicePtrWrapper d_err_fmag_; + DevicePtrWrapper> d_exit_wave_; + DevicePtrWrapper> d_probe_obj_; + DevicePtrWrapper> d_out_; + int i_ = 0, m_ = 0, n_ = 0; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/interpolated_shift.cu b/archive/cuda_extension/cuda/func/interpolated_shift.cu new file mode 100644 index 000000000..212420849 --- /dev/null +++ b/archive/cuda_extension/cuda/func/interpolated_shift.cu @@ -0,0 +1,428 @@ +#include "interpolated_shift.h" + +#include "splines/bspline_kernel.cuh" +#include "splines/cubicPrefilter2D.cuh" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include +#include +#include +#include +#include + +/********* kernels ******************/ + +template +__global__ void integer_shift_kernel(const complex* in, + complex* out, + int rows, + int columns, + int rowOffset, + int colOffset) +{ + int tx = threadIdx.x + blockIdx.x * BlockX; + int ty = threadIdx.y + blockIdx.y * BlockY; + if (tx >= rows || ty >= columns) + return; + + int item = blockIdx.z; + in += item * rows * columns; + out += item * rows * columns; + + int gid_old = tx * columns + ty; + assert(gid_old < columns * rows); + assert(gid_old >= 0); + + auto val = in[gid_old]; + + int gid_new_x = tx + rowOffset; + int gid_new_y = ty + colOffset; + + // write zero on the other end + while (gid_new_x >= rows) + { + val = complex(); + gid_new_x -= rows; + } + while (gid_new_x < 0) + { + val = complex(); + gid_new_x += rows; + } + while (gid_new_y >= columns) + { + val = complex(); + gid_new_y -= columns; + } + while (gid_new_y < 0) + { + val = complex(); + gid_new_y += columns; + } + // do we need to do something with the corners? + + int gid_new = gid_new_x * columns + gid_new_y; + assert(gid_new < rows * columns); + assert(gid_new >= 0); + + out[gid_new] = val; +} + +__device__ inline complex& ascomplex(float2& f2) +{ + return reinterpret_cast&>(f2); +} + +__device__ inline void calcWeights(float* weights, float fraction) +{ + if (fraction < 0.0) + { + weights[2] = -fraction; + weights[1] = 1.0f + fraction; + weights[0] = 0.0f; + } + else + { + weights[2] = 0.0f; + weights[1] = 1.0f - fraction; + weights[0] = fraction; + } +} + +template +__global__ void linear_interpolate_kernel(const complex* in, + complex* out, + int rows, + int columns, + float offsetRow, + float offsetColumn) +{ + int offsetRowInt = int(offsetRow); + int offsetColInt = int(offsetColumn); + float offsetRowFrac = offsetRow - offsetRowInt; // positive or negative + float offsetColFrac = offsetColumn - offsetColInt; + + // calculate convolutional weights + float wx[3]; + calcWeights(wx, offsetRowFrac); + float wy[3]; + calcWeights(wy, offsetColFrac); + + // indices + int tx = threadIdx.x; + int ty = threadIdx.y; + int bx = blockIdx.x; + int by = blockIdx.y; + int gx = tx + bx * BlockX; + int gy = ty + by * BlockY; + int gx_old = gx - offsetRowInt; + int gy_old = gy - offsetColInt; + + // items index is blockIdx.z + // we just advance the data + int item = blockIdx.z; + in += item * rows * columns; + out += item * rows * columns; + + __shared__ float2 shr[BlockX + 2][BlockY + 2]; + + // read top Halo + if (tx == 0) + { + if (gx_old - 1 >= 0 && gx_old - 1 < rows && gy_old >= 0 && gy_old < columns) + { + ascomplex(shr[0][ty + 1]) = in[(gx_old - 1) * columns + gy_old]; + } + else + { + ascomplex(shr[0][ty + 1]) = complex(); + } + } + // read bottom Halo + if (tx == BlockX - 1) + { + if (gx_old + 1 >= 0 && gx_old + 1 < rows && gy_old >= 0 && gy_old < columns) + { + ascomplex(shr[BlockX + 1][ty + 1]) = in[(gx_old + 1) * columns + gy_old]; + } + else + { + ascomplex(shr[BlockX + 1][ty + 1]) = complex(); + } + } + // read left Halo + if (ty == 0) + { + if (gx_old >= 0 && gx_old < rows && gy_old - 1 >= 0 && gy_old - 1 < columns) + { + ascomplex(shr[tx + 1][0]) = in[gx_old * columns + gy_old - 1]; + } + else + { + ascomplex(shr[tx + 1][0]) = complex(); + } + } + // read right Halo + if (ty == BlockY - 1) + { + if (gx_old >= 0 && gx_old < rows && gy_old + 1 >= 0 && gy_old + 1 < columns) + { + ascomplex(shr[tx + 1][BlockY + 1]) = in[gx_old * columns + gy_old + 1]; + } + else + { + ascomplex(shr[tx + 1][BlockY + 1]) = complex(); + } + } + // read the rest + if (gx_old >= 0 && gx_old < rows && gy_old >= 0 && gy_old < columns) + { + ascomplex(shr[tx + 1][ty + 1]) = in[gx_old * columns + gy_old]; + } + else + { + ascomplex(shr[tx + 1][ty + 1]) = complex(); + } + + // now we have a block + halos in shared memory - do the interpolation + __syncthreads(); + + // interpolate rows in x + __shared__ float2 shry[BlockX][BlockY + 2]; + + ascomplex(shry[tx][ty + 1]) = wx[0] * ascomplex(shr[tx][ty + 1]) + + wx[1] * ascomplex(shr[tx + 1][ty + 1]) + + wx[2] * ascomplex(shr[tx + 2][ty + 1]); + if (ty == 0) + { + ascomplex(shry[tx][0]) = wx[0] * ascomplex(shr[tx][0]) + + wx[1] * ascomplex(shr[tx + 1][0]) + + wx[2] * ascomplex(shr[tx + 2][0]); + } + if (ty == BlockY - 1) + { + ascomplex(shry[tx][BlockY + 1]) = + wx[0] * ascomplex(shr[tx][BlockY + 1]) + + wx[1] * ascomplex(shr[tx + 1][BlockY + 1]) + + wx[2] * ascomplex(shr[tx + 2][BlockY + 1]); + } + + __syncthreads(); + + if (gx >= columns || gy >= rows) + { + return; + } + + auto intv = wy[0] * ascomplex(shry[tx][ty]) + + wy[1] * ascomplex(shry[tx][ty + 1]) + + wy[2] * ascomplex(shry[tx][ty + 2]); + + // write back + + // if the point lies outside of the original frame and we're shifting in + // that direction, it gets a zero value in any case + // otherwise we take the interpolated value + bool rightzero = offsetColFrac < 0.0f; + bool leftzero = offsetColFrac > 0.0f; + bool topzero = offsetRowFrac > 0.0f; + bool bottomzero = offsetRowFrac < 0.0f; + if ((gx_old == 0 && topzero) || (gx_old == rows - 1 && bottomzero) || + (gy_old == 0 && leftzero) || (gy_old == columns - 1 && rightzero)) + { + out[gx * columns + gy] = complex(); + } + else + { + out[gx * columns + gy] = intv; + } +} + +/** this kernel is not working and shouldn't be used. It's also not optimised at + * all. */ +template +__global__ void spline_interpolate_x(const complex* in, + complex* out, + int rows, + int columns, + float offsetRowFrac, + float offsetColFrac) +{ + int tx = threadIdx.x + blockIdx.x * BlockX; + int ty = threadIdx.y + blockIdx.y * BlockY; + if (tx >= rows || ty >= columns) + return; + + float w0, w1, w2, w3; + bspline_weights(offsetRowFrac, w0, w1, w2, w3); + int gid = tx * columns + ty; + + auto x_0 = tx > 1 ? in[gid - 2 * columns] : complex(); + auto x_1 = tx > 0 ? in[gid - columns] : complex(); + auto x_2 = in[gid]; + auto x_3 = tx < rows - 1 ? in[gid + 1 * columns] : complex(); + + out[gid] = w3 * x_0 + w2 * x_1 + w1 * x_2 + w0 * x_3; +} + +/** this kernel is not working and shouldn't be used. It's also not optimised at + * all. */ +template +__global__ void spline_interpolate_y(const complex* in, + complex* out, + int rows, + int columns, + float offsetRowFrac, + float offsetColFrac) +{ + int tx = threadIdx.x + blockIdx.x * BlockX; + int ty = threadIdx.y + blockIdx.y * BlockY; + if (tx >= rows || ty >= columns) + return; + + float w0, w1, w2, w3; + bspline_weights(offsetColFrac, w0, w1, w2, w3); + + int gid = tx * columns + ty; + + auto y_0 = ty > 1 ? in[gid - 2] : complex(); + auto y_1 = ty > 0 ? in[gid - 1] : complex(); + auto y_2 = in[gid]; + auto y_3 = ty < columns - 1 ? in[gid + 1] : complex(); + + out[gid] = w3 * y_0 + w2 * y_1 + w1 * y_2 + w0 * y_3; +} + +/********* class implementation *******/ + +InterpolatedShift::InterpolatedShift() : CudaFunction("interpolated_shift") {} + +void InterpolatedShift::setParameters(int items, int rows, int columns) +{ + items_ = items; + rows_ = rows; + columns_ = columns; +} + +void InterpolatedShift::setDeviceBuffers(complex* d_in, + complex* d_out) +{ + d_in_ = d_in; + d_out_ = d_out; +} + +void InterpolatedShift::allocate() +{ + ScopedTimer t(this, "allocate"); + d_in_.allocate(items_ * rows_ * columns_); + d_out_.allocate(items_ * rows_ * columns_); +} + +void InterpolatedShift::transfer_in(const complex* in) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_in_.get(), in, items_ * rows_ * columns_); +} + +void InterpolatedShift::run(float offsetRow, float offsetColumn, bool do_linear) +{ + ScopedTimer t(this, "run"); + + dim3 threadsPerBlock = {32u, 32u, 1u}; + dim3 blocks = {unsigned((rows_ + 31) / 32), + unsigned((columns_ + 31) / 32), + unsigned(items_)}; + + // get fractional and integer parts + auto offsetRowInt = int(offsetRow); + auto offsetColInt = int(offsetColumn); + auto offsetRowFrac = offsetRow - int(offsetRow); + auto offsetColFrac = offsetColumn - int(offsetColumn); + + if (std::abs(offsetRowFrac) < 1e-6f && std::abs(offsetColFrac) < 1e-6f) + { + if (offsetRowInt == 0 && offsetColInt == 0) + { + // no transformation at all + gpu_memcpy_d2d(d_out_.get(), d_in_.get(), items_ * rows_ * columns_); + } + else + { + // no fractional part, so we can just use a shifted copy + integer_shift_kernel<32, 32> + <<>>(d_in_.get(), + d_out_.get(), + rows_, + columns_, + int(offsetRow), + int(offsetColumn)); + checkLaunchErrors(); + } + } + else + { + if (do_linear) + { + linear_interpolate_kernel<32, 32><<>>( + d_in_.get(), d_out_.get(), rows_, columns_, offsetRow, offsetColumn); + checkLaunchErrors(); + } + else + { + // bicubic + // this version has not been adapted for 3D yet + + // first, prefilter the data for cubic splines + // Note: this prefilter does not match the scipy version completely + // !!! IMPORTANT: this modifies the data in-place + CubicBSplinePrefilter2D( + d_in_.get(), columns_ * sizeof(complex), columns_, rows_); + checkLaunchErrors(); + + // then interpolate in x and y directions + // Note: these kernels are not working yet + // and the buffers should be modified to avoid the final copy and to + // avoid in-place operations as inputs might be re-used (?) + spline_interpolate_y<32, 32><<>>( + d_in_.get(), d_out_.get(), rows_, columns_, offsetRow, offsetColumn); + checkLaunchErrors(); + spline_interpolate_x<32, 32><<>>( + d_out_.get(), d_in_.get(), rows_, columns_, offsetRow, offsetColumn); + checkLaunchErrors(); + + // make sure result is in d_out_ + gpu_memcpy_d2d(d_in_.get(), d_out_.get(), rows_ * columns_); + } + } + + timing_sync(); +} + +void InterpolatedShift::transfer_out(complex* out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), items_ * rows_ * columns_); +} + +/********* interface ************/ + +extern "C" void interpolated_shift_c(const float* f_in, + float* f_out, + int items, + int rows, + int columns, + float offsetRow, + float offsetCol, + int ido_linear) +{ + auto in = reinterpret_cast*>(f_in); + auto out = reinterpret_cast*>(f_out); + + auto is = gpuManager.get_cuda_function( + "interpolated_shift", items, rows, columns); + is->allocate(); + is->transfer_in(in); + is->run(offsetRow, offsetCol, ido_linear != 0); + is->transfer_out(out); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/interpolated_shift.h b/archive/cuda_extension/cuda/func/interpolated_shift.h new file mode 100644 index 000000000..b8dfdb980 --- /dev/null +++ b/archive/cuda_extension/cuda/func/interpolated_shift.h @@ -0,0 +1,30 @@ +#pragma once + +#include "utils/CudaFunction.h" +#include "utils/Memory.h" +#include "utils/Complex.h" + +/** Note: this function only does linear interpolation right so far. */ +class InterpolatedShift : public CudaFunction { +public: + InterpolatedShift(); + void setParameters(int items, int rows, int columns); + void setDeviceBuffers( + complex* d_in, + complex* d_out + ); + complex* getOutput() const { return d_out_.get(); } + void allocate(); + void transfer_in( + const complex* in + ); + void run(float offsetRow, float offsetColumn, bool do_linear=false); + void transfer_out( + complex* out + ); + +private: + DevicePtrWrapper> d_in_; + DevicePtrWrapper> d_out_; + int rows_ = 0, columns_ = 0, items_ = 0; +}; diff --git a/archive/cuda_extension/cuda/func/log_likelihood.cu b/archive/cuda_extension/cuda/func/log_likelihood.cu new file mode 100644 index 000000000..21c5876d1 --- /dev/null +++ b/archive/cuda_extension/cuda/func/log_likelihood.cu @@ -0,0 +1,269 @@ +#include "log_likelihood.h" + +#include "utils/GpuManager.h" +#include "utils/Memory.h" +#include "utils/ScopedTimer.h" + +#include +#include + +/*************** Kernels **********************/ + +__global__ void calc_LLError_kernel(const unsigned char *mask, + const float *LL, + const float *Idata, + const int *addr_info, + float *LLError, + int m, + int n, + const int *da_unique) +{ + // buffer to sum the matrices in shared memory + extern __shared__ float sumbuffer[]; + + int batch = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + int txy = tx * blockDim.y + ty; + sumbuffer[txy] = 0.0f; + + auto da = addr_info + da_unique[batch] * 3 * 5 + 9; + auto ma = da + 3; + + auto da0 = da[0]; + auto ma0 = ma[0]; + LL += da0 * m * n; + Idata += da0 * m * n; + mask += ma0 * m * n; + LLError += da0; + + for (int i = tx; i < m; i += blockDim.x) + { + for (int j = ty; j < n; j += blockDim.y) + { + auto vLL = LL[i * n + j]; + auto vIdata = Idata[i * n + j]; + auto vMask = mask[i * n + j]; + + auto m_by_LL_minus_Idata = vMask ? vLL - vIdata : 0.0f; + auto m_by_LL_minus_Idata_sqr = m_by_LL_minus_Idata * m_by_LL_minus_Idata; + + auto vIdata_p_1 = vIdata + 1.0f; + auto sumval = m_by_LL_minus_Idata_sqr / vIdata_p_1; + sumbuffer[txy] += sumval; + } + } + + // now add up sumbuffer in shared memory + __syncthreads(); + int nt = blockDim.x * blockDim.y; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (txy < half) + { + sumbuffer[txy] += sumbuffer[c - txy - 1]; + } + __syncthreads(); + c = c - half; + } + + if (txy == 0) + { + auto v = sumbuffer[0] / float(n * m); + LLError[0] = v; + } +} + +/*************** Class implementation **********/ + +LogLikelihood::LogLikelihood() : CudaFunction("log_likelihood") {} + +void LogLikelihood::setParameters(int i, int m, int n, int addr_i, int Idata_i) +{ + i_ = i; + m_ = m; + n_ = n; + Idata_i_ = Idata_i; // size of mask as well + addr_i_ = addr_i; // this is same as I + + ffprop_ = gpuManager.get_cuda_function( + "loglikelihood.farfield_propagator", i, m, n); + abs2_ = gpuManager.get_cuda_function, float>>( + "loglikelihood.abs2", i * m * n); + sum2buffer_ = gpuManager.get_cuda_function>( + "loglikelihood.sum2buffer", i_, m, n, Idata_i_, m, n, addr_i, addr_i); + sum2buffer_->setAddrStride(5 * 3); +} + +void LogLikelihood::setDeviceBuffers(complex *d_probe_obj, + unsigned char *d_mask, + float *d_Idata, + complex *d_prefilter, + complex *d_postfilter, + int *d_addr_info, + float *d_out, + int *d_outidx, + int *d_startidx, + int *d_indices, + int outidx_size) +{ + d_probe_obj_ = d_probe_obj; + d_mask_ = d_mask; + d_Idata_ = d_Idata; + d_prefilter_ = d_prefilter; + d_postfilter_ = d_postfilter; + d_addr_info_ = d_addr_info; + d_out_ = d_out; + d_outidx_ = d_outidx; + d_startidx_ = d_startidx; + d_indices_ = d_indices; + outidx_size_ = outidx_size; +} + +int LogLikelihood::calculateAddrIndices(const int *out1_addr) +{ + outidx_size_ = sum2buffer_->calculateAddrIndices(out1_addr); + return outidx_size_; +} + +void LogLikelihood::calculateUniqueDaIndices(const int *da_addr) +{ + std::vector unique; + unique.reserve(addr_i_); + std::set values; + + for (auto i = 0; i < addr_i_; ++i) + { + if (values.insert(da_addr[i * 15]).second) + unique.push_back(i); + } + + // for (auto i : unique) { + // std::cout << i << std::endl; + //} + + d_da_unique_.allocate(unique.size()); + gpu_memcpy_h2d(d_da_unique_.get(), unique.data(), unique.size()); +} + +void LogLikelihood::allocate() +{ + ScopedTimer t(this, "allocate"); + d_probe_obj_.allocate(i_ * m_ * n_); + d_mask_.allocate(Idata_i_ * m_ * n_); + d_Idata_.allocate(Idata_i_ * m_ * n_); + d_prefilter_.allocate(m_ * n_); + d_postfilter_.allocate(m_ * n_); + d_addr_info_.allocate(addr_i_ * 5 * 3); + d_out_.allocate(Idata_i_); + d_LL_.allocate(Idata_i_ * m_ * n_); + d_ft_.allocate(i_ * m_ * n_); + d_abs2_ft_.allocate(i_ * m_ * n_); + + ffprop_->setDeviceBuffers( + d_probe_obj_.get(), d_ft_.get(), d_prefilter_.get(), d_postfilter_.get()); + ffprop_->allocate(); + + abs2_->setDeviceBuffers(d_ft_.get(), d_abs2_ft_.get()); + abs2_->allocate(); + + sum2buffer_->setDeviceBuffers(d_abs2_ft_.get(), + d_LL_.get(), + d_addr_info_.get() + 6, + d_addr_info_.get() + 9, + d_outidx_, + d_startidx_, + d_indices_, + outidx_size_); + sum2buffer_->allocate(); +} + +void LogLikelihood::updateErrorOutput(float *d_out) { d_out_ = d_out; } + +float *LogLikelihood::getOutput() const { return d_out_.get(); } + +void LogLikelihood::transfer_in(const complex *probe_obj, + const unsigned char *mask, + const float *Idata, + const complex *prefilter, + const complex *postfilter, + const int *addr_info) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_probe_obj_.get(), probe_obj, i_ * m_ * n_); + gpu_memcpy_h2d(d_mask_.get(), mask, Idata_i_ * m_ * n_); + gpu_memcpy_h2d(d_Idata_.get(), Idata, Idata_i_ * m_ * n_); + gpu_memcpy_h2d(d_prefilter_.get(), prefilter, m_ * n_); + gpu_memcpy_h2d(d_postfilter_.get(), postfilter, m_ * n_); + // TODO: handle this case more explicitly + if (!d_addr_info_.isExternal()) + { + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, addr_i_ * 5 * 3); + } + + // transfer-in on sum_to_buffer needs to be called, for the internal + // outidx buffers + sum2buffer_->transfer_in(nullptr, nullptr, nullptr); + + calculateUniqueDaIndices(addr_info + 9); +} + +void LogLikelihood::transfer_out(float *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), Idata_i_); +} + +void LogLikelihood::run() +{ + ScopedTimer t(this, "run"); + ffprop_->run(true, true, true); + abs2_->run(); + sum2buffer_->run(); + + dim3 threadsPerBlock = {32u, 32u, 1u}; + dim3 blocks = {unsigned(d_da_unique_.size()), 1u, 1u}; + calc_LLError_kernel<<>>(d_mask_.get(), + d_LL_.get(), + d_Idata_.get(), + d_addr_info_.get(), + d_out_.get(), + m_, + n_, + d_da_unique_.get()); + checkLaunchErrors(); + + // sync device if timing is enabled + timing_sync(); +} + +extern "C" void log_likelihood_c(const float *fprobe_obj, + const unsigned char *mask, + const float *Idata, + const float *fprefilter, + const float *fpostfilter, + const int *addr_info, + float *out, + int i, + int m, + int n, + int addr_i, + int Idata_i) +{ + auto probe_obj = reinterpret_cast *>(fprobe_obj); + auto prefilter = reinterpret_cast *>(fprefilter); + auto postfilter = reinterpret_cast *>(fpostfilter); + + auto ll = gpuManager.get_cuda_function( + "loglikelihood", i, m, n, addr_i, Idata_i); + ll->calculateAddrIndices(addr_info + 9); + ll->allocate(); + ll->transfer_in(probe_obj, mask, Idata, prefilter, postfilter, addr_info); + ll->run(); + ll->transfer_out(out); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/log_likelihood.h b/archive/cuda_extension/cuda/func/log_likelihood.h new file mode 100644 index 000000000..83a30079e --- /dev/null +++ b/archive/cuda_extension/cuda/func/log_likelihood.h @@ -0,0 +1,67 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +#include "func/abs2.h" +#include "func/farfield_propagator.h" +#include "func/sum_to_buffer.h" + +class LogLikelihood : public CudaFunction +{ +public: + LogLikelihood(); + void setParameters(int i, int m, int n, int addr_i, int Idata_i); + + void setDeviceBuffers(complex *d_probe_obj, + unsigned char *d_mask, + float *d_Idata, + complex *d_prefilter, + complex *d_postfilter, + int *d_addr_info, + float *d_out, + int *d_outidx, + int *d_startidx, + int *d_indices, + int outidx_size); + int calculateAddrIndices(const int *out1_addr); + void calculateUniqueDaIndices(const int *da_addr); + void allocate(); + void updateErrorOutput(float *d_out); + float *getOutput() const; + void transfer_in(const complex *probe_obj, + const unsigned char *mask, + const float *Idata, + const complex *prefilter, + const complex *postfilter, + const int *addr_info); + void run(); + void transfer_out(float *out); + +private: + DevicePtrWrapper> d_probe_obj_; + DevicePtrWrapper d_mask_; + DevicePtrWrapper d_Idata_; + DevicePtrWrapper> d_prefilter_; + DevicePtrWrapper> d_postfilter_; + DevicePtrWrapper d_addr_info_; + DevicePtrWrapper d_out_; + DevicePtrWrapper d_LL_; + // internal buffer for intermediate results + DevicePtrWrapper> d_ft_; + DevicePtrWrapper d_abs2_ft_; + // these three are for bookkeeping between setDeviceBuffers and allocate + // so that they can be forwarded to sum2buffer + int *d_outidx_ = nullptr; + int *d_startidx_ = nullptr; + int *d_indices_ = nullptr; + int outidx_size_ = 0; + // unique indices for da + DevicePtrWrapper d_da_unique_; + + int i_ = 0, m_ = 0, n_ = 0, addr_i_ = 0; + int Idata_i_ = 0; + FarfieldPropagator *ffprop_ = nullptr; + Abs2, float> *abs2_ = nullptr; + SumToBuffer *sum2buffer_ = nullptr; +}; diff --git a/archive/cuda_extension/cuda/func/mass_center.cu b/archive/cuda_extension/cuda/func/mass_center.cu new file mode 100644 index 000000000..8de528d01 --- /dev/null +++ b/archive/cuda_extension/cuda/func/mass_center.cu @@ -0,0 +1,257 @@ +#include "mass_center.h" +#include "utils/FinalSumKernel.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include +#include + +/************ kernels ***************/ + +template +__global__ void indexed_sum_middim( + const float* data, + float* sums, + int i, + int m, // dim we work on - 1 block per output + int n, + float scale) +{ + int bid = blockIdx.x; + int tid = threadIdx.x; + + data += bid * n; + + auto val = 0.0f; + for (int x = 0; x < i; ++x) + { + auto d_inner = data + x * n * m; + for (int z = tid; z < n; z += BlockX) + { + val += d_inner[z]; + } + } + + __shared__ float sumshr[BlockX]; + sumshr[tid] = val; + + __syncthreads(); + int c = BlockX; + while (c > 1) + { + int half = c / 2; + if (tid < half) + { + sumshr[tid] += sumshr[c - tid - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tid == 0) + { + sums[bid] = sumshr[0] * float(bid) * scale; + } +} + +template +__global__ void indexed_sum_lastdim( + const float* data, float* sums, int n, int i, float scale) +{ + int ty = threadIdx.y + blockIdx.y * BlockY; + int tx = threadIdx.x; + + auto val = 0.0f; + if (ty < i) + { + data += ty; // column to work on + + // we collaborate along the x axis (columns) to get more threads in case i + // is small + for (int r = tx; r < n; r += BlockX) + { + val += data[r * i]; + } + } + + // reduce along X dimension in shared memory (column sum) + __shared__ float blocksums[BlockX][BlockY]; + blocksums[tx][threadIdx.y] = val; + + __syncthreads(); + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + blocksums[tx][threadIdx.y] += blocksums[c - tx - 1][threadIdx.y]; + } + __syncthreads(); + c = c - half; + } + + if (ty >= i) + { + return; + } + + if (tx == 0) + { + sums[ty] = blocksums[0][threadIdx.y] * float(ty) * scale; + } +} + +template +__global__ void final_sums(const float* sum_i, + int i, + const float* sum_m, + int m, + const float* sum_n, + int n, + float* output) +{ + int bid = blockIdx.x; + int tid = threadIdx.x; + // each block works on a single dimension + int nn = bid == 0 ? i : (bid == 1 ? m : n); + const float* data = bid == 0 ? sum_i : (bid == 1 ? sum_m : sum_n); + + __shared__ float shared[BlockX]; + auto val = 0.0f; + for (int i = tid; i < nn; i += blockDim.x) + { + val += data[i]; + } + shared[tid] = val; + + // now add up sumbuffer in shared memory + __syncthreads(); + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tid < half) + { + shared[tid] += shared[c - tid - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tid == 0) + { + output[bid] = shared[0]; + } +} + +/************ class implementation ********/ + +MassCenter::MassCenter() : CudaFunction("mass_center") {} + +void MassCenter::setParameters(int i, int m, int n) +{ + i_ = i; + m_ = m; + n_ = n; +} + +void MassCenter::setDeviceBuffers(float* d_data, float* d_out) +{ + d_data_ = d_data; + d_out_ = d_out; +} + +void MassCenter::allocate() +{ + ScopedTimer t(this, "allocate"); + d_data_.allocate(i_ * m_ * n_); + d_i_sum_.allocate(i_); + d_m_sum_.allocate(m_); + d_n_sum_.allocate(n_); + d_out_.allocate(n_ > 1 ? 3 : 2); +} + +void MassCenter::transfer_in(const float* data) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_data_.get(), data, i_ * m_ * n_); +} + +void MassCenter::run() +{ + ScopedTimer t(this, "run"); + + const int threadsPerBlock = 256; + + // first, calculate the total sum of all entries (using thrust) + thrust::device_ptr raw(d_data_.get()); + auto total_sum = thrust::reduce(raw, raw + i_ * n_ * m_); + auto sc = 1.0f / total_sum; + + // sum all dims except the first, multiplying by the index and scaling factor + indexed_sum_middim<<>>( + d_data_.get(), d_i_sum_.get(), 1, i_, n_ * m_, sc); + checkLaunchErrors(); + + if (n_ > 2) + { + // 3d case + + // sum all dims, except the middle, multiplying by the index and scaling + // factor + indexed_sum_middim<<>>( + d_data_.get(), d_m_sum_.get(), i_, n_, m_, sc); + checkLaunchErrors(); + + // sum the all dims except the last, multiplying by the index and scaling + // factor + dim3 threads = {32u, 32u, 1u}; + dim3 blk = {1u, unsigned(n_ + 32 - 1) / 32u, 1u}; + indexed_sum_lastdim<32, 32> + <<>>(d_data_.get(), d_n_sum_.get(), i_ * m_, n_, sc); + checkLaunchErrors(); + } + else + { + // 2d case + + // sum the all dims except the last, multiplying by the index and scaling + // factor + dim3 threads = {32u, 32u, 1u}; + dim3 blk = {1u, unsigned(m_ + 32 - 1) / 32u, 1u}; + indexed_sum_lastdim<32, 32> + <<>>(d_data_.get(), d_m_sum_.get(), i_, m_, sc); + checkLaunchErrors(); + } + + // summing for final results (TODO:: can we combine these?) + final_sums<256><< 1 ? 3 : 2, 256>>> (d_i_sum_.get(), + i_, + d_m_sum_.get(), + m_, + d_n_sum_.get(), + n_, + d_out_.get()); + checkLaunchErrors(); +} + +void MassCenter::transfer_out(float* out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), n_ > 1 ? 3 : 2); +} + +/************ interface *******************/ + +extern "C" void mass_center_c( + const float* data, int i, int m, int n, float* output) +{ + auto mc = gpuManager.get_cuda_function("mass_center", i, m, n); + mc->allocate(); + mc->transfer_in(data); + mc->run(); + mc->transfer_out(output); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/mass_center.h b/archive/cuda_extension/cuda/func/mass_center.h new file mode 100644 index 000000000..458a4491c --- /dev/null +++ b/archive/cuda_extension/cuda/func/mass_center.h @@ -0,0 +1,21 @@ +#pragma once +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class MassCenter : public CudaFunction +{ +public: + MassCenter(); + void setParameters(int i, int m, int n = 1); + void setDeviceBuffers(float* d_data, float* d_out); + void allocate(); + void transfer_in(const float* data); + void run(); + void transfer_out(float* out); + +private: + DevicePtrWrapper d_data_; + DevicePtrWrapper d_i_sum_, d_m_sum_, d_n_sum_; + DevicePtrWrapper d_out_; + int i_ = 0, m_ = 0, n_ = 0; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/norm2.cu b/archive/cuda_extension/cuda/func/norm2.cu new file mode 100644 index 000000000..06e6ad943 --- /dev/null +++ b/archive/cuda_extension/cuda/func/norm2.cu @@ -0,0 +1,160 @@ + +#include "norm2.h" +#include "utils/Complex.h" +#include "utils/FinalSumKernel.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include +#include + +/************ Kernels ***************/ + +__device__ inline float dev_abs2(float x) { return x * x; } +__device__ inline float dev_abs2(complex x) +{ + return x.real() * x.real() + x.imag() * x.imag(); +} + +template +__global__ void norm2_kernel(const T* input, float* output, int size) +{ + int gid = threadIdx.x + blockIdx.x * blockDim.x; + int tid = threadIdx.x; + extern __shared__ float sum[]; + + using std::abs; + + if (gid < size) + { + sum[tid] = dev_abs2(input[gid]); + } + else + { + sum[tid] = 0.0f; + } + + // now add up sumbuffer in shared memory + __syncthreads(); + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tid < half) + { + sum[tid] += sum[c - tid - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tid == 0) + { + output[blockIdx.x] = sum[0]; + } +} + +/************ Class implementation **********/ + +template +Norm2::Norm2() : CudaFunction("norm2") +{ +} + +template +void Norm2::setParameters(int size) +{ + size_ = size; + numblocks_ = (size + BLOCK_SIZE - 1) / BLOCK_SIZE; +} + +template +void Norm2::setDeviceBuffers(T* d_input, float* d_output) +{ + d_input_ = d_input; + d_output_ = d_output; +} + +template +void Norm2::allocate() +{ + ScopedTimer t(this, "allocate"); + d_input_.allocate(size_); + if (numblocks_ > 1) + { + d_intermediate_.allocate(numblocks_); + } + d_output_.allocate(1); +} + +template +float* Norm2::getOutput() const +{ + return d_output_.get(); +} + +template +void Norm2::transfer_in(const T* input) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_input_.get(), input, size_); +} + +template +void Norm2::run() +{ + ScopedTimer t(this, "run"); + if (numblocks_ == 1) + { + norm2_kernel<<>>( + d_input_.get(), d_output_.get(), size_); + } + else + { + norm2_kernel<<>>( + d_input_.get(), d_intermediate_.get(), size_); + // now we have one output per block, so a final kernel is needed + int nthreads = numblocks_; + if (nthreads > 1024) + nthreads = 1024; + final_sum<<<1, nthreads, nthreads * sizeof(float)>>>( + d_intermediate_.get(), d_output_.get(), numblocks_); + } + timing_sync(); +} + +template +void Norm2::transfer_out(float* output) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(output, d_output_.get(), 1); +} + +/*********** interface function *************/ + +template +void norm2_tc(const T* data, float* out, int size) +{ + auto n2 = gpuManager.get_cuda_function>( + "norm2<" + getTypeName() + ">", size); + n2->allocate(); + n2->transfer_in(data); + n2->run(); + n2->transfer_out(out); +} + +template class Norm2; +template class Norm2>; + +extern "C" void norm2_c(const float* data, float* out, int size, int isComplex) +{ + if (isComplex != 0) + { + norm2_tc(reinterpret_cast*>(data), out, size); + } + else + { + norm2_tc(data, out, size); + } +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/norm2.h b/archive/cuda_extension/cuda/func/norm2.h new file mode 100644 index 000000000..da244b8e7 --- /dev/null +++ b/archive/cuda_extension/cuda/func/norm2.h @@ -0,0 +1,26 @@ +#pragma once + +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +template +class Norm2 : public CudaFunction +{ +public: + Norm2(); + void setParameters(int size); + void setDeviceBuffers(T* d_input, float* d_output); + void allocate(); + float* getOutput() const; + void transfer_in(const T* input); + void run(); + void transfer_out(float* output); + +private: + static const int BLOCK_SIZE = 1024; + DevicePtrWrapper d_input_; + DevicePtrWrapper d_intermediate_; + DevicePtrWrapper d_output_; + int size_ = 0; + int numblocks_ = 0; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/realspace_error.cu b/archive/cuda_extension/cuda/func/realspace_error.cu new file mode 100644 index 000000000..2435a6e32 --- /dev/null +++ b/archive/cuda_extension/cuda/func/realspace_error.cu @@ -0,0 +1,160 @@ +#include "realspace_error.h" + +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +/*************** kernels **********************/ + +__global__ void realspace_error_kernel(const complex *difference, + const int *ea_first_column, + const int *da_first_column, + int addr_stride, + float *out, + int m, + int n) +{ + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + int txy = tx * blockDim.y + ty; + + extern __shared__ float sum[]; + sum[txy] = 0.0; + + int eaidx = ea_first_column[bid * addr_stride]; + int daidx = da_first_column[bid * addr_stride]; + + for (int i = tx; i < m; i += blockDim.x) + { + for (int j = ty; j < n; j += blockDim.y) + { + auto idx = eaidx * m * n + i * n + j; + auto v = difference[idx]; + auto abs2 = v.real() * v.real() + v.imag() * v.imag(); + sum[txy] += abs2; + } + } + + __syncthreads(); + int nt = blockDim.x * blockDim.y; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (txy < half) + { + sum[txy] += sum[c - txy - 1]; + } + __syncthreads(); + c = c - half; + } + + if (txy == 0) + { + auto eaerror = sum[0] / float(m * n); + atomicAdd(&out[daidx], eaerror); + } +} + +/*************** class implementation ***************/ + +RealspaceError::RealspaceError() : CudaFunction("realspace_error") {} + +void RealspaceError::setParameters( + int i, int m, int n, int addr_len, int outlen) +{ + i_ = i; + m_ = m; + n_ = n; + addr_len_ = addr_len; + outlen_ = outlen; +} + +void RealspaceError::setAddrStride(int stride) { addr_stride_ = stride; } + +void RealspaceError::setDeviceBuffers(complex *d_difference, + int *d_ea_first_column, + int *d_da_first_column, + float *d_out) +{ + d_difference_ = d_difference; + d_ea_first_column_ = d_ea_first_column; + d_da_first_column_ = d_da_first_column; + d_out_ = d_out; +} + +void RealspaceError::allocate() +{ + ScopedTimer t(this, "allocate"); + d_difference_.allocate(i_ * m_ * n_); + d_ea_first_column_.allocate(addr_len_ * addr_stride_); + d_da_first_column_.allocate(addr_len_ * addr_stride_); + d_out_.allocate(outlen_); +} + +void RealspaceError::updateErrorOutput(float *d_out) +{ + d_out_ = d_out; +} + +void RealspaceError::transfer_in(const complex *difference, + const int *ea_first_column, + const int *da_first_column) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_difference_.get(), difference, i_ * n_ * m_); + gpu_memcpy_h2d( + d_ea_first_column_.get(), ea_first_column, addr_len_ * addr_stride_); + gpu_memcpy_h2d( + d_da_first_column_.get(), da_first_column, addr_len_ * addr_stride_); +} + +void RealspaceError::run() +{ + ScopedTimer t(this, "run"); + + checkCudaErrors(cudaMemset(d_out_.get(), 0, outlen_ * sizeof(*d_out_.get()))); + + // always use a 32x32 block of threads + dim3 threadsPerBlock = {32u, 32u, 1u}; + dim3 blocks = {unsigned(addr_len_), 1u, 1u}; + realspace_error_kernel<<>>( + d_difference_.get(), + d_ea_first_column_.get(), + d_da_first_column_.get(), + addr_stride_, + d_out_.get(), + m_, + n_); + checkLaunchErrors(); + + timing_sync(); +} + +void RealspaceError::transfer_out(float *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), outlen_); +} + +/************** interface function ***************/ + +extern "C" void realspace_error_c(const float *difference, + const int *ea_first_column, + const int *da_first_column, + int addr_len, + float *out, + int i, + int m, + int n, + int outlen) +{ + auto rse = gpuManager.get_cuda_function( + "realspace_error", i, m, n, addr_len, outlen); + rse->allocate(); + rse->transfer_in(reinterpret_cast *>(difference), + ea_first_column, + da_first_column); + rse->run(); + rse->transfer_out(out); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/realspace_error.h b/archive/cuda_extension/cuda/func/realspace_error.h new file mode 100644 index 000000000..ca2b59acc --- /dev/null +++ b/archive/cuda_extension/cuda/func/realspace_error.h @@ -0,0 +1,31 @@ +#pragma once +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class RealspaceError : public CudaFunction +{ +public: + RealspaceError(); + void setParameters(int i, int m, int n, int addr_len, int outlen); + void setAddrStride(int addr_stride); + void setDeviceBuffers(complex* d_difference, + int* d_ea_first_column, + int* d_da_first_column, + float* d_out); + void allocate(); + void updateErrorOutput(float* d_out); + void transfer_in(const complex* difference, + const int* ea_first_column, + const int* da_first_column); + void run(); + void transfer_out(float* out); + +private: + DevicePtrWrapper> d_difference_; + DevicePtrWrapper d_ea_first_column_; + DevicePtrWrapper d_da_first_column_; + DevicePtrWrapper d_out_; + int i_ = 0, m_ = 0, n_ = 0, addr_len_ = 0, outlen_ = 0; + int addr_stride_ = 1; +}; diff --git a/archive/cuda_extension/cuda/func/renormalise_fourier_magnitudes.cu b/archive/cuda_extension/cuda/func/renormalise_fourier_magnitudes.cu new file mode 100644 index 000000000..7e66f36ae --- /dev/null +++ b/archive/cuda_extension/cuda/func/renormalise_fourier_magnitudes.cu @@ -0,0 +1,229 @@ +#include "renormalise_fourier_magnitudes.h" + +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include +#include +#include + +/********** kernels *****************/ + +// template, to switch between pbound and not at compile-time +template +__global__ void renormalise_fourier_magnitudes_kernel(const complex *f, + const float *af, + const float *fmag, + const unsigned char *mask, + const float *err_fmag, + const int *addr_info, + complex *out, + float pbound, + int A, + int B) +{ + using std::sqrt; + + int batch = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + + auto ea = addr_info + batch * 3 * 5 + 2 * 3; + auto da = ea + 3; + auto ma = da + 3; + + auto ea_0 = ea[0]; + auto ea_1 = 0; + auto ea_2 = 0; + + auto da_0 = da[0]; + auto da_1 = 0; + auto da_2 = 0; + + auto ma_0 = ma[0]; + auto ma_1 = 0; + auto ma_2 = 0; + + for (int i = tx; i < A; i += blockDim.x) + { + for (int j = ty; j < B; j += blockDim.y) + { + auto maidx = ma_0 * A * B + (ma_1 + i) * B + (ma_2 + j); + auto eaidx = ea_0 * A * B + (ea_1 + i) * B + (ea_2 + j); + auto daidx = da_0 * A * B + (da_1 + i) * B + (da_2 + j); + + auto m = mask[maidx]; + auto magnitudes = fmag[daidx]; + auto absolute_magnitudes = af[daidx]; + auto fourier_space_solution = f[eaidx]; + auto fourier_error = err_fmag[da_0]; + + if (!usePbound) + { + auto fm = m ? magnitudes / (absolute_magnitudes + 1e-10f) : 1.0f; + auto v = fm * fourier_space_solution; + out[eaidx] = v; + } + else if (fourier_error > pbound) + { + // power bound is applied + auto fdev = absolute_magnitudes - magnitudes; + auto renorm = sqrt(pbound / fourier_error); + auto fm = + m ? (magnitudes + fdev * renorm) / (absolute_magnitudes + 1e-10f) + : 1.0f; + out[eaidx] = fm * fourier_space_solution; + } + else + { + out[eaidx] = 0.0f; + } + } + } +} + +/********** class implementation ********/ + +RenormaliseFourierMagnitudes::RenormaliseFourierMagnitudes() + : CudaFunction("renormalise_fourier_magnitudes") +{ +} + +void RenormaliseFourierMagnitudes::setParameters(int M, int N, int A, int B) +{ + M_ = M; + N_ = N; + A_ = A; + B_ = B; +} + +void RenormaliseFourierMagnitudes::setDeviceBuffers(complex *d_f, + float *d_af, + float *d_fmag, + unsigned char *d_mask, + float *d_err_fmag, + int *d_addr_info, + complex *d_out) +{ + d_f_ = d_f; + d_af_ = d_af; + d_fmag_ = d_fmag; + d_mask_ = d_mask; + d_err_fmag_ = d_err_fmag; + d_addr_info_ = d_addr_info; + d_out_ = d_out; +} + +void RenormaliseFourierMagnitudes::allocate() +{ + ScopedTimer t(this, "allocate"); + d_f_.allocate(M_ * A_ * B_); + d_af_.allocate(N_ * A_ * B_); + d_fmag_.allocate(N_ * A_ * B_); + d_mask_.allocate(N_ * A_ * B_); + d_err_fmag_.allocate(N_); + d_addr_info_.allocate(M_ * 5 * 3); + d_out_.allocate(M_ * A_ * B_); +} + +void RenormaliseFourierMagnitudes::updateErrorInput(float *d_err_fmag) +{ + d_err_fmag_ = d_err_fmag; +} + +complex *RenormaliseFourierMagnitudes::getOutput() const +{ + return d_out_.get(); +} + +void RenormaliseFourierMagnitudes::transfer_in(const complex *f, + const float *af, + const float *fmag, + const unsigned char *mask, + const float *err_fmag, + const int *addr_info) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_f_.get(), f, M_ * A_ * B_); + gpu_memcpy_h2d(d_af_.get(), af, N_ * A_ * B_); + gpu_memcpy_h2d(d_fmag_.get(), fmag, N_ * A_ * B_); + gpu_memcpy_h2d(d_mask_.get(), mask, N_ * A_ * B_); + gpu_memcpy_h2d(d_err_fmag_.get(), err_fmag, N_); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, M_ * 5 * 3); +} + +void RenormaliseFourierMagnitudes::run(float pbound, bool usePbound) +{ + ScopedTimer t(this, "run"); + + // always use a 32x32 block of threads + dim3 threadsPerBlock = {32, 32, 1u}; + dim3 blocks = {unsigned(M_), 1u, 1u}; + + if (usePbound) + { + renormalise_fourier_magnitudes_kernel + <<>>(d_f_.get(), + d_af_.get(), + d_fmag_.get(), + d_mask_.get(), + d_err_fmag_.get(), + d_addr_info_.get(), + d_out_.get(), + pbound, + A_, + B_); + checkLaunchErrors(); + } + else + { + renormalise_fourier_magnitudes_kernel + <<>>(d_f_.get(), + d_af_.get(), + d_fmag_.get(), + d_mask_.get(), + d_err_fmag_.get(), + d_addr_info_.get(), + d_out_.get(), + pbound, + A_, + B_); + checkLaunchErrors(); + } + + timing_sync(); +} + +void RenormaliseFourierMagnitudes::transfer_out(complex *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), M_ * A_ * B_); +} + +/********* interface function *********/ + +extern "C" void renormalise_fourier_magnitudes_c( + const float *f_f, // M x A x B + const float *af, // N x A x B + const float *fmag, // N x A x B + const unsigned char *mask, // N x A x B + const float *err_fmag, // N + const int *addr_info, // M x 5 x 3 + float pbound, + float *f_out, // M x A x B + int M, + int N, + int A, + int B, + int usePbound) +{ + auto f = reinterpret_cast *>(f_f); + auto out = reinterpret_cast *>(f_out); + + auto rfm = gpuManager.get_cuda_function( + "renormalise_fourier_magnitudes", M, N, A, B); + rfm->allocate(); + rfm->transfer_in(f, af, fmag, mask, err_fmag, addr_info); + rfm->run(pbound, usePbound != 0); + rfm->transfer_out(out); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/renormalise_fourier_magnitudes.h b/archive/cuda_extension/cuda/func/renormalise_fourier_magnitudes.h new file mode 100644 index 000000000..50ad5a910 --- /dev/null +++ b/archive/cuda_extension/cuda/func/renormalise_fourier_magnitudes.h @@ -0,0 +1,40 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class RenormaliseFourierMagnitudes : public CudaFunction +{ +public: + RenormaliseFourierMagnitudes(); + void setParameters(int M, int N, int A, int B); + void setDeviceBuffers(complex *d_f, + float *d_af, + float *d_fmag, + unsigned char *d_mask, + float *d_err_fmag, + int *d_addr_info, + complex *d_out); + void allocate(); + void updateErrorInput(float* d_err_fmag); + complex *getOutput() const; + void transfer_in(const complex *f, + const float *af, + const float *fmag, + const unsigned char *mask, + const float *err_fmag, + const int *addr_info); + void run(float pbound = 0.0, bool usePbound = false); + void transfer_out(complex *out); + +private: + DevicePtrWrapper> d_f_; // M x A x B + DevicePtrWrapper d_af_; // M x A x B + DevicePtrWrapper d_fmag_; // N x A x B + DevicePtrWrapper d_mask_; // N x A X B + DevicePtrWrapper d_err_fmag_; // N + DevicePtrWrapper d_addr_info_; // M x 5 x 3 + DevicePtrWrapper> d_out_; // M x A x B + int M_ = 0, N_ = 0, A_ = 0, B_= 0; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/scan_and_multiply.cu b/archive/cuda_extension/cuda/func/scan_and_multiply.cu new file mode 100644 index 000000000..40867fe93 --- /dev/null +++ b/archive/cuda_extension/cuda/func/scan_and_multiply.cu @@ -0,0 +1,194 @@ +#include "scan_and_multiply.h" + +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include + +/*********** Kernels ******************/ + +template +__global__ void scan_and_multiply_kernel( + complex *out, + const int *addr_info, // note: __restrict__ (texture cache) makes it slower + const complex *probe, + const complex *obj, + int probe_m, + int probe_n, + int obj_m, + int obj_n, + int m, + int n) +{ + int batch = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + + // each of these are 3-d arrays with indices + auto pa = addr_info + batch * 3 * 5; + auto oa = pa + 3; + auto ea = pa + 6; + + // these are the start indices for the batch item - m x n array from here + // probe start + auto p_i0 = pa[0]; + auto p_i1 = pa[1]; + auto p_i2 = pa[2]; + + // obj start + auto o_i0 = oa[0]; + auto o_i1 = oa[1]; + auto o_i2 = oa[2]; + + // output start + auto po_i0 = ea[0]; + + auto ooffset = o_i0 * obj_m * obj_n + o_i1 * obj_n + o_i2; + auto poffset = p_i0 * probe_m * probe_n + p_i1 * probe_n + p_i2; + auto pooffset = po_i0 * m * n; + +// let each thread jump by blockDim.x / y, so a 32x32 thread block works on +// any shape of m x n data +#pragma unroll(2) + for (int i = tx; i < m; i += BlockX) + { +#pragma unroll(1) + for (int j = ty; j < n; j += BlockY) + { + auto oidx = ooffset + i * obj_n + j; + auto pidx = poffset + i * probe_n + j; + auto poidx = pooffset + i * n + j; + + out[poidx] = probe[pidx] * obj[oidx]; + } + } +} + +/*********** Class implementation *****************/ + +ScanAndMultiply::ScanAndMultiply() : CudaFunction("scan_and_multiply") {} + +void ScanAndMultiply::setParameters(int batch_size, + int m, + int n, + int probe_i, + int probe_m, + int probe_n, + int obj_i, + int obj_m, + int obj_n, + int addr_len) +{ + batch_size_ = batch_size; + m_ = m; + n_ = n; + probe_i_ = probe_i; + probe_m_ = probe_m; + probe_n_ = probe_n; + obj_i_ = obj_i; + obj_m_ = obj_m; + obj_n_ = obj_n; + addr_len_ = addr_len; +} + +void ScanAndMultiply::setDeviceBuffers(complex *d_probe, + complex *d_obj, + int *d_addr_info, + complex *d_out) +{ + d_probe_ = d_probe; + d_obj_ = d_obj; + d_addr_info_ = d_addr_info; + d_out_ = d_out; +} + +void ScanAndMultiply::allocate() +{ + ScopedTimer t(this, "allocate"); + d_out_.allocate(batch_size_ * m_ * n_); + d_addr_info_.allocate(addr_len_ * 5 * 3); + d_probe_.allocate(probe_i_ * probe_m_ * probe_n_); + d_obj_.allocate(obj_i_ * obj_m_ * obj_n_); +} + +complex *ScanAndMultiply::getOutput() const { return d_out_.get(); } + +void ScanAndMultiply::transfer_in(const complex *probe, + const complex *obj, + const int *addr_info) +{ + ScopedTimer t(this, "transfer in"); + gpu_memcpy_h2d(d_probe_.get(), probe, probe_i_ * probe_m_ * probe_n_); + gpu_memcpy_h2d(d_obj_.get(), obj, obj_i_ * obj_m_ * obj_n_); + gpu_memcpy_h2d(d_addr_info_.get(), addr_info, addr_len_ * 5 * 3); +} + +void ScanAndMultiply::transfer_out(complex *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), batch_size_ * m_ * n_); +} + +void ScanAndMultiply::run() +{ + ScopedTimer t(this, "run"); + checkCudaErrors(cudaMemset( + d_out_.get(), 0, batch_size_ * m_ * n_ * sizeof(complex))); + // always use a 32x32 block of threads + dim3 threadsPerBlock = {32, 32, 1u}; + dim3 blocks = {unsigned(addr_len_), 1u, 1u}; + + scan_and_multiply_kernel<32, 32> + <<>>(d_out_.get(), + d_addr_info_.get(), + d_probe_.get(), + d_obj_.get(), + probe_m_, + probe_n_, + obj_m_, + obj_n_, + m_, + n_); + checkLaunchErrors(); + + // sync device if timing is enabled + timing_sync(); +} + +/******* Interface function **********/ + +extern "C" void scan_and_multiply_c(const float *fprobe, + int probe_i, + int probe_m, + int probe_n, + const float *fobj, + int obj_i, + int obj_m, + int obj_n, + const int *addr_info, + int addr_len, + int batch_size, + int m, + int n, + float *fout) +{ + auto probe = reinterpret_cast *>(fprobe); + auto obj = reinterpret_cast *>(fobj); + auto out = reinterpret_cast *>(fout); + + auto sam = gpuManager.get_cuda_function("scan_and_multiply", + batch_size, + m, + n, + probe_i, + probe_m, + probe_n, + obj_i, + obj_m, + obj_n, + addr_len); + sam->allocate(); + sam->transfer_in(probe, obj, addr_info); + sam->run(); + sam->transfer_out(out); +} diff --git a/archive/cuda_extension/cuda/func/scan_and_multiply.h b/archive/cuda_extension/cuda/func/scan_and_multiply.h new file mode 100644 index 000000000..0d6cf2f9f --- /dev/null +++ b/archive/cuda_extension/cuda/func/scan_and_multiply.h @@ -0,0 +1,46 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +class ScanAndMultiply : public CudaFunction +{ +public: + ScanAndMultiply(); + void setParameters(int batch_size, + int m, + int n, + int probe_i, + int probe_m, + int probe_n, + int obj_i, + int obj_m, + int obj_n, + int addr_len); + + void setDeviceBuffers(complex *d_probe, + complex *d_obj, + int *d_addr_info, + complex *d_out); + void allocate(); + complex *getOutput() const; + void transfer_in(const complex *probe, + const complex *obj, + const int *addr_info); + + void transfer_out(complex *out); + + void run(); + +private: + DevicePtrWrapper> d_probe_; + DevicePtrWrapper> d_obj_; + DevicePtrWrapper d_addr_info_; + DevicePtrWrapper> d_out_; + int batch_size_ = 0; + int m_ = 0; + int n_ = 0; + int probe_i_ = 0, probe_m_ = 0, probe_n_ = 0, obj_i_ = 0, obj_m_ = 0, + obj_n_ = 0, addr_len_ = 0; +}; diff --git a/archive/cuda_extension/cuda/func/sqrt_abs.cu b/archive/cuda_extension/cuda/func/sqrt_abs.cu new file mode 100644 index 000000000..4a1382e6e --- /dev/null +++ b/archive/cuda_extension/cuda/func/sqrt_abs.cu @@ -0,0 +1,14 @@ +#include +#include + +// doing this on the CPU for now - this isn't needed anywhere on its own +// - this was used as a test for the cython interfacing +extern "C" void sqrt_abs_c(const float *in, float *out, int m, int n) +{ + auto cin = reinterpret_cast *>(in); + auto cout = reinterpret_cast *>(out); + for (int i = 0; i < m * n; ++i) + { + cout[i] = std::sqrt(std::abs(cin[i])); + } +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/sum_to_buffer.cu b/archive/cuda_extension/cuda/func/sum_to_buffer.cu new file mode 100644 index 000000000..281366ba8 --- /dev/null +++ b/archive/cuda_extension/cuda/func/sum_to_buffer.cu @@ -0,0 +1,347 @@ +#include "sum_to_buffer.h" + +#include "addr_info_helpers.h" +#include "utils/Complex.h" +#include "utils/GpuManager.h" +#include "utils/ScopedTimer.h" + +#include + +/********** Kernels ***********************/ + +template +__global__ void sum_to_buffer_kernel(T *out, + int os_1, + int os_2, + const T *in1, + int in1_1, + int in1_2, + const int *__restrict__ in1_addr, + const int *outidx, + const int *startidx, + const int *__restrict__ indices, + int addr_stride) +{ + auto outi = outidx[blockIdx.x]; + auto ind_start = indices + startidx[blockIdx.x]; + auto ind_end = indices + startidx[blockIdx.x + 1]; + + int tx = threadIdx.x; + int ty = threadIdx.y; + +#pragma unroll(2) + for (int i = tx; i < in1_1; i += BlockX) + { +#pragma unroll(1) + for (int j = ty; j < in1_2; j += BlockY) + { + auto oi = outi * os_1 * os_2 + i * os_2 + j; + T val = T(); + for (auto is = ind_start; is != ind_end; ++is) + { + auto i1 = in1_addr + *is * addr_stride; + auto i1_0 = i1[0]; + auto i1_1 = i1[1] + i; + auto i1_2 = i1[2] + j; + val += in1[i1_0 * in1_1 * in1_2 + i1_1 * in1_2 + i1_2]; + } + out[oi] = val; + } + } +} + +/********** Class implementation **************/ + +template +SumToBuffer::SumToBuffer() : CudaFunction("sum_to_buffer") +{ +} + +template +void SumToBuffer::setParameters(int in1_0, + int in1_1, + int in1_2, + int os_0, + int os_1, + int os_2, + int in1_addr_0, + int out1_addr_0) +{ + in1_0_ = in1_0; + in1_1_ = in1_1; + in1_2_ = in1_2; + os_0_ = os_0; + os_1_ = os_1; + os_2_ = os_2; + in1_addr_0_ = in1_addr_0; + out1_addr_0_ = out1_addr_0; +} + +template +void SumToBuffer::setAddrStride(int stride) +{ + addr_stride_ = stride; +} + +template +void SumToBuffer::setDeviceBuffers(T *d_in1, + T *d_out, + int *d_in1_addr, + int *d_out1_addr, + int *d_outidx, + int *d_startidx, + int *d_indices, + int outidx_size) +{ + d_in1_ = d_in1; + d_out_ = d_out; + d_in1_addr_ = d_in1_addr; + d_out1_addr_ = d_out1_addr; + d_outidx_ = d_outidx; + d_startidx_ = d_startidx; + d_indices_ = d_indices; + if (d_outidx) + { + outidx_size_ = outidx_size; + } +} + +template +void SumToBuffer::allocate() +{ + ScopedTimer t(this, "allocate"); + d_in1_.allocate(in1_0_ * in1_1_ * in1_2_); + d_out_.allocate(os_0_ * os_1_ * os_2_); + d_in1_addr_.allocate(in1_addr_0_ * addr_stride_); + d_out1_addr_.allocate(out1_addr_0_ * addr_stride_); + if (!outidx_.empty()) + { + d_outidx_.allocate(outidx_.size()); + d_startidx_.allocate(startidx_.size()); + d_indices_.allocate(indices_.size()); + outidx_size_ = outidx_.size(); + } +} + +template +int SumToBuffer::calculateAddrIndices(const int *out1_addr) +{ + // calculate the indexing map + outidx_.clear(); + startidx_.clear(); + indices_.clear(); + flatten_out_addr( + out1_addr, out1_addr_0_, addr_stride_, outidx_, startidx_, indices_); + outidx_size_ = outidx_.size(); + return outidx_size_; +} + +template +T *SumToBuffer::getOutput() const +{ + return d_out_.get(); +} + +template +void SumToBuffer::transfer_in(const T *in1, + const int *in1_addr, + const int *out1_addr) +{ + ScopedTimer t(this, "transfer in"); + + gpu_memcpy_h2d(d_in1_.get(), in1, in1_0_ * in1_1_ * in1_2_); + gpu_memcpy_h2d(d_in1_addr_.get(), in1_addr, in1_addr_0_ * addr_stride_); + gpu_memcpy_h2d(d_out1_addr_.get(), out1_addr, out1_addr_0_ * addr_stride_); + if (!outidx_.empty()) + { + gpu_memcpy_h2d(d_outidx_.get(), outidx_.data(), outidx_.size()); + gpu_memcpy_h2d(d_startidx_.get(), startidx_.data(), startidx_.size()); + gpu_memcpy_h2d(d_indices_.get(), indices_.data(), indices_.size()); + } +} + +template +void SumToBuffer::run() +{ + ScopedTimer t(this, "run"); + dim3 threadsPerBlock = {32u, 32u, 1u}; + dim3 blocks = {unsigned(outidx_size_), 1u, 1u}; + sum_to_buffer_kernel + <<>>(d_out_.get(), + os_1_, + os_2_, + d_in1_.get(), + in1_1_, + in1_2_, + d_in1_addr_.get(), + d_outidx_.get(), + d_startidx_.get(), + d_indices_.get(), + addr_stride_); + checkLaunchErrors(); + + // sync device if timing is enabled + timing_sync(); +} + +template +void SumToBuffer::transfer_out(T *out) +{ + ScopedTimer t(this, "transfer out"); + gpu_memcpy_d2h(out, d_out_.get(), os_0_ * os_1_ * os_2_); +} + +/***** interface function **************/ + +// instantiate here to force creating all symbols +template class SumToBuffer; +template class SumToBuffer>; + +template +void sum_to_buffer_tc(const T *in1, + int in1_0, + int in1_1, + int in1_2, + T *out, + int out_0, + int out_1, + int out_2, + const int *in_addr, + int in_addr_0, + const int *out_addr, + int out_addr_0) +{ + auto s2b = gpuManager.get_cuda_function>( + "sum_to_buffer<" + getTypeName() + ">", + in1_0, + in1_1, + in1_2, + out_0, + out_1, + out_2, + in_addr_0, + out_addr_0); + s2b->calculateAddrIndices(out_addr); + s2b->allocate(); + s2b->transfer_in(in1, in_addr, out_addr); + s2b->run(); + s2b->transfer_out(out); +} + +extern "C" void sum_to_buffer_c(const float *in1, + int in1_0, + int in1_1, + int in1_2, + float *out, + int out_0, + int out_1, + int out_2, + const int *in_addr, + int in_addr_0, + const int *out_addr, + int out_addr_0, + int isComplex) +{ + if (isComplex != 0) + { + sum_to_buffer_tc(reinterpret_cast *>(in1), + in1_0, + in1_1, + in1_2, + reinterpret_cast *>(out), + out_0, + out_1, + out_2, + in_addr, + in_addr_0, + out_addr, + out_addr_0); + } + else + { + sum_to_buffer_tc(in1, + in1_0, + in1_1, + in1_2, + out, + out_0, + out_1, + out_2, + in_addr, + in_addr_0, + out_addr, + out_addr_0); + } +} + +template +void sum_to_buffer_stride_tc(const T *in1, + int in1_0, + int in1_1, + int in1_2, + T *out, + int out_0, + int out_1, + int out_2, + const int *addr_info, + int addr_info_0) +{ + auto s2b = gpuManager.get_cuda_function>( + "sum_to_buffer<" + getTypeName() + ">", + in1_0, + in1_1, + in1_2, + out_0, + out_1, + out_2, + addr_info_0, + addr_info_0); + s2b->setAddrStride(15); + auto in_addr = addr_info + 6; + auto out_addr = addr_info + 9; + s2b->calculateAddrIndices(out_addr); + s2b->allocate(); + s2b->transfer_in(in1, in_addr, out_addr); + s2b->run(); + s2b->transfer_out(out); +} + +extern "C" void sum_to_buffer_stride_c(const float *in1, + int in1_0, + int in1_1, + int in1_2, + float *out, + int out_0, + int out_1, + int out_2, + const int *addr_info, + int addr_info_0, + int isComplex) +{ + if (isComplex != 0) + { + sum_to_buffer_stride_tc(reinterpret_cast *>(in1), + in1_0, + in1_1, + in1_2, + reinterpret_cast *>(out), + out_0, + out_1, + out_2, + addr_info, + addr_info_0); + } + else + { + sum_to_buffer_stride_tc(in1, + in1_0, + in1_1, + in1_2, + out, + out_0, + out_1, + out_2, + addr_info, + addr_info_0); + } +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/func/sum_to_buffer.h b/archive/cuda_extension/cuda/func/sum_to_buffer.h new file mode 100644 index 000000000..8f20a5258 --- /dev/null +++ b/archive/cuda_extension/cuda/func/sum_to_buffer.h @@ -0,0 +1,47 @@ +#pragma once + +#include "utils/CudaFunction.h" +#include "utils/Memory.h" + +template +class SumToBuffer : public CudaFunction +{ +public: + SumToBuffer(); + void setParameters(int in1_0, + int in1_1, + int in1_2, + int os_0, + int os_1, + int os_2, + int in1_addr_0, + int out1_addr_0); + void setAddrStride(int stride); + int calculateAddrIndices(const int *out1_addr); + void setDeviceBuffers(T *d_in1, + T *d_out, + int *d_in1_addr, + int *d_out1_addr, + int *d_outidx, + int *d_startidx, + int *d_indices, + int outidx_size); + void allocate(); + T *getOutput() const; + void transfer_in(const T *in1, const int *in1_addr, const int *out1_addr); + void run(); + void transfer_out(T *out); + +private: + DevicePtrWrapper d_in1_; + DevicePtrWrapper d_out_; + DevicePtrWrapper d_in1_addr_; + DevicePtrWrapper d_out1_addr_; + DevicePtrWrapper d_outidx_, d_startidx_, d_indices_; + std::vector outidx_, startidx_, indices_; + int in1_0_ = 0, in1_1_ = 0, in1_2_ = 0; + int os_0_ = 0, os_1_ = 0, os_2_ = 0; + int in1_addr_0_ = 0, out1_addr_0_ = 0; + int addr_stride_ = 3; + int outidx_size_ = 0; +}; diff --git a/archive/cuda_extension/cuda/splines/README.md b/archive/cuda_extension/cuda/splines/README.md new file mode 100644 index 000000000..28179bda2 --- /dev/null +++ b/archive/cuda_extension/cuda/splines/README.md @@ -0,0 +1,6 @@ +These files have been shamelessly taken from https://github.com/DannyRuijters/CubicInterpolationCUDA +and modified to our needs. + +The copyright notice on top of the files has been left intact - please review. + +Note that spline interpolation isn't working at the moment, so this part of the sources isn't used. \ No newline at end of file diff --git a/archive/cuda_extension/cuda/splines/bspline_kernel.cuh b/archive/cuda_extension/cuda/splines/bspline_kernel.cuh new file mode 100644 index 000000000..f92233ecc --- /dev/null +++ b/archive/cuda_extension/cuda/splines/bspline_kernel.cuh @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------*\ +Copyright (c) 2008-2010, Danny Ruijters. All rights reserved. +http://www.dannyruijters.nl/cubicinterpolation/ +This file is part of CUDA Cubic B-Spline Interpolation (CI). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied. + +When using this code in a scientific project, please cite one or all of the +following papers: +* Daniel Ruijters and Philippe Th�venaz, + GPU Prefilter for Accurate Cubic B-Spline Interpolation, + The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. + http://dannyruijters.nl/docs/cudaPrefilter3.pdf +* Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, + Efficient GPU-Based Texture Interpolation using Uniform B-Splines, + Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. +\*--------------------------------------------------------------------------*/ + +#ifndef _CUDA_BSPLINE_H_ +#define _CUDA_BSPLINE_H_ + +#include "math_func.cuh" + +// Cubic B-spline function +// The 3rd order Maximal Order and Minimum Support function, that it is maximally differentiable. +inline __host__ __device__ float bspline(float t) +{ + t = fabs(t); + const float a = 2.0f - t; + + if (t < 1.0f) return 2.0f/3.0f - 0.5f*t*t*a; + else if (t < 2.0f) return a*a*a / 6.0f; + else return 0.0f; +} + +// The first order derivative of the cubic B-spline +inline __host__ __device__ float bspline_1st_derivative(float t) +{ + if (-2.0f < t && t <= -1.0f) return 0.5f*t*t + 2.0f*t + 2.0f; + else if (-1.0f < t && t <= 0.0f) return -1.5f*t*t - 2.0f*t; + else if ( 0.0f < t && t <= 1.0f) return 1.5f*t*t - 2.0f*t; + else if ( 1.0f < t && t < 2.0f) return -0.5f*t*t + 2.0f*t - 2.0f; + else return 0.0f; +} + +// The second order derivative of the cubic B-spline +inline __host__ __device__ float bspline_2nd_derivative(float t) +{ + t = fabs(t); + + if (t < 1.0f) return 3.0f*t - 2.0f; + else if (t < 2.0f) return 2.0f - t; + else return 0.0f; +} + +// Inline calculation of the bspline convolution weights, without conditional statements +template inline __device__ __host__ void bspline_weights(T fraction, T& w0, T& w1, T& w2, T& w3) +{ + const T one_frac = 1.0f - fraction; + const T squared = fraction * fraction; + const T one_sqd = one_frac * one_frac; + + w0 = 1.0f/6.0f * one_sqd * one_frac; + w1 = 2.0f/3.0f - 0.5f * squared * (2.0f-fraction); + w2 = 2.0f/3.0f - 0.5f * one_sqd * (2.0f-one_frac); + w3 = 1.0f/6.0f * squared * fraction; +} + +// Inline calculation of the first order derivative bspline convolution weights, without conditional statements +template inline __device__ void bspline_weights_1st_derivative(T fraction, T& w0, T& w1, T& w2, T& w3) +{ + const T squared = fraction * fraction; + + w0 = -0.5f * squared + fraction - 0.5f; + w1 = 1.5f * squared - 2.0f * fraction; + w2 = -1.5f * squared + fraction + 0.5f; + w3 = 0.5f * squared; +} + +// Inline calculation of the second order derivative bspline convolution weights, without conditional statements +template inline __device__ void bspline_weights_2nd_derivative(T fraction, T& w0, T& w1, T& w2, T& w3) +{ + w0 = 1.0f - fraction; + w1 = 3.0f * fraction - 2.0f; + w2 = -3.0f * fraction + 1.0f; + w3 = fraction; +} + +#endif // _CUDA_BSPLINE_H_ diff --git a/archive/cuda_extension/cuda/splines/cubicPrefilter2D.cu b/archive/cuda_extension/cuda/splines/cubicPrefilter2D.cu new file mode 100644 index 000000000..9c16bdcc8 --- /dev/null +++ b/archive/cuda_extension/cuda/splines/cubicPrefilter2D.cu @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------*\ +Copyright (c) 2008-2010, Danny Ruijters. All rights reserved. +http://www.dannyruijters.nl/cubicinterpolation/ +This file is part of CUDA Cubic B-Spline Interpolation (CI). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied. + +When using this code in a scientific project, please cite one or all of the +following papers: +* Daniel Ruijters and Philippe Th�venaz, + GPU Prefilter for Accurate Cubic B-Spline Interpolation, + The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. + http://dannyruijters.nl/docs/cudaPrefilter3.pdf +* Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, + Efficient GPU-Based Texture Interpolation using Uniform B-Splines, + Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. +\*--------------------------------------------------------------------------*/ + +#ifndef _2D_CUBIC_BSPLINE_PREFILTER_H_ +#define _2D_CUBIC_BSPLINE_PREFILTER_H_ + +#include +#include "cubicPrefilter_kernel.cuh" + +#include "utils/Errors.h" + +// *************************************************************************** +// * Global GPU procedures +// *************************************************************************** +template +__global__ void SamplesToCoefficients2DX( + floatN* image, // in-place processing + uint pitch, // width in bytes + uint width, // width of the image + uint height) // height of the image +{ + // process lines in x-direction + const uint y = blockIdx.x * blockDim.x + threadIdx.x; + floatN* line = (floatN*)((uchar*)image + y * pitch); //direct access + + ConvertToInterpolationCoefficients(line, width, sizeof(floatN)); +} + +template +__global__ void SamplesToCoefficients2DY( + floatN* image, // in-place processing + uint pitch, // width in bytes + uint width, // width of the image + uint height) // height of the image +{ + // process lines in x-direction + const uint x = blockIdx.x * blockDim.x + threadIdx.x; + floatN* line = image + x; //direct access + + ConvertToInterpolationCoefficients(line, height, pitch); +} + +// *************************************************************************** +// * Exported functions +// *************************************************************************** + +//! Convert the pixel values into cubic b-spline coefficients +//! @param image pointer to the image bitmap in GPU (device) memory +//! @param pitch width in bytes (including padding bytes) +//! @param width image width in number of pixels +//! @param height image height in number of pixels +template +extern void CubicBSplinePrefilter2D(floatN* image, uint pitch, uint width, uint height) +{ + dim3 dimBlockX(min(PowTwoDivider(height), 64)); + dim3 dimGridX(height / dimBlockX.x); + SamplesToCoefficients2DX<<>>(image, pitch, width, height); + checkLaunchErrors(); + + dim3 dimBlockY(min(PowTwoDivider(width), 64)); + dim3 dimGridY(width / dimBlockY.x); + SamplesToCoefficients2DY<<>>(image, pitch, width, height); + checkLaunchErrors(); +} + + +#endif //_2D_CUBIC_BSPLINE_PREFILTER_H_ diff --git a/archive/cuda_extension/cuda/splines/cubicPrefilter2D.cuh b/archive/cuda_extension/cuda/splines/cubicPrefilter2D.cuh new file mode 100644 index 000000000..e403bd76f --- /dev/null +++ b/archive/cuda_extension/cuda/splines/cubicPrefilter2D.cuh @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------*\ +Copyright (c) 2008-2010, Danny Ruijters. All rights reserved. +http://www.dannyruijters.nl/cubicinterpolation/ +This file is part of CUDA Cubic B-Spline Interpolation (CI). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied. + +When using this code in a scientific project, please cite one or all of the +following papers: +* Daniel Ruijters and Philippe Th�venaz, + GPU Prefilter for Accurate Cubic B-Spline Interpolation, + The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. + http://dannyruijters.nl/docs/cudaPrefilter3.pdf +* Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, + Efficient GPU-Based Texture Interpolation using Uniform B-Splines, + Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. +\*--------------------------------------------------------------------------*/ + +#ifndef _2D_CUBIC_BSPLINE_PREFILTER_H_ +#define _2D_CUBIC_BSPLINE_PREFILTER_H_ + +#include +#include "cubicPrefilter_kernel.cuh" + +#include "utils/Errors.h" + +// *************************************************************************** +// * Global GPU procedures +// *************************************************************************** +template +__global__ void SamplesToCoefficients2DX( + floatN* image, // in-place processing + uint pitch, // width in bytes + uint width, // width of the image + uint height) // height of the image +{ + // process lines in x-direction + const uint y = blockIdx.x * blockDim.x + threadIdx.x; + floatN* line = (floatN*)((uchar*)image + y * pitch); //direct access + + ConvertToInterpolationCoefficients(line, width, sizeof(floatN)); +} + +template +__global__ void SamplesToCoefficients2DY( + floatN* image, // in-place processing + uint pitch, // width in bytes + uint width, // width of the image + uint height) // height of the image +{ + // process lines in x-direction + const uint x = blockIdx.x * blockDim.x + threadIdx.x; + floatN* line = image + x; //direct access + + ConvertToInterpolationCoefficients(line, height, pitch); +} + +// *************************************************************************** +// * Exported functions +// *************************************************************************** + +//! Convert the pixel values into cubic b-spline coefficients +//! @param image pointer to the image bitmap in GPU (device) memory +//! @param pitch width in bytes (including padding bytes) +//! @param width image width in number of pixels +//! @param height image height in number of pixels +template +void CubicBSplinePrefilter2D(floatN* image, uint pitch, uint width, uint height) +{ + dim3 dimBlockX(min(PowTwoDivider(height), 64)); + dim3 dimGridX(height / dimBlockX.x); + SamplesToCoefficients2DX<<>>(image, pitch, width, height); + checkLaunchErrors(); + + dim3 dimBlockY(min(PowTwoDivider(width), 64)); + dim3 dimGridY(width / dimBlockY.x); + SamplesToCoefficients2DY<<>>(image, pitch, width, height); + checkLaunchErrors(); +} + + +#endif //_2D_CUBIC_BSPLINE_PREFILTER_H_ diff --git a/archive/cuda_extension/cuda/splines/cubicPrefilter_kernel.cu b/archive/cuda_extension/cuda/splines/cubicPrefilter_kernel.cu new file mode 100644 index 000000000..748be3e2a --- /dev/null +++ b/archive/cuda_extension/cuda/splines/cubicPrefilter_kernel.cu @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------*\ +Copyright (c) 2008-2012, Danny Ruijters. All rights reserved. +http://www.dannyruijters.nl/cubicinterpolation/ +This file is part of CUDA Cubic B-Spline Interpolation (CI). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied. + +When using this code in a scientific project, please cite one or all of the +following papers: +* Daniel Ruijters and Philippe Thévenaz, + GPU Prefilter for Accurate Cubic B-Spline Interpolation, + The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. + http://dannyruijters.nl/docs/cudaPrefilter3.pdf +* Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, + Efficient GPU-Based Texture Interpolation using Uniform B-Splines, + Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. +\*--------------------------------------------------------------------------*/ + +#ifndef _CUBIC_BSPLINE_PREFILTER_KERNEL_H_ +#define _CUBIC_BSPLINE_PREFILTER_KERNEL_H_ + +#include "math_func.cu" + +// The code below is based on the work of Philippe Thevenaz. +// See + +#define Pole (sqrt(3.0f)-2.0f) //pole for cubic b-spline + +//-------------------------------------------------------------------------- +// Local GPU device procedures +//-------------------------------------------------------------------------- +template +__host__ __device__ floatN InitialCausalCoefficient( + floatN* c, // coefficients + uint DataLength, // number of coefficients + int step) // element interleave in bytes +{ + const uint Horizon = UMIN(12, DataLength); + + // this initialization corresponds to clamping boundaries + // accelerated loop + float zn = Pole; + floatN Sum = *c; + for (uint n = 0; n < Horizon; n++) { + Sum += zn * *c; + zn *= Pole; + c = (floatN*)((uchar*)c + step); + } + return(Sum); +} + +template +__host__ __device__ floatN InitialAntiCausalCoefficient( + floatN* c, // last coefficient + uint DataLength, // number of samples or coefficients + int step) // element interleave in bytes +{ + // this initialization corresponds to clamping boundaries + return((Pole / (Pole - 1.0f)) * *c); +} + +template +__host__ __device__ void ConvertToInterpolationCoefficients( + floatN* coeffs, // input samples --> output coefficients + uint DataLength, // number of samples or coefficients + int step) // element interleave in bytes +{ + // compute the overall gain + const float Lambda = (1.0f - Pole) * (1.0f - 1.0f / Pole); + + // causal initialization + floatN* c = coeffs; + floatN previous_c; //cache the previously calculated c rather than look it up again (faster!) + *c = previous_c = Lambda * InitialCausalCoefficient(c, DataLength, step); + // causal recursion + for (uint n = 1; n < DataLength; n++) { + c = (floatN*)((uchar*)c + step); + *c = previous_c = Lambda * *c + Pole * previous_c; + } + // anticausal initialization + *c = previous_c = InitialAntiCausalCoefficient(c, DataLength, step); + // anticausal recursion + for (int n = DataLength - 2; 0 <= n; n--) { + c = (floatN*)((uchar*)c - step); + *c = previous_c = Pole * (previous_c - *c); + } +} + +#endif // _CUBIC_BSPLINE_PREFILTER_KERNEL_H_ diff --git a/archive/cuda_extension/cuda/splines/cubicPrefilter_kernel.cuh b/archive/cuda_extension/cuda/splines/cubicPrefilter_kernel.cuh new file mode 100644 index 000000000..5dc576428 --- /dev/null +++ b/archive/cuda_extension/cuda/splines/cubicPrefilter_kernel.cuh @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------*\ +Copyright (c) 2008-2012, Danny Ruijters. All rights reserved. +http://www.dannyruijters.nl/cubicinterpolation/ +This file is part of CUDA Cubic B-Spline Interpolation (CI). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied. + +When using this code in a scientific project, please cite one or all of the +following papers: +* Daniel Ruijters and Philippe Th�venaz, + GPU Prefilter for Accurate Cubic B-Spline Interpolation, + The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. + http://dannyruijters.nl/docs/cudaPrefilter3.pdf +* Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, + Efficient GPU-Based Texture Interpolation using Uniform B-Splines, + Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. +\*--------------------------------------------------------------------------*/ + +#ifndef _CUBIC_BSPLINE_PREFILTER_KERNEL_H_ +#define _CUBIC_BSPLINE_PREFILTER_KERNEL_H_ + +#include "math_func.cuh" + +// The code below is based on the work of Philippe Thevenaz. +// See + +#define Pole (sqrt(3.0f)-2.0f) //pole for cubic b-spline + +//-------------------------------------------------------------------------- +// Local GPU device procedures +//-------------------------------------------------------------------------- +template +__host__ __device__ floatN InitialCausalCoefficient( + floatN* c, // coefficients + uint DataLength, // number of coefficients + int step) // element interleave in bytes +{ + const uint Horizon = UMIN(12, DataLength); + + // this initialization corresponds to clamping boundaries + // accelerated loop + float zn = Pole; + floatN Sum = *c; + for (uint n = 0; n < Horizon; n++) { + Sum += zn * *c; + zn *= Pole; + c = (floatN*)((uchar*)c + step); + } + return(Sum); +} + +template +__host__ __device__ floatN InitialAntiCausalCoefficient( + floatN* c, // last coefficient + uint DataLength, // number of samples or coefficients + int step) // element interleave in bytes +{ + // this initialization corresponds to clamping boundaries + return((Pole / (Pole - 1.0f)) * *c); +} + +template +__host__ __device__ void ConvertToInterpolationCoefficients( + floatN* coeffs, // input samples --> output coefficients + uint DataLength, // number of samples or coefficients + int step) // element interleave in bytes +{ + // compute the overall gain + const float Lambda = (1.0f - Pole) * (1.0f - 1.0f / Pole); + + // causal initialization + floatN* c = coeffs; + floatN previous_c; //cache the previously calculated c rather than look it up again (faster!) + *c = previous_c = Lambda * InitialCausalCoefficient(c, DataLength, step); + // causal recursion + for (uint n = 1; n < DataLength; n++) { + c = (floatN*)((uchar*)c + step); + *c = previous_c = Lambda * *c + Pole * previous_c; + } + // anticausal initialization + *c = previous_c = InitialAntiCausalCoefficient(c, DataLength, step); + // anticausal recursion + for (int n = DataLength - 2; 0 <= n; n--) { + c = (floatN*)((uchar*)c - step); + *c = previous_c = Pole * (previous_c - *c); + } +} + +#endif // _CUBIC_BSPLINE_PREFILTER_KERNEL_H_ diff --git a/archive/cuda_extension/cuda/splines/math_func.cu b/archive/cuda_extension/cuda/splines/math_func.cu new file mode 100644 index 000000000..1f9799f23 --- /dev/null +++ b/archive/cuda_extension/cuda/splines/math_func.cu @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------*\ +Copyright (c) 2008-2009, Danny Ruijters. All rights reserved. +http://www.dannyruijters.nl/cubicinterpolation/ +This file is part of CUDA Cubic B-Spline Interpolation (CI). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied. + +When using this code in a scientific project, please cite one or all of the +following papers: +* Daniel Ruijters and Philippe Thévenaz, + GPU Prefilter for Accurate Cubic B-Spline Interpolation, + The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. + http://dannyruijters.nl/docs/cudaPrefilter3.pdf +* Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, + Efficient GPU-Based Texture Interpolation using Uniform B-Splines, + Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. +\*--------------------------------------------------------------------------*/ + +#ifndef _MATH_FUNC_CUDA_H_ +#define _MATH_FUNC_CUDA_H_ + +#include "version.cu" + +typedef unsigned int uint; +typedef unsigned short ushort; +typedef unsigned char uchar; +typedef signed char schar; + +inline __device__ __host__ uint UMIN(uint a, uint b) +{ + return a < b ? a : b; +} + +inline __device__ __host__ uint PowTwoDivider(uint n) +{ + if (n == 0) return 0; + uint divider = 1; + while ((n & divider) == 0) divider <<= 1; + return divider; +} + +inline __host__ __device__ float2 operator-(float a, float2 b) +{ + return make_float2(a - b.x, a - b.y); +} + +inline __host__ __device__ float3 operator-(float a, float3 b) +{ + return make_float3(a - b.x, a - b.y, a - b.z); +} + +#endif //_MATH_FUNC_CUDA_H_ diff --git a/archive/cuda_extension/cuda/splines/math_func.cuh b/archive/cuda_extension/cuda/splines/math_func.cuh new file mode 100644 index 000000000..16ae20866 --- /dev/null +++ b/archive/cuda_extension/cuda/splines/math_func.cuh @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------*\ +Copyright (c) 2008-2009, Danny Ruijters. All rights reserved. +http://www.dannyruijters.nl/cubicinterpolation/ +This file is part of CUDA Cubic B-Spline Interpolation (CI). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied. + +When using this code in a scientific project, please cite one or all of the +following papers: +* Daniel Ruijters and Philippe Th�venaz, + GPU Prefilter for Accurate Cubic B-Spline Interpolation, + The Computer Journal, vol. 55, no. 1, pp. 15-20, January 2012. + http://dannyruijters.nl/docs/cudaPrefilter3.pdf +* Daniel Ruijters, Bart M. ter Haar Romeny, and Paul Suetens, + Efficient GPU-Based Texture Interpolation using Uniform B-Splines, + Journal of Graphics Tools, vol. 13, no. 4, pp. 61-69, 2008. +\*--------------------------------------------------------------------------*/ + +#ifndef _MATH_FUNC_CUDA_H_ +#define _MATH_FUNC_CUDA_H_ + +typedef unsigned int uint; +typedef unsigned short ushort; +typedef unsigned char uchar; +typedef signed char schar; + +inline __device__ __host__ uint UMIN(uint a, uint b) +{ + return a < b ? a : b; +} + +inline __device__ __host__ uint PowTwoDivider(uint n) +{ + if (n == 0) return 0; + uint divider = 1; + while ((n & divider) == 0) divider <<= 1; + return divider; +} + +inline __host__ __device__ float2 operator-(float a, float2 b) +{ + return make_float2(a - b.x, a - b.y); +} + +inline __host__ __device__ float3 operator-(float a, float3 b) +{ + return make_float3(a - b.x, a - b.y, a - b.z); +} + +#endif //_MATH_FUNC_CUDA_H_ diff --git a/archive/cuda_extension/cuda/tests/gaussian_weights_test.cpp b/archive/cuda_extension/cuda/tests/gaussian_weights_test.cpp new file mode 100644 index 000000000..afe80bc85 --- /dev/null +++ b/archive/cuda_extension/cuda/tests/gaussian_weights_test.cpp @@ -0,0 +1,27 @@ +#include "utils/GaussianWeights.h" +#include + +#include + +TEST(GaussianFilter, weightsCompareToPython) +{ + std::vector expected = {1.9947465e-01f, + 1.7603576e-01f, + 1.2098749e-01f, + 6.4759940e-02f, + 2.6995959e-02f, + 8.7643042e-03f, + 2.2159631e-03f, + 4.3634902e-04f, + 6.6916291e-05f}; + auto actual = gaussian_kernel1d(2, 8); + EXPECT_EQ(actual.size(), expected.size()); + + for (int i = 0; i < expected.size(); ++i) + { + EXPECT_NEAR(expected[i], actual[i], 1e-4f); + } + auto sum_right = std::accumulate(actual.begin() + 1, actual.end(), 0.0f); + auto sum = 2 * sum_right + actual[0]; + EXPECT_NEAR(sum, 1.0f, 1e-4f); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/tests/indexing_test.cpp b/archive/cuda_extension/cuda/tests/indexing_test.cpp new file mode 100644 index 000000000..101c4eee0 --- /dev/null +++ b/archive/cuda_extension/cuda/tests/indexing_test.cpp @@ -0,0 +1,94 @@ +#include + +#include "utils/Indexing.h" + +TEST(IndexingTest, inRange) +{ + IndexReflect ind(1, 10); + EXPECT_EQ(1, ind(1)); + EXPECT_EQ(5, ind(5)); + EXPECT_EQ(3, ind(3)); + EXPECT_EQ(9, ind(9)); +} + +TEST(IndexingTest, reflectTop1) +{ + IndexReflect ind(1, 10); + EXPECT_EQ(9, ind(10)); + EXPECT_EQ(8, ind(11)); + EXPECT_EQ(7, ind(12)); + EXPECT_EQ(6, ind(13)); +} + +TEST(IndexingTest, reflectTop0) +{ + IndexReflect ind(0, 10); + EXPECT_EQ(9, ind(10)); + EXPECT_EQ(8, ind(11)); + EXPECT_EQ(7, ind(12)); + EXPECT_EQ(6, ind(13)); +} + +TEST(IndexingTest, reflectTopNeg) +{ + IndexReflect ind(-5, 0); + EXPECT_EQ(-2, ind(1)); + EXPECT_EQ(-3, ind(2)); + EXPECT_EQ(-4, ind(3)); + EXPECT_EQ(-5, ind(4)); +} + +TEST(IndexingTest, reflectBottom) +{ + IndexReflect ind(1, 10); + EXPECT_EQ(1, ind(0)); + EXPECT_EQ(2, ind(-1)); + EXPECT_EQ(3, ind(-2)); +} + +TEST(IndexingTest, reflectBottom0) +{ + IndexReflect ind(0, 10); + EXPECT_EQ(0, ind(0)); + EXPECT_EQ(0, ind(-1)); + EXPECT_EQ(1, ind(-2)); + EXPECT_EQ(2, ind(-3)); +} + +TEST(IndexingTest, reflectBottomNeg) +{ + IndexReflect ind(-5, -1); + EXPECT_EQ(-5, ind(-5)); + EXPECT_EQ(-5, ind(-6)); + EXPECT_EQ(-4, ind(-7)); + EXPECT_EQ(-3, ind(-8)); +} + +TEST(IndexingTest, reflectTopWide) +{ + IndexReflect ind(1, 3); + EXPECT_EQ(2, ind(3)); + EXPECT_EQ(1, ind(4)); + EXPECT_EQ(1, ind(5)); + EXPECT_EQ(2, ind(6)); +} + +TEST(IndexingTest, reflectTopWideNeg) +{ + IndexReflect ind(-4, -1); + EXPECT_EQ(-3, ind(0)); + EXPECT_EQ(-4, ind(1)); + EXPECT_EQ(-4, ind(2)); + EXPECT_EQ(-3, ind(3)); + EXPECT_EQ(-2, ind(4)); + EXPECT_EQ(-2, ind(5)); +} + +TEST(IndexingTest, reflectBottomWide) +{ + IndexReflect ind(1, 3); + EXPECT_EQ(2, ind(-2)); + EXPECT_EQ(1, ind(-3)); + EXPECT_EQ(1, ind(-4)); + EXPECT_EQ(2, ind(-5)); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/utils/Complex.h b/archive/cuda_extension/cuda/utils/Complex.h new file mode 100644 index 000000000..4192fe060 --- /dev/null +++ b/archive/cuda_extension/cuda/utils/Complex.h @@ -0,0 +1,5 @@ +#pragma once +#include + +using thrust::complex; + diff --git a/archive/cuda_extension/cuda/utils/CudaFunction.cpp b/archive/cuda_extension/cuda/utils/CudaFunction.cpp new file mode 100644 index 000000000..0238782f3 --- /dev/null +++ b/archive/cuda_extension/cuda/utils/CudaFunction.cpp @@ -0,0 +1,19 @@ +#include "utils/CudaFunction.h" + +#include + +CudaFunction::CudaFunction(const std::string& name) : name_(name) {} + +void CudaFunction::printTimes() const +{ +#if DO_GPU_TIMING + std::cout << "\nTiming stats for " << name_ << ":\n"; + for (auto& t : times_) + { + std::cout << t.first << "=" << t.second << "ms\n"; + } + std::cout.flush(); +#endif +} + +CudaFunction::~CudaFunction() { printTimes(); } \ No newline at end of file diff --git a/archive/cuda_extension/cuda/utils/CudaFunction.h b/archive/cuda_extension/cuda/utils/CudaFunction.h new file mode 100644 index 000000000..dac018cf0 --- /dev/null +++ b/archive/cuda_extension/cuda/utils/CudaFunction.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +/** Used as base class for all CUDA functions. + * + * Mainly used for factoring out gpu timing events, using + * ScopedTimer inside the child. + */ +class CudaFunction +{ +protected: + CudaFunction(const std::string &name); + +public: + void printTimes() const; + /** Calls printTimes() if timing is enabled */ + virtual ~CudaFunction(); + +private: + friend class ScopedTimer; + std::string name_; + std::vector> times_; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/utils/Errors.h b/archive/cuda_extension/cuda/utils/Errors.h new file mode 100644 index 000000000..244c1563f --- /dev/null +++ b/archive/cuda_extension/cuda/utils/Errors.h @@ -0,0 +1,122 @@ +#pragma once + +#include "utils/Patches.h" + +#include +#include +#include +#include + +/** GPU exceptions - typically thrown from CUDA API failures. + * + * Using the checkCudaErrors() macro around each call makes sure + * that this exception is thrown on error. + */ +class GPUException : public std::runtime_error +{ +public: + GPUException(int code, + const std::string &what, + const std::string &instruction = "", + const std::string &file = "", + int line = -1) + : std::runtime_error("GPU Exception at " + file + ":" + + std::to_string(line) + ": " + what + + " @ instruction '" + instruction + "'") + { + } + GPUException(const std::string &what) : std::runtime_error(what) {} +}; + +/// Wrap this around cuda or cufft API calls +#define checkCudaErrors(val) \ + detail::handleCudaErrors((val), #val, __FILE__, __LINE__) + +/// Use this macro to check if a kernel launch failed. +/// That is, call this straight after a manual kernel launch, without sync. +#define checkLaunchErrors() checkCudaErrors(cudaPeekAtLastError()) + +/// Implementation details, translating macro to exception +namespace detail +{ +inline void handleCudaErrors(cudaError_t e, + const char *func, + const char *file, + int line) +{ + if (e) + { + cudaDeviceReset(); + throw GPUException(int(e), cudaGetErrorString(e), func, file, line); + } +} + +inline void handleCudaErrors(cufftResult e, + const char *func, + const char *file, + int line) +{ + if (e) + { + const char *errstr = nullptr; + switch (e) + { + case CUFFT_INVALID_PLAN: + errstr = "cuFFT was passed an invalid plan handle"; + break; + case CUFFT_ALLOC_FAILED: + errstr = "cuFFT failed to allocate GPU or CPU memory"; + break; + case CUFFT_INVALID_TYPE: + errstr = "No longer used"; + break; + case CUFFT_INVALID_VALUE: + errstr = "User specified an invalid pointer or parameter"; + break; + case CUFFT_INTERNAL_ERROR: + errstr = "Driver or internal cuFFT library error"; + break; + case CUFFT_EXEC_FAILED: + errstr = "Failed to execute an FFT on the GPU"; + break; + case CUFFT_SETUP_FAILED: + errstr = "The cuFFT library failed to initialize"; + break; + case CUFFT_INVALID_SIZE: + errstr = "User specified an invalid transform size"; + break; + case CUFFT_UNALIGNED_DATA: + errstr = " No longer used"; + break; + case CUFFT_INCOMPLETE_PARAMETER_LIST: + errstr = " Missing parameters in call "; + break; + case CUFFT_INVALID_DEVICE: + errstr = "Execution of a plan was on different GPU than plan creation"; + break; + case CUFFT_PARSE_ERROR: + errstr = "Internal plan database error"; + break; + case CUFFT_NO_WORKSPACE: + errstr = "No workspace has been provided prior to plan execution"; + break; + case CUFFT_NOT_IMPLEMENTED: + errstr = + "Function does not implement functionality for parameters given."; + break; + case CUFFT_LICENSE_ERROR: + errstr = "Used in previous versions."; + break; + case CUFFT_NOT_SUPPORTED: + errstr = "Operation is not supported for parameters given."; + break; + default: + errstr = "Unknown"; + } + + cudaDeviceReset(); + throw GPUException(int(e), errstr, func, file, line); + } +} + +} // namespace detail diff --git a/archive/cuda_extension/cuda/utils/FinalSumKernel.h b/archive/cuda_extension/cuda/utils/FinalSumKernel.h new file mode 100644 index 000000000..47181b34d --- /dev/null +++ b/archive/cuda_extension/cuda/utils/FinalSumKernel.h @@ -0,0 +1,36 @@ + +// to be run in a single thread block +template +__global__ void final_sum(const T* data, T* output, int n) +{ + int tid = threadIdx.x; + extern __shared__ T sum[]; + + auto val = 0.0f; + // in case we have more data than threads in this block + for (int i = tid; i < n; i += blockDim.x) + { + val += data[i]; + } + sum[tid] = val; + + // now add up sumbuffer in shared memory + __syncthreads(); + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tid < half) + { + sum[tid] += sum[c - tid - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tid == 0) + { + output[0] = sum[0]; + } +} diff --git a/archive/cuda_extension/cuda/utils/GaussianWeights.h b/archive/cuda_extension/cuda/utils/GaussianWeights.h new file mode 100644 index 000000000..855437091 --- /dev/null +++ b/archive/cuda_extension/cuda/utils/GaussianWeights.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +// 1D Gaussian convolution kernel (symmetric) +inline std::vector gaussian_kernel1d(float sigma, int radius) +{ + float factor = -0.5f / (sigma * sigma); + std::vector weights(radius + 1); + float sum = 0.0f; + for (auto i = 0; i < radius + 1; ++i) + { + weights[i] = std::exp(factor * (i * i)); + if (i == 0) + sum += weights[i]; + else + sum += 2.0f * weights[i]; // for symmetry + } + for (auto i = 0; i < radius + 1; ++i) + { + weights[i] /= sum; + } + return std::move(weights); +} diff --git a/archive/cuda_extension/cuda/utils/GpuManager.cu b/archive/cuda_extension/cuda/utils/GpuManager.cu new file mode 100644 index 000000000..677f56eee --- /dev/null +++ b/archive/cuda_extension/cuda/utils/GpuManager.cu @@ -0,0 +1,104 @@ +#include "GpuManager.h" +#include +#include + +GpuManager::GpuManager() +{ + int numdev; + cudaGetDeviceCount(&numdev); + if (numdev == 0) + { + std::cout << "No GPUs found on this system\n"; + return; + } + + properties_.resize(numdev); + for (int i = 0; i < numdev; ++i) + { + cudaGetDeviceProperties(&properties_[i], i); + } + + fftDummyHandles_.resize(numdev); +} + +void GpuManager::selectDevice(int dev) +{ + if (dev >= int(properties_.size())) + { + throw GPUException("GPU " + std::to_string(dev) + " does not exist"); + } + checkCudaErrors(cudaSetDevice(dev)); + if (selectedDevice_ == dev) + { + return; + } + selectedDevice_ = dev; + + // not initialised yet + if (fftDummyHandles_[dev] == 0) + { + // init cufft and cuda context by creating a plan + checkCudaErrors(cufftPlan1d(&fftDummyHandles_[dev], 32, CUFFT_C2C, 1)); + } +} + +int GpuManager::getNumDevices() const { return int(properties_.size()); } + +std::string GpuManager::getDeviceName(int id) const +{ + return properties_[id].name; +} + +int GpuManager::getDeviceComputeCapability(int id) const +{ + return properties_[id].major * 10 + properties_[id].minor; +} + +int GpuManager::getDeviceMemoryMB(int id) const +{ + return int(properties_[id].totalGlobalMem / 1024ul / 1024ul); +} + +void GpuManager::resetFunctionCache() { functions_cache_.clear(); } + +GpuManager::~GpuManager() +{ + for (int i = 0, ni = int(fftDummyHandles_.size()); i < ni; ++i) + { + if (fftDummyHandles_[i] != 0) + { + cudaSetDevice(i); + cufftDestroy(fftDummyHandles_[i]); + cudaDeviceReset(); + } + } +} + +// for memory tracking - see Memory.cpp +// needs to be here so that it gets destructed +// after the GpuManager object +std::map alloc_map_; +size_t alloc_total = 0; + +// instatiate the object +GpuManager gpuManager; + +/**** interface functions for python ******/ + +extern "C" +{ + int get_num_gpus_c() { return gpuManager.getNumDevices(); } + + int get_gpu_compute_capability_c(int dev) + { + return gpuManager.getDeviceComputeCapability(dev); + } + + void select_gpu_device_c(int dev) { gpuManager.selectDevice(dev); } + + int get_gpu_memory_mb_c(int dev) { return gpuManager.getDeviceMemoryMB(dev); } + + std::string get_gpu_name_c(int dev) { return gpuManager.getDeviceName(dev); } + + void reset_function_cache_c() { gpuManager.resetFunctionCache(); } +} diff --git a/archive/cuda_extension/cuda/utils/GpuManager.h b/archive/cuda_extension/cuda/utils/GpuManager.h new file mode 100644 index 000000000..8f6cbfbd4 --- /dev/null +++ b/archive/cuda_extension/cuda/utils/GpuManager.h @@ -0,0 +1,163 @@ +#pragma once + +#include "utils/Complex.h" +#include "utils/Memory.h" + +#include +#include +#include +#include + +#include "utils/CudaFunction.h" +#include +#include + +/** Responsible for managing the GPUs globally. + * + * It creates the context and might eventually hold persistent + * CudaFunction objects, to keep memory allocated and re-usable. + * + * Don't create an instance directly - use the global object + * gpuManager instead. + */ +class GpuManager +{ +public: + /** Queries available devices and their properties, but doesn't initialise the + * context + */ + GpuManager(); + + /** Get number of available GPUs. + * + * Note the Cuda GPUs are numbered with integer indices, always continuous. + */ + int getNumDevices() const; + + /** Get the name of a specific GPU */ + std::string getDeviceName(int id) const; + + /** Get the compute capability of a specific GPU, in format 35 for 3.5. */ + int getDeviceComputeCapability(int id) const; + + /** Get global memory available in MB */ + int getDeviceMemoryMB(int id) const; + + /** Select a specific device to use for all calculations that follow. + * + * Note that this function creates the context on the given device, + * to avoid setup delays. Also, this device should not be changed once + * functions have been executed already. A resetFunctionCache call is needed + * before in this case. + */ + void selectDevice(int dev = 0); + + /** Resets (clears) the internal cache for CudaFunction objects + * + * Subsequent calls to CudaFunctions will re-create them, allocating new + * memory, etc. + */ + void resetFunctionCache(); + + /** Function to obtain an instance of a specific CudaFunction, given its name + * key and parameters. + * + * This functions looks up in the cache if an object with the given name + * already exists. The name is used as a key in the cache (so it should be + * unique for each distinct CudaFunction type). + * + * If the function isn't in the cache, it is created. + * + * After that, it calls setParameters on the CudaFunction, passing the + * variable parameters given to this function after the name key. + * + * Therefore all CudaFunction objects created with this functions must: + * - derive from the CudaFunction base class + * - have a default constructor + * - have a setParameters() method which accepts the arguments passed this + * this method. + * - should be written so that it respects updated setParameters in subsquent + * calls to allocate, transfer_in, setDeviceBuffers, etc... + */ + template + T* get_cuda_function(const std::string& key, Args&&... args) + { + // setup device if not already done + if (selectedDevice_ == -1) + { + selectDevice(0); + } + + // create if not already there (with default constructor) + if (functions_cache_.find(key) == functions_cache_.end()) + { + functions_cache_[key] = std::unique_ptr(new T()); + } + + // retrieve and set the parameters (they might have changed + // since we cached them) + auto ret = dynamic_cast(functions_cache_[key].get()); + if (ret == nullptr) + { + throw GPUException("cached CudaFunction with key " + key + + " doesn't match the requested type"); + } + ret->setParameters(std::forward(args)...); + + return ret; + } + + ~GpuManager(); + +private: + std::vector properties_; + std::vector fftDummyHandles_; + int selectedDevice_ = -1; + std::map> functions_cache_; +}; + +/** Global object to manage the GPUs */ +extern GpuManager gpuManager; + +/** Type helper - converting type signature into a string. + * + * This is used for generating a signature key to use in the function_cache + * above - to avoid for example Abs2 and Abs2 to have the same + * key in the cache. + **/ +template +inline std::string getTypeName() +{ + return "unknown"; +} +template <> +inline std::string getTypeName() +{ + return "float"; +} +template <> +inline std::string getTypeName() +{ + return "double"; +} +template <> +inline std::string getTypeName>() +{ + return "c_float"; +} +template <> +inline std::string getTypeName>() +{ + return "c_double"; +} + +/**** interface functions for python ******/ +extern "C" +{ + int get_num_gpus_c(); + int get_gpu_compute_capability_c(int dev); + void select_gpu_device_c(int dev); + int get_gpu_memory_mb_c(int dev); + std::string get_gpu_name_c(int dev); + void reset_function_cache_c(); +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/utils/Indexing.h b/archive/cuda_extension/cuda/utils/Indexing.h new file mode 100644 index 000000000..c2087f1ba --- /dev/null +++ b/archive/cuda_extension/cuda/utils/Indexing.h @@ -0,0 +1,40 @@ +#pragma once +#include + +// allow testing this on CPU +#ifndef __NVCC__ +#define __device__ +#endif + +/** Implements reflect-mode index wrapping + * + * maps indexes like this (reflects on both ends): + * Extension | Input | Extension + * 5 6 6 5 4 3 2 | 2 3 4 5 6 | 6 5 4 3 2 2 3 4 5 6 6 + * + */ +class IndexReflect +{ +public: + /** Create index range. maxX is not included in the valid range, + * i.e., the range is [minX, maxX) + */ + __device__ IndexReflect(int minX, int maxX) : maxX_(maxX), minX_(minX) {} + + /// Map given index to the valid range using reflect mode + __device__ int operator()(int idx) const + { + if (idx < maxX_ && idx >= minX_) + return idx; + auto ddd = (idx - minX_) / (maxX_ - minX_); + auto mmm = (idx - minX_) % (maxX_ - minX_); + if (mmm < 0) + mmm = -mmm - 1; + // if odd it goes backwards from max + // if even it goes upwards from min + return ddd % 2 == 0 ? minX_ + mmm : maxX_ - mmm - 1; + } + +private: + int maxX_, minX_; +}; diff --git a/archive/cuda_extension/cuda/utils/Memory.cpp b/archive/cuda_extension/cuda/utils/Memory.cpp new file mode 100644 index 000000000..19abd8ef4 --- /dev/null +++ b/archive/cuda_extension/cuda/utils/Memory.cpp @@ -0,0 +1,31 @@ +#include +#include + +// we have to define this in GpuManager.cu +// otherwise it gets destructed before the CudaFunctions +// are destroyed (potentially at least) +extern std::map alloc_map_; +extern size_t alloc_total; + +void debug_addMemory(void* ptr, size_t size) +{ + alloc_map_[ptr] = size; + alloc_total += size; +} + +void debug_freeMemory(void* ptr) +{ + if (alloc_map_.find(ptr) != alloc_map_.end()) + { + auto size = alloc_map_[ptr]; + alloc_total -= size; + alloc_map_.erase(ptr); + } + else + { + std::cerr << "WARNING: freeing memory of ptr " << ptr + << " that hasn't been registered before" << std::endl; + } +} + +size_t debug_getMemory() { return alloc_total; } \ No newline at end of file diff --git a/archive/cuda_extension/cuda/utils/Memory.h b/archive/cuda_extension/cuda/utils/Memory.h new file mode 100644 index 000000000..9d5b8846d --- /dev/null +++ b/archive/cuda_extension/cuda/utils/Memory.h @@ -0,0 +1,195 @@ +#pragma once + +#include "utils/Errors.h" +#include +#include + +// debugging functions, to record allocated and freed GPU memory +// allows inspection of how much memory has been allocated on the gpu +void debug_addMemory(void *ptr, size_t size); +void debug_freeMemory(void *ptr); +size_t debug_getMemory(); + +/** Allocate GPU memory, giving size in unit of sizeof(T) */ +template +inline void gpu_malloc(T *&ptr, size_t size) +{ +#ifndef NDEBUG + std::cout << "allocating " << double(size) << " on the GPU" << std::endl; +#endif + checkCudaErrors(cudaMalloc((void **)&ptr, sizeof(T) * size)); + // set it to zero + checkCudaErrors(cudaMemset(ptr, 0, sizeof(T) * size)); +#ifndef NDEBUG + debug_addMemory((void *)ptr, size); + std::cout << "Allocated " << (void *)ptr + << ", total: " << double(debug_getMemory()) << std::endl; +#endif +} + +template +inline void gpu_free(T *ptr) +{ + if (ptr) + { +#ifndef NDEBUG + std::cout << "freeing for pointer: " << (void *)ptr << std::endl; + debug_freeMemory((void *)ptr); +#endif + cudaFree(ptr); +#ifndef NDEBUG + std::cout << "Total allocated: " << double(debug_getMemory()) << std::endl; +#endif + } +} + +/** Transfers data from host to device. + * + * If any pointer is null, it silently doesn't transfer. + * + * @param device Pointer to device-allocated memory + * @param host Pointer to host-memory + * @param size Number of elmentary T items to transfer + */ +template +inline void gpu_memcpy_h2d(T *device, const T *host, size_t size) +{ + if (!host || !device) + return; + checkCudaErrors( + cudaMemcpy(device, host, sizeof(T) * size, cudaMemcpyHostToDevice)); +} + +/** Transfers data from device to host. + * + * If any pointer is null, it silently doesn't transfer. + * + * @param device Pointer to device-allocated memory + * @param host Pointer to host-memory + * @param size Number of elmentary T items to transfer + */ +template +inline void gpu_memcpy_d2h(T *host, const T *device, size_t size) +{ + if (!host || !device) + return; + checkCudaErrors( + cudaMemcpy(host, device, sizeof(T) * size, cudaMemcpyDeviceToHost)); +} + +template +inline void gpu_memcpy_d2d(T *dev_dst, const T *dev_src, size_t size) +{ + if (!dev_dst || !dev_src) + return; + checkCudaErrors( + cudaMemcpy(dev_dst, dev_src, sizeof(T) * size, cudaMemcpyDeviceToDevice)); +} + +/** Wraps a device pointer in RAII fashion, allowing to set an externally + * allocated pointer as well. + * + * If an externally-allocated pointer is set, it will return this one instead + * and avoid internal allocation. + */ +template +class DevicePtrWrapper +{ +public: + /** Default constructor */ + DevicePtrWrapper() = default; + /** Not copyable */ + DevicePtrWrapper(const DevicePtrWrapper &) = delete; + /** Not copyable */ + DevicePtrWrapper &operator=(const DevicePtrWrapper &) = delete; + /** Movable */ + DevicePtrWrapper(DevicePtrWrapper &&) = default; + /** Movable */ + DevicePtrWrapper &operator=(DevicePtrWrapper &&) = default; + + /** Assign an external pointer to use. + * + * If null, the internal pointer will be used instead + */ + DevicePtrWrapper &operator=(T *dptr) + { + set_external(dptr); + return *this; + } + + /** Allocates memory for the internal pointer. + * + * If an external pointer was set before, this function does not allocate + * anything. + * + * If the internal pointer was already allocated and size <= previous size, + * it doesn't allocate again. + * + * @param size Number of items to allocate memory for. + */ + void allocate(size_t size) + { + if (!isExternal()) + { + if (d_internal_ && size_ < size) + { + gpu_free(d_internal_); + } + if (d_internal_ && size_ >= size) + { + return; + } + + gpu_malloc(d_internal_, size); + size_ = size; + } + } + + /** Sets the external device pointer. + * + * Same as operator= + */ + void set_external(T *d) { d_external_ = d; } + + /** Unsets the external pointer - interal will be used from then on. + * + * Effectively sets the external pointer to null. + */ + void unset_external() { d_external_ = nullptr; } + + /** Check if external pointer is set */ + bool isExternal() const { return d_external_ != nullptr; } + + /** Amount of memory allocated for internal pointer. */ + size_t size() const { return size_; } + + /** Implicit conversion to bool - see if a pointer is set */ + operator bool() const { return get() != nullptr; } + + /** Get the underlying point (internal or external) */ + T *get() const + { + if (isExternal()) + { + return d_external_; + } + else + { + return d_internal_; + } + } + + /** Destructor - deallocate memory for internal pointer. */ + ~DevicePtrWrapper() + { + if (d_internal_) + { + gpu_free(d_internal_); + } + } + +private: + T *d_external_ = nullptr; + T *d_internal_ = nullptr; + size_t size_ = 0; +}; diff --git a/archive/cuda_extension/cuda/utils/Patches.h b/archive/cuda_extension/cuda/utils/Patches.h new file mode 100644 index 000000000..a0dbf111a --- /dev/null +++ b/archive/cuda_extension/cuda/utils/Patches.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +// some GCC versions don't provide the following functions, so we patch them in + +namespace patch +{ +template +inline std::string to_string(const T& x) +{ + std::ostringstream sstr; + sstr << x; + return sstr.str(); +} +} // namespace patch + +namespace std +{ +using namespace ::patch; +} \ No newline at end of file diff --git a/archive/cuda_extension/cuda/utils/ScopedTimer.h b/archive/cuda_extension/cuda/utils/ScopedTimer.h new file mode 100644 index 000000000..8205cb981 --- /dev/null +++ b/archive/cuda_extension/cuda/utils/ScopedTimer.h @@ -0,0 +1,51 @@ +#pragma once + +#include "utils/CudaFunction.h" +#include "utils/Errors.h" +#include "utils/Timer.h" +#include + +/** If we do timing, this function syncs all device execution code first. + * + * Otherwise it doesn't do anything + */ +inline void timing_sync() +{ +#if DO_GPU_TIMING + checkCudaErrors(cudaDeviceSynchronize()); +#endif +} + +/** Scoped RAII-style timer class, that records times from construction + * to destruction and logs it in a CudaFunction object. + */ +class ScopedTimer +{ +public: + /** Constructor. Starts recording time immediately. + * + * @param func CudaFunction object to store the resulting time in. + * @param name Name of the operation that is timed. + */ + ScopedTimer(CudaFunction *func, const std::string &name) + : func_(func), name_(name) + { + } + + /** Stops timing and records the time in the CudaFunction given + * to the constructor (only if DO_GPU_TIMING is defined). + */ + ~ScopedTimer() + { +#if DO_GPU_TIMING + func_->times_.emplace_back(name_, t_.get_time()); +#endif + } + +private: +#if DO_GPU_TIMING + Timer t_; +#endif + CudaFunction *func_; + std::string name_; +}; \ No newline at end of file diff --git a/archive/cuda_extension/cuda/utils/Timer.h b/archive/cuda_extension/cuda/utils/Timer.h new file mode 100644 index 000000000..53f880bab --- /dev/null +++ b/archive/cuda_extension/cuda/utils/Timer.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +/** Generic timer class, based on std::chrono . + * + * Use as: + * @code + * Timer t; + * ... // do expensive stuff + * auto time = t.get_time(); // in ms + * @encode + */ +class Timer +{ +public: + Timer() { start(); } + void start() { start_ = std::chrono::high_resolution_clock::now(); } + double get_time() + { + auto end = std::chrono::high_resolution_clock::now(); + return double(std::chrono::duration_cast(end - + start_) + .count()) / + 1e3; + } + +private: + std::chrono::high_resolution_clock::time_point start_; +}; diff --git a/archive/cuda_extension/engines/DM_gpu.py b/archive/cuda_extension/engines/DM_gpu.py new file mode 100644 index 000000000..399eb143c --- /dev/null +++ b/archive/cuda_extension/engines/DM_gpu.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine that uses numpy arrays instead of iteration. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import time + +from ..utils import parallel +from .DM_npy import DMNpy +from ptypy import defaults_tree +from ..core.manager import Full, Vanilla +from archive.cuda_extension.accelerate.cuda import difference_map_iterator +from . import register +import numpy as np + +__all__ = ['DMGpu'] + + +@register() +class DMGpu(DMNpy): + """ + A full-fledged Difference Map engine that uses numpy arrays instead of iteration. + + + Defaults: + + [name] + default = DMGpu + type = str + help = + doc = + + [alpha] + default = 1 + type = float + lowlim = 0.0 + help = Difference map parameter + + [probe_update_start] + default = 2 + type = int + lowlim = 0 + help = Number of iterations before probe update starts + + [subpix_start] + default = 0 + type = int + lowlim = 0 + help = Number of iterations before starting subpixel interpolation + + [subpix] + default = 'linear' + type = str + help = Subpixel interpolation; 'fourier','linear' or None for no interpolation + + [update_object_first] + default = True + type = bool + help = If True update object before probe + + [overlap_converge_factor] + default = 0.05 + type = float + lowlim = 0.0 + help = Threshold for interruption of the inner overlap loop + doc = The inner overlap loop refines the probe and the object simultaneously. This loop is escaped as soon as the overall change in probe, relative to the first iteration, is less than this value. + + [overlap_max_iterations] + default = 10 + type = int + lowlim = 1 + help = Maximum of iterations for the overlap constraint inner loop + + [probe_inertia] + default = 1e-9 + type = float + lowlim = 0.0 + help = Weight of the current probe estimate in the update + + [object_inertia] + default = 1e-4 + type = float + lowlim = 0.0 + help = Weight of the current object in the update + + [fourier_relax_factor] + default = 0.05 + type = float + lowlim = 0.0 + help = If rms error of model vs diffraction data is smaller than this fraction, Fourier constraint is met + doc = Set this value higher for noisy data. + + [obj_smooth_std] + default = None + type = int + lowlim = 0 + help = Gaussian smoothing (pixel) of the current object prior to update + doc = If None, smoothing is deactivated. This smoothing can be used to reduce the amplitude of spurious pixels in the outer, least constrained areas of the object. + + [clip_object] + default = None + type = tuple + help = Clip object amplitude into this interval + + [probe_center_tol] + default = None + type = float + lowlim = 0.0 + help = Pixel radius around optical axes that the probe mass center must reside in + + """ + + SUPPORTED_MODELS = [Vanilla, Full] + + def __init__(self, ptycho_parent, pars=None): + """ + Difference map reconstruction engine. + """ + super(DMGpu, self).__init__(ptycho_parent, pars) + + def engine_iterate(self, num=1): + """ + Compute `num` iterations. + """ + + for dID, _diffs in self.di.S.items(): + + cfact_probe = (self.p.probe_inertia * len(self.vectorised_scan[dID]['meta']['addr']) / + self.vectorised_scan[dID]['probe'].shape[0]) * np.ones_like( + self.vectorised_scan[dID]['probe']) + + + cfact_object = self.p.object_inertia * self.mean_power * (self.vectorised_scan[dID]['object viewcover'] + 1.) + + + pre_fft = self.propagator[dID].pre_fft + post_fft = self.propagator[dID].post_fft + psupp = self.probe_support[self.vectorised_scan[dID]['meta']['poe_IDs'][0]].astype(np.complex64) + #print("{}, {}".format(psupp.shape, psupp.dtype)) + + errors =difference_map_iterator(diffraction=self.vectorised_scan[dID]['diffraction'], + obj=self.vectorised_scan[dID]['obj'], + object_weights=self.vectorised_scan[dID]['object weights'].astype(np.float32), + cfact_object=cfact_object, + mask=self.vectorised_scan[dID]['mask'], + probe=self.vectorised_scan[dID]['probe'], + cfact_probe=cfact_probe, + probe_support=psupp, #self.probe_support[self.vectorised_scan[dID]['meta']['poe_IDs'][0]], + probe_weights=self.vectorised_scan[dID]['probe weights'].astype(np.float32), + exit_wave=self.vectorised_scan[dID]['exit wave'], + addr=self.vectorised_scan[dID]['meta']['addr'], + pre_fft=pre_fft, + post_fft=post_fft, + pbound=self.pbound[dID], + overlap_max_iterations=self.p.overlap_max_iterations, + update_object_first=self.p.update_object_first, + obj_smooth_std=self.p.obj_smooth_std, + overlap_converge_factor=self.p.overlap_converge_factor, + probe_center_tol=self.p.probe_center_tol, + probe_update_start=0, + alpha=self.p.alpha, + clip_object=self.p.clip_object, + LL_error=True, + num_iterations=num) + + + #yuk yuk yuk + error_dct = {} + print(errors.shape) + jx =0 + for jx in range(num): + k = 0 + for idx, name in self.di.views.items(): + error_dct[idx] = errors[jx, :, k] + k += 1 + jx +=1 + error = parallel.gather_dict(error_dct) + + # count up + self.curiter += num + + return error + + \ No newline at end of file diff --git a/archive/cuda_extension/engines/DM_npy.py b/archive/cuda_extension/engines/DM_npy.py new file mode 100644 index 000000000..f601a46dd --- /dev/null +++ b/archive/cuda_extension/engines/DM_npy.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine that uses numpy arrays instead of iteration. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +from ..utils import parallel +from .DM import DM +from ..core.manager import Full, Vanilla +from ptypy.accelerate.array_based import constraints as con, data_utils as du +import numpy as np +from . import register + +#from memory_profiler import profile +__all__ = ['DMNpy'] + + +@register() +class DMNpy(DM): + """ + A full-fledged Difference Map engine that uses numpy arrays instead of iteration. + + + Defaults: + + [name] + default = DMNpy + type = str + help = + doc = + + [alpha] + default = 1 + type = float + lowlim = 0.0 + help = Difference map parameter + + [probe_update_start] + default = 2 + type = int + lowlim = 0 + help = Number of iterations before probe update starts + + [subpix_start] + default = 0 + type = int + lowlim = 0 + help = Number of iterations before starting subpixel interpolation + + [subpix] + default = 'linear' + type = str + help = Subpixel interpolation; 'fourier','linear' or None for no interpolation + + [update_object_first] + default = True + type = bool + help = If True update object before probe + + [overlap_converge_factor] + default = 0.05 + type = float + lowlim = 0.0 + help = Threshold for interruption of the inner overlap loop + doc = The inner overlap loop refines the probe and the object simultaneously. This loop is escaped as soon as the overall change in probe, relative to the first iteration, is less than this value. + + [overlap_max_iterations] + default = 10 + type = int + lowlim = 1 + help = Maximum of iterations for the overlap constraint inner loop + + [probe_inertia] + default = 1e-9 + type = float + lowlim = 0.0 + help = Weight of the current probe estimate in the update + + [object_inertia] + default = 1e-4 + type = float + lowlim = 0.0 + help = Weight of the current object in the update + + [fourier_relax_factor] + default = 0.05 + type = float + lowlim = 0.0 + help = If rms error of model vs diffraction data is smaller than this fraction, Fourier constraint is met + doc = Set this value higher for noisy data. + + [obj_smooth_std] + default = None + type = int + lowlim = 0 + help = Gaussian smoothing (pixel) of the current object prior to update + doc = If None, smoothing is deactivated. This smoothing can be used to reduce the amplitude of spurious pixels in the outer, least constrained areas of the object. + + [clip_object] + default = None + type = tuple + help = Clip object amplitude into this interval + + [probe_center_tol] + default = None + type = float + lowlim = 0.0 + help = Pixel radius around optical axes that the probe mass center must reside in + + """ + + SUPPORTED_MODELS = [Vanilla, Full] + + def __init__(self, ptycho_parent, pars=None): + """ + Difference map reconstruction engine. + """ + super(DMNpy, self).__init__(ptycho_parent, pars) + + def engine_initialize(self): + self.error = [] + self.ob_viewcover = self.ob.copy(self.ob.ID + '_vcover', fill=0.) + + def engine_prepare(self): + """ + Last minute initialization. + + Everything that needs to be recalculated when new data arrives. + """ + super(DMNpy, self).engine_prepare() + # and then something to convert the arrays to numpy + self.vectorised_scan = {} + self.propagator = {} + for dID, _diffs in self.di.S.items(): + self.vectorised_scan[dID] = du.pod_to_arrays(self, dID) + first_view_id = self.vectorised_scan[dID]['meta']['view_IDs'][0] + self.propagator[dID] = self.di.V[first_view_id].pod.geometry.propagator + + + + def engine_iterate(self, num=1): + """ + Compute `num` iterations. + """ + to = 0. + tf = 0. + # num=5 + for dID, _diffs in self.di.S.items(): + + + + + + + cfact_probe = (self.p.probe_inertia * len(self.vectorised_scan[dID]['meta']['addr']) / + self.vectorised_scan[dID]['probe'].shape[0]) * np.ones_like( + self.vectorised_scan[dID]['probe']) + + + + + cfact_object = self.p.object_inertia * self.mean_power * (self.vectorised_scan[dID]['object viewcover'] + 1.) + + # path_to_numpy_file = "/dls/mx-scratch/aaron/gpu_real_world_test_cases/i08_iterate_inputs_p100_2modes.npy" + # path_to_hdf_file = "/dls/mx-scratch/aaron/gpu_real_world_test_cases/i08_iterate_inputs_p100_2modes.h5" + + # import h5py as h5 + + # print "saving out the data" + # f = h5.File(path_to_hdf_file, 'w') + # f['diffraction'] = self.vectorised_scan[dID]['diffraction'] + # f['exit_wave'] = self.vectorised_scan[dID]['exit wave'] + # f['mask'] = self.vectorised_scan[dID]['mask'] + # f['probe'] = self.vectorised_scan[dID]['probe'] + # f['obj'] = self.vectorised_scan[dID]['obj'] + # f.close() + # print "done" + + probe_support = self.probe_support[self.vectorised_scan[dID]['meta']['poe_IDs'][0]] + mean_power = self.mean_power + object_weights = self.vectorised_scan[dID]['object weights'] + probe_weights = self.vectorised_scan[dID]['probe weights'] + pbound = self.pbound + object_viewcover = self.vectorised_scan[dID]['object viewcover'] + # overlap_max_iterations = self.vectorised_scan[dID]['overlap_max_iterations'] + # update_object_first = self.vectorised_scan[dID]['update_object_first'] + # obj_smooth_std = self.vectorised_scan[dID]['obj_smooth_std'] + # overlap_converge_factor = self.vectorised_scan[dID]['overlap_converge_factor'] + # probe_center_tol = self.vectorised_scan[dID]['probe_center_tol'] + # update_probe_after = self.vectorised_scan[dID]['update_probe_after'] + # alpha = self.vectorised_scan[dID]['alpha'] + # clip_object = self.vectorised_scan[dID]['clip_object'] + # LL_error = self.vectorised_scan[dID]['LL_error'] + # cfact_object = self.vectorised_scan[dID]['cfact_object'] + # cfact_probe = self.vectorised_scan[dID]['cfact_probe'] + pre_fft = self.propagator[dID].pre_fft + post_fft = self.propagator[dID].post_fft + # + # out_dict = {'pre_fft': pre_fft, + # 'post_fft' :post_fft, + # 'probe_support': self.probe_support[self.vectorised_scan[dID]['meta']['poe_IDs'][0]], + # 'mean_power': mean_power, + # 'object weights': object_weights, + # 'probe weights': probe_weights, + # 'pbound': pbound, + # 'object_viewcover': object_viewcover, + # 'overlap_max_iterations': self.p.overlap_max_iterations, + # 'update_object_first': self.p.update_object_first, + # 'obj_smooth_std' : self.p.obj_smooth_std, + # 'overlap_converge_factor': self.p.overlap_converge_factor, + # 'probe_center_tol': self.p.probe_center_tol, + # 'update_probe_after' : 0, + # 'alpha' : self.p.alpha, + # 'clip_object' : self.p.clip_object, + # 'LL_error' : False, + # 'cfact_object': cfact_object, + # 'cfact_probe': cfact_probe, + # 'pre_fft' : pre_fft, + # 'post_fft': post_fft, + # 'addr' : self.vectorised_scan[dID]['meta']['addr'] + # } + # print "saving out the metadata" + # np.save(path_to_numpy_file, out_dict) + # print "done" + errors = con.difference_map_iterator(diffraction=self.vectorised_scan[dID]['diffraction'], + obj=self.vectorised_scan[dID]['obj'], + object_weights=self.vectorised_scan[dID]['object weights'], + cfact_object=cfact_object, + mask=self.vectorised_scan[dID]['mask'], + probe=self.vectorised_scan[dID]['probe'], + cfact_probe=cfact_probe, + probe_support=self.probe_support[self.vectorised_scan[dID]['meta']['poe_IDs'][0]], + probe_weights=self.vectorised_scan[dID]['probe weights'], + exit_wave=self.vectorised_scan[dID]['exit wave'], + addr=self.vectorised_scan[dID]['meta']['addr'], + pre_fft=pre_fft, + post_fft=post_fft, + pbound=self.pbound[dID], + overlap_max_iterations=self.p.overlap_max_iterations, + update_object_first=self.p.update_object_first, + obj_smooth_std=self.p.obj_smooth_std, + overlap_converge_factor=self.p.overlap_converge_factor, + probe_center_tol=self.p.probe_center_tol, + probe_update_start=0, + alpha=self.p.alpha, + clip_object=self.p.clip_object, + LL_error=True, + num_iterations=num) + + + + + #yuk yuk yuk + error_dct = {} + print(errors.shape) + jx =0 + for jx in range(num): + k = 0 + for idx, name in self.di.views.items(): + error_dct[idx] = errors[jx, :, k] + k += 1 + jx +=1 + error = parallel.gather_dict(error_dct) + # t3 = time.time() + # to += t3 - t2 + + # count up + self.curiter += num + + # self.mpi_numpy_overlap_update() + # logger.info('Time spent in Fourier update: %.2f' % tf) + # logger.info('Time spent in Overlap update: %.2f' % to) + # error = parallel.gather_dict(error_dct) + return error + + def engine_finalize(self): + """ + Try deleting ever helper container. + """ + + del self.ptycho.containers[self.ob_viewcover.ID] + del self.ob_viewcover diff --git a/archive/cuda_extension/engines/gpu_testing.py b/archive/cuda_extension/engines/gpu_testing.py new file mode 100644 index 000000000..2a03de716 --- /dev/null +++ b/archive/cuda_extension/engines/gpu_testing.py @@ -0,0 +1,177 @@ +#from pytpy.array_based import COMPLEX_TYPE, FLOAT_TYPE +import numpy as np + +from archive.cuda_extension.accelerate.cuda import difference_map_fourier_constraint +#from ptypy.accelerate.array_based.constraints import difference_map_fourier_constraint + +from archive.cuda_extension.accelerate.cuda import difference_map_overlap_update +#from ptypy.accelerate.array_based.constraints import difference_map_overlap_update + + +from archive.cuda_extension.accelerate.cuda import far_field_error, realspace_error +#from ptypy.accelerate.array_based.error_metrics import far_field_error, realspace_error + +#from ptypy.accelerate.array_based.error_metrics import log_likelihood +from archive.cuda_extension.accelerate.cuda import log_likelihood + +from archive.cuda_extension.accelerate.cuda import difference_map_realspace_constraint +#from ptypy.accelerate.array_based.object_probe_interaction import difference_map_realspace_constraint + +from archive.cuda_extension.accelerate.cuda import scan_and_multiply +#from ptypy.accelerate.array_based.object_probe_interaction import scan_and_multiply + +from archive.cuda_extension.accelerate.cuda import renormalise_fourier_magnitudes +#from ptypy.accelerate.array_based.constraints import renormalise_fourier_magnitudes + +from archive.cuda_extension.accelerate.cuda import get_difference +#from ptypy.accelerate.array_based.constraints import get_difference + +from archive.cuda_extension.accelerate.cuda import abs2 +#from ptypy.accelerate.array_based.array_utils import abs2 + +from archive.cuda_extension.accelerate.cuda import sum_to_buffer +#from ptypy.accelerate.array_based.array_utils import sum_to_buffer + +from archive.cuda_extension.accelerate.cuda import farfield_propagator +#from ptypy.accelerate.array_based.propagation import farfield_propagator + + +FLOAT_TYPE = np.float32 +COMPLEX_TYPE = np.complex64 + +# def difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, postfilter, pbound=None, alpha=1.0, LL_error=True, do_realspace_error=True): +# ''' +# This kernel just performs the fourier renormalisation. +# :param mask. The nd mask array +# :param diffraction. The nd diffraction data +# :param farfield_stack. The current iterant. +# :param addr. The addresses of the stacks. +# :return: The updated iterant +# : fourier errors +# ''' + +# probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + +# # Buffer for accumulated photons +# # For log likelihood error # need to double check this adp +# if LL_error is True: +# print("probe_object={}, {}".format(probe_object.shape, probe_object.dtype)) +# print("mask={}, {}".format(mask.shape, mask.dtype)) +# print("Idata={}, {}".format(Idata.shape, Idata.dtype)) +# print("prefilter={}, {}".format(prefilter.shape, prefilter.dtype)) +# print("postfilter={}, {}".format(postfilter.shape, postfilter.dtype)) +# print("addr_info={}, {}".format(addr_info.shape, addr_info.dtype)) +# err_phot = log_likelihood(probe_object, mask, Idata, prefilter, postfilter, addr_info) +# print("err_phot={}, {}".format(err_phot.shape, err_phot.dtype)) +# else: +# err_phot = np.zeros(Idata.shape[0], dtype=FLOAT_TYPE) + + +# constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) +# #print("constrained=\n{}".format(constrained[0])) +# #constr = constrained.flatten() +# #for i in xrange(1000): +# # if abs(constr[i]) > 0.0: +# # print("{}: {}".format(i, constr[i])) + +# f = farfield_propagator(constrained, prefilter, postfilter, direction='forward') +# pa, oa, ea, da, ma = zip(*addr_info) +# af2 = sum_to_buffer(abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + +# fmag = np.sqrt(np.abs(Idata)) +# af = np.sqrt(af2) +# # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) +# err_fmag = far_field_error(af, fmag, mask) + +# # print("f={}, {}".format(f.shape, f.dtype)) +# # print("af={}, {}".format(af.shape, af.dtype)) +# # print("fmag={}, {}".format(fmag.shape, fmag.dtype)) +# # print("mask={}, {}".format(mask.shape, mask.dtype)) +# # print("err_fmag={}, {}".format(err_fmag.shape, err_fmag.dtype)) +# # print("addr_info={}, {}".format(addr_info.shape, addr_info.dtype)) +# # print("pbound={}".format(pbound)) +# vectorised_rfm = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + +# # print("vectorised_rfm={}, {}".format(vectorised_rfm.shape, vectorised_rfm.dtype)) + +# backpropagated_solution = farfield_propagator(vectorised_rfm, +# postfilter.conj(), +# prefilter.conj(), +# direction='backward') + + +# df = get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + + + +# exit_wave += df +# if do_realspace_error: +# ea_first_column = np.array(ea)[:, 0] +# da_first_column = np.array(da)[:, 0] +# err_exit = realspace_error(df, ea_first_column, da_first_column, Idata.shape[0]) +# else: +# err_exit = np.zeros((Idata.shape[0])) + +# if pbound is not None: +# err_fmag /= pbound + +# #print("err_fmag: {}".format(err_fmag.shape)) +# #print("err_phot: {}".format(err_phot.shape)) +# #print("err_exit: {}".format(err_exit.shape)) +# return np.array([err_fmag, err_phot, err_exit]) + + + +def difference_map_iterator(diffraction, obj, object_weights, cfact_object, mask, probe, cfact_probe, probe_support, + probe_weights, exit_wave, addr, pre_fft, post_fft, pbound, overlap_max_iterations, update_object_first, + obj_smooth_std, overlap_converge_factor, probe_center_tol, probe_update_start, alpha=1, + clip_object=None, LL_error=False, num_iterations=1): + curiter = 0 + + errors = np.zeros((num_iterations, 3, len(diffraction)), dtype=FLOAT_TYPE) + for it in range(num_iterations): + if (((it+1) % 10) == 0) and (it>0): + print("iteration:%s" % (it+1)) # it's probably a good idea to print this if possible for some idea of progress + # numpy dump here for 64x64 and 4096x4096 + + #print("mask: {}".format(mask.shape)) + #print("diffraction: {}".format(diffraction.shape)) + #print("obj: {}".format(obj.shape)) + #print("probe: {}".format(probe.shape)) + #print("exit_wave: {}".format(exit_wave.shape)) + #print("addr: {}".format(addr.shape)) + #print("prefilter: {}".format(pre_fft.shape)) + #print("postfiler: {}".format(post_fft.shape)) + err = difference_map_fourier_constraint(mask, + diffraction, + obj, + probe, + exit_wave, + addr, + prefilter=pre_fft, + postfilter=post_fft, + pbound=pbound, + alpha=alpha, + LL_error=LL_error) + #print("err: {}".format(err.shape)) + errors[it] = err + + do_update_probe = (probe_update_start <= curiter) + difference_map_overlap_update(addr, + cfact_object, + cfact_probe, + do_update_probe, + exit_wave, + obj, + object_weights, + probe, + probe_support, + probe_weights, + overlap_max_iterations, + update_object_first, + obj_smooth_std, + overlap_converge_factor, + probe_center_tol, + clip_object=clip_object) + curiter += 1 + return errors \ No newline at end of file diff --git a/archive/cuda_extension/extensions.py b/archive/cuda_extension/extensions.py new file mode 100644 index 000000000..f0dfc0010 --- /dev/null +++ b/archive/cuda_extension/extensions.py @@ -0,0 +1,102 @@ +''' +These are the optional extensions for ptypy +''' + + +from distutils.version import LooseVersion +from distutils.extension import Extension +import os +import multiprocessing +import subprocess +import re +import numpy as np + + +# this is a hacky version, but is the desired behaviour +class AccelerationExtension(object): + def __init__(self, debug=False): + self.debug = debug + self._options = None + + def get_full_options(self): + return self._options + + def get_reflection_options(self): + user_options = [] + boolean_options = [] + for name, description in self._options.items(): + if isinstance(description['default'], str): + user_options.append((name+'=', None, description['doc'])) + elif isinstance(description['default'], bool): + user_options.append((name, None, description['doc'])) + boolean_options.append(name) + else: + raise NotImplementedError("Don't know what to do with parameter:%s of type: %s" % (name, type(description['default']))) + return user_options, boolean_options + + def build(self, options): + raise NotImplementedError('You need to implement the build method!') + + def getExtension(self): + raise NotImplementedError('You need to return cython extension object.') + + +class CudaExtension(AccelerationExtension): # probably going to inherit from something. + def __init__(self, *args, **kwargs): + super(CudaExtension, self).__init__(*args, **kwargs) + self._options = {'cudadir': {'default': '', + 'doc': 'CUDA directory'}, + 'cudaflags': {'default': '-gencode arch=compute_35,\\"code=sm_35\\" ' + + '-gencode arch=compute_37,\\"code=sm_37\\" ' + + '-gencode arch=compute_52,\\"code=sm_52\\" ' + + '-gencode arch=compute_60,\\"code=sm_60\\" ' + + '-gencode arch=compute_70,\\"code=sm_70\\" ', + 'doc': 'Flags to the CUDA compiler'}, + 'gputiming': {'default': False, + 'doc': 'Do GPU timing'}} + + def build(self, options): + cudadir = options['cudadir'] + cudaflags = options['cudaflags'] + gputiming = options['gputiming'] + try: + out = subprocess.check_output(['cmake', '--version']) + except OSError: + raise RuntimeError( + "CMake must be installed to build the CUDA extensions.") + + cmake_version = LooseVersion(re.search(r'version\s*([\d.]+)', + out.decode()).group(1)) + if cmake_version < '3.8.0': + raise RuntimeError("CMake >= 3.8.0 is required") + + srcdir = os.path.abspath('cuda') + buildtmp = os.path.abspath(os.path.join('build', 'cuda')) + cmake_args = [ + "-DCMAKE_BUILD_TYPE=" + ("Debug" if self.debug else "Release"), + '-DCMAKE_CUDA_FLAGS={}'.format(cudaflags), + '-DGPU_TIMING={}'.format("ON" if gputiming else "OFF") + ] + if cudadir: + cmake_args += '-DCMAKE_CUDA_COMPILER="{}/bin/nvcc"'.format(cudadir) + build_args = ["--config", "Debug" if self.debug else "Release", "--", "-j{}".format(multiprocessing.cpu_count() + 1)] + if not os.path.exists(buildtmp): + os.makedirs(buildtmp) + env = os.environ.copy() + subprocess.check_call(['cmake', srcdir] + cmake_args, + cwd=buildtmp, env=env) + subprocess.check_call(['cmake', '--build', '.'] + build_args, + cwd=buildtmp) + print("Complete.") + + def getExtension(self): + libdirs = ['build/cuda'] + if 'LD_LIBRARY_PATH' in os.environ: + libdirs += os.environ['LD_LIBRARY_PATH'].split(':') + return Extension('*', + sources=['ptypy/accelerate/cuda/gpu_extension.pyx'], + include_dirs=[np.get_include()], + libraries=['gpu_extension', 'cudart', 'cufft'], + library_dirs=libdirs, + depends=['build/cuda/libgpu_extension.a', ], + language="c++") diff --git a/archive/cuda_extension/minimal_DMGpu_iterate_benchmark.py b/archive/cuda_extension/minimal_DMGpu_iterate_benchmark.py new file mode 100644 index 000000000..861c368a3 --- /dev/null +++ b/archive/cuda_extension/minimal_DMGpu_iterate_benchmark.py @@ -0,0 +1,53 @@ +from ptypy.core import Ptycho +from ptypy import utils as u +import cProfile +p = u.Param() +p.verbose_level = 3 +p.io = u.Param() +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.ipython_kernel = False +p.scans = u.Param() +p.scans.MF = u.Param() +p.scans.MF.name = 'Full' +p.scans.MF.propagation = 'farfield' +p.scans.MF.data = u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.positions_theory = None +p.scans.MF.data.auto_center = None +p.scans.MF.data.min_frames = 1 +p.scans.MF.data.orientation = None +p.scans.MF.data.num_frames = 500 +p.scans.MF.data.energy = 6.2 +p.scans.MF.data.shape = 64 +p.scans.MF.data.chunk_format = '.chunk%02d' +p.scans.MF.data.rebin = None +p.scans.MF.data.experimentID = None +p.scans.MF.data.label = None +p.scans.MF.data.version = 0.1 +p.scans.MF.data.dfile = None +p.scans.MF.data.psize = 0.000172 +p.scans.MF.data.load_parallel = None +p.scans.MF.data.distance = 7.0 +p.scans.MF.data.save = None +p.scans.MF.data.center = 'fftshift' +p.scans.MF.data.photons = 100000000.0 +p.scans.MF.data.psf = 0.2 +p.scans.MF.data.density = 0.2 +p.scans.MF.data.add_poisson_noise = False +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 2 # currently breaks when this is =2 + +p.engines = u.Param() + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DMGpu' +p.engines.engine00.numiter = 500 +# p.engines.engine00.overlap_max_iterations = 1 +# prepare and run + + +P = Ptycho(p, level=4) +P.run() diff --git a/archive/cuda_extension/minimal_DMNpy_iterate_benchmark.py b/archive/cuda_extension/minimal_DMNpy_iterate_benchmark.py new file mode 100644 index 000000000..499bda25e --- /dev/null +++ b/archive/cuda_extension/minimal_DMNpy_iterate_benchmark.py @@ -0,0 +1,53 @@ +from ptypy.core import Ptycho +from ptypy import utils as u +import cProfile +p = u.Param() +p.verbose_level = 3 +p.io = u.Param() +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=True) +p.ipython_kernel = False +p.scans = u.Param() +p.scans.MF = u.Param() +p.scans.MF.name = 'Full' +p.scans.MF.propagation = 'farfield' +p.scans.MF.data = u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.positions_theory = None +p.scans.MF.data.auto_center = None +p.scans.MF.data.min_frames = 1 +p.scans.MF.data.orientation = None +p.scans.MF.data.num_frames = 500 +p.scans.MF.data.energy = 6.2 +p.scans.MF.data.shape = 64 +p.scans.MF.data.chunk_format = '.chunk%02d' +p.scans.MF.data.rebin = None +p.scans.MF.data.experimentID = None +p.scans.MF.data.label = None +p.scans.MF.data.version = 0.1 +p.scans.MF.data.dfile = None +p.scans.MF.data.psize = 0.000172 +p.scans.MF.data.load_parallel = None +p.scans.MF.data.distance = 7.0 +p.scans.MF.data.save = None +p.scans.MF.data.center = 'fftshift' +p.scans.MF.data.photons = 100000000.0 +p.scans.MF.data.psf = 0.2 +p.scans.MF.data.density = 0.2 +p.scans.MF.data.add_poisson_noise = False +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 2 # currently breaks when this is =2 + +p.engines = u.Param() + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DMNpy' +p.engines.engine00.numiter = 500 +# p.engines.engine00.overlap_max_iterations = 1 +# prepare and run + + +P = Ptycho(p, level=4) +P.run() diff --git a/archive/cuda_extension/python/__init__.py b/archive/cuda_extension/python/__init__.py new file mode 100644 index 000000000..4221257bd --- /dev/null +++ b/archive/cuda_extension/python/__init__.py @@ -0,0 +1,7 @@ +''' +A module for gpu acceleration + +''' +import numpy as np +COMPLEX_TYPE= np.complex64 +FLOAT_TYPE = np.float32 \ No newline at end of file diff --git a/archive/cuda_extension/python/array_utils.py b/archive/cuda_extension/python/array_utils.py new file mode 100644 index 000000000..c065738ce --- /dev/null +++ b/archive/cuda_extension/python/array_utils.py @@ -0,0 +1,8 @@ +''' +useful utilities from ptypy that should be ported to gpu. These don't ahve external dependencies +''' +import numpy as np +from . import COMPLEX_TYPE +from .gpu_extension import abs2, sum_to_buffer, sum_to_buffer_stride, \ + norm2, mass_center, clip_complex_magnitudes_to_range, interpolated_shift, \ + complex_gaussian_filter diff --git a/archive/cuda_extension/python/config.py b/archive/cuda_extension/python/config.py new file mode 100644 index 000000000..f874dbb41 --- /dev/null +++ b/archive/cuda_extension/python/config.py @@ -0,0 +1,17 @@ +from .gpu_extension import get_num_gpus, \ + get_gpu_compute_capability, select_gpu_device, \ + get_gpu_memory_mb, get_gpu_name, reset_function_cache + +def init_gpus(device = 0): + n = get_num_gpus() + if n > 0: + print("Detected GPUs:") + for i in range(n): + comp = get_gpu_compute_capability(i) + mem = get_gpu_memory_mb(i) + name = get_gpu_name(i) + print("{}: {} (Compute Capability: {}, Memory: {:.3f}GB)".format( + i, name, comp / 10.0, mem /1024.0 + )) + print("Initialising GPU {}...".format(device)) + select_gpu_device(device) diff --git a/archive/cuda_extension/python/constraints.py b/archive/cuda_extension/python/constraints.py new file mode 100644 index 000000000..1bde4bb86 --- /dev/null +++ b/archive/cuda_extension/python/constraints.py @@ -0,0 +1,13 @@ +''' +a module to holds the constraints +''' + +from .gpu_extension import ( + get_difference, + renormalise_fourier_magnitudes, + difference_map_fourier_constraint, + difference_map_iterator +) + + + diff --git a/archive/cuda_extension/python/cuda_functions.pxd b/archive/cuda_extension/python/cuda_functions.pxd new file mode 100644 index 000000000..2f7f4e297 --- /dev/null +++ b/archive/cuda_extension/python/cuda_functions.pxd @@ -0,0 +1,281 @@ +from libcpp.string cimport string + +cdef extern void difference_map_realspace_constraint_c( + const float* fobj_and_probe, + const float* fexit_wave, + float alpha, + int i, + int m, + int n, + float* fout +) except + + +cdef extern void scan_and_multiply_c( + const float* fprobe, + int i_probe, + int m_probe, + int n_probe, + const float* fobj, + int i_obj, + int m_obj, + int n_obj, + const int* addr_info, + int addr_len, + int batch_size, + int m, + int n, + float* fout +) except + + +cdef extern void sqrt_abs_c( + const float* indata, + float* outdata, + int m, + int n +) except + + +cdef extern void log_likelihood_c( + const float* probe_obj, + const unsigned char* mask, + const float* Idata, + const float* prefilter, + const float* postfilter, + const int* addr_info, + float* out, + int i, + int m, + int n, + int addr_i, + int Idata_i +) except + + +cdef extern void farfield_propagator_c( + const float* indata, + const float* prefilter, + const float* postfilter, + float* out, + int b, + int m, + int n, + int isForward +) except + + +cdef extern void sum_to_buffer_c(const float *in1, int in1_0, int in1_1, + int in1_2, float *out, int out_0, int out_1, + int out_2, const int *in_addr, int in_addr_0, + const int *out_addr, int out_addr_0, + int isComplex) except + +cdef extern void sum_to_buffer_stride_c(const float* in1, int in1_0, int in1_1, int in1_2, + float* out, int out_0, int out_1, int out_2, const int* addr_info, int addr_info_0, + int isComplex) except + + +cdef extern void abs2_c( + const float* indata, + float* out, + int n, + int isComplex +) except + + +cdef extern void abs2d_c( + const double* indata, + double* out, + int n, + int isComplex +) except + + +cdef extern void far_field_error_c( + const float* current, + const float* measured, + const unsigned char* mask, + float* out, + int i, int m, int n +) except + + +cdef extern void realspace_error_c( + const float* difference, + const int* ea_first_column, + const int* da_first_column, + int addr_len, + float* out, + int i, int m, int n, + int outlen +) except + + +cdef extern void get_difference_c( + const int* addr_info, + float alpha, + const float* fbackpropagated_solution, + const float* err_fmag, + const float* fexit_wave, + float pbound, + const float* fprobe_obj, + float* fout, + int i, int m, int n, + int usePbound +) except + + +cdef extern void renormalise_fourier_magnitudes_c( + const float* f_f, + const float* af, + const float* fmag, + const unsigned char* mask, + const float* err_fmag, + const int* addr_info, + float pbound, + float* f_out, + int M, int N, int A, int B, + int usePbound +) except + + +cdef extern void difference_map_fourier_constraint_c(const unsigned char *mask, + const float *Idata, + const float *f_obj, + const float *f_probe, + float *f_exit_wave, + const int *addr_info, + const float *f_prefilter, + const float *f_postfilter, + float pbound, + float alpha, + int do_LL_error, + int do_realspace_error, + int doPbound, + int M, + int N, + int A, + int B, + int C, + int D, + int ob_modes, + int pr_modes, + float *errors +) except + + +cdef extern void norm2_c(const float* data, float* out, int size, int isComplex) except + +cdef extern void mass_center_c(const float* data, int i, int m, int n, float* output) except + +cdef extern void clip_complex_magnitudes_to_range_c(float* f_data, int n, float clip_min, float clip_max) except + +cdef extern void extract_array_from_exit_wave_c( + const float* f_exit_wave, + int A, + int B, + int C, + const int* exit_addr, + const float* f_array_to_be_extracted, + int D, + int E, + int F, + const int* extract_addr, + float* f_array_to_be_updated, + int G, + int H, + int I, + const int* update_addr, + const float* weights, + const float* f_cfact +) except + +cdef extern void interpolated_shift_c(const float* f_in, float* f_out, int items, int rows, int columns, float offsetRow, float offsetCol, int doLinear) except+ +cdef extern int get_num_gpus_c() except + +cdef extern int get_gpu_compute_capability_c(int dev) except + +cdef extern void select_gpu_device_c(int dev) except + +cdef extern int get_gpu_memory_mb_c(int dev) except + +cdef extern string get_gpu_name_c(int dev) except + +cdef extern void reset_function_cache_c() except + +cdef extern void center_probe_c(float* f_probe, float center_tolerance, int i, int m, int n) except + +cdef extern float difference_map_update_probe_c( + const float* f_obj, + const float* probe_weights, + float* f_probe, + const float* f_exit_wave, + const int* addr_info, + const float* f_cfact_probe, + const float* f_probe_support, + int A, int B, int C, + int D, int E, int F, + int G, int H, int I +) except + +cdef extern void difference_map_update_object_c( + float* f_obj, + const float* object_weights, + const float* f_probe, + const float* f_exit_wave, + const int* addr_info, + const float* f_cfact_object, + float ob_smooth_std, + float clip_min, + float clip_max, + int doSmoothing, + int doClipping, + int A, int B, int C, int D, + int E, int F, int G, int H, int I) except + +cdef extern void difference_map_overlap_constraint_c( + const int* addr_info, + const float* f_cfact_object, + const float* f_cfact_probe, + const float* f_exit_wave, + float* f_obj, + const float* object_weights, + float* f_probe, + const float* f_probe_support, + const float* probe_weights, + float obj_smooth_std, + float clip_min, + float clip_max, + float probe_center_tol, + float overlap_converge_factor, + int max_iterations, + int doUpdateObjectFirst, + int doUpdateProbe, + int doSmoothing, + int doClipping, + int doCentering, + int A, int B, int C, int D, + int E, int F, int G, int H, int I) except + + + +cdef extern void complex_gaussian_filter_c(const float* f_input, + float* f_output, + const float* mfs, + int ndims, + const int* shape) except + +cdef extern void difference_map_iterator_c( + const float* diffraction, + float* f_obj, + const float* object_weights, + const float* f_cfact_object, + const unsigned char* mask, + float* f_probe, + const float* f_cfact_probe, + const float* f_probe_support, + const float* probe_weights, + float* f_exit_wave, + const int* addr_info, + const float* f_pre_fft, + const float* f_post_fft, + float* errors, + float pbound, + int overlap_max_iterations, + int doUpdateObjectFirst, + float obj_smooth_std, + float overlap_converge_factor, + float probe_center_tol, + int probe_update_start, + float alpha, + float clip_min, + float clip_max, + int do_LL_error, + int do_realspace_error, + int num_iterations, + int A, + int B, + int C, + int D, + int E, + int F, + int G, + int H, + int I, + int N, + int doSmoothing, + int doClipping, + int doCentering, + int doPbound) except+ \ No newline at end of file diff --git a/archive/cuda_extension/python/error_metrics.py b/archive/cuda_extension/python/error_metrics.py new file mode 100644 index 000000000..055e1ebb4 --- /dev/null +++ b/archive/cuda_extension/python/error_metrics.py @@ -0,0 +1,5 @@ +''' +A module of the relevant error metrics +''' + +from .gpu_extension import log_likelihood, far_field_error, realspace_error diff --git a/archive/cuda_extension/python/gpu_extension.pyx b/archive/cuda_extension/python/gpu_extension.pyx new file mode 100644 index 000000000..aa0b36402 --- /dev/null +++ b/archive/cuda_extension/python/gpu_extension.pyx @@ -0,0 +1,1020 @@ +''' +Wrapper of the CUDA extensions + +Contains all cython wrappers to the CUDA extension module, +to allow a single library build and interactions instead of many +''' + +import numpy as np +from . import COMPLEX_TYPE +from cuda_functions cimport * +cimport numpy as np +import cython +import time +from libcpp.string cimport string + +@cython.boundscheck(False) +@cython.wraparound(False) +def difference_map_realspace_constraint(obj_and_probe, exit_wave, alpha): + cdef np.complex64_t [:,:,::1] obj_and_probe_c = np.ascontiguousarray(obj_and_probe) + cdef np.complex64_t [:,:,::1] exit_wave_c = np.ascontiguousarray(exit_wave) + cdef float alpha_c = alpha + out = np.empty(exit_wave.shape, dtype=np.complex64, order='C') + cdef np.complex64_t [:,:,::1] out_c = out + difference_map_realspace_constraint_c( + &obj_and_probe_c[0,0,0], + &exit_wave_c[0,0,0], + alpha_c, + obj_and_probe.shape[0], + obj_and_probe.shape[1], + obj_and_probe.shape[2], + &out_c[0,0,0] + ) + return out + + + +@cython.boundscheck(False) +@cython.wraparound(False) +def scan_and_multiply(probe, obj, exit_shape, addresses): + cdef np.complex64_t [:,:,::1] probe_c = np.ascontiguousarray(probe) + cdef np.complex64_t [:,:,::1] obj_c = np.ascontiguousarray(obj) + cdef i_probe = probe.shape[0] + cdef m_probe = probe.shape[1] + cdef n_probe = probe.shape[2] + cdef i_obj = obj.shape[0] + cdef m_obj = obj.shape[1] + cdef n_obj = obj.shape[2] + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addresses) + cdef int addr_len = addresses.shape[0] + cdef batch_size = exit_shape[0] + cdef m = exit_shape[1] + cdef n = exit_shape[2] + out = np.empty(exit_shape, dtype=np.complex64, order='C') + cdef np.complex64_t [:,:,::1] out_c = out + scan_and_multiply_c( + &probe_c[0,0,0], + i_probe, m_probe, n_probe, + &obj_c[0,0,0], + i_obj, m_obj, n_obj, + &addr_info_c[0,0,0], + addr_len, + batch_size, m, n, + &out_c[0,0,0] + ) + return out + +@cython.wraparound(False) +@cython.boundscheck(False) +def farfield_propagator( + data_to_be_transformed not None, + prefilter=None, + postfilter=None, + direction='forward' + ): + cdef np.complex64_t [:,:,::1] x = np.ascontiguousarray(data_to_be_transformed) + out = np.empty_like(x) + cdef np.complex64_t [:,:,::1] out_c = out + + dtype = np.complex64 + + cdef np.complex64_t [:,::1] prefilter_c = None + if not prefilter is None: + prefilter_c = np.ascontiguousarray(prefilter.astype(dtype)) + + cdef np.complex64_t [:,::1] postfilter_c = None + if not postfilter is None: + postfilter_c = np.ascontiguousarray(postfilter.astype(dtype)) + + cdef b = data_to_be_transformed.shape[0] + cdef m = data_to_be_transformed.shape[1] + cdef n = data_to_be_transformed.shape[2] + cdef isForward = 0 + if direction == 'forward': + isForward = 1 + farfield_propagator_c( + &x[0,0,0], + &prefilter_c[0,0], + &postfilter_c[0,0], + &out_c[0,0,0], + b, m, n, isForward + ) + return out + + +@cython.boundscheck(False) +@cython.wraparound(False) +def sqrt_abs(np.complex64_t [:,::1] diffraction not None): + out = np.empty_like(diffraction) + cdef np.complex64_t[:,::1] out_c = out + cdef m = diffraction.shape[0] + cdef n = diffraction.shape[1] + sqrt_abs_c(&diffraction[0,0], &out_c[0,0], m, n) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def log_likelihood(probe_obj, mask, Idata, prefilter, postfilter, addr_info): + cdef np.complex64_t [:,:,::1] probe_obj_c = np.ascontiguousarray(probe_obj) + cdef np.uint8_t [::1] mask_c = np.frombuffer(np.ascontiguousarray(mask.astype(np.bool)), dtype=np.uint8) + cdef np.float32_t [:,:,::1] Idata_c = np.ascontiguousarray(Idata) + cdef np.complex64_t [:,::1] prefilter_c = np.ascontiguousarray(prefilter) + cdef np.complex64_t [:,::1] postfilter_c = np.ascontiguousarray(postfilter) + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + out = np.empty(Idata.shape[0], np.float32) + cdef np.float32_t [::1] out_c = out + cdef int i = probe_obj.shape[0] + cdef int m = probe_obj.shape[1] + cdef int n = probe_obj.shape[2] + log_likelihood_c( + &probe_obj_c[0,0,0], + &mask_c[0], + &Idata_c[0,0,0], + &prefilter_c[0,0], + &postfilter_c[0,0], + &addr_info_c[0,0,0], + &out_c[0], + i, m, n, addr_info.shape[0], + Idata.shape[0] + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def abs2(input): + cin = np.ascontiguousarray(input) + outtype = cin.dtype + if cin.dtype == np.complex64: + outtype = np.float32 + elif cin.dtype == np.complex128: + outtype = np.float64 + cout = np.empty(cin.shape, dtype=outtype) + cdef np.float32_t [:,::1] cout_2c + cdef np.float32_t [:,:,::1] cout_3c + cdef np.float64_t [:,::1] cout_d2c + cdef np.float64_t [:,:,::1] cout_d3c + cdef int n = np.product(cin.shape) + + cdef np.float32_t [:, ::1] cin_f2c + cdef np.complex64_t [:, ::1] cin_c2c + cdef np.float32_t [:,:, ::1] cin_f3c + cdef np.complex64_t [:,:, ::1] cin_c3c + cdef np.float64_t [:, ::1] cin_d2c + cdef np.complex128_t [:, ::1] cin_z2c + cdef np.float64_t [:,:, ::1] cin_d3c + cdef np.complex128_t [:,:, ::1] cin_z3c + + if len(cin.shape) == 2: + if (cin.dtype == np.float32): + cout_2c = cout + cin_f2c = cin + abs2_c(&cin_f2c[0,0], &cout_2c[0,0], n, 0) + elif (cin.dtype == np.complex64): + cout_2c = cout + cin_c2c = cin + abs2_c(&cin_c2c[0,0], &cout_2c[0,0], n, 1) + elif (cin.dtype == np.float64): + cout_d2c = cout + cin_d2c = cin + abs2d_c(&cin_d2c[0,0], &cout_d2c[0,0], n, 0) + elif (cin.dtype == np.complex128): + cout_d2c = cout + cin_z2c = cin + abs2d_c(&cin_z2c[0,0], &cout_d2c[0,0], n, 1) + else: + raise ValueError("unsupported datatype {}".format(cin.dtype)) + elif len(cin.shape) == 3: + if (cin.dtype == np.float32): + cout_3c = cout + cin_f3c = cin + abs2_c(&cin_f3c[0,0,0], &cout_3c[0,0,0], n, 0) + elif (cin.dtype == np.complex64): + cout_3c = cout + cin_c3c = cin + abs2_c(&cin_c3c[0,0,0], &cout_3c[0,0,0], n, 1) + elif (cin.dtype == np.float64): + cout_d3c = cout + cin_d3c = cin + abs2d_c(&cin_d3c[0,0,0], &cout_d3c[0,0,0], n, 0) + elif (cin.dtype == np.complex128): + cout_d3c = cout + cin_z3c = cin + abs2d_c(&cin_z3c[0,0,0], &cout_d3c[0,0,0], n, 1) + else: + raise ValueError("unsupported datatype {}".format(cin.dtype)) + else: + raise ValueError("unsupported dimensionality: {}".format(len(cin.shape))) + return cout + +@cython.boundscheck(False) +@cython.wraparound(False) +def _sum_to_buffer_real(in1, outshape, in1_addr, out1_addr): + cdef np.float32_t [:,:,::1] in1_c = np.ascontiguousarray(in1) + cdef int os_0 = outshape[0] + cdef int os_1 = outshape[1] + cdef int os_2 = outshape[2] + cdef np.int32_t [:,:] in1_addr_c = np.ascontiguousarray(in1_addr) + cdef np.int32_t [:,:] out1_addr_c = np.ascontiguousarray(out1_addr) + out = np.empty(outshape, dtype=np.float32) + cdef np.float32_t [:,:,::1] out_c = out + # dimensions + cdef int in1_0 = in1.shape[0] + cdef int in1_1 = in1.shape[1] + cdef int in1_2 = in1.shape[2] + cdef int in_addr_0 = in1_addr.shape[0] + cdef int out_addr_0 = out1_addr.shape[0] + sum_to_buffer_c( + &in1_c[0,0,0], + in1_0, in1_1, in1_2, + &out_c[0,0,0], + os_0, os_1, os_2, + &in1_addr_c[0,0], + in_addr_0, + &out1_addr_c[0,0], + out_addr_0, + 0 + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def _sum_to_buffer_stride_real(in1, outshape, addr_info): + cdef np.float32_t [:,:,::1] in1_c = np.ascontiguousarray(in1) + cdef int os_0 = outshape[0] + cdef int os_1 = outshape[1] + cdef int os_2 = outshape[2] + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + out = np.empty(outshape, dtype=np.float32) + cdef np.float32_t [:,:,::1] out_c = out + # dimensions + cdef int in1_0 = in1.shape[0] + cdef int in1_1 = in1.shape[1] + cdef int in1_2 = in1.shape[2] + cdef int addr_info_0 = addr_info.shape[0] + sum_to_buffer_stride_c( + &in1_c[0,0,0], + in1_0, in1_1, in1_2, + &out_c[0,0,0], + os_0, os_1, os_2, + &addr_info_c[0,0,0], + addr_info_0, + 0 + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def _sum_to_buffer_complex(in1, outshape, in1_addr, out1_addr): + cdef np.complex64_t [:,:,::1] in1_c = np.ascontiguousarray(in1) + cdef int os_0 = outshape[0] + cdef int os_1 = outshape[1] + cdef int os_2 = outshape[2] + cdef np.int32_t [:,:] in1_addr_c = np.ascontiguousarray(in1_addr) + cdef np.int32_t [:,:] out1_addr_c = np.ascontiguousarray(out1_addr) + out = np.empty(outshape, dtype=np.complex64) + cdef np.complex64_t [:,:,::1] out_c = out + # dimensions + cdef int in1_0 = in1.shape[0] + cdef int in1_1 = in1.shape[1] + cdef int in1_2 = in1.shape[2] + cdef int in_addr_0 = in1_addr.shape[0] + cdef int out_addr_0 = out1_addr.shape[0] + sum_to_buffer_c( + &in1_c[0,0,0], + in1_0, in1_1, in1_2, + &out_c[0,0,0], + os_0, os_1, os_2, + &in1_addr_c[0,0], + in_addr_0, + &out1_addr_c[0,0], + out_addr_0, + 1 + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def _sum_to_buffer_stride_complex(in1, outshape, addr_info): + cdef np.complex64_t [:,:,::1] in1_c = np.ascontiguousarray(in1) + cdef int os_0 = outshape[0] + cdef int os_1 = outshape[1] + cdef int os_2 = outshape[2] + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + out = np.empty(outshape, dtype=np.complex64) + cdef np.complex64_t [:,:,::1] out_c = out + # dimensions + cdef int in1_0 = in1.shape[0] + cdef int in1_1 = in1.shape[1] + cdef int in1_2 = in1.shape[2] + cdef int addr_info_0 = addr_info.shape[0] + sum_to_buffer_stride_c( + &in1_c[0,0,0], + in1_0, in1_1, in1_2, + &out_c[0,0,0], + os_0, os_1, os_2, + &addr_info_c[0,0,0], + addr_info_0, + 1 + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype): + if not isinstance(in1_addr, np.ndarray): + in1_addr = np.array(in1_addr, dtype=np.int32) + if not isinstance(out1_addr, np.ndarray): + out1_addr = np.array(out1_addr, dtype=np.int32) + if dtype == np.float32: + return _sum_to_buffer_real(in1, outshape, in1_addr.astype(np.int32), out1_addr.astype(np.int32)) + elif dtype == np.complex64: + return _sum_to_buffer_complex(in1, outshape, in1_addr.astype(np.int32), out1_addr.astype(np.int32)) + +@cython.boundscheck(False) +@cython.wraparound(False) +def sum_to_buffer_stride(in1, outshape, addr_info, dtype): + if not isinstance(addr_info, np.ndarray): + addr_info = np.array(addr_info, dtype=np.int32) + if dtype == np.float32: + return _sum_to_buffer_stride_real(in1, outshape, addr_info.astype(np.int32)) + elif dtype == np.complex64: + return _sum_to_buffer_stride_complex(in1, outshape, addr_info.astype(np.int32)) + + +@cython.boundscheck(False) +@cython.wraparound(False) +def far_field_error(current_solution, measured_solution, mask): + cdef np.float32_t [:,:,::1] current_c = np.ascontiguousarray(current_solution) + cdef np.float32_t [:,:,::1] measured_c = np.ascontiguousarray(measured_solution) + cdef np.uint8_t [::1] mask_c = np.frombuffer(np.ascontiguousarray(mask.astype(np.bool)), dtype=np.uint8) + out = np.empty((current_c.shape[0],), np.float32) + cdef np.float32_t [::1] out_c = out + cdef i = current_solution.shape[0] + cdef m = current_solution.shape[1] + cdef n = current_solution.shape[2] + far_field_error_c( + ¤t_c[0,0,0], + &measured_c[0,0,0], + &mask_c[0], + &out_c[0], + i, m, n + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def realspace_error(difference, ea_first_column, da_first_column, out_length): + cdef np.complex64_t [:,:,::1] difference_c = np.ascontiguousarray(difference) + out = np.empty((out_length,), dtype=np.float32) + cdef np.float32_t [::1] out_c = out + cdef i = difference.shape[0] + cdef m = difference.shape[1] + cdef n = difference.shape[2] + if not isinstance(ea_first_column, np.ndarray): + ea_first_column = np.array(ea_first_column, dtype=np.int32) + if not isinstance(da_first_column, np.ndarray): + da_first_column = np.array(da_first_column, dtype=np.int32) + cdef np.int32_t [::1] ea_first_column_c = np.ascontiguousarray(ea_first_column) + cdef np.int32_t [::1] da_first_column_c = np.ascontiguousarray(da_first_column) + cdef int addr_len = min(ea_first_column.shape[0], da_first_column.shape[0]) + cdef int out_length_c = out_length + realspace_error_c( + &difference_c[0,0,0], + &ea_first_column_c[0], + &da_first_column_c[0], + addr_len, + &out_c[0], + i, m, n, + out_length_c + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object): + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + cdef alpha_c = alpha + bp_sol = backpropagated_solution.astype(np.complex64) + cdef np.complex64_t [:,:,::1] bp_sol_c = np.ascontiguousarray(bp_sol) + err_fmag_f32 = err_fmag.astype(np.float32) + cdef np.float32_t [::1] err_fmag_c = np.ascontiguousarray(err_fmag_f32) + cdef np.complex64_t [:,:,::1] exit_wave_c = np.ascontiguousarray(exit_wave) + cdef pbound_c = 0.0 + if not pbound is None: + pbound_c = pbound + cdef np.complex64_t [:,:,::1] probe_obj_c = np.ascontiguousarray(probe_object) + out = np.empty_like(exit_wave) + cdef np.complex64_t [:,:,::1] out_c = out + cdef int i = exit_wave.shape[0] + cdef int m = exit_wave.shape[1] + cdef int n = exit_wave.shape[2] + get_difference_c( + &addr_info_c[0,0,0], + alpha_c, + &bp_sol_c[0,0,0], + &err_fmag_c[0], + &exit_wave_c[0,0,0], + pbound_c, + &probe_obj_c[0,0,0], + &out_c[0,0,0], + i, m, n, + 0 if pbound == None else 1 + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound): + cdef np.complex64_t [:,:,::1] f_c = np.ascontiguousarray(f) + cdef np.float32_t [:,:,::1] af_c = np.ascontiguousarray(af) + cdef np.float32_t [:, :, ::1] fmag_c = np.ascontiguousarray(fmag) + cdef np.uint8_t [::1] mask_c = np.frombuffer(np.ascontiguousarray(mask.astype(np.bool)), dtype=np.uint8) + cdef np.float32_t [::1] err_fmag_c = np.ascontiguousarray(err_fmag.astype(np.float32)) + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + cdef pbound_c = 0.0 + if not pbound is None: + pbound_c = pbound + out = np.empty_like(f) + cdef np.complex64_t [:,:,::1] out_c = out + cdef int M = f.shape[0] + cdef int N = fmag.shape[0] + cdef int A = f.shape[1] + cdef int B = f.shape[2] + assert(M == out.shape[0]) + # assert(N == af.shape[0]) + assert(N == mask.shape[0]) + assert(N == err_fmag.shape[0]) + assert(len(err_fmag.shape) == 1) + assert(M == addr_info.shape[0]) + renormalise_fourier_magnitudes_c( + &f_c[0,0,0], + &af_c[0,0,0], + &fmag_c[0,0,0], + &mask_c[0], + &err_fmag_c[0], + &addr_info_c[0,0,0], + pbound_c, + &out_c[0,0,0], + M, N, A, B, + 0 if pbound == None else 1 + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, postfilter, pbound=None, alpha=1.0, LL_error=True, do_realspace_error=True): + cdef np.uint8_t [::1] mask_c = np.frombuffer(np.ascontiguousarray(mask.astype(np.bool)), dtype=np.uint8) + cdef np.float32_t [:,:,::1] Idata_c = np.ascontiguousarray(Idata) + cdef np.complex64_t [:,:,::1] obj_c = np.ascontiguousarray(obj) + cdef np.complex64_t [:,:,::1] probe_c = np.ascontiguousarray(probe) + # can't cast to continous array here, as it might copy and we update in-place + cdef np.complex64_t [:,:,::1] exit_wave_c = exit_wave + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + cdef np.complex64_t [:,::1] prefilter_c = None + if not prefilter is None: + prefilter_c = np.ascontiguousarray(prefilter) + cdef np.complex64_t [:,::1] postfilter_c = None + if not postfilter is None: + postfilter_c = np.ascontiguousarray(postfilter) + cdef float pbound_c = 0.0 + if not pbound is None: + pbound_c = pbound + errors = np.empty((3, mask.shape[0]), dtype=np.float32) + cdef np.float32_t [:,::1] errors_c = errors + cdef float alpha_c = alpha + cdef int doLLError = 1 if LL_error else 0 + cdef int doRealspaceError = 1 if do_realspace_error else 0 + cdef int doPbound = 0 if pbound == None else 1 + cdef int M = exit_wave.shape[0] + cdef int A = exit_wave.shape[1] + cdef int B = exit_wave.shape[2] + cdef int ob_modes = obj.shape[0] + cdef int C = obj.shape[1] + cdef int D = obj.shape[2] + cdef int pr_modes = probe.shape[0] + cdef int N = mask.shape[0] + # assertions to make sure dimensions are as expected + assert(probe.shape[1] == A) + assert(probe.shape[2] == B) + assert(mask.shape[1] == A) + assert(mask.shape[2] == B) + assert(Idata.shape[0] == N) + assert(Idata.shape[1] == A) + assert(Idata.shape[2] == B) + assert(addr_info.shape[0] == M) + assert(errors.shape[1] == N) + + difference_map_fourier_constraint_c( + &mask_c[0], + &Idata_c[0,0,0], + &obj_c[0,0,0], + &probe_c[0,0,0], + &exit_wave_c[0,0,0], + &addr_info_c[0,0,0], + &prefilter_c[0,0], + &postfilter_c[0,0], + pbound_c, alpha_c, + doLLError, + doRealspaceError, + doPbound, + M, + N, + A, B, C, D, + ob_modes, pr_modes, + &errors_c[0,0] + ) + return errors + +@cython.boundscheck(False) +@cython.wraparound(False) +def norm2f(input): + cdef np.float32_t [::1] input_c = np.frombuffer(np.ascontiguousarray(input), dtype=np.float32) + cdef int size = np.prod(input.shape) + cdef float out = 0.0 + norm2_c( + &input_c[0], + &out, + size, + 0 + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def norm2c(input): + cdef np.complex64_t [::1] input_c = np.frombuffer(np.ascontiguousarray(input), dtype=np.complex64) + cdef int size = np.prod(input.shape) + cdef float out = 0.0 + norm2_c( + &input_c[0], + &out, + size, + 1 + ) + return out + + +@cython.boundscheck(False) +@cython.wraparound(False) +def norm2(input): + if input.dtype == np.float32: + return norm2f(input) + elif input.dtype == np.complex64: + return norm2c(input) + else: + raise NotImplementedError("Norm2 is only implemented for single precision") + +@cython.boundscheck(False) +@cython.wraparound(False) +def mass_center(A): + if A.dtype != np.float32: + raise NotImplementedError("Only single precision is supported") + + cdef np.float32_t [::1] input_c = np.frombuffer(np.ascontiguousarray(A), dtype=np.float32) + cdef int i = A.shape[0] + cdef int m = A.shape[1] + cdef int n = 1 + if len(A.shape) == 3: + n = A.shape[2] + out = np.empty(len(A.shape), np.float32) + cdef np.float32_t [::1] out_c = out + mass_center_c( + &input_c[0], + i, m, n, + &out_c[0] + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def clip_complex_magnitudes_to_range(complex_input, clip_min, clip_max): + if complex_input.dtype != np.complex64: + raise NotImplementedError("Only single precision complex data supported") + if not complex_input.flags['C_CONTIGUOUS']: + raise NotImplementedError("Can only handle contiguous arrays, due to in-place updates") + cdef int n = np.prod(complex_input.shape) + cdef float c_min = clip_min + cdef float c_max = clip_max + # discard all dimensionality information + cdef np.complex64_t [::1] input_c = np.frombuffer(np.ascontiguousarray(complex_input), dtype=np.complex64) + clip_complex_magnitudes_to_range_c( + &input_c[0], + n, c_min, c_max + ) + +@cython.boundscheck(False) +@cython.wraparound(False) +def extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, array_to_be_updated, update_addr, cfact, weights): + cdef np.complex64_t [:,:,::1] exit_wave_c = np.ascontiguousarray(exit_wave) + cdef np.int32_t [:,::1] exit_addr_c = np.ascontiguousarray(exit_addr).astype(np.int32) + cdef np.complex64_t [:,:,::1] array_to_be_extracted_c = np.ascontiguousarray(array_to_be_extracted) + cdef np.int32_t [:,::1] extract_addr_c = np.ascontiguousarray(extract_addr).astype(np.int32) + cdef np.complex64_t [:,:,::1] array_to_be_updated_c = np.ascontiguousarray(array_to_be_updated) + cdef np.int32_t [:,::1] update_addr_c = np.ascontiguousarray(update_addr).astype(np.int32) + cdef np.complex64_t [:,:,::1] cfact_c = np.ascontiguousarray(cfact) + cdef np.float32_t [::1] weights_c = np.ascontiguousarray(weights) + cdef int A = exit_wave.shape[0] + cdef int B = exit_wave.shape[1] + cdef int C = exit_wave.shape[2] + cdef int D = array_to_be_extracted.shape[0] + cdef int E = array_to_be_extracted.shape[1] + cdef int F = array_to_be_extracted.shape[2] + cdef int G = array_to_be_updated.shape[0] + cdef int H = array_to_be_updated.shape[1] + cdef int I = array_to_be_updated.shape[2] + extract_array_from_exit_wave_c( + &exit_wave_c[0,0,0], + A, B, C, + &exit_addr_c[0,0], + &array_to_be_extracted_c[0,0,0], + D, E, F, + &extract_addr_c[0,0], + &array_to_be_updated_c[0,0,0], + G, H, I, + &update_addr_c[0,0], + &weights_c[0], + &cfact_c[0,0,0] + ) + +@cython.boundscheck(False) +@cython.wraparound(False) +def interpolated_shift(c, shift, do_linear=False): + if not do_linear and all([int(s) != s for s in shift]): + raise NotImplementedError("Bicubic interpolated shifts are not implemented yet") + if c.dtype != np.complex64: + raise NotImplementedError("Only complex single precision type supported") + cdef int items = 0 + cdef int rows = 0 + cdef int columns = 0 + if len(c.shape) == 3: + items = c.shape[0] + rows = c.shape[1] + columns = c.shape[2] + else: + items = 1 + rows = c.shape[0] + columns = c.shape[1] + cdef np.complex64_t [::1] c_c = \ + np.frombuffer(np.ascontiguousarray(c), dtype=np.complex64) + out = np.zeros(c.shape, dtype=np.complex64) + cdef np.complex64_t [::1] out_c = np.frombuffer(out, dtype=np.complex64) + cdef float offsetRow = shift[0] + cdef float offsetCol = shift[1] + interpolated_shift_c( + &c_c[0], + &out_c[0], + items, rows, columns, + offsetRow, offsetCol, + 1 + ) + return out + +def get_num_gpus(): + return get_num_gpus_c() + +def get_gpu_compute_capability(dev): + cdef int dev_c = dev + return get_gpu_compute_capability_c(dev_c) + +def select_gpu_device(dev): + cdef int dev_c = dev + select_gpu_device_c(dev_c) + +def get_gpu_memory_mb(dev): + cdef int dev_c = dev + return get_gpu_memory_mb_c(dev_c) + +def get_gpu_name(dev): + cdef int dev_c = dev + cdef string name = get_gpu_name_c(dev_c) + return name.decode('UTF-8') + +def reset_function_cache(): + reset_function_cache_c() + +@cython.boundscheck(False) +@cython.wraparound(False) +def center_probe(probe, center_tolerance): + # can't convert to contiguous array here, as it's in-place returned + # we let Cython flag this if not contiguous + cdef np.complex64_t [:,:,::1] probe_c = probe + cdef float tol = center_tolerance + cdef int i = probe.shape[0] + cdef int m = probe.shape[1] + cdef int n = probe.shape[2] + center_probe_c( + &probe_c[0,0,0], + tol, i, m, n + ) + + +@cython.boundscheck(False) +@cython.wraparound(False) +def difference_map_update_probe(obj, probe_weights, probe, exit_wave, addr_info, cfact_probe, probe_support=None): + cdef np.complex64_t [:,:,::1] obj_c = np.ascontiguousarray(obj) + cdef np.float32_t [::1] probe_weights_c = np.ascontiguousarray(probe_weights) + # updated in-place, so can't use ascontiguousarray + cdef np.complex64_t [:,:,::1] probe_c = probe + cdef np.complex64_t [:,:,::1] exit_wave_c = np.ascontiguousarray(exit_wave) + if not isinstance(addr_info, np.ndarray): + addr_info = np.array(addr_info, dtype=np.int32) + else: + addr_info = addr_info.astype(np.int32) + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + cdef np.complex64_t [:,:,::1] cfact_probe_c = np.ascontiguousarray(cfact_probe) + cdef np.complex64_t [:,:,::1] probe_support_c = None + if probe_support is not None: + probe_support_c = np.ascontiguousarray(probe_support) + cdef int A = exit_wave.shape[0] + cdef int B = exit_wave.shape[1] + cdef int C = exit_wave.shape[2] + cdef int D = obj.shape[0] + cdef int E = obj.shape[1] + cdef int F = obj.shape[2] + cdef int G = cfact_probe.shape[0] + cdef int H = cfact_probe.shape[1] + cdef int I = cfact_probe.shape[2] + return difference_map_update_probe_c( + &obj_c[0,0,0], + &probe_weights_c[0], + &probe_c[0,0,0], + &exit_wave_c[0,0,0], + &addr_info_c[0,0,0], + &cfact_probe_c[0,0,0], + &probe_support_c[0,0,0], + A,B,C,D,E,F,G,H,I + ) + +@cython.boundscheck(False) +@cython.wraparound(False) +def difference_map_update_object(obj, object_weights, probe, exit_wave, addr_info, cfact_object, ob_smooth_std=None, clip_object=None): + # updated in-place, so can't use ascontiguousarray + cdef np.complex64_t [:,:,::1] obj_c = obj + cdef np.complex64_t [:,:,::1] probe_c = np.ascontiguousarray(probe) + cdef np.float32_t [::1] object_weights_c = np.ascontiguousarray(object_weights) + cdef np.complex64_t [:,:,::1] exit_wave_c = np.ascontiguousarray(exit_wave) + if not isinstance(addr_info, np.ndarray): + addr_info = np.array(addr_info, dtype=np.int32) + else: + addr_info = addr_info.astype(np.int32) + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + cdef np.complex64_t [:,:,::1] cfact_object_c = np.ascontiguousarray(cfact_object) + cdef float ob_smooth_std_c = 0 + cdef int doSmoothing = 0 + cdef int doClipping = 0 + if ob_smooth_std is not None: + ob_smooth_std_c = ob_smooth_std + doSmoothing = 1 + cdef float clip_min_c = 0 + cdef float clip_max_c = 0 + if clip_object is not None: + clip_min_c = clip_object[0] + clip_max_c = clip_object[1] + doClipping = 1 + cdef int A = exit_wave.shape[0] + cdef int B = exit_wave.shape[1] + cdef int C = exit_wave.shape[2] + cdef int D = probe.shape[0] + cdef int E = probe.shape[1] + cdef int F = probe.shape[2] + cdef int G = obj.shape[0] + cdef int H = obj.shape[1] + cdef int I = obj.shape[2] + + difference_map_update_object_c( + &obj_c[0,0,0], + &object_weights_c[0], + &probe_c[0,0,0], + &exit_wave_c[0,0,0], + &addr_info_c[0,0,0], + &cfact_object_c[0,0,0], + ob_smooth_std_c, clip_min_c, clip_max_c, + doSmoothing, doClipping, + A,B,C,D,E,F,G,H,I + ) + +@cython.boundscheck(False) +@cython.wraparound(False) +def difference_map_overlap_update(addr_info, cfact_object, cfact_probe, do_update_probe, exit_wave, ob, object_weights, + probe, probe_support, probe_weights,max_iterations, update_object_first, + obj_smooth_std, overlap_converge_factor, probe_center_tol, clip_object=None): + # updated in-place, so can't use ascontiguousarray + cdef np.complex64_t [:,:,::1] obj_c = ob + cdef np.complex64_t [:,:,::1] probe_c = probe + cdef np.float32_t [::1] object_weights_c = np.ascontiguousarray(object_weights) + cdef np.float32_t [::1] probe_weights_c = np.ascontiguousarray(probe_weights) + cdef np.complex64_t [:,:,::1] exit_wave_c = np.ascontiguousarray(exit_wave) + cdef np.complex64_t [:,:,::1] cfact_object_c = np.ascontiguousarray(cfact_object) + cdef np.complex64_t [:,:,::1] cfact_probe_c = np.ascontiguousarray(cfact_probe) + cdef np.complex64_t [:,:,::1] probe_support_c = None + if probe_support is not None: + probe_support_c = np.ascontiguousarray(probe_support) + if not isinstance(addr_info, np.ndarray): + addr_info = np.array(addr_info, dtype=np.int32) + else: + addr_info = addr_info.astype(np.int32) + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + cdef float overlap_converge_factor_c = overlap_converge_factor + cdef int max_iterations_c = max_iterations + + cdef int doUpdateObjectFirst = 0 + cdef int doUpdateProbe = 0 + cdef int doSmoothing = 0 + cdef int doClipping = 0 + cdef int doCentering = 0 + + cdef float ob_smooth_std_c = 0 + cdef float clip_min_c = 0 + cdef float clip_max_c = 0 + cdef float probe_center_tol_c = 0 + + if update_object_first: + doUpdateObjectFirst = 1 + if do_update_probe: + doUpdateProbe = 1 + + if obj_smooth_std is not None: + ob_smooth_std_c = obj_smooth_std + doSmoothing = 1 + + if clip_object is not None: + clip_min_c = clip_object[0] + clip_max_c = clip_object[1] + doClipping = 1 + + if probe_center_tol is not None: + probe_center_tol_c = probe_center_tol + doCentering = 1 + + cdef int A = exit_wave.shape[0] + cdef int B = exit_wave.shape[1] + cdef int C = exit_wave.shape[2] + cdef int D = probe.shape[0] + cdef int E = probe.shape[1] + cdef int F = probe.shape[2] + cdef int G = ob.shape[0] + cdef int H = ob.shape[1] + cdef int I = ob.shape[2] + + difference_map_overlap_constraint_c( + &addr_info_c[0,0,0], + &cfact_object_c[0,0,0], + &cfact_probe_c[0,0,0], + &exit_wave_c[0,0,0], + &obj_c[0,0,0], + &object_weights_c[0], + &probe_c[0,0,0], + &probe_support_c[0,0,0], + &probe_weights_c[0], + ob_smooth_std_c, + clip_min_c, clip_max_c, + probe_center_tol_c, overlap_converge_factor_c, + max_iterations_c, + doUpdateObjectFirst, doUpdateProbe, doSmoothing, + doClipping, doCentering, + A, B, C, D, E, F, G, H, I + ) + +@cython.boundscheck(False) +@cython.wraparound(False) +def complex_gaussian_filter(input, mfs): + cdef int ndims = len(input.shape) + shape = np.ascontiguousarray(np.array(input.shape, dtype=np.int32)) + cdef np.int32_t [::1] shape_c = shape + mfs = np.ascontiguousarray(np.array(mfs, dtype=np.float32)) + cdef np.float32_t [::1] mfs_c = mfs + if input.dtype != np.complex64: + raise NotImplementedError("only complex64 type is supported") + cdef np.complex64_t [::1] input_c = np.frombuffer(np.ascontiguousarray(input), dtype=np.complex64) + out = np.empty_like(input) + cdef np.complex64_t [::1] output_c = np.frombuffer(out, dtype=np.complex64) + complex_gaussian_filter_c( + &input_c[0], + &output_c[0], + &mfs_c[0], + len(shape), + &shape_c[0] + ) + return out + +@cython.boundscheck(False) +@cython.wraparound(False) +def difference_map_iterator(diffraction, obj, object_weights, cfact_object, mask, probe, cfact_probe, probe_support, + probe_weights, exit_wave, addr, pre_fft, post_fft, pbound, overlap_max_iterations, update_object_first, + obj_smooth_std, overlap_converge_factor, probe_center_tol, probe_update_start, alpha=1, + clip_object=None, LL_error=False, num_iterations=1, do_realspace_error=True): + cdef np.float32_t [:,:,::1] diffraction_c = np.ascontiguousarray(diffraction) + # updated in-place, so can't use ascontiguousarray + cdef np.complex64_t [:,:,::1] obj_c = obj + cdef np.float32_t [::1] object_weights_c = np.ascontiguousarray(object_weights) + cdef np.complex64_t [:,:,::1] cfact_object_c = np.ascontiguousarray(cfact_object) + # converted to np.bool if needed! + cdef np.uint8_t [::1] mask_c = np.frombuffer(np.ascontiguousarray(mask.astype(np.bool)), dtype=np.uint8) + # updated in-place, so can't use ascontiguousarray + cdef np.complex64_t [:,:,::1] probe_c = probe + cdef np.complex64_t [:,:,::1] cfact_probe_c = np.ascontiguousarray(cfact_probe) + cdef np.complex64_t [:,:,::1] probe_support_c = None + if probe_support is not None: + probe_support_c = np.ascontiguousarray(probe_support) + cdef np.float32_t [::1] probe_weights_c = np.ascontiguousarray(probe_weights) + # updated in-place, so can't use ascontiguousarray + cdef np.complex64_t [:,:,::1] exit_wave_c = exit_wave + + if not isinstance(addr, np.ndarray): + addr_info = np.array(addr, dtype=np.int32) + else: + addr_info = addr.astype(np.int32) + cdef np.int32_t [:,:,::1] addr_info_c = np.ascontiguousarray(addr_info) + cdef np.complex64_t [:,::1] pre_fft_c = np.ascontiguousarray(pre_fft) + cdef np.complex64_t [:,::1] post_fft_c = np.ascontiguousarray(post_fft) + errors = np.empty((num_iterations, 3, diffraction.shape[0]), dtype=np.float32) + cdef np.float32_t [:,:,::1] errors_c = errors + + cdef float pbound_c = 0.0 + cdef int doPbound = 0 + if not pbound is None: + pbound_c = pbound + doPbound = 1 + + cdef int overlap_max_iterations_c = overlap_max_iterations + + cdef int doUpdateObjectFirst = 0 + if update_object_first: + doUpdateObjectFirst = 1 + + cdef float ob_smooth_std_c = 0 + cdef int doSmoothing = 0 + if obj_smooth_std is not None: + ob_smooth_std_c = obj_smooth_std + doSmoothing = 1 + + cdef float overlap_converge_factor_c = overlap_converge_factor + + cdef int doCentering = 0 + cdef float probe_center_tol_c = 0 + if probe_center_tol is not None: + probe_center_tol_c = probe_center_tol + doCentering = 1 + + cdef int probe_update_start_c = probe_update_start + cdef float alpha_c = alpha + + cdef int doClipping = 0 + cdef float clip_min_c = 0 + cdef float clip_max_c = 0 + if clip_object is not None: + clip_min_c = clip_object[0] + clip_max_c = clip_object[1] + doClipping = 1 + + cdef int do_LL_Error = 0 + if LL_error: + do_LL_Error = 1 + + cdef int doRealspaceError = 0 + if do_realspace_error: + doRealspaceError = 1 + + cdef int num_iterations_c = num_iterations + + cdef int A = exit_wave.shape[0] + cdef int B = exit_wave.shape[1] + cdef int C = exit_wave.shape[2] + cdef int D = probe.shape[0] + cdef int E = probe.shape[1] + cdef int F = probe.shape[2] + cdef int G = obj.shape[0] + cdef int H = obj.shape[1] + cdef int I = obj.shape[2] + cdef int N = diffraction.shape[0] + + difference_map_iterator_c( + &diffraction_c[0,0,0], + &obj_c[0,0,0], + &object_weights_c[0], + &cfact_object_c[0,0,0], + &mask_c[0], + &probe_c[0,0,0], + &cfact_probe_c[0,0,0], + &probe_support_c[0,0,0], + &probe_weights_c[0], + &exit_wave_c[0,0,0], + &addr_info_c[0,0,0], + &pre_fft_c[0,0], + &post_fft_c[0,0], + &errors_c[0,0,0], + pbound_c, + overlap_max_iterations_c, + doUpdateObjectFirst, + ob_smooth_std_c, + overlap_converge_factor_c, + probe_center_tol_c, + probe_update_start_c, + alpha_c, + clip_min_c, clip_max_c, + do_LL_Error, doRealspaceError, + num_iterations_c, + A, B, C, D, E, F, G, H, I, N, + doSmoothing, doClipping, doCentering, doPbound + ) + return errors diff --git a/archive/cuda_extension/python/object_probe_interaction.py b/archive/cuda_extension/python/object_probe_interaction.py new file mode 100644 index 000000000..6186ab9be --- /dev/null +++ b/archive/cuda_extension/python/object_probe_interaction.py @@ -0,0 +1,11 @@ +''' +object_probe_interaction + +Contains things pertinent to the probe and object interaction. +Should have all the engine updates +''' + +from .gpu_extension import difference_map_realspace_constraint, \ + scan_and_multiply, extract_array_from_exit_wave, center_probe, \ + difference_map_update_probe, difference_map_update_object, \ + difference_map_overlap_update diff --git a/archive/cuda_extension/python/propagation.py b/archive/cuda_extension/python/propagation.py new file mode 100644 index 000000000..05f562c0f --- /dev/null +++ b/archive/cuda_extension/python/propagation.py @@ -0,0 +1,6 @@ +''' +All propagation based kernels +''' + +from .gpu_extension import farfield_propagator, sqrt_abs + diff --git a/archive/cuda_extension/setup.py.cuda_extension b/archive/cuda_extension/setup.py.cuda_extension new file mode 100644 index 000000000..34acfb0cb --- /dev/null +++ b/archive/cuda_extension/setup.py.cuda_extension @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +import setuptools, setuptools.command.build_ext +from distutils.core import setup +#from Cython.Build import cythonize +import sys + +from extensions import CudaExtension + +CLASSIFIERS = """\ +Development Status :: 3 - Alpha +Intended Audience :: Science/Research +License :: OSI Approved +Programming Language :: Python +Topic :: Scientific/Engineering +Topic :: Software Development +Operating System :: Unix +""" + + +MAJOR = 0 +MINOR = 4 +MICRO = 1 +ISRELEASED = False +VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO) + + +# import os +# if os.path.exists('MANIFEST'): os.remove('MANIFEST') + +DEBUG = False + +def write_version_py(filename='ptypy/version.py'): + cnt = """ +# THIS FILE IS GENERATED FROM ptypy/setup.py +short_version='%(version)s' +version='%(version)s' +release=%(isrelease)s + +if not release: + version += '.dev' + import subprocess + try: + git_commit = subprocess.Popen(["git","log","-1","--pretty=oneline","--abbrev-commit"], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL).communicate()[0].split()[0] + except: + pass + else: + version += git_commit.strip().decode() + +""" + a = open(filename, 'w') + try: + a.write(cnt % {'version': VERSION, 'isrelease': str(ISRELEASED)}) + finally: + a.close() + + +if __name__ == '__main__': + write_version_py() + write_version_py('doc/version.py') + try: + execfile('ptypy/version.py') + vers = version + except: + vers = VERSION + + +# optional packages that we don't always want to build +exclude_packages = ['*test*', + '*.accelerate.cuda*'] + +acceleration_build_steps = [] + +# I don't like this particularly, but I can't currently find a better way to give the desired result... +if '--tests' in sys.argv: + sys.argv.remove('--tests') + exclude_packages.remove('*test*') + +if '--with-cuda' in sys.argv: + sys.argv.remove('--with-cuda') + acceleration_build_steps.append(CudaExtension(DEBUG)) + exclude_packages.remove('*.accelerate.cuda*') + +if '--all-acceleration' in sys.argv: + sys.argv.remove('--all-acceleration') + # cuda + acceleration_build_steps.append(CudaExtension(DEBUG)) + exclude_packages.remove('*.accelerate.cuda*') + #exclude_packages.remove('*array_based*') + + +# chain this before build_ext +class BuildExtAcceleration(setuptools.command.build_ext.build_ext): + """Custom build command, extending the build with CUDA / Cmake.""" + # add the build parameters via reflection for each extension. + for ext in acceleration_build_steps: + user_options, boolean_options = ext.get_reflection_options() + setuptools.command.build_ext.build_ext.user_options.append(user_options) + setuptools.command.build_ext.build_ext.boolean_options.append(boolean_options) + + def initialize_options(self): + # initialise the options for each extension + setuptools.command.build_ext.build_ext.initialize_options(self) + for ext in acceleration_build_steps: + for key, desc in ext.get_full_options().items(): + self.__dict__[key] = desc['default'] + + def run(self): + # run the build for each extension + for ext in acceleration_build_steps: + options = {} + for key, desc in ext.get_full_options().items(): + options[key] = self.__dict__[key] + ext.build(options) + setuptools.command.build_ext.build_ext.run(self) + + +extensions = [ext.getExtension() for ext in acceleration_build_steps] + +package_list = setuptools.find_packages(exclude=exclude_packages) +#print(package_list) +setup( + name='Python Ptychography toolbox', + version=VERSION, + author='Pierre Thibault, Bjoern Enders, Martin Dierolf and others', + description='Ptychographic reconstruction toolbox', + long_description=open('README.rst', 'r').read(), + package_dir={'ptypy': 'ptypy'}, + packages=package_list, + package_data={'ptypy': ['resources/*',], + 'ptypy.accelerate.py_cuda.cuda': ['*.cu'], + 'ptypy.accelerate.py_cuda.cuda.filtered_fft': ['*.hpp', '*.cpp', 'Makefile', '*.cu', '*.h']}, + scripts=['scripts/ptypy.plot', + 'scripts/ptypy.inspect', + 'scripts/ptypy.plotclient', + 'scripts/ptypy.new', + 'scripts/ptypy.csv2cp', + 'scripts/ptypy.run'], + #ext_modules=cythonize(extensions), + #cmdclass={'build_ext': BuildExtAcceleration + #} +) diff --git a/archive/cuda_extension/tests/__init__.py b/archive/cuda_extension/tests/__init__.py new file mode 100644 index 000000000..a5f33ddcc --- /dev/null +++ b/archive/cuda_extension/tests/__init__.py @@ -0,0 +1,10 @@ +import unittest + +def have_cuda(): + try: + from archive.cuda_extension.accelerate.cuda import gpu_extension + return True + except: + return False + +only_if_cuda_available = unittest.skipIf(not have_cuda(), "no (cythonized) CUDA extension available") diff --git a/archive/cuda_extension/tests/array_utils_test.py b/archive/cuda_extension/tests/array_utils_test.py new file mode 100644 index 000000000..69e00d10b --- /dev/null +++ b/archive/cuda_extension/tests/array_utils_test.py @@ -0,0 +1,416 @@ +''' +Tests for the array_utils module +''' + + +import unittest +from ptypy.accelerate.array_based import array_utils as au +from ptypy.accelerate.array_based import FLOAT_TYPE, COMPLEX_TYPE +from copy import deepcopy +import numpy as np + +from . import have_cuda, only_if_cuda_available +if have_cuda(): + from archive.cuda_extension.accelerate.cuda import array_utils as gau + from archive.cuda_extension.accelerate.cuda.config import init_gpus, reset_function_cache + init_gpus(0) + +@only_if_cuda_available +class ArrayUtilsTest(unittest.TestCase): + + def tearDown(self): + # reset the cached GPU functions after each test + reset_function_cache() + + def test_abs2_real_input_2D_float32_UNITY(self): + x = np.ones((3,3), dtype=np.float32) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_abs2_complex_input_2D_float32_UNITY(self): + x = np.ones((3,3)) + 1j*np.ones((3,3)) + x = x.astype(np.complex64) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_abs2_real_input_3D_float32_UNITY(self): + x = np.ones((3,3,3), dtype=np.float32) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_abs2_complex_input_3D_float32_UNITY(self): + x = np.ones((3,3,3)) + 1j*np.ones((3,3,3)) + x = x.astype(np.complex64) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_abs2_real_input_2D_float64_UNITY(self): + x = np.ones((3,3)) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_abs2_complex_input_2D_float64_UNITY(self): + x = np.ones((3,3)) + 1j*np.ones((3,3)) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_abs2_real_input_3D_float64_UNITY(self): + x = np.ones((3,3,3)) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_abs2_complex_input_3D_float64_UNITY(self): + x = np.ones((3,3,3)) + 1j*np.ones((3,3,3)) + np.testing.assert_array_equal(au.abs2(x), gau.abs2(x)) + + def test_sum_to_buffer_real_UNITY(self): + + in1 = np.array([np.ones((4, 4)), + np.ones((4, 4))*2.0, + np.ones((4, 4))*3.0, + np.ones((4, 4))*4.0], dtype=FLOAT_TYPE) + + outshape = (2, 4, 4) + + in1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (2, 0, 0), + (3, 0, 0)]) + + out1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (0, 0, 0), + (1, 0, 0)]) + + s1 = au.sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype=FLOAT_TYPE) + s2 = gau.sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype=FLOAT_TYPE) + + np.testing.assert_array_equal(s1, s2) + + def test_sum_to_buffer_stride_real_UNITY(self): + + in1 = np.array([np.ones((4, 4)), + np.ones((4, 4))*2.0, + np.ones((4, 4))*3.0, + np.ones((4, 4))*4.0], dtype=FLOAT_TYPE) + + outshape = (2, 4, 4) + + addr_info = np.zeros((4, 5, 3), dtype=np.int) + addr_info[:,2,:] = np.array([(0, 0, 0), + (1, 0, 0), + (2, 0, 0), + (3, 0, 0)]) + + addr_info[:,3,:] = np.array([(0, 0, 0), + (1, 0, 0), + (0, 0, 0), + (1, 0, 0)]) + + s1 = au.sum_to_buffer(in1, outshape, addr_info[:,2,:], addr_info[:,3,:], dtype=FLOAT_TYPE) + s2 = gau.sum_to_buffer_stride(in1, outshape, addr_info, dtype=FLOAT_TYPE) + + np.testing.assert_array_equal(s1, s2) + + def test_sum_to_buffer_complex_UNITY(self): + + in1 = np.array([np.ones((4,4)), + np.ones((4, 4))*2.0, + np.ones((4, 4))*3.0, + np.ones((4, 4))*4.0], dtype=FLOAT_TYPE) \ + + 1j*np.array([np.ones((4,4)), + np.ones((4, 4))*2.0, + np.ones((4, 4))*3.0, + np.ones((4, 4))*4.0], dtype=FLOAT_TYPE) + + outshape = (2, 4, 4) + + in1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (2, 0, 0), + (3, 0, 0)]) + + out1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (0, 0, 0), + (1, 0, 0)]) + + s1 = au.sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype=in1.dtype) + s2 = gau.sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype=in1.dtype) + + np.testing.assert_array_equal(s1, s2) + + def test_sum_to_buffer_stride_complex_UNITY(self): + + in1 = np.array([np.ones((4,4)), + np.ones((4, 4))*2.0, + np.ones((4, 4))*3.0, + np.ones((4, 4))*4.0], dtype=FLOAT_TYPE) \ + + 1j*np.array([np.ones((4,4)), + np.ones((4, 4))*2.0, + np.ones((4, 4))*3.0, + np.ones((4, 4))*4.0], dtype=FLOAT_TYPE) + + outshape = (2, 4, 4) + + addr_info = np.zeros((4, 5, 3), dtype=np.int) + addr_info[:,2,:] = np.array([(0, 0, 0), + (1, 0, 0), + (2, 0, 0), + (3, 0, 0)]) + + addr_info[:,3,:] = np.array([(0, 0, 0), + (1, 0, 0), + (0, 0, 0), + (1, 0, 0)]) + + s1 = au.sum_to_buffer(in1, outshape, addr_info[:,2,:], addr_info[:,3,:], dtype=in1.dtype) + s2 = gau.sum_to_buffer_stride(in1, outshape, addr_info, dtype=in1.dtype) + + np.testing.assert_array_equal(s1, s2) + + + def test_norm2_1d_real_UNITY(self): + a = np.array([1.0, 2.0], dtype=FLOAT_TYPE) + out = au.norm2(a) + outg =gau.norm2(a) + np.testing.assert_array_equal(out, outg) + + def test_norm2_1d_complex_UNITY(self): + a = np.array([1.0+1.0j, 2.0+2.0j], dtype=COMPLEX_TYPE) + out = au.norm2(a) + outg = gau.norm2(a) + np.testing.assert_array_equal(out, outg) + + def test_norm2_2d_real_UNITY(self): + a = np.array([[1.0, 2.0], + [3.0, 4.0]], dtype=FLOAT_TYPE) + out = au.norm2(a) + outg = gau.norm2(a) + np.testing.assert_array_equal(out, outg) + + def test_norm2_2d_complex_UNITY(self): + a = np.array([[1.0+1.0j, 2.0+2.0j], + [3.0+3.0j, 4.0+4.0j]], dtype=COMPLEX_TYPE) + out = au.norm2(a) + outg = gau.norm2(a) + np.testing.assert_array_equal(out, outg) + + def test_norm2_3d_real_UNITY(self): + a = np.array([[[1.0, 2.0], + [3.0, 4.0]], + [[5.0, 6.0], + [7.0, 8.0]]], dtype=FLOAT_TYPE) + out = au.norm2(a) + outg = gau.norm2(a) + np.testing.assert_array_equal(out, outg) + + def test_norm2_3d_complex_UNITY(self): + a = np.array([[[1.0+1.0j, 2.0+2.0j], + [3.0+3.0j, 4.0+4.0j]], + [[5.0 + 5.0j, 6.0 + 6.0j], + [7.0 + 7.0j, 8.0 + 8.0j]]], dtype=COMPLEX_TYPE) + out = au.norm2(a) + outg = gau.norm2(a) + np.testing.assert_array_equal(out, outg) + + def test_norm2_1d_real_large_UNITY(self): + a = np.ones(2000000, dtype=FLOAT_TYPE) # > 1M, to test multi-stage + out = au.norm2(a) + outg = gau.norm2(a) + np.testing.assert_array_equal(out, outg) + + def test_complex_gaussian_filter_1d_UNITY(self): + data = np.zeros((11,), dtype=COMPLEX_TYPE) + data[5] = 1.0 +1.0j + mfs = [1.0] + out = au.complex_gaussian_filter(data, mfs) + outg = gau.complex_gaussian_filter(data, mfs) + #for i in xrange(11): + # print("{} vs {}".format(out[i], outg[i])) + np.testing.assert_allclose(out, outg, rtol=1e-6) + + def test_complex_gaussian_filter_2d_simple_UNITY(self): + data = np.zeros((11, 11), dtype=COMPLEX_TYPE) + data[5, 5] = 1.0+1.0j + mfs = 1.0,0.0 + out = au.complex_gaussian_filter(data, mfs) + outg = gau.complex_gaussian_filter(data, mfs) + np.testing.assert_allclose(out, outg, rtol=1e-6) + + def test_complex_gaussian_filter_2d_simple2_UNITY(self): + data = np.zeros((11, 11), dtype=COMPLEX_TYPE) + data[5, 5] = 1.0+1.0j + mfs = 0.0,1.0 + out = au.complex_gaussian_filter(data, mfs) + outg = gau.complex_gaussian_filter(data, mfs) + np.testing.assert_allclose(out, outg, rtol=1e-6) + + def test_complex_gaussian_filter_2d_UNITY(self): + data = np.zeros((8, 8), dtype=COMPLEX_TYPE) + data[3:5, 3:5] = 2.0+2.0j + mfs = 3.0,4.0 + out = au.complex_gaussian_filter(data, mfs) + outg = gau.complex_gaussian_filter(data, mfs) + np.testing.assert_allclose(out, outg, rtol=1e-6) + + def test_complex_gaussian_filter_2d_batched(self): + batch_number = 2 + A = 5 + B = 5 + + data = np.zeros((batch_number, A, B), dtype=COMPLEX_TYPE) + data[:, 2:3, 2:3] = 2.0+2.0j + mfs = 3.0,4.0 + out = au.complex_gaussian_filter(data, mfs) + gout = gau.complex_gaussian_filter(data, mfs) + + np.testing.assert_allclose(out, gout, rtol=1e-6) + + def test_mass_center_2d_simple(self): + data = np.array([[0,0,0,0], + [0,1,1,0], + [0,1,1,0], + [0,1,1,0], + [0,1,1,0]], dtype=FLOAT_TYPE) + comg = gau.mass_center(data) + np.testing.assert_array_equal([2.5, 1.5], comg) + + def test_mass_center_2d_UNITY(self): + npts = 64 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X-Xoff)**2 + (Y-Yoff)**2 < rad**2] = probe_vals + + com = au.mass_center(np.abs(probe[0])) + comg = gau.mass_center(np.abs(probe[0])) + + np.testing.assert_array_almost_equal(com, comg) + + def test_mass_center_3d_UNITY(self): + npts = 64 + probe = np.zeros((npts, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y, Z = np.meshgrid(x, x, x) + Xoff = 5.0 + Yoff = 2.0 + Zoff = 10.0 + probe[(X-Xoff)**2 + (Y-Yoff)**2 + (Z-Zoff)**2< rad**2] = probe_vals + + com = au.mass_center(np.abs(probe)) + comg = gau.mass_center(np.abs(probe)) + np.testing.assert_allclose(com, comg, rtol=1e-6) + self.assertEqual(com.dtype, comg.dtype) + + def test_interpolated_shift_integer(self): + A = np.array([[0,0,0,0,0], + [0,1,1,0,0], + [0,1,1,0,0], + [0,0,0,0,0], + [0,0,0,0,0]], dtype=COMPLEX_TYPE) + \ + 1j * np.array([[0,0,0,0,0], + [0,0,1,1,1], + [1,0,1,0,1], + [0,0,0,1,1], + [0,0,0,0,0]], dtype=COMPLEX_TYPE) + A = A.real + 1j * A.real + + shifts = [[-1, 0], + [1, 1], + [2,2], + [-5,0], + [0,4], + [-3,-3], + [0,0]] + for s in shifts: + B1 = au.interpolated_shift(A, s) + gB1 = gau.interpolated_shift(A, s) + np.testing.assert_allclose(B1, gB1, rtol=1e-7, atol=1e-7) + + def test_interpolated_shift_linear_simple(self): + A = np.array([[0,0,0,0,0], + [0,1,1,0,0], + [0,1,1,0,0], + [0,0,0,0,0], + [0,0,0,0,0]], dtype=COMPLEX_TYPE) + \ + 1j * np.array([ + [0,0,0,0,0], + [0,0,1,1,1], + [1,0,1,0,1], + [0,0,0,1,1], + [0,0,0,0,0]], dtype=COMPLEX_TYPE) + + + shifts = [ + [0,-0.25], + [-1.1,1.4], + [0.5,-0.12], + [-2.4,2.3], + [-5.6,1.2], + [0.2, 6.2], + [-0.2, -7.6], + [-102.1, 951.12], + ] + for s in shifts: + B1 = au.interpolated_shift(A, s, do_linear=True) + gB1 = gau.interpolated_shift(A, s, do_linear=True) + np.testing.assert_allclose(B1, gB1, rtol=1e-6, atol=1e-7, + err_msg="Failure for shift {}:\n CPU={}\nGPU={}".format(s, B1, gB1)) + + def test_interpolated_shift_linear(self): + npts = 32 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X-Xoff)**2 + (Y-Yoff)**2 < rad**2] = probe_vals + + shifts = [[-Xoff, -Yoff], + [0.5,0], + #[-5.2, -1.9] + ] + + for s in shifts: + B = au.interpolated_shift(probe[0], s, do_linear=True) + gB = gau.interpolated_shift(probe[0], s, do_linear=True) + np.testing.assert_allclose(B, gB, rtol=1e-6, atol=1e-3, err_msg="failed for shifts {}".format(s)) + + @unittest.skip("not implemented yet") + def test_interpolated_shift_bicubic(self): + npts = 32 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.1 + Yoff = 2.2 + probe[0, (X-Xoff)**2 + (Y-Yoff)**2 < rad**2] = probe_vals + + shifts = [[-Xoff, -Yoff], + [2.3,1.3], + [-5.2, -1.9]] + + for s in shifts: + B = au.interpolated_shift(probe[0], s) + gB = gau.interpolated_shift(probe[0], s) + np.testing.assert_allclose(B, gB, rtol=1e-6, atol=1e-3) + + def test_clip_magnitudes_to_range(self): + data = np.ones((5,5), dtype=COMPLEX_TYPE) + data[2, 4] = 20.0*np.exp(1j*np.pi/2) + data[3, 1] = 0.2*np.exp(1j*np.pi/3) + gdata = deepcopy(data) + clip_min = 0.5 + clip_max = 2.0 + + au.clip_complex_magnitudes_to_range(data, clip_min, clip_max) + gau.clip_complex_magnitudes_to_range(gdata, clip_min, clip_max) + np.testing.assert_array_almost_equal(data, gdata) + + +if __name__=='__main__': + unittest.main() diff --git a/archive/cuda_extension/tests/constraints_regression_test.py b/archive/cuda_extension/tests/constraints_regression_test.py new file mode 100644 index 000000000..79864c1e7 --- /dev/null +++ b/archive/cuda_extension/tests/constraints_regression_test.py @@ -0,0 +1,551 @@ +''' +The tests for the constraints +''' + +import unittest +import numpy as np +from copy import deepcopy +from ptypy.accelerate.array_based import constraints as con, FLOAT_TYPE, COMPLEX_TYPE +from . import have_cuda, only_if_cuda_available + +if have_cuda(): + from archive.cuda_extension.accelerate.cuda import constraints as gcon + +@only_if_cuda_available +class ConstraintsRegressionTest(unittest.TestCase): + ''' + a module to holds the constraints + ''' + + def test_renormalise_fourier_magnitudes_pbound_none(self): + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + pbound = None # the power bound + + fmag = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE)# the measured magnitudes NxAxB + mask = np.empty(shape=(N, A, B), dtype=np.int32)# the masks for the measured magnitudes either 1xAxB or NxAxB + err_fmag = np.empty(shape=(N, ), dtype=FLOAT_TYPE)# deviation from the diffraction pattern for each af + f = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # the current iterant + af = np.empty(shape=(M, A, B), dtype=FLOAT_TYPE)# the absolute magnitudes of f + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32)# the address book + + ## now lets fill them with some values, these are junk and not supposed to be indicative of real values. + + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + err_fmag[:] = np.ones((N,)) # this shouldn't be used a pbound is None + + f_fill = np.array([ix + 1j*(ix**2) for ix in range(np.prod(f.shape))]).reshape((M, A, B)) + f[:] = f_fill + + af[:] = np.sqrt((f*f.conj()).real) + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + ma = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + out = con.renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + gout = gcon.renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + out_test = out.reshape((np.prod(out.shape),)) + gout_test = gout.reshape((np.prod(gout.shape),)) + for idx in range(len(out)): + np.testing.assert_allclose(out_test[idx], + gout_test[idx], + err_msg=("failed on index:%s\n" % (idx))) + + + def test_renormalise_fourier_magnitudes_pbound_not_none(self): + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + pbound = 5.0 # the power bound + + fmag = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE)# the measured magnitudes NxAxB + mask = np.empty(shape=(N, A, B), dtype=np.int32)# the masks for the measured magnitudes either 1xAxB or NxAxB + err_fmag = np.empty(shape=(N, ), dtype=FLOAT_TYPE)# deviation from the diffraction pattern for each af + f = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # the current iterant + af = np.empty(shape=(M, A, B), dtype=FLOAT_TYPE)# the absolute magnitudes of f + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32)# the address book + + ## now lets fill them with some values, these are junk and not supposed to be indicative of real values. + + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + err_fmag_fill = np.ones((N,))*(pbound+0.1) # should be greater than the pbound + err_fmag_fill[N//2] = 4.0 # this one should be less than the pbound and not update + err_fmag[:] = err_fmag_fill # this shouldn't be used a pbound is None + + f_fill = np.array([ix + 1j*(ix**2) for ix in range(np.prod(f.shape))]).reshape((M, A, B)) + f[:] = f_fill + + af[:] = np.sqrt((f*f.conj()).real) + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + ma = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gout = gcon.renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + out = con.renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + out_test = out.reshape((np.prod(out.shape),)) + gout_test = gout.reshape((np.prod(gout.shape),)) + for idx in range(len(out)): + np.testing.assert_allclose(out_test[idx], + gout_test[idx], + err_msg=("failed on index:%s\n" % (idx))) + + + def test_get_difference_pbound_is_none(self): + alpha = 1.0 # feedback constant + pbound = 5.0 # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + + backpropagated_solution = np.empty(shape=(M, A, B), + dtype=COMPLEX_TYPE) # The current iterant backpropagated + probe_object = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # the probe multiplied by the object + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + + # now fill it with stuff + + backpropagated_solution_fill = np.array( + [ix + 1j * (ix ** 2) for ix in range(np.prod(backpropagated_solution.shape))]).reshape((M, A, B)) + backpropagated_solution[:] = backpropagated_solution_fill + + probe_object_fill = np.array( + [ix + 1j * ix for ix in range(10, 10 + np.prod(backpropagated_solution.shape), 1)]).reshape( + (M, A, B)) + probe_object[:] = probe_object_fill + + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(backpropagated_solution.shape), 1)]).reshape( + (M, A, B)) + exit_wave[:] = exit_wave_fill + + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gout = gcon.get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, + probe_object) + + out = con.get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, + probe_object) + out_test = out.reshape((np.prod(out.shape),)) + gout_test = gout.reshape((np.prod(gout.shape),)) + for idx in range(len(out)): + np.testing.assert_allclose(out_test[idx], + gout_test[idx], + err_msg=("failed on index:%s\n" % (idx))) + + def test_get_difference_pbound_is_not_none(self): + alpha = 1.0 # feedback constant + pbound = 5.0 # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + + backpropagated_solution = np.empty(shape=(M, A, B), + dtype=COMPLEX_TYPE) # The current iterant backpropagated + probe_object = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # the probe multiplied by the object + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + + # now fill it with stuff + + backpropagated_solution_fill = np.array( + [ix + 1j * (ix ** 2) for ix in range(np.prod(backpropagated_solution.shape))]).reshape((M, A, B)) + backpropagated_solution[:] = backpropagated_solution_fill + + probe_object_fill = np.array( + [ix + 1j * ix for ix in range(10, 10 + np.prod(backpropagated_solution.shape), 1)]).reshape( + (M, A, B)) + probe_object[:] = probe_object_fill + + err_fmag_fill = np.ones((N,)) * (pbound + 0.1) # should be higher than pbound + err_fmag_fill[N // 2] = 4.0 # except for this one!! + err_fmag[:] = err_fmag_fill + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(backpropagated_solution.shape), 1)]).reshape( + (M, A, B)) + exit_wave[:] = exit_wave_fill + + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gout = gcon.get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, + probe_object) + + out = con.get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, + probe_object) + out_test = out.reshape((np.prod(out.shape),)) + gout_test = gout.reshape((np.prod(gout.shape),)) + + for idx in range(len(out)): + np.testing.assert_allclose(out_test[idx], + gout_test[idx], + err_msg=("failed on index:%s\n" % (idx))) + + def test_difference_map_fourier_constraint_pbound_is_none_with_realspace_error_and_LL_error(self): + + alpha = 1.0 # feedback constant + pbound = None # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx > 0: + pa[::idx, 0] = idx # multimodal could work like this, but this is not a concrete thing. + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx > 0: + oa[::idx, + 0] = idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gexit_wave = deepcopy(exit_wave) + + out = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + + gout = gcon.difference_map_fourier_constraint(mask, Idata, obj, probe, gexit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + np.testing.assert_allclose(out, + gout, + rtol=1e-6, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(exit_wave, + gexit_wave, + rtol=1e-6, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + + def test_difference_map_fourier_constraint_pbound_is_none_no_error(self): + + alpha = 1.0 # feedback constant + pbound = None # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx > 0: + pa[::idx, 0] = idx # multimodal could work like this, but this is not a concrete thing. + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx > 0: + oa[::idx, + 0] = idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + + gexit_wave = deepcopy(exit_wave) + + out = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=False, + do_realspace_error=False) + + gout = con.difference_map_fourier_constraint(mask, Idata, obj, probe, gexit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=False, + do_realspace_error=False) + + np.testing.assert_allclose(out, + gout, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(exit_wave, + gexit_wave, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + def test_difference_map_fourier_constraint_pbound_is_not_none_with_realspace_and_LL_error(self): + ''' + mixture of high and low p bound values respect to the fourier error + ''' + #expected_fourier_error = np.array([6.07364329e+12, 8.87756439e+13, 9.12644403e+12, 1.39851186e+14]) + pbound = 8.86e13 # this should now mean some of the arrays update differently through the logic + alpha = 1.0 # feedback constant + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx > 0: + pa[::idx, 0] = idx # multimodal could work like this, but this is not a concrete thing. + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx > 0: + oa[::idx, + 0] = idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gexit_wave = deepcopy(exit_wave) + + out = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + gout = gcon.difference_map_fourier_constraint(mask, Idata, obj, probe, gexit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + np.testing.assert_allclose(out, + gout, + rtol=1e-6, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(exit_wave, + gexit_wave, + rtol=1e-6, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + +if __name__ == '__main__': + unittest.main() + + + diff --git a/archive/cuda_extension/tests/constraints_test.py b/archive/cuda_extension/tests/constraints_test.py new file mode 100644 index 000000000..1c27d8827 --- /dev/null +++ b/archive/cuda_extension/tests/constraints_test.py @@ -0,0 +1,1079 @@ +''' +The tests for the constraints +''' + +import unittest +from . import utils as tu +import numpy as np +from copy import deepcopy + +from ptypy.accelerate.array_based import constraints as con +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based.constraints import difference_map_fourier_constraint, renormalise_fourier_magnitudes, get_difference +from archive.array_based.error_metrics import far_field_error +from ptypy.accelerate.array_based.object_probe_interaction import difference_map_realspace_constraint, scan_and_multiply +from archive.array_based.propagation import farfield_propagator +import ptypy.accelerate.array_based.array_utils as au +from ptypy.accelerate.array_based import COMPLEX_TYPE, FLOAT_TYPE + +from . import have_cuda, only_if_cuda_available +if have_cuda(): + from archive.cuda_extension.accelerate.cuda import constraints as gcon + from archive.cuda_extension.accelerate.cuda import get_difference as gget_difference + from archive.cuda_extension.accelerate.cuda import renormalise_fourier_magnitudes as grenormalise_fourier_magnitudes + from archive.cuda_extension.accelerate.cuda import difference_map_fourier_constraint as gdifference_map_fourier_constraint + from archive.cuda_extension.accelerate.cuda.config import init_gpus, reset_function_cache + init_gpus(0) + +@only_if_cuda_available +class ConstraintsTest(unittest.TestCase): + + def tearDown(self): + # reset the cached GPU functions after each test + reset_function_cache() + + def test_get_difference_UNITY(self): + alpha = 1.0 + pbound = None + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + Idata = vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + + # # Propagate the exit waves + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) + f = farfield_propagator(constrained, propagator.pre_fft, propagator.post_fft, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(Idata)) + af = np.sqrt(af2) + # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) + err_fmag = far_field_error(af, fmag, mask) + renormed_f = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + + backpropagated_solution = farfield_propagator(renormed_f, + propagator.post_fft.conj(), + propagator.pre_fft.conj(), + direction='backward') + + difference = get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + gdifference = gget_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + + np.testing.assert_allclose(difference, gdifference, rtol=6e-5) + # Detailed errors + max_relerr = np.max(np.abs((gdifference-difference) / difference)) + max_abserr = np.max(np.abs(gdifference-difference)) + print("Max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + + def test_get_difference_pbound_UNITY(self): + alpha = 1.0 + pbound = 0.597053604126 + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + Idata = vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + + # # Propagate the exit waves + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) + f = farfield_propagator(constrained, propagator.pre_fft, propagator.post_fft, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(Idata)) + af = np.sqrt(af2) + # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) + err_fmag = far_field_error(af, fmag, mask) + err_fmag = np.ones_like(err_fmag) * 145.824958919 + renormed_f = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + backpropagated_solution = farfield_propagator(renormed_f, + propagator.post_fft.conj(), + propagator.pre_fft.conj(), + direction='backward') + + difference = get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + gdifference = gget_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + + np.testing.assert_allclose(difference, gdifference, rtol=6e-5) + # Detailed errors + max_relerr = np.max(np.abs((gdifference-difference) / difference)) + max_abserr = np.max(np.abs(gdifference-difference)) + print("Max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + + def test_get_difference_no_update_UNITY(self): + alpha = 1.0 + pbound = 0.597053604126 + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + Idata = vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + + # # Propagate the exit waves + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) + f = farfield_propagator(constrained, propagator.pre_fft, propagator.post_fft, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(Idata)) + af = np.sqrt(af2) + # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) + err_fmag = far_field_error(af, fmag, mask) + err_fmag = np.ones_like(err_fmag) * 0.4 + renormed_f = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + backpropagated_solution = farfield_propagator(renormed_f, + propagator.post_fft.conj(), + propagator.pre_fft.conj(), + direction='backward') + + difference = get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + gdifference = gget_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + + # result is actually all-zero, so array_equal works + np.testing.assert_array_equal(difference, gdifference) + + def test_renormalise_fourier_magnitudes_UNITY(self): + alpha = 1.0 + pbound = None + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + Idata = vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + # # Propagate the exit waves + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) + f = farfield_propagator(constrained, propagator.pre_fft, propagator.post_fft, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(Idata)) + af = np.sqrt(af2) + # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) + err_fmag = far_field_error(af, fmag, mask) + + renormed_f = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + grenormed_f = grenormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + np.testing.assert_allclose(renormed_f, grenormed_f, rtol=1e-6) + + def test_renormalise_fourier_magnitudes_pbound_UNITY(self): + alpha = 1.0 + pbound = 0.597053604126 + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + Idata = vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + + # # Propagate the exit waves + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) + f = farfield_propagator(constrained, propagator.pre_fft, propagator.post_fft, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(Idata)) + af = np.sqrt(af2) + # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) + err_fmag = far_field_error(af, fmag, mask) + err_fmag = np.ones_like(err_fmag) * 145.824958919 + renormed_f = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + grenormed_f = grenormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + np.testing.assert_allclose(renormed_f, grenormed_f, rtol=1e-6) + + def test_renormalise_fourier_magnitudes_no_update_UNITY(self): + alpha = 1.0 + pbound = 0.597053604126 + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + Idata = vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + + # # Propagate the exit waves + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha) + f = farfield_propagator(constrained, propagator.pre_fft, propagator.post_fft, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), Idata.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(Idata)) + af = np.sqrt(af2) + # # Fourier magnitudes deviations(current_solution, pbound, measured_solution, mask, addr) + err_fmag = far_field_error(af, fmag, mask) + err_fmag = np.ones_like(err_fmag) * 0.4 + renormed_f = renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + grenormed_f = grenormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + + np.testing.assert_array_equal(renormed_f, grenormed_f) + + def test_difference_map_fourier_constraint_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + + # take a copy of the starting point, as function updates in-place + exit_wave_start = np.copy(vectorised_scan['exit wave']) + + errors = difference_map_fourier_constraint( + vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=None, + alpha=1.0, + LL_error=True) + + # keep result, copy original back + exit_wave = vectorised_scan['exit wave'] + vectorised_scan['exit wave'] = exit_wave_start + + gerrors = gdifference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=None, + alpha=1.0, + LL_error=True) + + gexit_wave = vectorised_scan['exit wave'] + + max_relerr = np.max(np.abs((exit_wave-gexit_wave) / exit_wave)) + mean_relerr = np.mean(np.abs((exit_wave-gexit_wave) / exit_wave)) + max_abserr = np.max(np.abs(exit_wave-gexit_wave)) + mean_abserr = np.mean(np.abs(exit_wave-gexit_wave)) + print("Exit wave max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + print("Exit wave mean errors: rel={}, abs={}".format(mean_relerr, mean_abserr)) + + max_relerr = np.max(np.abs((errors-gerrors) / errors), axis=None) + mean_relerr = np.mean(np.abs((errors-gerrors) / errors), axis=None) + max_abserr = np.max(np.abs(errors-gerrors), axis=None) + mean_abserr = np.mean(np.abs(errors-gerrors), axis=None) + print("Errors max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + print("Errors mean errors: rel={}, abs={}".format(mean_relerr, mean_abserr)) + + np.testing.assert_allclose(exit_wave, gexit_wave, rtol=3e-1, atol=18) + np.testing.assert_allclose(errors, gerrors, rtol=3e-4) + + def test_difference_map_fourier_constraint_pbound_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + pbound = 0.597053604126 + + # take a copy of the starting point, as function updates in-place + exit_wave_start = np.copy(vectorised_scan['exit wave']) + + errors = difference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=pbound, + alpha=1.0, + LL_error=True) + + # keep result, copy original back + exit_wave = vectorised_scan['exit wave'] + vectorised_scan['exit wave'] = exit_wave_start + + gerrors = gdifference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=pbound, + alpha=1.0, + LL_error=True) + + gexit_wave = vectorised_scan['exit wave'] + + max_relerr = np.max(np.abs((exit_wave-gexit_wave) / exit_wave)) + mean_relerr = np.mean(np.abs((exit_wave-gexit_wave) / exit_wave)) + max_abserr = np.max(np.abs(exit_wave-gexit_wave)) + mean_abserr = np.mean(np.abs(exit_wave-gexit_wave)) + print("Exit wave max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + print("Exit wave mean errors: rel={}, abs={}".format(mean_relerr, mean_abserr)) + + max_relerr = np.max(np.abs((errors-gerrors) / errors), axis=None) + mean_relerr = np.mean(np.abs((errors-gerrors) / errors), axis=None) + max_abserr = np.max(np.abs(errors-gerrors), axis=None) + mean_abserr = np.mean(np.abs(errors-gerrors), axis=None) + print("Errors max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + print("Errors mean errors: rel={}, abs={}".format(mean_relerr, mean_abserr)) + + np.testing.assert_allclose(exit_wave, gexit_wave, rtol=4e-1, atol=16) + np.testing.assert_allclose(errors, gerrors, rtol=4e-4) + + def test_difference_map_fourier_constraint_no_update_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + pbound = 200.0 + + # take a copy of the starting point, as function updates in-place + exit_wave_start = deepcopy(vectorised_scan['exit wave']) + + errors = difference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=pbound, + alpha=1.0, + LL_error=True) + # keep result, copy original back + exit_wave = vectorised_scan['exit wave'] + vectorised_scan['exit wave'] = exit_wave_start + + + gerrors = gdifference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=pbound, + alpha=1.0, + LL_error=True) + + gexit_wave = vectorised_scan['exit wave'] + + max_relerr = np.nanmax(np.abs((exit_wave-gexit_wave) / exit_wave)) + mean_relerr = np.nanmean(np.abs((exit_wave-gexit_wave) / exit_wave)) + max_abserr = np.max(np.abs(exit_wave-gexit_wave)) + mean_abserr = np.mean(np.abs(exit_wave-gexit_wave)) + print("Exit wave max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + print("Exit wave mean errors: rel={}, abs={}".format(mean_relerr, mean_abserr)) + + max_relerr = np.nanmax(np.abs((errors-gerrors) / errors), axis=None) + mean_relerr = np.nanmean(np.abs((errors-gerrors) / errors), axis=None) + max_abserr = np.max(np.abs(errors-gerrors), axis=None) + mean_abserr = np.mean(np.abs(errors-gerrors), axis=None) + print("Errors max errors: rel={}, abs={}".format(max_relerr, max_abserr)) + print("Errors mean errors: rel={}, abs={}".format(mean_relerr, mean_abserr)) + + np.testing.assert_allclose(exit_wave, gexit_wave, rtol=5e-1, atol=10) + np.testing.assert_allclose(errors, gerrors, rtol=3e-4) + + def test_difference_map_fourier_constraint_pbound_is_none_with_realspace_error_and_LL_error(self): + + alpha = 1.0 # feedback constant + pbound = None # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE)# deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32)# the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE)# the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), dtype=np.int32)# the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j*(ix**2) for ix in range(np.prod(obj.shape))]).reshape((num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j*ix for ix in range(10, 10+np.prod(probe.shape), 1)]).reshape((num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j)# this would actually vary + postfilter.fill(20.0 + 3.0j)# this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array([ix**2 + 1j*ix for ix in range(20, 20+np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx>0: + pa[::idx,0]=idx # multimodal could work like this, but this is not a concrete thing. + + + X, Y = np.meshgrid(range(npts_greater_than), range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx>0: + oa[::idx,0]=idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gexit_wave = deepcopy(exit_wave) + + errors = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, postfilter, pbound=pbound, alpha=alpha, LL_error=True, do_realspace_error=True) + gerrors = gcon.difference_map_fourier_constraint(mask, Idata, obj, probe, gexit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + + np.testing.assert_allclose(gerrors, + errors, + rtol=1e-6, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(gexit_wave, + exit_wave, + rtol=1e-6, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + def test_difference_map_fourier_constraint_pbound_is_none_no_error(self): + + alpha = 1.0 # feedback constant + pbound = None # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx > 0: + pa[::idx, 0] = idx # multimodal could work like this, but this is not a concrete thing. + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx > 0: + oa[::idx, + 0] = idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gexit_wave = deepcopy(exit_wave) + + gerrors = gcon.difference_map_fourier_constraint(mask, Idata, obj, probe, gexit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=False, + do_realspace_error=False) + errors = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=False, + do_realspace_error=False) + + np.testing.assert_allclose(gerrors, + errors, + rtol=1e-6, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(gexit_wave, + exit_wave, + rtol=1e-6, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + + def test_difference_map_fourier_constraint_pbound_is_not_none_with_realspace_and_LL_error(self): + ''' + mixture of high and low p bound values respect to the fourier error + ''' + #expected_fourier_error = np.array([6.07364329e+12, 8.87756439e+13, 9.12644403e+12, 1.39851186e+14]) + pbound = 8.86e13 # this should now mean some of the arrays update differently through the logic + alpha = 1.0 # feedback constant + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx > 0: + pa[::idx, 0] = idx # multimodal could work like this, but this is not a concrete thing. + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx > 0: + oa[::idx, + 0] = idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + gexit_wave = deepcopy(exit_wave) + + gerrors = gcon.difference_map_fourier_constraint(mask, Idata, obj, probe, gexit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + errors = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + + np.testing.assert_allclose(gerrors, + errors, + rtol=1e-6, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(gexit_wave, + exit_wave, + rtol=1e-6, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + + @unittest.skip("The test doesn't work, but moonflower sample shows that this is actually working ok") + def test_difference_map_iterator_with_probe_update(self): + ''' + This test, assumes the logic below this function works fine, and just does some iterations of difference map on + some spoof data to check that the combination works. + ''' + num_iter = 2 + + pbound = 8.86e13 # this should now mean some of the arrays update differently through the logic + alpha = 1.0 # feedback constant + # diffraction frame size + B = 2 # for example + C = 4 # for example + + # probe dimensions + D = 2 # for example + E = B + F = C + + scan_pts = 2 # a 2x2 grid + N = scan_pts**2 # the number of measurement points in a scan + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + + # object dimensions + G = 1 # for example + H = B + npts_greater_than + I = C + npts_greater_than + + A = scan_pts ** 2 * G * D # number of exit waves + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(A, 5, 3), dtype=np.int32) # the address book + diffraction = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, B, C), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + diffraction_fill = np.arange(np.prod(diffraction.shape)).reshape(diffraction.shape).astype(diffraction.dtype) + diffraction[:] = diffraction_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (G, H, I)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (D, B, C)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((A, B, C)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + # mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((A, 3), dtype=np.int32) + pa[:N,0] = 0 + pa[N:, 0] = 1 + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((A, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + + + ea = np.array([np.array([ix, 0, 0]) for ix in range(A)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * D * G) + ma = np.zeros((A, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + + for idx in range(D): + cfact_probe[idx] = np.ones((B, C)) * 5 * (idx + 1) + + gexit_wave = deepcopy(exit_wave) + gprobe = deepcopy(probe) + gobj = deepcopy(obj) + + errors = con.difference_map_iterator(diffraction=diffraction, + obj=obj, + object_weights=obj_weights, + cfact_object=cfact_object, + mask=mask, + probe=probe, + cfact_probe=cfact_probe, + probe_support=None, + probe_weights=probe_weights, + exit_wave=exit_wave, + addr=addr_info, + pre_fft=prefilter, + post_fft=postfilter, + pbound=pbound, + overlap_max_iterations=10, + update_object_first=False, + obj_smooth_std=None, + overlap_converge_factor=1.4e-3, + probe_center_tol=None, + probe_update_start=1, + alpha=alpha, + clip_object=None, + LL_error=True, + num_iterations=num_iter) + + gerrors = gcon.difference_map_iterator(diffraction=diffraction, + obj=gobj, + object_weights=obj_weights, + cfact_object=cfact_object, + mask=mask, + probe=gprobe, + cfact_probe=cfact_probe, + probe_support=None, + probe_weights=probe_weights, + exit_wave=gexit_wave, + addr=addr_info, + pre_fft=prefilter, + post_fft=postfilter, + pbound=pbound, + overlap_max_iterations=10, + update_object_first=False, + obj_smooth_std=None, + overlap_converge_factor=1.4e-3, + probe_center_tol=None, + probe_update_start=1, + alpha=alpha, + clip_object=None, + LL_error=True, + num_iterations=num_iter) + + np.testing.assert_allclose(gerrors, + errors, + rtol=1e-6, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(gprobe, + probe, + rtol=1e-6, + err_msg="The returned probes are not consistent.") + + np.testing.assert_allclose(gobj, + obj, + rtol=1e-6, + err_msg="The returned objects are not consistent.") + + np.testing.assert_allclose(gexit_wave[3,:,:], + exit_wave[3,:,:], + rtol=1e-6, + atol=1e-4, + err_msg="The returned exit_waves are not consistent.") + + def test_difference_map_iterator_with_no_probe_update_and_object_update(self): + ''' + This test, assumes the logic below this function works fine, and just does some iterations of difference map on + some spoof data to check that the combination works. + ''' + num_iter = 1 + + pbound = 8.86e13 # this should now mean some of the arrays update differently through the logic + alpha = 1.0 # feedback constant + # diffraction frame size + B = 2 # for example + C = 4 # for example + + # probe dimensions + D = 2 # for example + E = B + F = C + + scan_pts = 2 # a 2x2 grid + N = scan_pts**2 # the number of measurement points in a scan + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + + # object dimensions + G = 1 # for example + H = B + npts_greater_than + I = C + npts_greater_than + + A = scan_pts ** 2 * G * D # number of exit waves + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(A, 5, 3), dtype=np.int32) # the address book + diffraction = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, B, C), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + diffraction_fill = np.arange(np.prod(diffraction.shape)).reshape(diffraction.shape).astype(diffraction.dtype) + diffraction[:] = diffraction_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (G, H, I)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (D, B, C)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((A, B, C)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + # mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((A, 3), dtype=np.int32) + pa[:N,0] = 0 + pa[N:, 0] = 1 + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((A, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + + + ea = np.array([np.array([ix, 0, 0]) for ix in range(A)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * D * G) + ma = np.zeros((A, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + + for idx in range(D): + cfact_probe[idx] = np.ones((B, C)) * 5 * (idx + 1) + + gexit_wave = deepcopy(exit_wave) + gprobe = deepcopy(probe) + gobj = deepcopy(obj) + + errors = con.difference_map_iterator(diffraction=diffraction, + obj=obj, + object_weights=obj_weights, + cfact_object=cfact_object, + mask=mask, + probe=probe, + cfact_probe=cfact_probe, + probe_support=None, + probe_weights=probe_weights, + exit_wave=exit_wave, + addr=addr_info, + pre_fft=prefilter, + post_fft=postfilter, + pbound=pbound, + overlap_max_iterations=10, + update_object_first=True, + obj_smooth_std=None, + overlap_converge_factor=1.4e-3, + probe_center_tol=None, + probe_update_start=1, + alpha=alpha, + clip_object=None, + LL_error=True, + num_iterations=num_iter) + + gerrors = gcon.difference_map_iterator(diffraction=diffraction, + obj=gobj, + object_weights=obj_weights, + cfact_object=cfact_object, + mask=mask, + probe=gprobe, + cfact_probe=cfact_probe, + probe_support=None, + probe_weights=probe_weights, + exit_wave=gexit_wave, + addr=addr_info, + pre_fft=prefilter, + post_fft=postfilter, + pbound=pbound, + overlap_max_iterations=10, + update_object_first=True, + obj_smooth_std=None, + overlap_converge_factor=1.4e-3, + probe_center_tol=None, + probe_update_start=1, + alpha=alpha, + clip_object=None, + LL_error=True, + num_iterations=num_iter) + + + np.testing.assert_allclose(gerrors, + errors, + rtol=1e-6, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(gprobe, + probe, + rtol=1e-6, + err_msg="The returned probes are not consistent.") + + np.testing.assert_allclose(gobj, + obj, + rtol=1e-6, + err_msg="The returned objects are not consistent.") + + np.testing.assert_allclose(gexit_wave, + exit_wave, + rtol=1e-6, + err_msg="The returned exit_waves are not consistent.") + + +if __name__ == '__main__': + unittest.main() + + + diff --git a/archive/cuda_extension/tests/data_utils_test.py b/archive/cuda_extension/tests/data_utils_test.py new file mode 100644 index 000000000..1fc37b24d --- /dev/null +++ b/archive/cuda_extension/tests/data_utils_test.py @@ -0,0 +1,54 @@ +''' +Created on 4 Jan 2018 + +@author: clb02321 +''' +import unittest +from . import utils as tu +import numpy as np + +from ptypy.accelerate.array_based import data_utils as du + + + +class DataUtilsTest(unittest.TestCase): + ''' + tests the conversion between pods and numpy arrays + ''' + + def test_pod_to_numpy(self): + ''' + tests if the vectorisation process works + ''' + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + du.pod_to_arrays(PtychoInstance, 'S0000', scan_model='Full') + + def test_numpy_pod_consistency(self): + ''' + vectorises the Ptycho instance. + ''' + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr = vectorised_scan['meta']['addr'] + view_IDS = vectorised_scan['meta']['view_IDs'] + + # check the probe references match up + vectorised_scan['probe'] *= np.random.rand(*vectorised_scan['probe'].shape) + + pa, oa, ea, da, ma = zip(*addr) + + for idx, vID in enumerate(view_IDS): + np.testing.assert_array_equal(vectorised_scan['probe'][pa[idx][0]], PtychoInstance.pr.V[vID].data) + + + + vectorised_scan['exit wave'] *= np.random.rand(*vectorised_scan['exit wave'].shape) + + for idx, vID in enumerate(view_IDS): + np.testing.assert_array_equal(vectorised_scan['exit wave'][ea[idx][0]], PtychoInstance.ex.V[vID].data) + + + + +if __name__ == "__main__": + unittest.main() diff --git a/archive/cuda_extension/tests/engine_iterate_unity_test.py b/archive/cuda_extension/tests/engine_iterate_unity_test.py new file mode 100644 index 000000000..b73c428ec --- /dev/null +++ b/archive/cuda_extension/tests/engine_iterate_unity_test.py @@ -0,0 +1,209 @@ +''' +This test checks the GPU vs Array based iterate methods + +''' + +import unittest +import numpy as np +from copy import deepcopy + +from . import utils as tu +from ptypy import defaults_tree +from ptypy.accelerate.array_based import data_utils as du + +from . import have_cuda, only_if_cuda_available +if have_cuda(): + from archive.cuda_extension.accelerate.cuda import constraints as gcon + from ptypy.accelerate.array_based import constraints as con + from archive.cuda_extension.accelerate.cuda.config import init_gpus, reset_function_cache + init_gpus(0) + +@only_if_cuda_available +class EngineIterateUnityTest(unittest.TestCase): + + def tearDown(self): + # reset the cached GPU functions after each test + reset_function_cache() + + @unittest.skip("Is not currently used. I suspect this comes from the race condition in the atomic adds in overlap update") + def test_DM_engine_iterate_mathod(self): + num_probe_modes = 2 # number of modes + num_iters = 10 # number of iterations + frame_size = 64 # frame size + num_points = 50 # number of points in the scan (length of the diffraction array). + fourier_relax_factor = defaults_tree['engine']['DM']['fourier_relax_factor'].default + obj_inertia = defaults_tree['engine']['DM']['object_inertia'].default + probe_inertia = defaults_tree['engine']['DM']['probe_inertia'].default + alpha = 1.0 # this is basically always 1 + + for m in range(num_probe_modes): + number_of_probe_modes = m + 1 + PtychoInstanceVec = tu.get_ptycho_instance('testing_iterate', num_modes=number_of_probe_modes, + frame_size=frame_size, + scan_length=num_points) # this one we run with GPU + vectorised_scan = du.pod_to_arrays(PtychoInstanceVec, 'S0000') + diffraction_storage = PtychoInstanceVec.di.storages['S0000'] + + pbound = (0.25 * fourier_relax_factor ** 2 * diffraction_storage.pbound_stub) + mean_power = diffraction_storage.tot_power / np.prod(diffraction_storage.shape) + + print("pbound:%s" % pbound) + print("mean_power:%s" % mean_power) + + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstanceVec.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + + # this is for the numpy based + diffraction = vectorised_scan['diffraction'] + obj = vectorised_scan['obj'] + probe = vectorised_scan['probe'] + mask = vectorised_scan['mask'] + exit_wave = vectorised_scan['exit wave'] + addr_info = vectorised_scan['meta']['addr'] + # NOTE: these come out as double, but should be single! + object_weights = vectorised_scan['object weights'].astype(np.float32) + probe_weights = vectorised_scan['probe weights'].astype(np.float32) + + prefilter = propagator.pre_fft + postfilter = propagator.post_fft + cfact_object = obj_inertia * mean_power * \ + (vectorised_scan['object viewcover'] + 1.) + cfact_probe = (probe_inertia * len(addr_info) / + vectorised_scan['probe'].shape[0]) * np.ones_like(vectorised_scan['probe']) + + probe_support = np.zeros_like(probe) + X, Y = np.meshgrid(range(probe.shape[1]), range(probe.shape[2])) + R = (0.7 * probe.shape[1]) / 2 + for idx in range(probe.shape[0]): + probe_support[idx, X ** 2 + Y ** 2 < R ** 2] = 1.0 + + print("For number of probe modes: %s\n" + "number of scan points: %s\n" + "and frame size: %s\n" % (number_of_probe_modes, num_points, frame_size)) + + print("The sizes and types of the arrays are:\n" + "diffraction: %s (%s)\n" + "obj: %s (%s)\n" + "probe: %s (%s)\n" + "mask: %s (%s)\n" + "exit wave: %s (%s)\n" + "addr_info: %s (%s)\n" + "object_weights: %s (%s)\n" + "probe_weights: %s (%s)\n" + "prefilter: %s (%s)\n" + "postfilter: %s (%s)\n" + "cfact_object: %s (%s)\n" + "cfact_probe: %s (%s)\n" + "probe_support: %s (%s)\n" % (diffraction.shape, diffraction.dtype, + obj.shape, obj.dtype, + probe.shape, probe.dtype, + mask.shape, mask.dtype, + exit_wave.shape, exit_wave.dtype, + addr_info.shape, addr_info.dtype, + object_weights.shape, object_weights.dtype, + probe_weights.shape, probe_weights.dtype, + prefilter.shape, prefilter.dtype, + postfilter.shape, postfilter.dtype, + cfact_object.shape, cfact_object.dtype, + cfact_probe.shape, cfact_probe.dtype, + probe_support.shape, probe_support.dtype)) + + # take exact copies for the cuda implementation + gdiffraction = deepcopy(diffraction) + gobj = deepcopy(obj) + gprobe = deepcopy(probe) + gmask = deepcopy(mask) + gexit_wave = deepcopy(exit_wave) + gaddr_info = deepcopy(addr_info) + gobject_weights = deepcopy(object_weights) + gprobe_weights = deepcopy(probe_weights) + + gprefilter = deepcopy(prefilter) + gpostfilter = deepcopy(postfilter) + gcfact_object = deepcopy(cfact_object) + gcfact_probe = deepcopy(cfact_probe) + + gpbound = deepcopy(pbound) + galpha = deepcopy(alpha) + gprobe_support = deepcopy(probe_support) + + errors = con.difference_map_iterator(diffraction=diffraction, + obj=obj, + object_weights=object_weights, + cfact_object=cfact_object, + mask=mask, + probe=probe, + cfact_probe=cfact_probe, + probe_support=probe_support, + probe_weights=probe_weights, + exit_wave=exit_wave, + addr=addr_info, + pre_fft=prefilter, + post_fft=postfilter, + pbound=pbound, + overlap_max_iterations=10, + update_object_first=False, + obj_smooth_std=None, + overlap_converge_factor=0.05, + probe_center_tol=None, + probe_update_start=1, + alpha=alpha, + clip_object=None, + LL_error=True, + num_iterations=num_iters) + + gerrors = gcon.difference_map_iterator(diffraction=gdiffraction, + obj=gobj, + object_weights=gobject_weights, + cfact_object=gcfact_object, + mask=gmask, + probe=gprobe, + cfact_probe=gcfact_probe, + probe_support=gprobe_support, + probe_weights=gprobe_weights, + exit_wave=gexit_wave, + addr=gaddr_info, + pre_fft=gprefilter, + post_fft=gpostfilter, + pbound=gpbound, + overlap_max_iterations=10, + update_object_first=False, + obj_smooth_std=None, + overlap_converge_factor=0.05, + probe_center_tol=None, + probe_update_start=1, + alpha=galpha, + clip_object=None, + LL_error=True, + num_iterations=num_iters, + do_realspace_error=True) + + # NOTE: + # Have to put large tolerances here, as after 10 iterations a discrepancy is expected + # it would be much better to have a metric of the quality of the reconstruction, + # like the mean squared error across the whole image, or something similar + # as array_close is bound by the max error, and that will be large + + for idx in range(len(errors)): + #print("errors[{}]: atol={}, rtol={}".format(idx, np.max(np.abs(gerrors[idx]-errors[idx])), np.max(np.abs(gerrors[idx]-errors[idx])/np.abs(errors[idx])) )) + np.testing.assert_allclose(gerrors[idx], errors[idx], rtol=10e-2, atol=10, err_msg="Output errors for index {} don't match".format(idx)) + + for idx in range(len(probe)): + #print("probe[{}]: atol={}, rtol={}".format(idx, np.max(np.abs(gprobe[idx]-probe[idx])), np.max(np.abs(gprobe[idx]-probe[idx])/np.abs(probe[idx])) )) + np.testing.assert_allclose(gprobe[idx], probe[idx], rtol=10e-2, atol=10, err_msg="Output probes for index {} don't match".format(idx)) + + # NOTE: these are completely different, but it still works fine with the visual sample + #for idx in range(len(exit_wave)): + #print("exit_wave[{}]: atol={}, rtol={}".format(idx, np.max(np.abs(gexit_wave[idx]-exit_wave[idx])), np.max(np.abs(gexit_wave[idx]-exit_wave[idx])/np.abs(exit_wave[idx])) )) + #np.testing.assert_allclose(gexit_wave[idx], exit_wave[idx], rtol=10e-2, atol=10, err_msg="Output exit waves for index {} don't match".format(idx)) + + for idx in range(len(obj)): + #print("obj[{}]: atol={}, rtol={}".format(idx, np.max(np.abs(gobj[idx]-obj[idx])), np.max(np.abs(gobj[idx]-obj[idx])/np.abs(obj[idx])) )) + np.testing.assert_allclose(obj, gobj, rtol=20e-2, atol=15, err_msg="The output objects don't match.") + + # clean this up to prevent a leak. + del PtychoInstanceVec + +if __name__=='__main__': + unittest.main() diff --git a/archive/cuda_extension/tests/error_metric_test.py b/archive/cuda_extension/tests/error_metric_test.py new file mode 100644 index 000000000..e359102cb --- /dev/null +++ b/archive/cuda_extension/tests/error_metric_test.py @@ -0,0 +1,112 @@ +''' +A test for the module of the relevant error metrics +''' + +import unittest +import numpy as np +from . import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based.constraints import difference_map_realspace_constraint, scan_and_multiply +from archive.array_based.propagation import farfield_propagator +import ptypy.accelerate.array_based.array_utils as au +from archive.array_based.error_metrics import log_likelihood, far_field_error, realspace_error +from ptypy.accelerate.array_based import COMPLEX_TYPE, FLOAT_TYPE + +from . import have_cuda, only_if_cuda_available +if have_cuda(): + from archive.cuda_extension.accelerate.cuda.error_metrics import log_likelihood as glog_likelihood + from archive.cuda_extension.accelerate.cuda.error_metrics import far_field_error as gfar_field_error + from archive.cuda_extension.accelerate.cuda.error_metrics import realspace_error as grealspace_error + from archive.cuda_extension.accelerate.cuda.config import init_gpus, reset_function_cache + init_gpus(0) + +@only_if_cuda_available +class ErrorMetricTest(unittest.TestCase): + + def tearDown(self): + # reset the cached GPU functions after each test + reset_function_cache() + + def test_loglikelihood_numpy_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + diffraction=vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + exit_wave = vectorised_scan['exit wave'] + + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + + ll = log_likelihood(probe_object, mask, diffraction, propagator.pre_fft, propagator.post_fft, addr_info) + gll = glog_likelihood(probe_object, mask, diffraction, propagator.pre_fft, propagator.post_fft, addr_info) + np.testing.assert_allclose(ll, gll, rtol=1e-6, atol=5e-4) + + def test_far_field_error_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + diffraction=vectorised_scan['diffraction'] + mask = vectorised_scan['mask'] + exit_wave = vectorised_scan['exit wave'] + + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + constrained = difference_map_realspace_constraint(probe_object, exit_wave, alpha=1.0) + f = farfield_propagator(constrained, propagator.pre_fft, propagator.post_fft, direction='forward') + pa, oa, ea, da, ma = zip(*addr_info) + af2 = au.sum_to_buffer(au.abs2(f), diffraction.shape, ea, da, dtype=FLOAT_TYPE) + + fmag = np.sqrt(np.abs(diffraction)) + af = np.sqrt(af2) + + ff_error = far_field_error(af, fmag, mask).astype(np.float32) + + gff_error = gfar_field_error(af, fmag, mask) + np.testing.assert_allclose(ff_error, gff_error, rtol=1e-6) + + def test_realspace_error_regression1_UNITY(self): + I = 5 + M = 20 + N = 30 + out_length = I + ea_first_column = range(I) + da_first_column = range(I) + + difference = np.empty(shape=(I, M, N), dtype=COMPLEX_TYPE) + for idx in range(I): + difference[idx] = np.ones((M, N)) *idx + 1j * np.ones((M, N)) *idx + + error = realspace_error(difference, ea_first_column, da_first_column, out_length) + gerror = grealspace_error(difference, ea_first_column, da_first_column, out_length) + + np.testing.assert_allclose(error, gerror, rtol=1e-6) + + def test_realspace_error_regression2_UNITY(self): + I = 5 + M = 20 + N = 30 + out_length = 5 + ea_first_column = range(I) + da_first_column = list(range(int(I/2))) + list(range(int(I/2))) + + difference = np.empty(shape=(I, M, N), dtype=COMPLEX_TYPE) + for idx in range(I): + difference[idx] = np.ones((M, N)) * idx + 1j * np.ones((M, N)) * idx + + error = realspace_error(difference, ea_first_column, da_first_column, out_length) + gerror = grealspace_error(difference, ea_first_column, da_first_column, out_length) + + np.testing.assert_allclose(error, gerror, rtol=1e-6) + + +if __name__ == '__main__': + unittest.main() + diff --git a/archive/cuda_extension/tests/farfield_propagator_test.py b/archive/cuda_extension/tests/farfield_propagator_test.py new file mode 100644 index 000000000..433a6621a --- /dev/null +++ b/archive/cuda_extension/tests/farfield_propagator_test.py @@ -0,0 +1,229 @@ +''' +Test for the propagation in numpy +''' + +import unittest +import numpy as np +from . import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based import object_probe_interaction as opi +from archive.array_based import propagation as prop +import time + +from . import have_cuda, only_if_cuda_available +if have_cuda(): + from archive.cuda_extension.accelerate.cuda import propagation as gprop + from archive.cuda_extension.accelerate.cuda.config import init_gpus, reset_function_cache + init_gpus(0) + +doTiming = False + +def calculatePrintErrors(expected, actual): + abserr = np.abs(expected-actual) + max_abserr = np.max(abserr) + mean_abserr = np.mean(abserr) + min_abserr = np.min(abserr) + std_abserr = np.std(abserr) + relerr = abserr / np.abs(expected) + max_relerr = np.nanmax(relerr) + mean_relerr = np.nanmean(relerr) + min_relerr = np.nanmin(relerr) + std_relerr = np.nanstd(relerr) + print("Abs Errors: max={}, min={}, mean={}, stddev={}".format( + max_abserr, min_abserr, mean_abserr, std_abserr)) + print("Rel Errors: max={}, min={}, mean={}, stddev={}".format( + max_relerr, min_relerr, mean_relerr, std_relerr)) + +@only_if_cuda_available +class FarfieldPropagatorTest(unittest.TestCase): + + def tearDown(self): + # reset the cached GPU functions after each test + reset_function_cache() + + #@unittest.skip("This method is not implemented yet") + def test_fourier_transform_farfield_nofilter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + + if doTiming: tstart = time.time() + array_propagated = prop.farfield_propagator(exit_wave, prefilter=None, postfilter=None) + if doTiming: + tend = time.time() + pytime = tend-tstart + tstart = time.time() + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=None, postfilter=None) + if doTiming: + tend = time.time() + gtime = tend-tstart + + print("Times: CPU={}, GPU={}, speedup={}x".format( + pytime, gtime, pytime/gtime + )) + + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=3e-4,verbose=True + ) + + + #@unittest.skip("This method is not implemented yet") + def test_fourier_transform_farfield_with_prefilter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + + array_propagated = prop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=None) + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=None) + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=4e-4,verbose=True + ) + + #@unittest.skip("This method is not implemented yet") + def test_fourier_transform_farfield_with_postfilter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + + array_propagated = prop.farfield_propagator(exit_wave, prefilter=None, postfilter=propagator.post_fft) + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=None, postfilter=propagator.post_fft) + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=3e-4,verbose=True + ) + + #@unittest.skip("This method is not implemented yet") + def test_fourier_transform_farfield_with_pre_and_post_filter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + if doTiming: tstart = time.time() + array_propagated = prop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=propagator.post_fft) + if doTiming: + tend = time.time() + pytime = tend-tstart + tstart = time.time() + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=propagator.post_fft) + if doTiming: + tend = time.time() + gtime = tend-tstart + + print("Times: CPU={}, GPU={}, speedup={}x".format( + pytime, gtime, pytime/gtime + )) + + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=5e-4,verbose=True + ) + + #@unittest.skip("This method is not implemented yet") + def test_inverse_fourier_transform_farfield_nofilter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000',) + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + + array_propagated = prop.farfield_propagator(exit_wave, prefilter=None, postfilter=None, direction='backward') + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=None, postfilter=None, direction='backward') + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=5e-4,verbose=True + ) + + #@unittest.skip("This method is not implemented yet") + def test_inverse_fourier_transform_farfield_with_prefilter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + + array_propagated = prop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=None, direction='backward') + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=None, direction='backward') + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=5e-4,verbose=True + ) + + #@unittest.skip("This method is not implemented yet") + def test_inverse_fourier_transform_farfield_with_postfilter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + + array_propagated = prop.farfield_propagator(exit_wave, prefilter=None, postfilter=propagator.post_fft, direction='backward') + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=None, postfilter=propagator.post_fft, direction='backward') + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=5e-4,verbose=True + ) + + #@unittest.skip("This method is not implemented yet") + def test_inverse_fourier_transform_farfield_with_pre_and_post_filter_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.di.V[first_view_id].pod.geometry.propagator + exit_wave = opi.scan_and_multiply(vectorised_scan['probe'], + vectorised_scan['obj'], + vectorised_scan['exit wave'].shape, + vectorised_scan['meta']['addr']) + + array_propagated = prop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=propagator.post_fft, direction='backward') + gpu_propagated = gprop.farfield_propagator(exit_wave, prefilter=propagator.pre_fft, postfilter=propagator.post_fft, direction='backward') + + calculatePrintErrors(array_propagated, gpu_propagated) + np.testing.assert_allclose( + gpu_propagated, + array_propagated, rtol=1e-6, atol=5e-4,verbose=True + ) + +# + +if __name__ == "__main__": + unittest.main() diff --git a/archive/cuda_extension/tests/minimal_numpy_DM_test.py b/archive/cuda_extension/tests/minimal_numpy_DM_test.py new file mode 100644 index 000000000..b3d08501a --- /dev/null +++ b/archive/cuda_extension/tests/minimal_numpy_DM_test.py @@ -0,0 +1,56 @@ +from ptypy.core import Ptycho +from ptypy import utils as u +import cProfile +p = u.Param() +p.verbose_level = 3 +p.io = u.Param() +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=True) +p.ipython_kernel = False +p.scans = u.Param() +p.scans.MF = u.Param() +p.scans.MF.name = 'Full' +p.scans.MF.propagation = 'farfield' +p.scans.MF.data = u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.positions_theory = None +p.scans.MF.data.auto_center = None +p.scans.MF.data.min_frames = 1 +p.scans.MF.data.orientation = None +p.scans.MF.data.num_frames = 100 +p.scans.MF.data.energy = 6.2 +p.scans.MF.data.shape = 256 +p.scans.MF.data.chunk_format = '.chunk%02d' +p.scans.MF.data.rebin = None +p.scans.MF.data.experimentID = None +p.scans.MF.data.label = None +p.scans.MF.data.version = 0.1 +p.scans.MF.data.dfile = None +p.scans.MF.data.psize = 0.000172 +p.scans.MF.data.load_parallel = None +p.scans.MF.data.distance = 7.0 +p.scans.MF.data.save = None +p.scans.MF.data.center = 'fftshift' +p.scans.MF.data.photons = 100000000.0 +p.scans.MF.data.psf = 0.0 +p.scans.MF.data.density = 0.2 +p.scans.MF.data.add_poisson_noise = False +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 1 # currently breaks when this is =2 + +p.engines = u.Param() + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DMNpy' +p.engines.engine00.numiter = 50 +# p.engines.engine00.overlap_max_iterations = 1 +# prepare and run + + +P = Ptycho(p, level=4) +P.run() +# cProfile.run('P.run()', +# '/home/clb02321/Desktop/profiling_thing_with_realspace_error.prof', +# 'tottime') \ No newline at end of file diff --git a/archive/cuda_extension/tests/minimal_numpy_DM_test_4096x4096.py b/archive/cuda_extension/tests/minimal_numpy_DM_test_4096x4096.py new file mode 100644 index 000000000..30e83067d --- /dev/null +++ b/archive/cuda_extension/tests/minimal_numpy_DM_test_4096x4096.py @@ -0,0 +1,60 @@ +from ptypy.core import Ptycho +from ptypy import utils as u +import cProfile + +beamlines = {} +beamlines['i08'] = [64, 20000]#45000] +# beamlines['i14'] = [4096, 70000] +# beamlines['i13'] = [4096, 5000] + +for beamline in beamlines.keys(): + print("####### RUNNING %s #########" % beamline) + p = u.Param() + p.verbose_level = 3 + p.io = u.Param() + p.io.autosave = u.Param(active=False) + p.io.autoplot = u.Param(active=True) + p.ipython_kernel = False + p.scans = u.Param() + p.scans.MF = u.Param() + p.scans.MF.name = 'Full' + p.scans.MF.propagation = 'farfield' + p.scans.MF.data = u.Param() + p.scans.MF.data.name = 'MoonFlowerScan' + p.scans.MF.data.positions_theory = None + p.scans.MF.data.auto_center = None + p.scans.MF.data.min_frames = 1 + p.scans.MF.data.orientation = None + p.scans.MF.data.num_frames = beamlines[beamline][1] + p.scans.MF.data.energy = 6.2 + p.scans.MF.data.shape = beamlines[beamline][0] + p.scans.MF.data.chunk_format = '.chunk%02d' + p.scans.MF.data.rebin = None + p.scans.MF.data.experimentID = None + p.scans.MF.data.label = None + p.scans.MF.data.version = 0.1 + p.scans.MF.data.dfile = None + p.scans.MF.data.psize = 0.000172 + p.scans.MF.data.load_parallel = None + p.scans.MF.data.distance = 7.0 + p.scans.MF.data.save = None + p.scans.MF.data.center = 'fftshift' + p.scans.MF.data.photons = 100000000.0 + p.scans.MF.data.psf = 0.0 + p.scans.MF.data.density = 0.2 + p.scans.MF.data.add_poisson_noise = False + p.scans.MF.coherence = u.Param() + p.scans.MF.coherence.num_probe_modes = 2 # currently breaks when this is =2 + p.engines = u.Param() + # attach a reconstrucion engine + p.engines = u.Param() + p.engines.engine00 = u.Param() + p.engines.engine00.name = 'DMGpu' + p.engines.engine00.numiter = 50 + # p.engines.engine00.overlap_max_iterations = 1 + # prepare and run + + + P = Ptycho(p, level=4) + P.run() + # cProfile.run("P.run()", filename="/home/clb02321/profiling_%s_%s_50iterations_DMCpu" % (beamline, '28826'), sort='tottime') diff --git a/archive/cuda_extension/tests/minimal_numpy_DM_test_64x64.py b/archive/cuda_extension/tests/minimal_numpy_DM_test_64x64.py new file mode 100644 index 000000000..b3d08501a --- /dev/null +++ b/archive/cuda_extension/tests/minimal_numpy_DM_test_64x64.py @@ -0,0 +1,56 @@ +from ptypy.core import Ptycho +from ptypy import utils as u +import cProfile +p = u.Param() +p.verbose_level = 3 +p.io = u.Param() +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=True) +p.ipython_kernel = False +p.scans = u.Param() +p.scans.MF = u.Param() +p.scans.MF.name = 'Full' +p.scans.MF.propagation = 'farfield' +p.scans.MF.data = u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.positions_theory = None +p.scans.MF.data.auto_center = None +p.scans.MF.data.min_frames = 1 +p.scans.MF.data.orientation = None +p.scans.MF.data.num_frames = 100 +p.scans.MF.data.energy = 6.2 +p.scans.MF.data.shape = 256 +p.scans.MF.data.chunk_format = '.chunk%02d' +p.scans.MF.data.rebin = None +p.scans.MF.data.experimentID = None +p.scans.MF.data.label = None +p.scans.MF.data.version = 0.1 +p.scans.MF.data.dfile = None +p.scans.MF.data.psize = 0.000172 +p.scans.MF.data.load_parallel = None +p.scans.MF.data.distance = 7.0 +p.scans.MF.data.save = None +p.scans.MF.data.center = 'fftshift' +p.scans.MF.data.photons = 100000000.0 +p.scans.MF.data.psf = 0.0 +p.scans.MF.data.density = 0.2 +p.scans.MF.data.add_poisson_noise = False +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 1 # currently breaks when this is =2 + +p.engines = u.Param() + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DMNpy' +p.engines.engine00.numiter = 50 +# p.engines.engine00.overlap_max_iterations = 1 +# prepare and run + + +P = Ptycho(p, level=4) +P.run() +# cProfile.run('P.run()', +# '/home/clb02321/Desktop/profiling_thing_with_realspace_error.prof', +# 'tottime') \ No newline at end of file diff --git a/archive/cuda_extension/tests/minimal_numpy_ML_test.py b/archive/cuda_extension/tests/minimal_numpy_ML_test.py new file mode 100644 index 000000000..b3d08501a --- /dev/null +++ b/archive/cuda_extension/tests/minimal_numpy_ML_test.py @@ -0,0 +1,56 @@ +from ptypy.core import Ptycho +from ptypy import utils as u +import cProfile +p = u.Param() +p.verbose_level = 3 +p.io = u.Param() +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=True) +p.ipython_kernel = False +p.scans = u.Param() +p.scans.MF = u.Param() +p.scans.MF.name = 'Full' +p.scans.MF.propagation = 'farfield' +p.scans.MF.data = u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.positions_theory = None +p.scans.MF.data.auto_center = None +p.scans.MF.data.min_frames = 1 +p.scans.MF.data.orientation = None +p.scans.MF.data.num_frames = 100 +p.scans.MF.data.energy = 6.2 +p.scans.MF.data.shape = 256 +p.scans.MF.data.chunk_format = '.chunk%02d' +p.scans.MF.data.rebin = None +p.scans.MF.data.experimentID = None +p.scans.MF.data.label = None +p.scans.MF.data.version = 0.1 +p.scans.MF.data.dfile = None +p.scans.MF.data.psize = 0.000172 +p.scans.MF.data.load_parallel = None +p.scans.MF.data.distance = 7.0 +p.scans.MF.data.save = None +p.scans.MF.data.center = 'fftshift' +p.scans.MF.data.photons = 100000000.0 +p.scans.MF.data.psf = 0.0 +p.scans.MF.data.density = 0.2 +p.scans.MF.data.add_poisson_noise = False +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 1 # currently breaks when this is =2 + +p.engines = u.Param() + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DMNpy' +p.engines.engine00.numiter = 50 +# p.engines.engine00.overlap_max_iterations = 1 +# prepare and run + + +P = Ptycho(p, level=4) +P.run() +# cProfile.run('P.run()', +# '/home/clb02321/Desktop/profiling_thing_with_realspace_error.prof', +# 'tottime') \ No newline at end of file diff --git a/archive/cuda_extension/tests/object_probe_interaction_test.py b/archive/cuda_extension/tests/object_probe_interaction_test.py new file mode 100644 index 000000000..e7b702aff --- /dev/null +++ b/archive/cuda_extension/tests/object_probe_interaction_test.py @@ -0,0 +1,1272 @@ +''' +tests for the object-probe interactions, including the specific DM, ePIE etc updates + +''' + +import unittest +import numpy as np +from . import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based import object_probe_interaction as opi +from ptypy.accelerate.array_based import COMPLEX_TYPE, FLOAT_TYPE +from copy import deepcopy + +from . import have_cuda, only_if_cuda_available +if have_cuda(): + from archive.cuda_extension.accelerate.cuda import object_probe_interaction as gopi + from archive.cuda_extension.accelerate.cuda.config import init_gpus, reset_function_cache + init_gpus(0) + +@only_if_cuda_available +class ObjectProbeInteractionTest(unittest.TestCase): + + def tearDown(self): + # reset the cached GPU functions after each test + reset_function_cache() + + def test_scan_and_multiply_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + + # add one, to avoid having a lot of zeros and hence disturbing the result + probe = np.add(probe, 1) + obj = np.add(obj,1) + + po = opi.scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + gpo = gopi.scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + + np.testing.assert_array_equal(po, gpo) + #np.testing.assert_allclose(po, gpo) + + def test_difference_map_realspace_constraint_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + probe_object = opi.scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + po = opi.difference_map_realspace_constraint(probe_object, exit_wave, alpha=1.0) + gpo = gopi.difference_map_realspace_constraint(probe_object, exit_wave, alpha=1.0) + np.testing.assert_array_equal(po, gpo) + + def test_extract_array_from_exit_wave_UNITY_case_a(self): + # two cases for this a) the array to be updated is bigger than the extracted array (which is the same size as the exit wave) + # b) the other way round + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + garray_to_be_updated = deepcopy(array_to_be_updated) + gcfact = deepcopy(cfact) + + opi.extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, array_to_be_updated, + update_addr, cfact, weights) + + gopi.extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, garray_to_be_updated, + update_addr, gcfact, weights) + + + np.testing.assert_array_equal(array_to_be_updated, + garray_to_be_updated, + err_msg="The array has not been extracted properly from the exit wave.") + + def test_extract_array_from_exit_wave_UNITY_case_b(self): + # two cases for this a) the array to be updated is bigger than the extracted array (which is the same size as the exit wave) + # b) the other way round + # + npts_greater_than = 2 + B = 5 + C = 5 + + D = 2 + E = C + npts_greater_than + F = C + npts_greater_than + + G = 2 + H = B + I = C + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + extract_addr[index::scan_pts ** 2, 1] = X + extract_addr[index::scan_pts ** 2, 2] = Y + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + update_addr[:, 1] = np.zeros((A,)) + update_addr[:, 2] = np.zeros((A,)) + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + garray_to_be_updated = deepcopy(array_to_be_updated) + gcfact = deepcopy(cfact) + + opi.extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, + array_to_be_updated, + update_addr, cfact, weights) + gopi.extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, + garray_to_be_updated, + update_addr, gcfact, weights) + + + np.testing.assert_array_equal(array_to_be_updated, garray_to_be_updated) + + def test_difference_map_update_probe_UNITY_with_support(self): + ''' + This tests difference_map_update_probe, which wraps extract_array_from_exit_wave + ''' + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(update_addr, extract_addr, exit_addr, dummy_addr, dummy_addr)) + probe_support = np.ones_like(array_to_be_updated) * 100.0 + #(ob, probe_weights, probe, exit_wave, addr_info, cfact_probe, probe_support = None) + + garray_to_be_updated = deepcopy(array_to_be_updated) + gcfact = deepcopy(cfact) + err = opi.difference_map_update_probe(array_to_be_extracted, weights, array_to_be_updated, exit_wave, addr_info, cfact, probe_support=probe_support) + + gerr = gopi.difference_map_update_probe(array_to_be_extracted, weights, garray_to_be_updated, exit_wave, addr_info, + gcfact, probe_support=probe_support) + + self.assertAlmostEqual(err, gerr, 6) + np.testing.assert_array_equal(array_to_be_updated, garray_to_be_updated) + + def test_difference_map_update_probe_UNITY_without_support(self): + ''' + This tests difference_map_update_probe, which wraps extract_array_from_exit_wave + ''' + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(update_addr, extract_addr, exit_addr, dummy_addr, dummy_addr)) + #(ob, probe_weights, probe, exit_wave, addr_info, cfact_probe, probe_support = None) + + garray_to_be_updated = deepcopy(array_to_be_updated) + gcfact = deepcopy(cfact) + opi.difference_map_update_probe(array_to_be_extracted, weights, array_to_be_updated, exit_wave, addr_info, cfact, probe_support=None) + gopi.difference_map_update_probe(array_to_be_extracted, weights, garray_to_be_updated, exit_wave, addr_info, + gcfact, probe_support=None) + + np.testing.assert_array_equal(array_to_be_updated, garray_to_be_updated) + + def test_difference_map_update_object_with_no_smooth_or_clip_UNITY(self): + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(extract_addr, update_addr , exit_addr, dummy_addr, dummy_addr)) + + garray_to_be_updated = deepcopy(array_to_be_updated) + gcfact = deepcopy(cfact) + opi.difference_map_update_object(array_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, cfact, ob_smooth_std=None, clip_object=None) + gopi.difference_map_update_object(garray_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, + gcfact, ob_smooth_std=None, clip_object=None) + + np.testing.assert_array_equal(array_to_be_updated, garray_to_be_updated) + + def test_difference_map_update_object_with_smooth_but_no_clip_UNITY(self): + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(extract_addr, update_addr , exit_addr, dummy_addr, dummy_addr)) + obj_smooth_std = 2.0 # integer + + garray_to_be_updated = deepcopy(array_to_be_updated) + gopi.difference_map_update_object(garray_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, + cfact, ob_smooth_std=obj_smooth_std, clip_object=None) + opi.difference_map_update_object(array_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, cfact, ob_smooth_std=obj_smooth_std, clip_object=None) + + print("Gpu={}".format(garray_to_be_updated)) + print("Cpu={}".format(array_to_be_updated)) + + np.testing.assert_allclose( + array_to_be_updated, + garray_to_be_updated, + rtol=1e-6 + ) + + def test_difference_map_update_object_with_no_smooth_but_clipping_UNITY(self): + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(extract_addr, update_addr , exit_addr, dummy_addr, dummy_addr)) + clip = (0.8, 1.0) + + garray_to_be_updated = deepcopy(array_to_be_updated) + gcfact = deepcopy(cfact) + opi.difference_map_update_object(array_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, cfact, ob_smooth_std=None, clip_object=clip) + gopi.difference_map_update_object(garray_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, + gcfact, ob_smooth_std=None, clip_object=clip) + + np.testing.assert_allclose(array_to_be_updated, garray_to_be_updated) + + + def test_center_probe_no_change_UNITY(self): + npts = 64 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X-Xoff)**2 + (Y-Yoff)**2 < rad**2] = probe_vals + center_tolerance = 10.0 + + gprobe = deepcopy(probe) + opi.center_probe(probe, center_tolerance) + gopi.center_probe(gprobe, center_tolerance) + + np.testing.assert_array_equal(probe, gprobe) + + def test_center_probe_with_change_UNITY(self): + npts = 64 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X-Xoff)**2 + (Y-Yoff)**2 < rad**2] = probe_vals + center_tolerance = 1.0 + gprobe = deepcopy(probe) + + gopi.center_probe(gprobe, center_tolerance) + opi.center_probe(probe, center_tolerance) + np.testing.assert_array_almost_equal(probe, gprobe, decimal=8) # interpolation obviously won't make this exact! + + def test_difference_map_overlap_update_test_order_of_updates_a(self): + ''' + This tests the order in which the object and probe are updated + ''' + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = True + do_update_probe = False + # this should mean that the object gets updated but the probe does not change + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr , exit_addr, dummy_addr, dummy_addr)) + + gobj = deepcopy(obj) + gprobe = deepcopy(probe) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + gopi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=gobj, + object_weights=obj_weights, + probe=gprobe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_allclose(gprobe, + probe, + rtol=1e-6, + err_msg="The cuda and numpy probes are different.") + np.testing.assert_allclose(gobj, + obj, + rtol=1e-6, + err_msg="The cuda and numpy object are different.") + + def test_difference_map_overlap_update_test_order_of_updates_b(self): + ''' + This tests the order in which the object and probe are updated + ''' + + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = False + do_update_probe = True + # This should mean that the probe is updated, but not the object since max_iterations=1 + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr , exit_addr, dummy_addr, dummy_addr)) + + gobj = deepcopy(obj) + gprobe = deepcopy(probe) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + gopi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=gobj, + object_weights=obj_weights, + probe=gprobe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_allclose(gprobe, + probe, + rtol=1e-6, + err_msg="The cuda and numpy probes are different.") + np.testing.assert_allclose(gobj, + obj, + rtol=1e-6, + err_msg="The cuda and numpy object are different.") + + def test_difference_map_overlap_update_test_order_of_updates_c(self): + ''' + This tests the order in which the object and probe are updated + ''' + + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = False + do_update_probe = False + # neither the probe or the object are updated + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr , exit_addr, dummy_addr, dummy_addr)) + + + gobj = deepcopy(obj) + gprobe = deepcopy(probe) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + gopi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=gobj, + object_weights=obj_weights, + probe=gprobe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_allclose(gprobe, + probe, + rtol=1e-6, + err_msg="The cuda and numpy probes are different.") + np.testing.assert_allclose(gobj, + obj, + rtol=1e-6, + err_msg="The cuda and numpy object are different.") + + def test_difference_map_overlap_update_test_order_of_updates_d(self): + ''' + This tests the order in which the object and probe are updated + ''' + + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = True + do_update_probe = True + # both the object and the probe are updated + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like( + probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr, exit_addr, dummy_addr, dummy_addr)) + + gobj = deepcopy(obj) + gprobe = deepcopy(probe) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + gopi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=gobj, + object_weights=obj_weights, + probe=gprobe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_allclose(gprobe, + probe, + rtol=1e-6, + err_msg="The cuda and numpy probes are different.") + np.testing.assert_allclose(gobj, + obj, + rtol=1e-6, + err_msg="The cuda and numpy object are different.") + + + + + def test_difference_map_overlap_update_break_when_in_tolerance(self): + ''' + This tests if the loop breaks according to the convergence criterion. + ''' + + ''' + This tests the order in which the object and probe are updated + ''' + + smooth_std = None # anything else currently not supported + max_iterations = 100 + update_object_first = False + do_update_probe = True + # both the object and the probe are updated + ocf = 4.2e-2 # chosen so that this should terminate on teh 6th iteration + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr, exit_addr, dummy_addr, dummy_addr)) + + expected_probe = np.array([[[ 9.09412193-0.70329767j, 9.09412193-0.70329767j, 9.09412193-0.70329767j, + 9.09412193-0.70329767j, 9.09412193-0.70329767j], + [ 6.54680109-0.50316954j, 6.54680109-0.50316954j, 6.54680109-0.50316954j, + 6.54680109-0.50316954j, 6.54680109-0.50316954j], + [ 6.14579964-0.47170654j, 6.14579964-0.47170654j, 6.14579964-0.47170654j, + 6.14579964-0.47170654j, 6.14579964-0.47170654j], + [ 5.90408182-0.4541125j , 5.90408182-0.4541125j, 5.90408182-0.4541125j, + 5.90408182-0.4541125j , 5.90408182-0.4541125j ], + [ 4.61261368-0.35782164j, 4.61261368-0.35782164j, 4.61261368-0.35782164j, + 4.61261368-0.35782164j, 4.61261368-0.35782164j]], + + [[ 3.49120140+0.0705148j, 3.49120140+0.0705148j, 3.49120140+0.0705148j, + 3.49120140+0.0705148j, 3.49120140+0.0705148j ], + [ 3.14379764+0.06552192j, 3.14379764+0.06552192j, 3.14379764+0.06552192j, + 3.14379764+0.06552192j, 3.14379764+0.06552192j], + [ 3.08963704+0.06493596j, 3.08963704+0.06493596j, 3.08963704+0.06493596j, + 3.08963704+0.06493596j, 3.08963704+0.06493596j], + [ 3.04668784+0.06343807j, 3.04668784+0.06343807j, 3.04668784+0.06343807j, + 3.04668784+0.06343807j, 3.04668784+0.06343807j], + [ 2.78638887+0.05607619j, 2.78638887+0.05607619j, 2.78638887+0.05607619j, + 2.78638887+0.05607619j, 2.78638887+0.05607619j]]], dtype=COMPLEX_TYPE) + + expected_object=np.array([[[0.27179495+0.31753245j,0.27179495+0.31753245j,0.27179495+0.31753245j, + 0.27179495+0.31753245j,0.27179495+0.31753245j,1.00000000+1.j, + 1.00000000+1.j], + [0.58112150+0.67839354j,0.58112150+0.67839354j,0.58112150+0.67839354j, + 0.58112150+0.67839354j,0.58112150+0.67839354j,1.00000000+1.j, + 1.00000000+1.j], + [0.66542435+0.77613211j,0.66542435+0.77613211j,0.66542435+0.77613211j, + 0.66542435+0.77613211j,0.66542435+0.77613211j,1.00000000+1.j, + 1.00000000+1.j], + [0.68367511+0.7973848j,0.68367511+0.7973848j,0.68367511+0.7973848j, + 0.68367511+0.7973848j,0.68367511+0.7973848j,1.00000000+1.j, + 1.00000000+1.j], + [0.77851987+0.90848058j,0.77851987+0.90848058j,0.77851987+0.90848058j, + 0.77851987+0.90848058j,0.77851987+0.90848058j,1.00000000+1.j, + 1.00000000+1.j], + [1.20245206+1.40492487j,1.20245206+1.40492487j,1.20245206+1.40492487j, + 1.20245206+1.40492487j,1.20245206+1.40492487j,1.00000000+1.j, + 1.00000000+1.j], + [1.00000000+1.j,1.00000000+1.j,1.00000000+1.j, + 1.00000000+1.j,1.00000000+1.j,1.00000000+1.j, + 1.00000000+1.j]], + + [[4.00000000+4.j,3.17660427+3.05242205j,3.17660427+3.05242205j, + 3.17660427+3.05242205j,3.17660427+3.05242205j,3.17660427+3.05242205j, + 4.00000000+4.j], + [4.00000000+4.j,3.94026518+3.78214717j,3.94026518+3.78214717j, + 3.94026518+3.78214717j,3.94026518+3.78214717j,3.94026518+3.78214717j, + 4.00000000+4.j,], + [4.00000000+4.j,4.11254692+3.94367075j,4.11254692+3.94367075j, + 4.11254692+3.94367075j,4.11254692+3.94367075j,4.11254692+3.94367075j, + 4.00000000+4.j], + [4.00000000+4.j,4.14782619+3.9773953j,4.14782619+3.9773953j, + 4.14782619+3.9773953j,4.14782619+3.9773953j,4.14782619+3.9773953j, + 4.00000000+4.j,], + [4.00000000+4.j,4.31528330+4.14124584j,4.31528330+4.14124584j, + 4.31528330+4.14124584j,4.31528330+4.14124584j,4.31528330+4.14124584j, + 4.00000000+4.j], + [4.00000000+4.j,5.13210011+4.93122625j,5.13210011+4.93122625j, + 5.13210011+4.93122625j, 5.13210011+4.93122625j,5.13210011+4.93122625j, + 4.00000000+4.j,], + [4.00000000+4.j,4.00000000+4.j,4.00000000+4.j, + 4.00000000+4.j,4.00000000+4.j,4.00000000+4.j, + 4.00000000+4.j]]],dtype=COMPLEX_TYPE) + + gobj = deepcopy(obj) + gprobe = deepcopy(probe) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + gopi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=gobj, + object_weights=obj_weights, + probe=gprobe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_allclose(gprobe, + probe, + rtol=5e-5, + err_msg="The cuda and numpy probes are different.") + np.testing.assert_allclose(gobj, + obj, + rtol=5e-5, + err_msg="The cuda and numpy object are different.") + + +if __name__ == "__main__": + unittest.main() diff --git a/archive/cuda_extension/tests/utils.py b/archive/cuda_extension/tests/utils.py new file mode 100644 index 000000000..b0ef73607 --- /dev/null +++ b/archive/cuda_extension/tests/utils.py @@ -0,0 +1,69 @@ +''' +Created on 4 Jan 2018 + +@author: clb02321 +''' +import unittest +from ptypy.core import Ptycho +from ptypy import utils as u + +def print_array_info(a, name): + print("{}: {}, {}".format(name, a.shape, a.dtype)) + +def get_ptycho_instance(label=None, num_modes=1, frame_size=64, scan_length=8): + ''' + new ptypy probably has a better way of doing this. + ''' + p = u.Param() + p.verbose_level = 0 + p.data_type = "single" + p.run = label + p.io = u.Param() + p.io.home = "/tmp/ptypy/" + p.io.interaction = u.Param(active=False) + p.io.autoplot = u.Param(active=False) + p.scans = u.Param() + p.scans.MF = u.Param() + p.scans.MF.name = 'Full' + p.scans.MF.propagation = 'farfield' + p.scans.MF.data = u.Param() + p.scans.MF.data.name = 'MoonFlowerScan' + p.scans.MF.data.positions_theory = None + p.scans.MF.data.auto_center = None + p.scans.MF.data.min_frames = 1 + p.scans.MF.data.orientation = None + p.scans.MF.data.num_frames =scan_length + p.scans.MF.data.energy = 6.2 + p.scans.MF.data.shape = frame_size + p.scans.MF.data.chunk_format = '.chunk%02d' + p.scans.MF.data.rebin = None + p.scans.MF.data.experimentID = None + p.scans.MF.data.label = None + p.scans.MF.data.version = 0.1 + p.scans.MF.data.dfile = None + p.scans.MF.data.psize = 0.000172 + p.scans.MF.data.load_parallel = None + p.scans.MF.data.distance = 7.0 + p.scans.MF.data.save = None + p.scans.MF.data.center = 'fftshift' + p.scans.MF.data.photons = 100000000.0 + p.scans.MF.data.psf = 0.0 + p.scans.MF.data.add_poisson_noise = False + p.scans.MF.data.density = 0.2 + p.scans.MF.illumination = u.Param() + p.scans.MF.illumination.model = None + p.scans.MF.illumination.aperture = u.Param() + p.scans.MF.illumination.aperture.diffuser = None + p.scans.MF.illumination.aperture.form = "circ" + p.scans.MF.illumination.aperture.size = 3e-6 + p.scans.MF.illumination.aperture.edge = 10 + p.scans.MF.coherence = u.Param() + p.scans.MF.coherence.num_probe_modes = num_modes + P = Ptycho(p, level=4) + P.di = P.diff + P.ma = P.mask + P.ex = P.exit + P.pr = P.probe + P.ob = P.obj + return P + diff --git a/ptypy/engines/DM.py b/archive/engines/DM.py similarity index 98% rename from ptypy/engines/DM.py rename to archive/engines/DM.py index 0fa4721b8..50936bd42 100644 --- a/ptypy/engines/DM.py +++ b/archive/engines/DM.py @@ -15,12 +15,11 @@ from .utils import basic_fourier_update from . import register from .base import PositionCorrectionEngine -from .. import defaults_tree from ..core.manager import Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull __all__ = ['DM'] -@register() +#@register() class DM(PositionCorrectionEngine): """ A full-fledged Difference Map engine. @@ -194,7 +193,8 @@ def engine_prepare(self): if self.p.fourier_power_bound is None: pb = .25 * self.p.fourier_relax_factor**2 * s.pbound_stub else: - pb = self.p.fourier_power_bound + pb = self.p.fourier_power_bound + log(4, "power bound for scan %s = %f" %(s.label, pb)) if not self.pbound_scan.get(s.label): self.pbound_scan[s.label] = pb else: @@ -240,8 +240,7 @@ def engine_iterate(self, num=1): logger.info('Time spent in Fourier update: %.2f' % tf) logger.info('Time spent in Overlap update: %.2f' % to) logger.info('Time spent in Position update: %.2f' % tp) - error = parallel.gather_dict(error_dct) - return error + return error_dct def engine_finalize(self): """ @@ -364,8 +363,7 @@ def object_update(self): # array and therefore underestimate the strength of the probe terms. cfact = self.p.object_inertia * self.mean_power if self.p.obj_smooth_std is not None: - logger.info( - 'Smoothing object, average cfact is %.2f' + log(4, 'Smoothing object, average cfact is %.2f' % np.mean(cfact).real) smooth_mfs = [0, self.p.obj_smooth_std, diff --git a/ptypy/engines/DM_simple.py b/archive/engines/DM_simple.py similarity index 98% rename from ptypy/engines/DM_simple.py rename to archive/engines/DM_simple.py index c99bb3ff2..aaac28139 100644 --- a/ptypy/engines/DM_simple.py +++ b/archive/engines/DM_simple.py @@ -15,7 +15,6 @@ from . import BaseEngine, register from ..utils.verbose import logger from ..utils import parallel -from .. import defaults_tree from ..core.manager import Full, Vanilla __all__ = ['DM_simple'] @@ -137,8 +136,7 @@ def engine_iterate(self, num): logger.info('Time spent in Fourier update: %.2f' % tf) logger.info('Time spent in Overlap update: %.2f' % to) - error = parallel.gather_dict(error_dct) - return error + return error_dct def engine_finalize(self): """ diff --git a/ptypy/engines/dummy.py b/archive/engines/dummy.py similarity index 98% rename from ptypy/engines/dummy.py rename to archive/engines/dummy.py index 08b8ff3f2..4a5cf943e 100644 --- a/ptypy/engines/dummy.py +++ b/archive/engines/dummy.py @@ -13,7 +13,6 @@ from ..utils import parallel from . import BaseEngine, register -from .. import defaults_tree from ..core.manager import Full, Vanilla __all__ = ['Dummy'] diff --git a/archive/engines/projectional_pycuda_streams.py b/archive/engines/projectional_pycuda_streams.py new file mode 100644 index 000000000..b766c0689 --- /dev/null +++ b/archive/engines/projectional_pycuda_streams.py @@ -0,0 +1,866 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import numpy as np +import time +from pycuda import gpuarray +import pycuda.driver as cuda + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines import register +from ptypy.engines.projectional import DMMixin, RAARMixin +from . import projectional_pycuda + +# factor how many more exit waves we wanna keep on GPU compared to +# ma / mag data +EX_MA_BLOCKS_RATIO = 2 +MAX_STREAMS = 500 # max number of streams to use +MAX_BLOCKS = 99999 # can be used to limit the number of blocks, simulating that they don't fit + +__all__ = ['DM_pycuda_streams'] + +class GpuData: + """ + Manages one block of GPU data with corresponding CPU data. + Keeps track of which cpu array is currently on GPU by its id, + and transfers if it's not already there. + + To be used for the exit wave, ma, and mag arrays. + Note: Allocator should be pooled for best performance + """ + + def __init__(self, nbytes, syncback=False): + """ + New instance of GpuData. Allocates the GPU-side array. + + :param nbytes: Number of bytes held by this instance. + :param syncback: Should the data be synced back to CPU any time it's swapped out + """ + + self.gpu = None + self.gpuraw = cuda.mem_alloc(nbytes) + self.nbytes = nbytes + self.nbytes_buffer = nbytes + self.gpuId = None + self.cpu = None + self.syncback = syncback + self.ev_done = None + + def _allocator(self, nbytes): + if nbytes > self.nbytes: + raise Exception('requested more bytes than maximum given before: {} vs {}'.format(nbytes, self.nbytes)) + return self.gpuraw + + def record_done(self, stream): + self.ev_done = cuda.Event() + self.ev_done.record(stream) + + def to_gpu(self, cpu, id, stream): + """ + Transfer cpu array to GPU on stream (async), keeping track of its id + """ + if self.gpuId != id: + if self.syncback: + self.from_gpu(stream) + self.gpuId = id + self.cpu = cpu + if self.ev_done is not None: + self.ev_done.synchronize() + self.gpu = gpuarray.to_gpu_async(cpu, allocator=self._allocator, stream=stream) + return self.gpu + + def from_gpu(self, stream): + """ + Transfer data back to CPU, into same data handle it was copied from + before. + """ + if self.cpu is not None and self.gpuId is not None and self.gpu is not None: + if self.ev_done is not None: + stream.wait_for_event(self.ev_done) + self.gpu.get_async(stream, self.cpu) + self.ev_done = cuda.Event() + self.ev_done.record(stream) + + def resize(self, nbytes): + """ + Resize the size of the underlying buffer, to allow re-use in different contexts. + Note that memory will only be freed/reallocated if the new number of bytes are + either larger than before, or if they are less than 90% of the original size - + otherwise it reuses the existing buffer + """ + if nbytes > self.nbytes_buffer or nbytes < self.nbytes_buffer * .9: + self.nbytes_buffer = nbytes + self.gpuraw.free() + self.gpuraw = cuda.mem_alloc(nbytes) + self.nbytes = nbytes + self.reset() + + def reset(self): + """ + Resets handles of cpu references and ids, so that all data will be transfered + again even if IDs match. + """ + self.gpuId = None + self.cpu = None + self.ev_done = None + + def free(self): + """ + Free the underlying buffer on GPU - this object should not be used afterwards + """ + self.gpuraw.free() + self.gpuraw = None + +class GpuDataManager: + """ + Manages a set of GpuData instances, to keep several blocks on device. + + Note that the syncback property is used so that during fourier updates, + the exit wave array is synced bck to cpu (it is updated), + while during probe update, it's not. + """ + + def __init__(self, nbytes, num, syncback=False): + """ + Create an instance of GpuDataManager. + Parameters are the same as for GpuData, and num is the number of + GpuData instances to create (blocks on device). + """ + self.data = [GpuData(nbytes, syncback) for _ in range(num)] + + @property + def syncback(self): + """ + Get if syncback of data to CPU on swapout is enabled. + """ + return self.data[0].syncback + + @syncback.setter + def syncback(self, whether): + """ + Adjust the syncback setting + """ + for d in self.data: + d.syncback = whether + + @property + def nbytes(self): + """ + Get the number of bytes in each block + """ + return self.data[0].nbytes + + @property + def memory(self): + """ + Get all memory occupied by all blocks + """ + m = 0 + for d in self.data: + m += d.nbytes_buffer + return m + + def __len__(self): + return len(self.data) + + def reset(self, nbytes, num): + """ + Reset this object as if these parameters were given to the constructor. + The syncback property is untouched. + """ + sync = self.syncback + # remove if too many, explictly freeing memory + for i in range(num, len(self.data)): + self.data[i].free() + # cut short if too many + self.data = self.data[:num] + # reset existing + for d in self.data: + d.resize(nbytes) + # append new ones + for i in range(len(self.data), num): + self.data.append(GpuData(nbytes, sync)) + + def free(self): + """ + Explicitly clear all data blocks - same as resetting to 0 blocks + """ + self.reset(0, 0) + + + def to_gpu(self, cpu, id, stream): + """ + Transfer a block to the GPU, given its ID and CPU data array + """ + idx = 0 + for x in self.data: + if x.gpuId == id: + break + idx += 1 + if idx == len(self.data): + idx = 0 + else: + pass + m = self.data.pop(idx) + self.data.append(m) + return m.to_gpu(cpu, id, stream) + + def record_done(self, id, stream): + for x in self.data: + if x.gpuId == id: + x.record_done(stream) + return + raise Exception('recording done for id not in pool') + + + def sync_to_cpu(self, stream): + """ + Sync back all data to CPU + """ + for x in self.data: + x.from_gpu(stream) + + +class GpuStreamData: + def __init__(self, ex_data, ma_data, mag_data): + self.queue = cuda.Stream() + self.ex_data = ex_data + self.ma_data = ma_data + self.mag_data = mag_data + self.ev_compute = None + + def end_compute(self): + """ + called at end of kernels using shared data (aux or probe), + to mark when computing is done and it can be re-used + """ + self.ev_compute = cuda.Event() + self.ev_compute.record(self.queue) + return self.ev_compute + + def start_compute(self, prev_event): + """ + called at start of kernels using shared data (aux or probe), + to wait for previous use of this data is finished + """ + + if prev_event is not None: + self.queue.wait_for_event(prev_event) + + def ex_to_gpu(self, dID, ex): + """ + copy exit wave to GPU, but check first if it's already there + If not, but a previous block is on GPU, sync it back to the host + before overwriting it + """ + + return self.ex_data.to_gpu(ex, dID, self.queue) + + def ma_to_gpu(self, dID, ma, mag): + """ + Copy MA array to GPU + """ + # wait for previous work on memory to complete + ma_gpu = self.ma_data.to_gpu(ma, dID, self.queue) + mag_gpu = self.mag_data.to_gpu(mag, dID, self.queue) + return ma_gpu, mag_gpu + + def record_done_ex(self, dID): + """ + Record when we're done with this stream, so that it can be re-used + """ + self.ex_data.record_done(dID, self.queue) + + def record_done_ma(self, dID): + self.ma_data.record_done(dID, self.queue) + self.mag_data.record_done(dID, self.queue) + + def synchronize(self): + """ + Wait for stream to finish its work + """ + self.queue.synchronize() + + +class _ProjectionEngine_pycuda_streams(projectional_pycuda._ProjectionEngine_pycuda): + + """ + Defaults: + + [fft_lib] + default = cuda + type = str + help = Choose the pycuda-compatible FFT module. + doc = One of: + - ``'reikna'`` : the reikna packaga (fast load, competitive compute for streaming) + - ``'cuda'`` : ptypy's cuda wrapper (delayed load, but fastest compute if all data is on GPU) + - ``'skcuda'`` : scikit-cuda (fast load, slowest compute due to additional store/load stages) + choices = 'reikna','cuda','skcuda' + userlevel = 2 + + """ + + def __init__(self, ptycho_parent, pars = None): + + super().__init__(ptycho_parent, pars) + self.streams = None + self.ma_data = None + self.mag_data = None + self.ex_data = None + self.cur_stream = 0 + self.stream_direction = 1 + + def engine_prepare(self): + + super(projectional_pycuda._ProjectionEngine_pycuda, self).engine_prepare() + + for name, s in self.ob.S.items(): + s.gpu = gpuarray.to_gpu(s.data) + # we use default mem_flags for ob/obn/pr/prn page-locking, as we are + # operating on them on CPU as well after each iteration. + # Write-Combined memory (flags=4) is for write-only on CPU side, + # reads are really slow. + for name, s in self.ob_buf.S.items(): + # obb + d = s.data + s.data = cuda.pagelocked_empty(d.shape, d.dtype, order="C", mem_flags=0) + s.data[:] = d + s.gpu = gpuarray.to_gpu(s.data) + for name, s in self.ob_nrm.S.items(): + # obn + d = s.data + s.data = cuda.pagelocked_empty(d.shape, d.dtype, order="c", mem_flags=0) + s.data[:] = d + s.gpu = gpuarray.to_gpu(s.data) + for name, s in self.pr.S.items(): + # pr + d = s.data + s.data = cuda.pagelocked_empty(d.shape, d.dtype, order="C", mem_flags=0) + s.data[:] = d + s.gpu = gpuarray.to_gpu(s.data) + for name, s in self.pr_buf.S.items(): + # pr + d = s.data + s.data = cuda.pagelocked_empty(d.shape, d.dtype, order="C", mem_flags=0) + s.data[:] = d + s.gpu = gpuarray.to_gpu(s.data) + for name, s in self.pr_nrm.S.items(): + # prn + d = s.data + s.data = cuda.pagelocked_empty(d.shape, d.dtype, order="C", mem_flags=0) + s.data[:] = d + s.gpu = gpuarray.to_gpu(s.data) + + use_tiles = (not self.p.probe_update_cuda_atomics) or (not self.p.object_update_cuda_atomics) + + # Extra object buffer for smoothing kernel + if self.p.obj_smooth_std is not None: + for name, s in self.ob_buf.S.items(): + s.tmp = gpuarray.empty(s.gpu.shape, s.gpu.dtype) + + ex_mem = ma_mem = mag_mem = 0 + idlist = list(self.di.S.keys()) + blocks = len(idlist) + for dID in idlist: + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + + prep.addr_gpu = gpuarray.to_gpu(prep.addr) + if use_tiles: + prep.addr2 = np.ascontiguousarray(np.transpose(prep.addr, (2, 3, 0, 1))) + prep.addr2_gpu = gpuarray.to_gpu(prep.addr2) + if self.do_position_refinement: + prep.mangled_addr_gpu = prep.addr_gpu.copy() + + prep.ma_sum_gpu = gpuarray.to_gpu(prep.ma_sum) + # prepare page-locked mems: + prep.err_fourier_gpu = gpuarray.to_gpu(prep.err_fourier) + prep.err_phot_gpu = gpuarray.to_gpu(prep.err_phot) + prep.err_exit_gpu = gpuarray.to_gpu(prep.err_exit) + if self.do_position_refinement: + prep.error_state_gpu = gpuarray.empty_like(prep.err_fourier_gpu) + ma = self.ma.S[dID].data.astype(np.float32) + prep.ma = cuda.pagelocked_empty(ma.shape, ma.dtype, order="C", mem_flags=4) + prep.ma[:] = ma + ex = self.ex.S[eID].data + prep.ex = cuda.pagelocked_empty(ex.shape, ex.dtype, order="C", mem_flags=4) + prep.ex[:] = ex + mag = prep.mag + prep.mag = cuda.pagelocked_empty(mag.shape, mag.dtype, order="C", mem_flags=4) + prep.mag[:] = mag + ex_mem = max(ex_mem, prep.ex.nbytes) + ma_mem = max(ma_mem, prep.ma.nbytes) + mag_mem = max(mag_mem, prep.mag.nbytes) + + # now check remaining memory and allocate as many blocks as would fit + mem = cuda.mem_get_info()[0] + if self.ex_data is not None: + mem += self.ex_data.memory # as we realloc these, consider as free memory + if self.ma_data is not None: + mem += self.ma_data.memory + if self.mag_data is not None: + mem += self.mag_data.memory + + blk = ex_mem * EX_MA_BLOCKS_RATIO + ma_mem + mag_mem + fit = int(mem - 200*1024*1024) // blk # leave 200MB room for safety + fit = min(MAX_BLOCKS, fit) + nex = min(fit * EX_MA_BLOCKS_RATIO, blocks) + nma = min(fit, blocks) + nstreams = min(MAX_STREAMS, blocks) + + log(4, 'PyCUDA blocks fitting on GPU: exit arrays={}, ma_arrays={}, streams={}, totalblocks={}'.format(nex, nma, nstreams, blocks)) + # reset memory or create new + if self.ex_data is not None: + self.ex_data.reset(ex_mem, nex) + else: + self.ex_data = GpuDataManager(ex_mem, nex, True) + if self.ma_data is not None: + self.ma_data.reset(ma_mem, nma) + else: + self.ma_data = GpuDataManager(ma_mem, nma, False) + if self.mag_data is not None: + self.mag_data.reset(mag_mem, nma) + else: + self.mag_data = GpuDataManager(mag_mem, nma, False) + self.streams = [GpuStreamData(self.ex_data, self.ma_data, self.mag_data) for _ in range(nstreams)] + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + self.dID_list = list(self.di.S.keys()) + + prev_event = None + + # atomics or tiled version for probe / object update kernels + atomics_probe = self.p.probe_update_cuda_atomics + atomics_object = self.p.object_update_cuda_atomics + use_tiles = (not atomics_object) or (not atomics_probe) + + for it in range(num): + + error = {} + + for inner in range(self.p.overlap_max_iterations): + + change = 0 + + do_update_probe = (self.curiter >= self.p.probe_update_start) + do_update_object = (self.p.update_object_first or (inner > 0) or not do_update_probe) + do_update_fourier = (inner == 0) + + # initialize probe and object buffer to receive an update + # we do this on the first stream we work on + streamdata = self.streams[self.cur_stream] + streamdata.start_compute(prev_event) + if do_update_object: + for oID, ob in self.ob.storages.items(): + cfact = self.ob_cfact[oID] + obn = self.ob_nrm.S[oID] + obb = self.ob_buf.S[oID] + + if self.p.obj_smooth_std is not None: + log(4,'Smoothing object, cfact is %.2f' % cfact) + smooth_mfs = [self.p.obj_smooth_std, self.p.obj_smooth_std] + # We need a third copy, because we still need ob.gpu for the fourier update + # obb.gpu[:] = ob.gpu[:] + cuda.memcpy_dtod_async(dest=obb.gpu.ptr, + src=ob.gpu.ptr, + size=ob.gpu.nbytes, + stream=streamdata.queue) + streamdata.queue.synchronize() + self.GSK.queue = streamdata.queue + self.GSK.convolution(obb.gpu, smooth_mfs, tmp=obb.tmp) + obb.gpu._axpbz(np.complex64(cfact), 0, obb.gpu, stream=streamdata.queue) + else: + ob.gpu._axpbz(np.complex64(cfact), 0, obb.gpu, stream=streamdata.queue) + obn.gpu.fill(np.float32(cfact), stream=streamdata.queue) + + self.ex_data.syncback = True + + # First cycle: Fourier + object update + for dID in self.dID_list: + t1 = time.time() + prep = self.diff_info[dID] + streamdata = self.streams[self.cur_stream] + + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + POK = kern.POK + + pbound = self.pbound_scan[prep.label] + aux = kern.aux + PROP = kern.PROP + + # set streams + queue = streamdata.queue + FUK.queue = queue + AWK.queue = queue + POK.queue = queue + PROP.queue = queue + + # get addresses and auxilliary array + addr = prep.addr_gpu + addr2 = prep.addr2_gpu if use_tiles else None + err_fourier = prep.err_fourier_gpu + err_phot = prep.err_phot_gpu + err_exit = prep.err_exit_gpu + ma_sum = prep.ma_sum_gpu + + # local references + ob = self.ob.S[oID].gpu + obn = self.ob_nrm.S[oID].gpu + obb = self.ob_buf.S[oID].gpu + pr = self.pr.S[pID].gpu + + # transfer exit wave to gpu + ex = streamdata.ex_to_gpu(dID, prep.ex) + + # transfer ma/mag data to gpu if needed + if do_update_fourier: + # transfer other input data in + ma, mag = streamdata.ma_to_gpu(dID, prep.ma, prep.mag) + # waits for compute on previous stream to finish before continuing + streamdata.start_compute(prev_event) + + # Fourier update + if do_update_fourier: + log(4, '------ Fourier update -----', True) + + ## compute log-likelihood + if self.p.compute_log_likelihood: + t1 = time.time() + AWK.build_aux_no_ex(aux, addr, ob, pr) + PROP.fw(aux, aux) + FUK.log_likelihood(aux, addr, mag, ma, err_phot) + self.benchmark.F_LLerror += time.time() - t1 + + ## prep + forward FFT + t1 = time.time() + #AWK.build_aux(aux, addr, ob, pr, ex, alpha=self.p.alpha) + AWK.make_aux(aux, addr, ob, pr, ex, c_po=self._c, c_e=1-self._c) + self.benchmark.A_Build_aux += time.time() - t1 + + t1 = time.time() + PROP.fw(aux, aux) + self.benchmark.B_Prop += time.time() - t1 + + ## Deviation from measured data + t1 = time.time() + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + FUK.fmag_all_update(aux, addr, mag, ma, err_fourier, pbound) + self.benchmark.C_Fourier_update += time.time() - t1 + streamdata.record_done_ma(dID) + + ## Backward FFT + t1 = time.time() + PROP.bw(aux, aux) + ## apply changes + #AWK.build_exit(aux, addr, ob, pr, ex, alpha=self.p.alpha) + AWK.make_exit(aux, addr, ob, pr, ex, c_a=self._b, c_po=self._a, c_e=-(self._a + self._b)) + FUK.exit_error(aux, addr) + FUK.error_reduce(addr, err_exit) + self.benchmark.E_Build_exit += time.time() - t1 + + self.benchmark.calls_fourier += 1 + + prestr = '%d Iteration (Overlap) #%02d: ' % (parallel.rank, inner) + + # Object update + if do_update_object: + log(4, prestr + '----- object update -----', True) + t1 = time.time() + + addrt = addr if atomics_object else addr2 + POK.ob_update(addrt, obb, obn, pr, ex, atomics=atomics_object) + self.benchmark.object_update += time.time() - t1 + self.benchmark.calls_object += 1 + + # end_compute is to allow aux + ob re-use, so we can mark it here + prev_event = streamdata.end_compute() + streamdata.record_done_ex(dID) + self.cur_stream = (self.cur_stream + self.stream_direction) % len(self.streams) + + # swap direction for next time + if do_update_fourier: + self.dID_list.reverse() + self.stream_direction = -self.stream_direction + # make sure we start with the same stream were we stopped + self.cur_stream = (self.cur_stream + self.stream_direction) % len(self.streams) + + if do_update_object: + self._object_allreduce() + + # Exit if probe should fnot yet be updated + if not do_update_probe: + break + + # Update probe + log(4, prestr + '----- probe update -----', True) + self.ex_data.syncback = False + change = self.probe_update() + + # swap direction for next time + self.dID_list.reverse() + self.stream_direction = -self.stream_direction + # make sure we start with the same stream were we stopped + self.cur_stream = (self.cur_stream + self.stream_direction) % len(self.streams) + + log(4, prestr + 'change in probe is %.3f' % change, True) + + # stop iteration if probe change is small + if change < self.p.overlap_converge_factor: break + + parallel.barrier() + + if self.do_position_refinement and (self.curiter): + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + log(4, "----------- START POS REF -------------") + prev_event = None + for dID in self.di.S.keys(): + streamdata = self.streams[self.cur_stream] + + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr_gpu + original_addr = prep.original_addr + mangled_addr = prep.mangled_addr_gpu + ma_sum = prep.ma_sum_gpu + ma, mag = streamdata.ma_to_gpu(dID, prep.ma, prep.mag) + err_fourier = prep.err_fourier_gpu + error_state = prep.error_state_gpu + + PCK = kern.PCK + TK = kern.TK + PROP = kern.PROP + PCK.queue = streamdata.queue + TK.queue = streamdata.queue + PROP.queue = streamdata.queue + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We need to re-calculate the current error + PCK.build_aux(aux, addr, ob, pr) + PROP.fw(aux, aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, addr, mag, ma, ma_sum) + PCK.error_reduce(addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, addr, mag, ma, err_fourier) + cuda.memcpy_dtod_async(dest=error_state.ptr, + src=err_fourier.ptr, + size=err_fourier.nbytes, + stream=streamdata.queue) + streamdata.start_compute(prev_event) + + log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + for i in range(PCK.mangler.nshifts): + streamdata.queue.synchronize() + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + PROP.fw(aux, aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, mangled_addr, mag, ma, ma_sum) + PCK.error_reduce(mangled_addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, mangled_addr, mag, ma, err_fourier) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_fourier) + + cuda.memcpy_dtod_async(dest=err_fourier.ptr, + src=error_state.ptr, + size=err_fourier.nbytes, + stream=streamdata.queue) + if use_tiles: + s1 = prep.addr_gpu.shape[0] * prep.addr_gpu.shape[1] + s2 = prep.addr_gpu.shape[2] * prep.addr_gpu.shape[3] + TK.transpose(prep.addr_gpu.reshape(s1, s2), prep.addr2_gpu.reshape(s2, s1)) + + prev_event = streamdata.end_compute() + + # next stream + self.cur_stream = (self.cur_stream + self.stream_direction) % len(self.streams) + + self.curiter += 1 + + #print('end loop, syncall and copy back') + for sd in self.streams: + sd.synchronize() + + for name, s in self.ob.S.items(): + s.gpu.get(s.data) + for name, s in self.pr.S.items(): + s.gpu.get(s.data) + + # FIXXME: copy to pinned memory + for dID, prep in self.diff_info.items(): + err_fourier = prep.err_fourier_gpu.get() + err_phot = prep.err_phot_gpu.get() + err_exit = prep.err_exit_gpu.get() + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error.update(zip(prep.view_IDs, errs)) + + self.error = error + return error + + def _object_allreduce(self): + # make sure that all transfers etc are finished + for sd in self.streams: + sd.synchronize() + # sync all + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + obb = self.ob_buf.S[oID] + self.multigpu.allReduceSum(obb.gpu) + self.multigpu.allReduceSum(obn.gpu) + obb.gpu /= obn.gpu + + self.clip_object(obb.gpu) + ob.gpu[:] = obb.gpu + + ## probe update + def probe_update(self, MPI=False): + t1 = time.time() + streamdata = self.streams[self.cur_stream] + use_atomics = self.p.probe_update_cuda_atomics + # storage for-loop + change_gpu = gpuarray.zeros((1,), dtype=np.float32) + prev_event = None + for pID, pr in self.pr.storages.items(): + prn = self.pr_nrm.S[pID] + cfact = self.pr_cfact[pID] + pr.gpu._axpbz(np.complex64(cfact), 0, pr.gpu, stream=streamdata.queue) + prn.gpu.fill(np.float32(cfact), stream=streamdata.queue) + + for dID in self.dID_list: + prep = self.diff_info[dID] + streamdata = self.streams[self.cur_stream] + + POK = self.kernels[prep.label].POK + POK.queue = streamdata.queue + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + ex = streamdata.ex_to_gpu(dID, prep.ex) + + # scan for-loop + addrt = prep.addr_gpu if use_atomics else prep.addr2_gpu + streamdata.start_compute(prev_event) + ev = POK.pr_update(addrt, + self.pr.S[pID].gpu, + self.pr_nrm.S[pID].gpu, + self.ob.S[oID].gpu, + ex, + atomics=use_atomics) + streamdata.record_done_ex(dID) + prev_event = streamdata.end_compute() + self.cur_stream = (self.cur_stream + self.stream_direction) % len(self.streams) + + # sync all streams first + for sd in self.streams: + sd.synchronize() + + for pID, pr in self.pr.storages.items(): + + buf = self.pr_buf.S[pID] + prn = self.pr_nrm.S[pID] + + self.multigpu.allReduceSum(pr.gpu) + self.multigpu.allReduceSum(prn.gpu) + pr.gpu /= prn.gpu + self.support_constraint(pr) + + ## calculate change on GPU + AUK = self.kernels[list(self.kernels)[0]].AUK + buf.gpu -= pr.gpu + change_gpu += (AUK.norm2(buf.gpu) / AUK.norm2(pr.gpu)) + buf.gpu[:] = pr.gpu + self.multigpu.allReduceSum(change_gpu) + change = change_gpu.get().item() / parallel.size + + # print 'probe update: ' + str(time.time()-t1) + self.benchmark.probe_update += time.time() - t1 + self.benchmark.calls_probe += 1 + + return np.sqrt(change) + + def engine_finalize(self, benchmark=False): + """ + Clear all GPU data, pinned memory, etc + """ + self.streams = None + self.ex_data = None + self.ma_data = None + self.mag_data = None + + super().engine_finalize(benchmark) + + +@register() +class DM_pycuda_streams(_ProjectionEngine_pycuda_streams, DMMixin): + """ + A full-fledged Difference Map engine accelerated with pycuda. + + Defaults: + + [name] + default = DM_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _ProjectionEngine_pycuda_streams.__init__(self, ptycho_parent, pars) + DMMixin.__init__(self, self.p.alpha) + ptycho_parent.citations.add_article(**self.article) + + +@register() +class RAAR_pycuda_streams(_ProjectionEngine_pycuda_streams, RAARMixin): + """ + A RAAR engine in accelerated with pycuda. + + Defaults: + + [name] + default = RAAR_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + + _ProjectionEngine_pycuda_streams.__init__(self, ptycho_parent, pars) + RAARMixin.__init__(self, self.p.beta) diff --git a/archive/misc/mpitest.cpp b/archive/misc/mpitest.cpp new file mode 100644 index 000000000..e4ff84577 --- /dev/null +++ b/archive/misc/mpitest.cpp @@ -0,0 +1,47 @@ +/** This is a simple C++ test to check if cuda-aware MPI works as + * expected. + * It allocates a GPU array and puts 1s into it, then sends it + * across MPI to the receiving rank, which transfers back to + * host and outputs the values. + * The expected output is: + * + * Received 1, 1 + * + * Compile with: + * mpic++ -o test mpitest.cpp -L/path/to/cuda/libs -lcudart + * + * Run with: + * mpirun -np 2 test + */ + +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + + int rank; + MPI_Status status; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + + if (rank == 0) { + int* d_send; + cudaMalloc((void**)&d_send, 2*sizeof(int)); + int h_send[] = {1, 1}; + cudaMemcpy(d_send, h_send, 2*sizeof(int), cudaMemcpyHostToDevice); + MPI_Send(d_send, 2, MPI_INT, 1, 99, MPI_COMM_WORLD); + std::cout << "Data has been sent...\n"; + } else if (rank == 1) { + int* d_recv; + cudaMalloc((void**)&d_recv, 2*sizeof(int)); + MPI_Recv(d_recv, 2, MPI_INT, 0, 99, MPI_COMM_WORLD, &status); + int h_recv[2]; + cudaMemcpy(h_recv, d_recv, 2*sizeof(int), cudaMemcpyDeviceToHost); + std::cout << "Received " << h_recv[0] << ", " << h_recv[1] << "\n"; + } + +} \ No newline at end of file diff --git a/benchmark/cufft_vs_reikna.py b/benchmark/cufft_vs_reikna.py new file mode 100644 index 000000000..d4e8b6caa --- /dev/null +++ b/benchmark/cufft_vs_reikna.py @@ -0,0 +1,82 @@ +""" +Tests cuFFT vs Reikna FFT performance (not accuracy). + +Together with a C++ implementation of cuFFT with callbacks, +we get the following numbers on a P100 GPU: + +For 100 calls of 256x256 with batch size 2000: +- Reikna with or without filters: 1,470ms +- cuFFT without filters : 792ms +- cuFFT with separate filters : 1,564ms +- cuFFT with callbacks : 916ms + +For 128x128 with batch size 2000: +- Reikna with or without filters: 389ms +- cuFFT without filters : 194ms +- cuFFT with separate filters : 388ms +- cuFFT with callbacks : 223ms +""" + + +import numpy as np +import pycuda.driver as cuda +from pycuda import gpuarray +from pycuda.tools import make_default_context +from ptypy.accelerate.cuda_pycuda.fft import FFT +from ptypy.accelerate.cuda_pycuda.cufft import FFT_cuda as cuFFT +import time + +ctx = make_default_context() +stream = cuda.Stream() + +A = 2000 +B = 256 +C = 256 + +COMPLEX_TYPE = np.complex64 + + +f = np.empty(shape=(A, B, C), dtype=np.complex64) +for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + +prefilter = (np.arange(B*C).reshape((B, C)) + 1j* np.arange(B*C).reshape((B, C))).astype(COMPLEX_TYPE) +postfilter = (np.arange(13, 13 + B*C).reshape((B, C)) + 1j*np.arange(13, 13 + B*C).reshape((B, C))).astype(COMPLEX_TYPE) + +f_d = gpuarray.to_gpu(f) + +prop_fwd = FFT(f, stream, pre_fft=prefilter, post_fft=postfilter, inplace=True, symmetric=True) + +start_reikna = cuda.Event() +stop_reikna = cuda.Event() +start_cufft = cuda.Event() +stop_cufft = cuda.Event() + +start_reikna.record(stream) +for p in range(100): + prop_fwd.ft(f_d, f_d) +stop_reikna.record(stream) +start_reikna.synchronize() +stop_reikna.synchronize() +time_reikna = stop_reikna.time_since(start_reikna) + +print('Reikna for {}: {}ms'.format((A,B,C), time_reikna)) + +# with pre- and post-filter +cuprop_fw = cuFFT(f, stream, pre_fft=prefilter, post_fft=postfilter, inplace=True, symmetric=True) +# without filters, and symmetric=False avoids scaling the result +#cuprop_fw = cuFFT(f, stream, pre_fft=None, post_fft=None, inplace=True, symmetric=False) + +start_cufft.record(stream) +for p in range(100): + cuprop_fw.ft(f_d, f_d) +stop_cufft.record(stream) +start_cufft.synchronize() +stop_cufft.synchronize() +time_cufft = stop_cufft.time_since(start_cufft) + +print('CUFFT for {}: {}ms'.format((A,B,C), time_cufft)) +print('CUFFT Speedup: {}x'.format(time_reikna/time_cufft)) + +ctx.pop() +ctx.detach() \ No newline at end of file diff --git a/benchmark/diamond_benchmarks/ML_accurracy_test.py b/benchmark/diamond_benchmarks/ML_accurracy_test.py new file mode 100644 index 000000000..a8da654ac --- /dev/null +++ b/benchmark/diamond_benchmarks/ML_accurracy_test.py @@ -0,0 +1,404 @@ +''' +Load real data and prepare an accuracy report of GPU vs numpy +''' + +import h5py +import numpy as np +import csv + +import pycuda.driver as cuda +from pycuda import gpuarray + +from ptypy.accelerate.cuda_pycuda.kernels import GradientDescentKernel +from ptypy.accelerate.base.kernels import GradientDescentKernel as BaseGradientDescentKernel + + +class GradientDescentAccuracyTester: + + datadir = "/dls/science/users/iat69393/gpu-hackathon/test-data-%s/" + rtol = 1e-6 + atol = 1e-6 + headings = ['Kernel', 'Version', 'Iter', 'MATH_TYPE', 'IN/OUT_TYPE', + 'ACC_TYPE', 'Array', 'num_elements', 'num_errors', 'max_relerr', 'max_abserr'] + + def __init__(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + cuda.init() + self.device = cuda.Device(0) + self.ctx = self.device.make_context() + self.stream = cuda.Stream() + self.results = [] + + def __del__(self): + np.set_printoptions() + self.ctx.pop() + self.ctx.detach() + + def test_make_model(self, name, iter, + math_type={'float', 'double'}, + data_type={'float', 'double'}): + + res = [] + + # Load data + with h5py.File(self.datadir % name + "make_model_%04d.h5" % iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + + # CPU Kernel + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.make_model(aux, addr) + ref = BGDK.npy.Imodel + + # GPU variants + addr_dev = gpuarray.to_gpu(addr) + for d in data_type: + if d == 'float': + aux_dev = gpuarray.to_gpu(aux.astype(np.complex64)) + else: + aux_dev = gpuarray.to_gpu(aux.astype(np.complex128)) + for m in math_type: + # data type will be determined based on aux_dev data type automatically + GDK = GradientDescentKernel( + aux_dev, addr.shape[1], queue=self.stream, math_type=m) + GDK.allocate() + GDK.make_model(aux_dev, addr_dev) + act = GDK.gpu.Imodel.get() + + num, num_mis, max_abs, max_rel = self._calc_diffs(act, ref) + + line = ['make_model', name, iter, d, m, 'N/A', + 'Imodel', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + return res + + def test_floating_intensity(self, name, iter, + math_type={'float', 'double'}, + data_type={'float', 'double'}, + acc_type={'float', 'double'}): + + # note that this is actually calling 4 kernels: + # - floating_intensity_cuda_step1 + # - error_reduce_cuda (2x) + # - floating_intensity_cuda_step2 + + res = [] + + # Load data + with h5py.File(self.datadir % name + "floating_intensities_%04d.h5" % iter, "r") as f: + w = f["w"][:] + addr = f["addr"][:] + I = f["I"][:] + fic = f["fic"][:] + Imodel = f["Imodel"][:] + with h5py.File(self.datadir % name + "make_model_%04d.h5" % iter, "r") as f: + aux = f["aux"][:] + + # CPU Kernel + ficref = np.copy(fic) + Iref = np.copy(Imodel) + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.npy.Imodel = Iref + BGDK.floating_intensity(addr, w, I, ficref) # modifies fic, Imodel + Iref = BGDK.npy.Imodel + + addr_dev = gpuarray.to_gpu(addr) + for d in data_type: + for m in math_type: + for a in acc_type: + if d == 'float': + aux_dev = gpuarray.to_gpu(aux.astype(np.complex64)) + I_dev = gpuarray.to_gpu(I.astype(np.float32)) + fic_dev = gpuarray.to_gpu(fic.astype(np.float32)) + w_dev = gpuarray.to_gpu(w.astype(np.float32)) + Imodel_dev = gpuarray.to_gpu(Imodel.astype(np.float32)) + else: + aux_dev = gpuarray.to_gpu(aux.astype(np.complex128)) + I_dev = gpuarray.to_gpu(I.astype(np.float64)) + fic_dev = gpuarray.to_gpu(fic.astype(np.float64)) + w_dev = gpuarray.to_gpu(w.astype(np.float64)) + Imodel_dev = gpuarray.to_gpu(Imodel.astype(np.float64)) + + # GPU kernel + GDK = GradientDescentKernel( + aux_dev, addr.shape[1], accumulate_type=a, math_type=m, queue=self.stream) + GDK.allocate() + GDK.gpu.Imodel = Imodel_dev + GDK.floating_intensity(addr_dev, w_dev, I_dev, fic_dev) + + Iact = GDK.gpu.Imodel.get() + fact = fic_dev.get() + + num, num_mis, max_abs, max_rel = self._calc_diffs( + Iact, Iref) + line = ['floating_intensity', name, iter, d, m, + a, 'Imodel', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + fact, ficref) + line = ['floating_intensity', name, iter, d, m, + a, 'fic', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + return res + + def test_main_and_error_reduce(self, name, iter, + math_type={'float', 'double'}, + data_type={'float', 'double'}, + acc_type={'float', 'double'}): + + res = [] + + # Load data + with h5py.File(self.datadir % name + "main_%04d.h5" % iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + w = f["w"][:] + I = f["I"][:] + # Load data + with h5py.File(self.datadir % name + "error_reduce_%04d.h5" % iter, "r") as f: + err_phot = f["err_phot"][:] + + # CPU Kernel + auxref = np.copy(aux) + errref = np.copy(err_phot) + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.main(auxref, addr, w, I) + BGDK.error_reduce(addr, errref) + LLerrref = BGDK.npy.LLerr + + addr_dev = gpuarray.to_gpu(addr) + for d in data_type: + for m in math_type: + for a in acc_type: + if d == 'float': + aux_dev = gpuarray.to_gpu(aux.astype(np.complex64)) + I_dev = gpuarray.to_gpu(I.astype(np.float32)) + w_dev = gpuarray.to_gpu(w.astype(np.float32)) + err_phot_dev = gpuarray.to_gpu( + err_phot.astype(np.float32)) + else: + aux_dev = gpuarray.to_gpu(aux.astype(np.complex128)) + I_dev = gpuarray.to_gpu(I.astype(np.float64)) + w_dev = gpuarray.to_gpu(w.astype(np.float64)) + err_phot_dev = gpuarray.to_gpu( + err_phot.astype(np.float64)) + + # GPU kernel + GDK = GradientDescentKernel( + aux_dev, addr.shape[1], accumulate_type=a, math_type=m) + GDK.allocate() + GDK.main(aux_dev, addr_dev, w_dev, I_dev) + GDK.error_reduce(addr_dev, err_phot_dev) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + auxref, aux_dev.get()) + line = ['main_and_error_reduce', name, iter, d, + m, a, 'aux', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + LLerrref, GDK.gpu.LLerr.get()) + line = ['main_and_error_reduce', name, iter, d, + m, a, 'LLerr', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + errref, err_phot_dev.get()) + line = ['main_and_error_reduce', name, iter, d, m, + a, 'err_phot', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + return res + + def test_make_a012(self, name, iter, + math_type={'float', 'double'}, + data_type={'float', 'double'}, + acc_type={'float', 'double'}): + + # Reduce the array size to make the tests run faster + Nmax = 10 + Ymax = 128 + Xmax = 128 + + res = [] + + # Load data + with h5py.File(self.datadir % name + "make_a012_%04d.h5" % iter, "r") as g: + addr = g["addr"][:Nmax] + I = g["I"][:Nmax, :Ymax, :Xmax] + b_f = g["f"][:Nmax, :Ymax, :Xmax] + b_a = g["a"][:Nmax, :Ymax, :Xmax] + b_b = g["b"][:Nmax, :Ymax, :Xmax] + fic = g["fic"][:Nmax] + with h5py.File(self.datadir % name + "make_model_%04d.h5" % iter, "r") as h: + aux = h["aux"][:Nmax, :Ymax, :Xmax] + + # CPU Kernel + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.make_a012(b_f, b_a, b_b, addr, I, fic) + Imodelref = BGDK.npy.Imodel + LLerrref = BGDK.npy.LLerr + LLdenref = BGDK.npy.LLden + + addr_dev = gpuarray.to_gpu(addr) + for d in data_type: + for m in math_type: + for a in acc_type: + if d == 'float': + aux_dev = gpuarray.to_gpu(aux.astype(np.complex64)) + I_dev = gpuarray.to_gpu(I.astype(np.float32)) + b_f_dev = gpuarray.to_gpu(b_f.astype(np.complex64)) + b_a_dev = gpuarray.to_gpu(b_a.astype(np.complex64)) + b_b_dev = gpuarray.to_gpu(b_b.astype(np.complex64)) + fic_dev = gpuarray.to_gpu(fic.astype(np.float32)) + else: + aux_dev = gpuarray.to_gpu(aux.astype(np.complex128)) + I_dev = gpuarray.to_gpu(I.astype(np.float64)) + b_f_dev = gpuarray.to_gpu(b_f.astype(np.complex128)) + b_a_dev = gpuarray.to_gpu(b_a.astype(np.complex128)) + b_b_dev = gpuarray.to_gpu(b_b.astype(np.complex128)) + fic_dev = gpuarray.to_gpu(fic.astype(np.float64)) + + GDK = GradientDescentKernel(aux_dev, addr.shape[1], queue=self.stream, + math_type=m, accumulate_type=a) + GDK.allocate() + GDK.gpu.Imodel.fill(np.nan) + GDK.gpu.LLerr.fill(np.nan) + GDK.gpu.LLden.fill(np.nan) + GDK.make_a012(b_f_dev, b_a_dev, b_b_dev, + addr_dev, I_dev, fic_dev) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + LLerrref, GDK.gpu.LLerr.get()) + line = ['make_a012', name, iter, d, m, a, + 'LLerr', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + LLdenref, GDK.gpu.LLden.get()) + line = ['make_a012', name, iter, d, m, a, + 'LLden', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + Imodelref, GDK.gpu.Imodel.get()) + line = ['make_a012', name, iter, d, m, a, + 'Imodel', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + return res + + def test_fill_b(self, name, iter, + math_type={'float', 'double'}, + data_type={'float', 'double'}, + acc_type={'float', 'double'}): + + res = [] + + # Load data + + Nmax = 10 + Ymax = 128 + Xmax = 128 + + with h5py.File(self.datadir % name + "fill_b_%04d.h5" % iter, "r") as f: + w = f["w"][:Nmax, :Ymax, :Xmax] + addr = f["addr"][:] + B = f["B"][:] + Brenorm = f["Brenorm"][...] + A0 = f["A0"][:Nmax, :Ymax, :Xmax] + A1 = f["A1"][:Nmax, :Ymax, :Xmax] + A2 = f["A2"][:Nmax, :Ymax, :Xmax] + with h5py.File(self.datadir % name + "make_model_%04d.h5" % iter, "r") as f: + aux = f["aux"][:Nmax, :Ymax, :Xmax] + + # CPU Kernel + Bref = np.copy(B) + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.npy.Imodel = A0 + BGDK.npy.LLerr = A1 + BGDK.npy.LLden = A2 + BGDK.fill_b(addr, Brenorm, w, Bref) + + addr_dev = gpuarray.to_gpu(addr) + for d in data_type: + for m in math_type: + for a in acc_type: + if d == 'float': + aux_dev = gpuarray.to_gpu(aux.astype(np.complex64)) + w_dev = gpuarray.to_gpu(w.astype(np.float32)) + B_dev = gpuarray.to_gpu(B.astype(np.float32)) + A0_dev = gpuarray.to_gpu(A0.astype(np.float32)) + A1_dev = gpuarray.to_gpu(A1.astype(np.float32)) + A2_dev = gpuarray.to_gpu(A2.astype(np.float32)) + else: + aux_dev = gpuarray.to_gpu(aux.astype(np.complex128)) + w_dev = gpuarray.to_gpu(w.astype(np.float64)) + B_dev = gpuarray.to_gpu(B.astype(np.float64)) + A0_dev = gpuarray.to_gpu(A0.astype(np.float64)) + A1_dev = gpuarray.to_gpu(A1.astype(np.float64)) + A2_dev = gpuarray.to_gpu(A2.astype(np.float64)) + + GDK = GradientDescentKernel( + aux_dev, addr.shape[1], queue=self.stream, math_type=m, accumulate_type=a) + GDK.allocate() + GDK.gpu.Imodel = A0_dev + GDK.gpu.LLerr = A1_dev + GDK.gpu.LLden = A2_dev + GDK.fill_b(addr_dev, Brenorm, w_dev, B_dev) + + num, num_mis, max_abs, max_rel = self._calc_diffs( + Bref, B_dev.get()) + line = ['fill_b', name, iter, d, m, a, + 'B', num, num_mis, max_rel, max_abs] + print(line) + res.append(line) + + return res + + def _calc_diffs(self, act, ref): + diffs = np.abs(ref - act) + max_abs = np.max(diffs[:]) + aref = np.abs(ref[:]) + max_rel = np.max( + np.divide(diffs[:], aref, out=np.zeros_like(diffs[:]), where=aref > 0)) + num_mis = np.count_nonzero(diffs[:] > self.atol + self.rtol * aref) + num = np.prod(ref.shape) + + return num, num_mis, max_abs, max_rel + + +tester = GradientDescentAccuracyTester() +print(tester.headings) + +res = [tester.headings] +for ver in [("base", 10), ("regul", 50), ("floating", 0)]: + res += tester.test_make_model(*ver) + res += tester.test_floating_intensity(*ver) + res += tester.test_main_and_error_reduce(*ver) + res += tester.test_make_a012(*ver) + res += tester.test_fill_b(*ver) + +with open('ML_accuracy_test_results.csv', 'w', newline='') as f: + writer = csv.writer(f) + writer.writerows(res) + +print('Done.') diff --git a/benchmark/diamond_benchmarks/moonflower_scripts/i08.py b/benchmark/diamond_benchmarks/moonflower_scripts/i08.py new file mode 100644 index 000000000..273a8ecbf --- /dev/null +++ b/benchmark/diamond_benchmarks/moonflower_scripts/i08.py @@ -0,0 +1,74 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines("cuda") +import time + +import os +import getpass +from pathlib import Path +username = getpass.getuser() +tmpdir = os.path.join('/dls/tmp', username, 'dumps', 'ptypy') +Path(tmpdir).mkdir(parents=True, exist_ok=True) + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 500 +# set home path +p.io = u.Param() +p.io.home = tmpdir +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param() +p.io.interaction.server = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.I08 = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.I08.name = 'BlockFull' +p.scans.I08.data= u.Param() +p.scans.I08.data.name = 'MoonFlowerScan' +p.scans.I08.data.shape = 128 +p.scans.I08.data.num_frames = 10000 # real is 50000 +p.scans.I08.data.save = None + +p.scans.I08.illumination = u.Param() +p.scans.I08.coherence = u.Param(num_probe_modes=20) +p.scans.I08.illumination.diversity = u.Param() +p.scans.I08.illumination.diversity.noise = (0.5, 1.0) +p.scans.I08.illumination.diversity.power = 0.1 + +# position distance in fraction of illumination frame +p.scans.I08.data.density = 0.05 +# total number of photon in empty beam +p.scans.I08.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.I08.data.psf = 1.5 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 20 +p.engines.engine00.probe_update_start = 1 +p.engines.engine00.probe_update_cuda_atomics = False +p.engines.engine00.object_update_cuda_atomics = True + +# prepare and run +P = Ptycho(p,level=4) +t1 = time.perf_counter() +P.run() +t2 = time.perf_counter() +P.print_stats() +print('Elapsed Compute Time: {} seconds'.format(t2-t1)) + diff --git a/benchmark/diamond_benchmarks/moonflower_scripts/i13.py b/benchmark/diamond_benchmarks/moonflower_scripts/i13.py new file mode 100644 index 000000000..1cf42d5e4 --- /dev/null +++ b/benchmark/diamond_benchmarks/moonflower_scripts/i13.py @@ -0,0 +1,75 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines("cuda") +import time + +import os +import getpass +from pathlib import Path +username = getpass.getuser() +tmpdir = os.path.join('/dls/tmp', username, 'dumps', 'ptypy') +Path(tmpdir).mkdir(parents=True, exist_ok=True) + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 100 +# set home path +p.io = u.Param() +p.io.home = tmpdir +p.io.autosave = u.Param(active=False) #(active=True, interval=50000) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param() +p.io.interaction.server = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.i13 = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.i13.name = 'BlockFull' # or 'Full' +p.scans.i13.data= u.Param() +p.scans.i13.data.name = 'MoonFlowerScan' +p.scans.i13.data.shape = 512 +p.scans.i13.data.num_frames = 5000 +p.scans.i13.data.save = None + +p.scans.i13.illumination = u.Param() +p.scans.i13.coherence = u.Param(num_probe_modes=1) +p.scans.i13.illumination.diversity = u.Param() +p.scans.i13.illumination.diversity.noise = (0.5, 1.0) +p.scans.i13.illumination.diversity.power = 0.1 + +# position distance in fraction of illumination frame +p.scans.i13.data.density = 0.2 +# total number of photon in empty beam +p.scans.i13.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.i13.data.psf = 0.2 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 20 +p.engines.engine00.probe_update_start = 1 +p.engines.engine00.probe_update_cuda_atomics = False +p.engines.engine00.object_update_cuda_atomics = True + + +# prepare and run +P = Ptycho(p,level=4) +t1 = time.perf_counter() +P.run() +t2 = time.perf_counter() +P.print_stats() +print('Elapsed Compute Time: {} seconds'.format(t2-t1)) + diff --git a/benchmark/diamond_benchmarks/moonflower_scripts/i14_1.py b/benchmark/diamond_benchmarks/moonflower_scripts/i14_1.py new file mode 100644 index 000000000..9d1abcccb --- /dev/null +++ b/benchmark/diamond_benchmarks/moonflower_scripts/i14_1.py @@ -0,0 +1,73 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines("cuda") +import time + +import os +import getpass +from pathlib import Path +username = getpass.getuser() +tmpdir = os.path.join('/dls/tmp', username, 'dumps', 'ptypy') +Path(tmpdir).mkdir(parents=True, exist_ok=True) + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 500 +# set home path +p.io = u.Param() +p.io.home = tmpdir +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param() +p.io.interaction.server = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.i14_1 = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.i14_1.name = 'BlockFull' +p.scans.i14_1.data= u.Param() +p.scans.i14_1.data.name = 'MoonFlowerScan' +p.scans.i14_1.data.shape = 256 +p.scans.i14_1.data.num_frames = 15000 +p.scans.i14_1.data.save = None + +p.scans.i14_1.illumination = u.Param() +p.scans.i14_1.coherence = u.Param(num_probe_modes=2) +p.scans.i14_1.illumination.diversity = u.Param() +p.scans.i14_1.illumination.diversity.noise = (0.5, 1.0) +p.scans.i14_1.illumination.diversity.power = 0.1 + +# position distance in fraction of illumination frame +p.scans.i14_1.data.density = 0.2 +# total number of photon in empty beam +p.scans.i14_1.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.i14_1.data.psf = 0.2 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 20 +p.engines.engine00.probe_update_start = 1 +p.engines.engine00.probe_update_cuda_atomics = False +p.engines.engine00.object_update_cuda_atomics = True + +# prepare and run +P = Ptycho(p,level=4) +t1 = time.perf_counter() +P.run() +t2 = time.perf_counter() +P.print_stats() +print('Elapsed Compute Time: {} seconds'.format(t2-t1)) diff --git a/benchmark/diamond_benchmarks/moonflower_scripts/i14_2.py b/benchmark/diamond_benchmarks/moonflower_scripts/i14_2.py new file mode 100644 index 000000000..8e3c7241e --- /dev/null +++ b/benchmark/diamond_benchmarks/moonflower_scripts/i14_2.py @@ -0,0 +1,75 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" + +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines("cuda") +import time + +import os +import getpass +from pathlib import Path +username = getpass.getuser() +tmpdir = os.path.join('/dls/tmp', username, 'dumps', 'ptypy') +Path(tmpdir).mkdir(parents=True, exist_ok=True) + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 1000 +# set home path +p.io = u.Param() +p.io.home = tmpdir +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param() +p.io.interaction.server = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.i14_2 = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.i14_2.name = 'BlockFull' +p.scans.i14_2.data= u.Param() +p.scans.i14_2.data.name = 'MoonFlowerScan' +p.scans.i14_2.data.shape = 128 +p.scans.i14_2.data.num_frames = 10000 #50000 is the real value +p.scans.i14_2.data.save = None + +p.scans.i14_2.illumination = u.Param() +p.scans.i14_2.coherence = u.Param(num_probe_modes=5) +p.scans.i14_2.illumination.diversity = u.Param() +p.scans.i14_2.illumination.diversity.noise = (0.5, 1.0) +p.scans.i14_2.illumination.diversity.power = 0.1 + +# position distance in fraction of illumination frame +p.scans.i14_2.data.density = 0.2 +# total number of photon in empty beam +p.scans.i14_2.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.i14_2.data.psf = 0.4 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 20 +p.engines.engine00.probe_update_start = 1 +p.engines.engine00.probe_update_cuda_atomics = False +p.engines.engine00.object_update_cuda_atomics = True + + +# prepare and run +P = Ptycho(p,level=4) +t1 = time.perf_counter() +P.run() +t2 = time.perf_counter() +P.print_stats() +print('Elapsed Compute Time: {} seconds'.format(t2-t1)) diff --git a/benchmark/diamond_benchmarks/moonflower_scripts/profile_all.sh b/benchmark/diamond_benchmarks/moonflower_scripts/profile_all.sh new file mode 100755 index 000000000..33667bf42 --- /dev/null +++ b/benchmark/diamond_benchmarks/moonflower_scripts/profile_all.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# exit on errors +set -e + +# Find folder of benchmarks +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd $SCRIPTDIR/../../.. # ptypy folder + +# scripts to run + output folder +scripts="i08 i13 i14_1 i14_2" +profdir=/dls/tmp/${USER}/nvprof + + +mkdir -p ${profdir} + + +# run all scripts +for script in $scripts +do + rm -f ${profdir}/${script}.*.nvprof + mpirun -np 4 \ + nvprof -f -o ${profdir}/${script}.%q{OMPI_COMM_WORLD_RANK}.nvprof \ + python benchmark/diamond_benchmarks/moonflower_scripts/${script}.py \ + 2>&1 | tee ${profdir}/${script}.log +done + +# Output summary +for script in $scripts +do + totaltime=$(awk '$0 ~ /Elapsed Compute Time:/ {print $4}' ${profdir}/${script}.log) + echo $script Time: $totaltime +done diff --git a/benchmark/model_speed.py b/benchmark/model_speed.py index 7abcdfd88..f05dd49dd 100644 --- a/benchmark/model_speed.py +++ b/benchmark/model_speed.py @@ -1,20 +1,23 @@ - from ptypy.core import Ptycho from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 2 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home = "/".join([tmpdir, "ptypy"]) p.io.autosave = None p.io.interaction = u.Param(active=False) p.scans = u.Param() p.scans.MF = u.Param() -p.scans.MF.name = 'BlockFull' # or 'Full' +p.scans.MF.name = 'BlockFull' p.scans.MF.data = u.Param() p.scans.MF.data.name = 'QuickScan' p.scans.MF.data.shape = 32 @@ -33,11 +36,11 @@ # prepare and run P = Ptycho(p,level=1) for scan in P.model.scans.values(): - scan.frames_per_call=1000 + scan.max_frames_per_block=1000 P.model.new_data() P.model.new_data() P.model.new_data() -u.verbose.set_level(3) +u.verbose.set_level("info") if u.parallel.master: P.print_stats() diff --git a/benchmark/mpi_allreduce_bench.sh b/benchmark/mpi_allreduce_bench.sh new file mode 100755 index 000000000..b4f214f6f --- /dev/null +++ b/benchmark/mpi_allreduce_bench.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Runs MPI all reduce benchmark with variable number of processes + +echo "Processes,i08,i13,i14_1,i14_2" +for p in {2..16} +do + echo -n $p + mpirun -np $p python mpi_allreduce_speed.py | \ + awk -F, '$0 ~ /^i[0-9]/ {printf(",%s", $2)} END {print ""}' +done \ No newline at end of file diff --git a/benchmark/mpi_allreduce_bench_launcher.sh b/benchmark/mpi_allreduce_bench_launcher.sh new file mode 100755 index 000000000..9bc2ff0db --- /dev/null +++ b/benchmark/mpi_allreduce_bench_launcher.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +for p in {2..32} +do + qsub -pe openmpi ${p}0 -l exclusive,gpu=4,gpu_arch=pascal \ + -P ptychography -o mpi.${p}.out -j y mpi_allreduce_bench_multinode.sh ${p} ; +done + diff --git a/benchmark/mpi_allreduce_bench_multinode.sh b/benchmark/mpi_allreduce_bench_multinode.sh new file mode 100755 index 000000000..7f1173372 --- /dev/null +++ b/benchmark/mpi_allreduce_bench_multinode.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +workdir=~/work/ptypy +export PYTHONPATH=$workdir +cd $workdir/benchmark + +numproc=$1 + +HOSTLIST=$(cat ${PE_HOSTFILE} | awk '{print $1}' | tr "\n" ",") # comma separated list of hosts +# echo "THE HOSTLIST IS $HOSTLIST" +# NUMCORES=$(cat ${PE_HOSTFILE} | awk 'NR==1{print $2}') # to be overridden soon, but is just the number of cores per host +NUMCORES=4 +# echo "THE number of cores per node is $NUMCORES" +HOST_LIST_WITH_CORES=${HOSTLIST//,/:$NUMCORES,} # puts in the number of cores where the comma would be +HOST_LIST_WITH_CORES=${HOST_LIST_WITH_CORES%?} # gets rid of the trailing comma +# echo "THE HOST LIST WITH CORES IS $HOST_LIST_WITH_CORES" + +mpirun -np $numproc --host ${HOST_LIST_WITH_CORES} python mpi_allreduce_speed.py \ No newline at end of file diff --git a/benchmark/mpi_allreduce_speed.py b/benchmark/mpi_allreduce_speed.py new file mode 100644 index 000000000..5102e35af --- /dev/null +++ b/benchmark/mpi_allreduce_speed.py @@ -0,0 +1,42 @@ +import numpy as np +from ptypy.utils import parallel +from mpi4py import MPI +import time + +sizes ={ + 'i08': (1, 960, 960), + 'i13': (1, 13408, 13408), + 'i14_1': (1, 8160, 8160), + 'i14_2': (1, 3360, 3360), +} + +def run_benchmark(shape): + megabytes = np.product(shape) * 8 / 1024 / 1024 * 2 + + data = np.zeros(shape, dtype=np.complex64) + + # average 5 runs + duration = 0 + for n in range(5): + t1 = time.perf_counter() + parallel.allreduce(data) # 2 calls to simulate ptypy obb / obn reduce + parallel.allreduce(data) + t2 = time.perf_counter() + duration += t2-t1 + duration /= 5 + + total = parallel.allreduce(duration, MPI.MAX) + + return megabytes, duration + +res = [] + +for name,sz in sizes.items(): + mb, dur = run_benchmark(sz) + res.append([name, dur, mb, mb/dur]) + +if parallel.rank == 0: + print('Final results for {} processes'.format(parallel.size)) + print(','.join(['Name', 'Duration', 'MB', 'MB/s'])) + for r in res: + print(','.join([str(x) for x in r])) \ No newline at end of file diff --git a/benchmark/tiled_vs_atomic.py b/benchmark/tiled_vs_atomic.py new file mode 100644 index 000000000..883f4de3d --- /dev/null +++ b/benchmark/tiled_vs_atomic.py @@ -0,0 +1,113 @@ + +import numpy as np +import pycuda.driver as cuda +from pycuda import gpuarray +from pycuda.tools import make_default_context +from ptypy.accelerate.cuda_pycuda.kernels import PoUpdateKernel as POK +import gc + +cuda.init() + + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +def prepare_arrays( + overlap=0.2, + scan_pts=20, + frame_size =128, + atomics=True, + num_pr_modes=2, + num_ob_modes=1): + + fsh = (frame_size,frame_size) + shift=int(frame_size*overlap) + X, Y = np.indices((scan_pts,scan_pts)) * shift + X = X.flatten() + Y = Y.flatten() + num_pts=len(X) + X+=5 + Y+=5 + osh=(X.max()+5+fsh[0],Y.max()+5+fsh[1]) # ob shape + #print(fsh, osh, X.min(), X.max()+fsh[0], Y.min(),Y.max()+fsh[1]) + num_modes = num_ob_modes * num_pr_modes + A = num_pts * num_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(num_pr_modes,fsh[0],fsh[1]), dtype=COMPLEX_TYPE) + for idx in range(num_pr_modes): + probe[idx] = np.ones(fsh) * (idx + 1) + 1j * np.ones(fsh) * (idx + 1) + + object_array = np.empty(shape=(num_ob_modes,osh[0],osh[1]), dtype=COMPLEX_TYPE) + for idx in range(num_ob_modes): + object_array[idx] = np.ones(osh) * (3 * idx + 1) + 1j * np.ones(osh) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A,fsh[0],fsh[1]), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones(fsh) * (idx + 1) + 1j * np.ones(fsh) * (idx + 1) + + addr = np.zeros((num_pts, num_modes, 5, 3), dtype=INT_TYPE) + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(num_pr_modes): + for ob_mode in range(num_ob_modes): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + if not atomics: + addr = np.ascontiguousarray(np.transpose(addr, (2, 3, 0, 1))) + + #print(addr) + object_array_denominator = np.empty_like(object_array, dtype=FLOAT_TYPE) + for idx in range(num_ob_modes): + object_array_denominator[idx] = np.ones(osh) * (5 * idx + 2) # + 1j * np.ones(osh) * (5 * idx + 2) + + probe_denominator = np.empty_like(probe, dtype=FLOAT_TYPE) + for idx in range(num_pr_modes): + probe_denominator[idx] = np.ones(fsh) * (5 * idx + 2) # + 1j * np.ones(fsh) * (5 * idx + 2) + + return (gpuarray.to_gpu(addr), + gpuarray.to_gpu(object_array), + gpuarray.to_gpu(object_array_denominator), + gpuarray.to_gpu(probe), + gpuarray.to_gpu(exit_wave), + gpuarray.to_gpu(probe_denominator)) + +#for overlap in [0.2]: +# for +ctx = make_default_context() +stream = cuda.Stream() +pok = POK(stream) + +overlap=0.2 +scan_pts=20 +for frame_size in [64,128,256,512]: + for atomics in [True, False]: + for overlap in [0.01,0.01,0.02,0.04,0.08]: + + addr, ob, obn, pr, ex, prn = prepare_arrays(overlap, scan_pts, frame_size, atomics) + stream.synchronize() + start = cuda.Event() + stop = cuda.Event() + start.record(stream) + for p in range(1): + pok.ob_update(addr, ob, obn, pr, ex, atomics) + stop.record(stream) + stop.synchronize() + dt = stop.time_since(start) + stream.synchronize() + print('10x for {}: {}ms'.format((overlap,frame_size,scan_pts, atomics), dt)) + +del stream +ctx.pop() +ctx.detach() +del addr, ob, obn, pr, ex, prn +gc.collect() \ No newline at end of file diff --git a/cufft/dependencies.yml b/cufft/dependencies.yml new file mode 100644 index 000000000..949079d36 --- /dev/null +++ b/cufft/dependencies.yml @@ -0,0 +1,10 @@ +name: ptypy_cufft +channels: + - conda-forge +dependencies: + - python=3.9 + - cmake>=3.8.0 + - pybind11 + - compilers + - cudatoolkit-dev + - pip \ No newline at end of file diff --git a/cufft/extensions.py b/cufft/extensions.py new file mode 100644 index 000000000..4fabf2d2c --- /dev/null +++ b/cufft/extensions.py @@ -0,0 +1,151 @@ +''' +Compilation tools for Nvidia builds of extension modules. +''' +import os, re +import subprocess +import sysconfig +import pybind11 +from distutils.unixccompiler import UnixCCompiler +from distutils.command.build_ext import build_ext + + +def find_in_path(name, path): + "Find a file in a search path" + # adapted fom http://code.activestate.com/recipes/52224-find-a-file-given-a-search-path/ + for dir in path.split(os.pathsep): + binpath = os.path.join(dir, name) + if os.path.exists(binpath): + return os.path.abspath(binpath) + return None + +def locate_cuda(): + """ + Locate the CUDA environment on the system + Returns a dict with keys 'home', 'nvcc', 'include', and 'lib64' + and values giving the absolute path to each directory. + Starts by looking for the CUDAHOME env variable. If not found, everything + is based on finding 'nvcc' in the PATH. + """ + # first check if the CUDAHOME env variable is in use + if 'CUDAHOME' in os.environ: + home = os.environ['CUDAHOME'] + nvcc = os.path.join(home, 'bin', 'nvcc') + else: + # otherwise, search the PATH for NVCC + nvcc = find_in_path('nvcc', os.environ['PATH']) + if nvcc is None: + raise EnvironmentError('The nvcc binary could not be ' + 'located in your $PATH. Either add it to your path, or set $CUDAHOME') + home = os.path.dirname(os.path.dirname(nvcc)) + + cudaconfig = {'home': home, 'nvcc': nvcc, + 'include': os.path.join(home, 'include'), + 'lib64': os.path.join(home, 'lib64')} + for k, v in cudaconfig.items(): + if not os.path.exists(v): + raise EnvironmentError('The CUDA %s path could not be located in %s' % (k, v)) + return cudaconfig + +def get_cuda_version(nvcc): + """ + Get the CUDA version py running `nvcc --version`. + """ + stdout = subprocess.check_output([nvcc,"--version"]).decode("utf-8") + if bool(stdout.rstrip()): + regex = r'release (\S+),' + match = re.search(regex, stdout) + if match: + return float(match.group(1)) + raise LookupError('Unable to parse nvcc version output from {}'.format(stdout)) + else: + return None + +def get_cuda_arch_flags(version): + if version in (10.0, 10.1, 10.2): + archflag = ' -gencode=arch=compute_60,code=sm_60' + \ + ' -gencode=arch=compute_61,code=sm_61' + \ + ' -gencode=arch=compute_70,code=sm_70' + \ + ' -gencode=arch=compute_75,code=sm_75' + \ + ' -gencode=arch=compute_75,code=compute_75' + elif version == 11.0: + archflag = ' -gencode=arch=compute_60,code=sm_60' + \ + ' -gencode=arch=compute_61,code=sm_61' + \ + ' -gencode=arch=compute_70,code=sm_70' + \ + ' -gencode=arch=compute_75,code=sm_75' + \ + ' -gencode=arch=compute_80,code=sm_80' + \ + ' -gencode=arch=compute_80,code=compute_80' + elif version >= 11.1: + archflag = ' -gencode=arch=compute_60,code=sm_60' + \ + ' -gencode=arch=compute_61,code=sm_61' + \ + ' -gencode=arch=compute_70,code=sm_70' + \ + ' -gencode=arch=compute_75,code=sm_75' + \ + ' -gencode=arch=compute_80,code=sm_80' + \ + ' -gencode=arch=compute_86,code=sm_86' + \ + ' -gencode=arch=compute_86,code=compute_86' + else: + raise ValueError("CUDA version %s not supported" %str(version)) + return archflag + +class NvccCompiler(UnixCCompiler): + def __init__(self, *args, **kwargs): + super(NvccCompiler, self).__init__(*args, **kwargs) + self.CUDA = locate_cuda() + self.CUDA_VERSION = get_cuda_version(self.CUDA["nvcc"]) + module_dir = os.path.join(__file__.strip('import_fft.py'), 'cuda', 'filtered_fft') + # by default, compile for all of these + archflag = get_cuda_arch_flags(self.CUDA_VERSION) + self.src_extensions.append('.cu') + self.LD_FLAGS = [archflag, "-lcufft_static", "-lculibos", "-ldl", "-lrt", "-lpthread", "-cudart shared"] + self.NVCC_FLAGS = ["-dc", archflag] + self.CXXFLAGS = ['"-fPIC"'] + pybind_includes = [pybind11.get_include(), sysconfig.get_path('include')] + INCLUDES = pybind_includes + [self.CUDA['lib64'], module_dir] + self.INCLUDES = ["-I%s" % ix for ix in INCLUDES] + self.OPTFLAGS = ["-O3", "-std=c++14"] + + def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): + default_compiler_so = self.compiler_so + CPPFLAGS = self.INCLUDES + extra_postargs # little hack here, since postargs usually goes at the end, which we won't do. + # makefile line is + # $(NVCC) $(NVCC_FLAGS) $(OPTFLAGS) -Xcompiler "$(CXXFLAGS)" $(CPPFLAGS) + compiler_command = [self.CUDA["nvcc"]] + self.NVCC_FLAGS + self.OPTFLAGS + ["-Xcompiler"] + self.CXXFLAGS + CPPFLAGS + compiler_exec = " ".join(compiler_command) + self.set_executable('compiler_so', compiler_exec) + postargs = [] # we don't actually have any postargs + super(NvccCompiler, self)._compile(obj, src, ext, cc_args, postargs, pp_opts) # the _compile method + # reset the default compiler_so, which we might have changed for cuda + self.compiler_so = default_compiler_so + + def link(self, target_desc, objects, + output_filename, output_dir=None, libraries=None, + library_dirs=None, runtime_library_dirs=None, + export_symbols=None, debug=0, extra_preargs=None, + extra_postargs=None, build_temp=None, target_lang=None): + default_linker_so = self.linker_so + # make file line is + # $(NVCC) $(OPTFLAGS) -shared $(LD_FLAGS) $(OBJ) $(OBJ_MOD) -o $@ + linker_command = [self.CUDA["nvcc"]] + self.OPTFLAGS + ["-shared"] + self.LD_FLAGS + linker_exec = " ".join(linker_command) + self.set_executable('linker_so', linker_exec) + super(NvccCompiler, self).link(target_desc, objects, + output_filename, output_dir=None, libraries=None, + library_dirs=None, runtime_library_dirs=None, + export_symbols=None, debug=0, extra_preargs=None, + extra_postargs=None, build_temp=None, target_lang=None) + self.linker_so = default_linker_so + +class CustomBuildExt(build_ext): + + def build_extension(self, ext): + has_cu = any([src.endswith('.cu') for src in ext.sources]) + if has_cu: + old_compiler = self.compiler + self.compiler = NvccCompiler(verbose=old_compiler.verbose, + dry_run=old_compiler.dry_run, + force=old_compiler.force) # this is our bespoke compiler + super(CustomBuildExt, self).build_extension(ext) + self.compiler=old_compiler + else: + super(CustomBuildExt, self).build_extension(ext) + + diff --git a/cufft/filtered_fft/.gitignore b/cufft/filtered_fft/.gitignore new file mode 100644 index 000000000..f54b22f60 --- /dev/null +++ b/cufft/filtered_fft/.gitignore @@ -0,0 +1,6 @@ +*.o +smoke_test +*.so +.module* +.rendered* +module_*_* \ No newline at end of file diff --git a/cufft/filtered_fft/Makefile b/cufft/filtered_fft/Makefile new file mode 100644 index 000000000..a8aa06866 --- /dev/null +++ b/cufft/filtered_fft/Makefile @@ -0,0 +1,29 @@ +NVCC = nvcc +NVCC_FLAGS += -dc -arch=sm_70 +CUDADIR = $(dir $(shell which nvcc) )/.. + +INCLUDES = $(shell python -m pybind11 --includes) -I$(CUDADIR)/include +PYMODEXT = $(shell python-config --extension-suffix) +CPPFLAGS += $(INCLUDES) -DMY_FFT_ROWS=128 -DMY_FFT_COLS=128 +OPTFLAGS = -O3 -std=c++14 +CXXFLAGS += -fPIC +LD_FLAGS += -L$(CUDADIR)/lib64 -lcufft_static -lculibos -ldl -lrt -lpthread -cudart shared +OBJ = filtered_fft.o +OBJ_MOD = module.o +MODULE = module$(PYMODEXT) + +all: $(MODULE) $(EXE) + +python: $(MODULE) + +clean: + rm -rf $(OBJ) $(EXE) $(MODULE) $(OBJ_EXE) $(OBJ_MOD) + +%.o: %.cu + $(NVCC) $(NVCC_FLAGS) $(OPTFLAGS) -Xcompiler "$(CXXFLAGS)" $(CPPFLAGS) -c $< -o $@ + +%.o: %.cpp + $(NVCC) $(NVCC_FLAGS) -x cu $(OPTFLAGS) -Xcompiler "$(CXXFLAGS)" $(CPPFLAGS) -c $< -o $@ + +$(MODULE): $(OBJ) $(OBJ_MOD) + $(NVCC) $(OPTFLAGS) -shared $(LD_FLAGS) $(OBJ) $(OBJ_MOD) -o $@ diff --git a/cufft/filtered_fft/compiler_flags_info.txt b/cufft/filtered_fft/compiler_flags_info.txt new file mode 100644 index 000000000..124e8bd62 --- /dev/null +++ b/cufft/filtered_fft/compiler_flags_info.txt @@ -0,0 +1,6 @@ +nvcc -dc -arch=sm_60 -gencode=arch=compute_30,code=sm_30 -gencode=arch=compute_50,code=sm_50 -gencode=arch=compute_52,code=sm_52 -gencode=arch=compute_60,code=sm_60 -gencode=arch=compute_61,code=sm_61 -gencode=arch=compute_70,code=sm_70 -gencode=arch=compute_70,code=compute_70 -x cu -O3 -std=c++14 -Xcompiler "-fPIC" -DMY_FFT_ROWS=128 -DMY_FFT_COLS=128 -I/dls_sw/apps/ptypy/accelerate_cppimport_pybind/miniconda/envs/full_dependencies/include/python3.7m -I/dls_sw/apps/ptypy/accelerate_cppimport_pybind/miniconda/envs/full_dependencies/include -I/dls_sw/apps/cuda/10.1/bin//../include -c filtered_fft.cpp -o filtered_fft.o +nvcc -dc -arch=sm_60 -gencode=arch=compute_30,code=sm_30 -gencode=arch=compute_50,code=sm_50 -gencode=arch=compute_52,code=sm_52 -gencode=arch=compute_60,code=sm_60 -gencode=arch=compute_61,code=sm_61 -gencode=arch=compute_70,code=sm_70 -gencode=arch=compute_70,code=compute_70 -x cu -O3 -std=c++14 -Xcompiler "-fPIC" -DMY_FFT_ROWS=128 -DMY_FFT_COLS=128 -I/dls_sw/apps/ptypy/accelerate_cppimport_pybind/miniconda/envs/full_dependencies/include/python3.7m -I/dls_sw/apps/ptypy/accelerate_cppimport_pybind/miniconda/envs/full_dependencies/include -I/dls_sw/apps/cuda/10.1/bin//../include -c module.cpp -o module.o +nvcc -O3 -std=c++14 -shared -L/dls_sw/apps/cuda/10.1/bin//../lib64 -lcufft_static -lculibos -cudart shared -ldl -lrt -lpthread filtered_fft.o module.o -o filtered_fft.so +nvcc -dc -arch=sm_60 -gencode=arch=compute_30,code=sm_30 -gencode=arch=compute_50,code=sm_50 -gencode=arch=compute_52,code=sm_52 -gencode=arch=compute_60,code=sm_60 -gencode=arch=compute_61,code=sm_61 -gencode=arch=compute_70,code=sm_70 -gencode=arch=compute_70,code=compute_70 -x cu -O3 -std=c++14 -Xcompiler "-fPIC" -DMY_FFT_ROWS=128 -DMY_FFT_COLS=128 -I/dls_sw/apps/ptypy/accelerate_cppimport_pybind/miniconda/envs/full_dependencies/include/python3.7m -I/dls_sw/apps/ptypy/accelerate_cppimport_pybind/miniconda/envs/full_dependencies/include -I/dls_sw/apps/cuda/10.1/bin//../include -c smoke_test.cpp -o smoke_test.o +nvcc -O3 -std=c++14 -o smoke_test -L/dls_sw/apps/cuda/10.1/bin//../lib64 -lcufft_static -lculibos -cudart shared -ldl -lrt -lpthread filtered_fft.o smoke_test.o + diff --git a/cufft/filtered_fft/errors.h b/cufft/filtered_fft/errors.h new file mode 100644 index 000000000..f14781c1f --- /dev/null +++ b/cufft/filtered_fft/errors.h @@ -0,0 +1,86 @@ +#pragma once +#include +#include +#include +#include + + +inline std::string getloc(const char* func, const char* file, int line) +{ + return std::string(file) + ":" + std::to_string(line) + ": in function " + + func; +} + +inline void handleCudaCheck(cudaError_t res, const char* func, const char *file, int line) +{ + if (res != 0) { + throw std::runtime_error(std::string("CUDA Error: ") + + getloc(func, file, line) + ": " + cudaGetErrorString(res)); + } +} + +inline void handleCudaCheck(cufftResult res, const char* func, const char* file, int line) { + if (res) { + const char* errstr; + switch (res) { + case CUFFT_INVALID_PLAN: + errstr = "cuFFT was passed an invalid plan handle"; + break; + case CUFFT_ALLOC_FAILED: + errstr = "cuFFT failed to allocate GPU or CPU memory"; + break; + case CUFFT_INVALID_TYPE: + errstr = "No longer used"; + break; + case CUFFT_INVALID_VALUE: + errstr = "User specified an invalid pointer or parameter"; + break; + case CUFFT_INTERNAL_ERROR: + errstr = "Driver or internal cuFFT library error"; + break; + case CUFFT_EXEC_FAILED: + errstr = "Failed to execute an FFT on the GPU"; + break; + case CUFFT_SETUP_FAILED: + errstr = "The cuFFT library failed to initialize"; + break; + case CUFFT_INVALID_SIZE: + errstr = "User specified an invalid transform size"; + break; + case CUFFT_UNALIGNED_DATA: + errstr = " No longer used"; + break; + case CUFFT_INCOMPLETE_PARAMETER_LIST: + errstr = " Missing parameters in call "; + break; + case CUFFT_INVALID_DEVICE: + errstr = "Execution of a plan was on different GPU than plan creation"; + break; + case CUFFT_PARSE_ERROR: + errstr = "Internal plan database error"; + break; + case CUFFT_NO_WORKSPACE: + errstr = "No workspace has been provided prior to plan execution"; + break; + case CUFFT_NOT_IMPLEMENTED: + errstr = + "Function does not implement functionality for parameters given."; + break; + case CUFFT_LICENSE_ERROR: + errstr = "Used in previous versions."; + break; + case CUFFT_NOT_SUPPORTED: + errstr = "Operation is not supported for parameters given."; + break; + default: + errstr = "Unknown"; + } + + throw std::runtime_error(std::string("CuFFT Error: ") + + getloc(func, file, line) + + ": " + errstr); + } +} + +#define cudaCheck(e) \ + handleCudaCheck(e, __FUNCTION__, __FILE__, __LINE__) diff --git a/cufft/filtered_fft/filtered_fft.cu b/cufft/filtered_fft/filtered_fft.cu new file mode 100644 index 000000000..586d7f356 --- /dev/null +++ b/cufft/filtered_fft/filtered_fft.cu @@ -0,0 +1,322 @@ +/** Core implementation of the filtered FFT using cuFFT + callbacks. + * Note, this is a .cu file actually. The .cpp is used to make it work with cppimport + * + * The FilteredFFTImpl class is implemented as a template, + * to allow a specific implementation with compile-time constants + * as the offset modulo operations can by optimised a lot if the + * compiler knows the modulo operands. + * + * Therefore this code assumes that the following pre-processor + * definitions are defined during compilation - otherwise it will + * generate an FFT with 128x128 arrays: + * + * - MY_FFT_ROWS + * - MY_FFT_COLUMNS + * + * Also note that this implementation only works on Linux 64bit, + * as this is a limitation of cuFFT. + * On other platforms, an implementation with separately implemented + * filters should be used, e.g. with pycuda and scikit-cuda. + * + */ + +#include "errors.h" +#include "filtered_fft.h" + +#include +#include +#include +#include +#include + +template +class FilteredFFTImpl : public FilteredFFT { +public: + + /** Sets up the plan on init. + * + * @param batches Number of batches + * @param prefilt Device pointer to prefilter array (can be NULL) + * @param postfilt Device pointer to postfilter array (can be NULL) + * @param stream Stream to use for the GPU + */ + FilteredFFTImpl(int batches, + complex* prefilt, complex* postfilt, + cudaStream_t stream) : + batches_(batches), + prefilt_(prefilt), + postfilt_(postfilt), + stream_(stream) + { + setupPlan(); + } + + int getBatches() const override { return batches_; } + int getRows() const override { return ROWS; } + int getColumns() const override { return COLUMNS; } + bool isForward() const override { return IS_FORWARD; } + void setStream(cudaStream_t stream) override { + stream_ = stream; + cudaCheck(cufftSetStream(plan_, stream)); + }; + virtual cudaStream_t getStream() const { + return stream_; + } + + /// Run the FFT (forward) - can be in-place + void fft(complex* input, complex* output) override + { + if (SYMMETRIC && !IS_FORWARD) { + throw std::runtime_error("Calling FFT on a reverse-initialised instance"); + } else { + cudaCheck(cufftExecC2C(plan_, + reinterpret_cast(input), + reinterpret_cast(output), + CUFFT_FORWARD + )); + } + } + + /// Run the IFFT (reverse) - can be in-place + void ifft(complex* input, complex* output) override + { + if (SYMMETRIC && IS_FORWARD) { + throw std::runtime_error("Calling IFFT on a forward-initialised instance"); + } else { + cudaCheck(cufftExecC2C(plan_, + reinterpret_cast(input), + reinterpret_cast(output), + CUFFT_INVERSE + )); + } + } + + ~FilteredFFTImpl() + { + cufftDestroy(plan_); + } + + + ///////// Different variants of the callback device functions ///// + + /// load with prefilter + __device__ static cufftComplex CB_prefilt( + void *dataIn, + size_t offset, + void* callerInf, + void* sharedPtr + ) { + auto inData = reinterpret_cast*>(dataIn); + auto filter = reinterpret_cast*>(callerInf); + auto v = inData[offset]; + // Note: + // Modulo with powers of 2 are replaced by bit operations by the compiler, + // which are much faster. + // If non-powers of 2 are needed, it might be possible to work out the + // per-array filter offset from the threadIdx / blockidx fields. + v *= filter[offset % (ROWS*COLUMNS)]; + return {v.real(), v.imag()}; + } + + /// store with postfilter + scaling + __device__ static void CB_postfilt( + void* dataOut, + size_t offset, + cufftComplex element, + void* callerInf, + void* sharedPtr + ) { + auto outData = reinterpret_cast*>(dataOut); + auto filter = reinterpret_cast*>(callerInf); + auto v = complex(element.x, element.y); + if (!SYMMETRIC && !IS_FORWARD) { + v *= filter[offset % (ROWS*COLUMNS)] / (ROWS*COLUMNS); + } + else if (IS_FORWARD && !SYMMETRIC) { + v *= filter[offset % (ROWS*COLUMNS)]; + } + else { + float fact; + if (ROWS == COLUMNS) { + fact = ROWS; + } else { + fact = sqrt(float(ROWS*COLUMNS)); + } + v *= filter[offset % (ROWS*COLUMNS)] / fact; + } + outData[offset] = v; + } + + /// store with scaling only (no postfilter) + __device__ static void CB_postfilt_scaleonly( + void* dataOut, + size_t offset, + cufftComplex element, + void* callerInf, + void* sharedPtr + ) { + auto outData = reinterpret_cast*>(dataOut); + auto v = complex(element.x, element.y); + if (!SYMMETRIC && !IS_FORWARD) { + v /= ROWS * COLUMNS; + } else { + float fact; + if (ROWS == COLUMNS) { + fact = ROWS; + } else { + fact = sqrt(float(ROWS*COLUMNS)); + } + v /= fact; + } + outData[offset] = v; + } + + +private: + /// the core of the plan setup + void setupPlan(); + + int batches_; ///< number of batchs + cufftHandle plan_; ///< cuFFT plan handle + complex* prefilt_; ///< prefilter pointer + complex* postfilt_; ///< postfilter pointer + cudaStream_t stream_; ///< stream to operate on +}; + + +/// Device-globals to keep function pointers +/// These need to be set on the device, copied to host, +/// and then passed to the cuFFT plan. + +__device__ cufftCallbackLoadC d_loadCallbackPtr; +__device__ cufftCallbackStoreC d_storeCallbackPtr; + +/// small kernel to set the load callback device function pointer +template +__global__ void setLoadDevFunPtr() +{ + d_loadCallbackPtr = FilteredFFTImpl::CB_prefilt; +} + +/// small kernel to set the store callback device function pointer +template +__global__ void setStoreDevFunPtr() +{ + d_storeCallbackPtr = FilteredFFTImpl::CB_postfilt; +} + +/// small kernel to set the store callback device function pointer for scale only +template +__global__ void setStoreScaleDevFunPtr() +{ + d_storeCallbackPtr = FilteredFFTImpl::CB_postfilt_scaleonly; +} + + +/// setup the plan +template +void FilteredFFTImpl::setupPlan() { + // basic plan setup + cudaCheck(cufftCreate(&plan_)); + int dims[] = {ROWS, COLUMNS}; + size_t workSize; + cudaCheck(cufftMakePlanMany( + plan_, 2, dims, 0, 0, 0, 0, 0, 0, CUFFT_C2C, batches_, &workSize + )); + cudaCheck(cufftSetStream(plan_, stream_)); + + /* + std::cout << "Created plan for " << ROWS << "x" << COLUMNS + << ", for " << batches_ << " with scratch memory of " + << double(workSize) / 1024.0 / 1024.0 << "MB" + << std::endl; + */ + + // pre-filter + if (prefilt_) // no need to set load callback if we're not prefiltering + { + setLoadDevFunPtr<<<1,1>>>(); + cufftCallbackLoadC h_loadCallbackPtr; + cudaCheck(cudaMemcpyFromSymbol(&h_loadCallbackPtr, d_loadCallbackPtr, sizeof(h_loadCallbackPtr))); + cudaCheck(cufftXtSetCallback(plan_, + (void**)&h_loadCallbackPtr, + CUFFT_CB_LD_COMPLEX, + (void**)&prefilt_)); + } + + // post-filter + if (!(IS_FORWARD && !SYMMETRIC) || postfilt_) // we scale in postCall, so also needed if not postfiltering + { + cufftCallbackStoreC h_storeCallbackPtr; + if (postfilt_) { + setStoreDevFunPtr<<<1,1>>>(); + } + else { + setStoreScaleDevFunPtr<<<1,1>>>(); + } + cudaCheck(cudaMemcpyFromSymbol(&h_storeCallbackPtr, d_storeCallbackPtr, sizeof(h_storeCallbackPtr))); + cudaCheck(cufftXtSetCallback(plan_, + (void**)&h_storeCallbackPtr, + CUFFT_CB_ST_COMPLEX, + (void**)&postfilt_)); + } +} + +template +static FilteredFFT* make(int batches, int rows, int cols, complex* prefilt, complex* postfilt, + cudaStream_t stream) +{ + // we only support rows / colums are equal and powers of 2, from 16x16 to 512x512 + if (rows != cols) + throw std::runtime_error("Only equal numbers of rows and columns are supported"); + switch (rows) + { + case 16: return new FilteredFFTImpl<16, 16, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + case 32: return new FilteredFFTImpl<32, 32, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + case 64: return new FilteredFFTImpl<64, 64, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + case 128: return new FilteredFFTImpl<128, 128, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + case 256: return new FilteredFFTImpl<256, 256, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + case 512: return new FilteredFFTImpl<512, 512, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + case 1024: return new FilteredFFTImpl<1024, 1024, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + case 2048: return new FilteredFFTImpl<2048, 2048, SYMMETRIC, FORWARD>(batches, prefilt, postfilt, stream); + default: throw std::runtime_error("Only powers of 2 from 16 to 2048 are supported"); + } +} + +//////////// Factory Functions for Python + +// Note: This will instantiate templates for 8 powers of 2, with 4 combinations of forward/reverse, symmetric/not, +// i.e. 32 different FFTs into the binary. Compile time might be quite long, but we intend to do this once +// during installation + +FilteredFFT* make_filtered( + int batches, + int rows, int cols, + bool symmetricScaling, + bool isForward, + complex* prefilt, complex* postfilt, + cudaStream_t stream) +{ + if (symmetricScaling) + { + if (isForward) { + return make(batches, rows, cols, prefilt, postfilt, stream); + } else { + return make(batches, rows, cols, prefilt, postfilt, stream); + } + } + else + { + if (isForward) { + return make(batches, rows, cols, prefilt, postfilt, stream); + } else { + return make(batches, rows, cols, prefilt, postfilt, stream); + } + } + +} + +void destroy_filtered(FilteredFFT* fft) +{ + delete fft; +} \ No newline at end of file diff --git a/cufft/filtered_fft/filtered_fft.h b/cufft/filtered_fft/filtered_fft.h new file mode 100644 index 000000000..9afa4e119 --- /dev/null +++ b/cufft/filtered_fft/filtered_fft.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +using thrust::complex; + +class FilteredFFT { +public: + virtual void fft(complex* input, complex* output) = 0; + virtual void ifft(complex* input, complex* output) = 0; + virtual int getBatches() const = 0; + virtual int getRows() const = 0; + virtual int getColumns() const = 0; + virtual bool isForward() const = 0; + virtual void setStream(cudaStream_t stream) = 0; + virtual cudaStream_t getStream() const = 0; + virtual ~FilteredFFT() {} +}; + +// we fix the rows/columns at compile-time, so not passing them +// to the factory here +// Note that cudaStream_t (runtime API) and CUStream (driver API) are +// the same type +FilteredFFT* make_filtered(int batches, + int rows, int columns, + bool symmetricScaling, + bool isForward, + complex* prefilt, complex* postfilt, + cudaStream_t stream); + +void destroy_filtered(FilteredFFT* fft); + + diff --git a/cufft/filtered_fft/module.cpp b/cufft/filtered_fft/module.cpp new file mode 100644 index 000000000..3eb0eb37e --- /dev/null +++ b/cufft/filtered_fft/module.cpp @@ -0,0 +1,102 @@ +#include +#include "filtered_fft.h" + + +/** Wrapper class to expose to Python, taking size_t instead of all + * the pointers, which should contain the addresses of the data. + * For gpuarrays, the gpuarray.gpudata member can be cast to int + * in Python and it will be the raw pointer address. + * cudaStreams can be cast to int as well. + * (this saves us a lot of type definition / conversion code with pybind11) + * + */ +class FilteredFFTPython +{ +public: + FilteredFFTPython(int batches, int rows, int columns, bool symmetric, + bool is_forward, + std::size_t prefilt_ptr, + std::size_t postfilt_ptr, + std::size_t stream) + { + fft_ = make_filtered( + batches, + rows, columns, + symmetric, + is_forward, + reinterpret_cast*>(prefilt_ptr), + reinterpret_cast*>(postfilt_ptr), + reinterpret_cast(stream) + ); + } + + int getBatches() const { return fft_->getBatches(); } + int getRows() const { return fft_->getRows(); } + int getColumns() const { return fft_->getColumns(); } + bool isForward() const { return fft_->isForward(); } + void setStream(std::size_t stream) { + fft_->setStream(reinterpret_cast(stream)); + } + std::size_t getStream() const { + return reinterpret_cast(fft_->getStream()); + } + + void fft(std::size_t in_ptr, std::size_t out_ptr) + { + fft_->fft( + reinterpret_cast*>(in_ptr), + reinterpret_cast*>(out_ptr) + ); + } + + void ifft(std::size_t in_ptr, std::size_t out_ptr) + { + fft_->ifft( + reinterpret_cast*>(in_ptr), + reinterpret_cast*>(out_ptr) + ); + } + + ~FilteredFFTPython() { + delete fft_; + } + +private: + FilteredFFT* fft_; +}; + + +/////////////// Pybind11 Export Definition /////////////// + +namespace py = pybind11; + + +PYBIND11_MODULE(filtered_cufft, m) { + m.doc() = "Filtered FFT for PtyPy"; + + py::class_(m, "FilteredFFT", py::module_local()) + .def(py::init(), + py::arg("batches"), + py::arg("rows"), + py::arg("columns"), + py::arg("symmetricScaling"), + py::arg("is_forward"), + py::arg("prefilt"), + py::arg("postfilt"), + py::arg("stream") + ) + .def("fft", &FilteredFFTPython::fft, + py::arg("input_ptr"), + py::arg("output_ptr") + ) + .def("ifft", &FilteredFFTPython::ifft, + py::arg("input_ptr"), + py::arg("output_ptr") + ) + .def_property_readonly("batches", &FilteredFFTPython::getBatches) + .def_property_readonly("rows", &FilteredFFTPython::getRows) + .def_property_readonly("columns", &FilteredFFTPython::getColumns) + .def_property_readonly("is_forward", &FilteredFFTPython::isForward) + .def_property("queue", &FilteredFFTPython::getStream, &FilteredFFTPython::setStream); +} + diff --git a/cufft/filtered_fft/smoke_test.cpp b/cufft/filtered_fft/smoke_test.cpp new file mode 100644 index 000000000..c1980a565 --- /dev/null +++ b/cufft/filtered_fft/smoke_test.cpp @@ -0,0 +1,65 @@ +#include "errors.h" +#include "filtered_fft.h" +#include +#include +#include + +#ifndef MY_FFT_ROWS +# define MY_FFT_ROWS 128 +# pragma GCC warning "MY_FFT_ROWS not set in preprocessor - defaulting to 128" +#endif + +#ifndef MY_FFT_COLS +# define MY_FFT_COLS 128 +# pragma GCC warning "MY_FFT_COLS not set in preprocessor - defaulting to 128" +#endif + + +///////////////// a quick smoke test +int main() { + cudaStream_t stream; + cudaCheck(cudaStreamCreate(&stream)); + + int batches = 2000; + int rows = MY_FFT_ROWS; + int cols = MY_FFT_COLS; + complex *pre, *post, *f; + cudaCheck(cudaMalloc((void**)&pre, rows*cols*sizeof(complex))); + cudaCheck(cudaMalloc((void**)&post, rows*cols*sizeof(complex))); + cudaCheck(cudaMalloc((void**)&f, batches*rows*cols*sizeof(complex))); + + auto fft = make_filtered(batches, true, true, pre, post, stream); + + if (rows != fft->getRows() || cols != fft->getColumns()) + throw std::runtime_error("Mismatch in rows/cols between smoke test and module"); + + cudaCheck(cudaDeviceSynchronize()); + auto start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < 100; ++i) + fft->fft(f, f); + cudaCheck(cudaDeviceSynchronize()); + auto end = std::chrono::high_resolution_clock::now(); + std::cout << "FFT " << batches << "x" << rows << "x" << cols << + ": " << std::chrono::duration_cast(end-start).count() << "ms\n"; + + auto ifft = make_filtered(batches, true, false, pre, post, stream); + + cudaCheck(cudaDeviceSynchronize()); + auto start2 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < 100; ++i) + ifft->ifft(f, f); + cudaCheck(cudaDeviceSynchronize()); + auto end2 = std::chrono::high_resolution_clock::now(); + std::cout << "IFFT " << batches << "x" << rows << "x" << cols << + ": " << std::chrono::duration_cast(end2-start2).count() << "ms\n"; + + std::cout << "Done\n"; + + destroy_filtered(fft); + destroy_filtered(ifft); + + cudaStreamDestroy(stream); + cudaFree(pre); + cudaFree(post); + cudaFree(f); +} diff --git a/cufft/filtered_fft/test_Makefile b/cufft/filtered_fft/test_Makefile new file mode 100644 index 000000000..f595b5c47 --- /dev/null +++ b/cufft/filtered_fft/test_Makefile @@ -0,0 +1,43 @@ +NVCC = nvcc +NVCC_FLAGS += -dc -arch=sm_60 \ + -gencode=arch=compute_30,code=sm_30 \ + -gencode=arch=compute_50,code=sm_50 \ + -gencode=arch=compute_52,code=sm_52 \ + -gencode=arch=compute_60,code=sm_60 \ + -gencode=arch=compute_61,code=sm_61 \ + -gencode=arch=compute_70,code=sm_70 \ + -gencode=arch=compute_70,code=compute_70 +CUDADIR = $(dir $(shell which nvcc) )/.. + +INCLUDES = $(shell python -m pybind11 --includes) -I$(CUDADIR)/include +PYMODEXT = $(shell python-config --extension-suffix) +CPPFLAGS += -DMY_FFT_ROWS=128 -DMY_FFT_COLS=128 $(INCLUDES) +OPTFLAGS = -O3 -std=c++14 +CXXFLAGS += -fPIC +LD_FLAGS += -L$(CUDADIR)/lib64 -lcufft_static -lculibos -cudart shared -ldl -lrt -lpthread +OBJ = filtered_fft.o +OBJ_MOD = module.o +OBJ_EXE = smoke_test.o +MODULE = filtered_fft$(PYMODEXT) +EXE = smoke_test + +all: $(MODULE) $(EXE) + +python: $(MODULE) + +clean: + rm -rf $(OBJ) $(EXE) $(MODULE) $(OBJ_EXE) $(OBJ_MOD) + +%.o: %.cu + $(NVCC) $(NVCC_FLAGS) $(OPTFLAGS) -Xcompiler "$(CXXFLAGS)" $(CPPFLAGS) -c $< -o $@ + +%.o: %.cpp + $(NVCC) $(NVCC_FLAGS) -x cu $(OPTFLAGS) -Xcompiler "$(CXXFLAGS)" $(CPPFLAGS) -c $< -o $@ + +$(MODULE): $(OBJ) $(OBJ_MOD) + $(NVCC) $(OPTFLAGS) -shared $(LD_FLAGS) $(OBJ) $(OBJ_MOD) -o $@ + +$(EXE): $(OBJ) $(OBJ_EXE) + $(NVCC) $(OPTFLAGS) -o $@ $(LD_FLAGS) $(OBJ) $(OBJ_EXE) + +$(OBJ) $(OBJ_EXE) $(OBJ_MOD): errors.h filtered_fft.h \ No newline at end of file diff --git a/cufft/setup.py b/cufft/setup.py new file mode 100644 index 000000000..8cba2f560 --- /dev/null +++ b/cufft/setup.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +# we should aim to remove the distutils dependency +import setuptools +from distutils.core import setup, Extension +import os + +ext_modules = [] +cmdclass = {} +# filtered Cuda FFT extension module +try: + from extensions import locate_cuda, get_cuda_version # this raises an error if pybind11 is not available + CUDA = locate_cuda() # this raises an error if CUDA is not available + CUDA_VERSION = get_cuda_version(CUDA['nvcc']) + if CUDA_VERSION < 10: + raise ValueError("filtered cufft requires CUDA >= 10") + from extensions import CustomBuildExt + cufft_dir = "filtered_fft" + ext_modules.append( + Extension("filtered_cufft", + sources=[os.path.join(cufft_dir, "module.cpp"), + os.path.join(cufft_dir, "filtered_fft.cu")] + ) + ) + cmdclass = {"build_ext": CustomBuildExt} + EXTBUILD_MESSAGE = "The filtered cufft extension has been successfully installed.\n" +except: + EXTBUILD_MESSAGE = '*' * 75 + "\n" + EXTBUILD_MESSAGE += "Could not install the filtered cufft extension.\n" + EXTBUILD_MESSAGE += "Make sure to have CUDA >= 10 and pybind11 installed.\n" + EXTBUILD_MESSAGE += '*' * 75 + "\n" + +exclude_packages = [] +package_list = setuptools.find_packages(exclude=exclude_packages) +setup( + name='filtered cufft', + version=0.1, + author='Bjoern Enders, Benedikt Daurer, Joerg Lotze', + description='Extension of CuFFT to include pre- and post-filters using callbacks', + packages=package_list, + ext_modules=ext_modules, + cmdclass=cmdclass +) + +print(EXTBUILD_MESSAGE) diff --git a/core_dependencies.yml b/dependencies_core.yml similarity index 63% rename from core_dependencies.yml rename to dependencies_core.yml index 2a449e8bf..32c492de1 100644 --- a/core_dependencies.yml +++ b/dependencies_core.yml @@ -1,8 +1,9 @@ -name: core_dependencies +name: ptypy_core channels: - conda-forge dependencies: - - python=3.7 + - python=3.9 - numpy - scipy - h5py + - pip \ No newline at end of file diff --git a/ptypy_full_dependencies.yml b/dependencies_dev.yml similarity index 76% rename from ptypy_full_dependencies.yml rename to dependencies_dev.yml index 9def5e284..230a7e190 100644 --- a/ptypy_full_dependencies.yml +++ b/dependencies_dev.yml @@ -1,8 +1,8 @@ -name: ptypy_full_dependencies +name: ptypy_full channels: - conda-forge dependencies: - - python=3.7 + - python=3.9 - numpy - scipy - matplotlib @@ -16,4 +16,3 @@ dependencies: - pip: - pytest-cov - coveralls - - fabio diff --git a/dependencies_full.yml b/dependencies_full.yml new file mode 100644 index 000000000..00ff5a4af --- /dev/null +++ b/dependencies_full.yml @@ -0,0 +1,15 @@ +name: ptypy_full +channels: + - conda-forge +dependencies: + - python=3.9 + - numpy + - scipy + - matplotlib + - h5py + - pyzmq + - mpi4py + - pillow + - pyfftw + - pip + diff --git a/doc/conf.py b/doc/conf.py index 3f345e5ee..64ad9fc0c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,7 @@ subprocess.check_call(['python', 'script2rst.py']) # We need this to have a clean sys.argv subprocess.check_call(['python', 'parameters2rst.py']) subprocess.check_call(['python', 'tmp2rst.py']) -exec(open('version.py').read()) +exec(open('../ptypy/version.py').read()) # -- General configuration ------------------------------------------------ @@ -99,7 +99,7 @@ def get_refs(dct, pd, depth=2, indent=''): def setup(app): app.connect('autodoc-process-docstring', remove_mod_docstring) app.connect('autodoc-process-docstring', truncate_docstring) - + pass napoleon_use_ivar = True napoleon_include_special_with_doc = True @@ -128,9 +128,9 @@ def setup(app): # built documents. # # The short X.Y version. -version = version #'0.0.1' +version = version #'0.0.1' # noqa: F821 # The full version, including alpha/beta/rc tags. -release = short_version #'0.0.1' +release = short_version #'0.0.1' # noqa: F821 # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -182,7 +182,7 @@ def setup(app): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'ptypysphinx'#'sphinxdoc'#'classic'#'scrolls' #'sphinxdoc' #'alabaster' +html_theme = 'ptypysphinx'#'sphinxdoc'#'classic'#'scrolls' #'sphinxdoc' #'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -190,7 +190,7 @@ def setup(app): html_theme_options = {'sidebarwidth':'280'} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['html_templates'] +html_theme_path = ['html_templates'] #, '../../../anaconda3/envs/ptypy/lib/python3.7/site-packages/sphinx/themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -211,7 +211,7 @@ def setup(app): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied diff --git a/doc/html_templates/ptypysphinx/download.html b/doc/html_templates/ptypysphinx/download.html index a91d599cb..0bdeb6e70 100644 --- a/doc/html_templates/ptypysphinx/download.html +++ b/doc/html_templates/ptypysphinx/download.html @@ -6,15 +6,16 @@
  • Download TAR Ball
  • + + +unpack and follow the step-by-step installation instructions. - - +

    Support

    If you are having trouble getting ptypy up und running please let us know ( diff --git a/doc/html_templates/ptypysphinx/layout.html b/doc/html_templates/ptypysphinx/layout.html index cf2a29ef6..b490bed96 100644 --- a/doc/html_templates/ptypysphinx/layout.html +++ b/doc/html_templates/ptypysphinx/layout.html @@ -1,4 +1,4 @@ -{% extends "!layout.html" %} +{% extends "!basic/layout.html" %} {# load free fonts #} {% block extrahead %} diff --git a/doc/html_templates/ptypysphinx/static/header.css_t b/doc/html_templates/ptypysphinx/static/header.css_t index bb2670a77..19c2a442b 100644 --- a/doc/html_templates/ptypysphinx/static/header.css_t +++ b/doc/html_templates/ptypysphinx/static/header.css_t @@ -115,8 +115,14 @@ div.related ul li a { } div.sphinxsidebar { - width: {{ theme_sidebarwidth|toint - 20 }}px; } +pre { + background-color: #f8f8f8; +} + +.highlight .hll { + display: unset; +} diff --git a/doc/index.rst b/doc/index.rst index bc755bc01..c99c71264 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,7 +5,7 @@ Welcome Ptychonaut! framework for scientific ptychography compiled by P.Thibault and B. Enders and licensed under the GPLv2 license. -It comprises 7 years of experience in the field of ptychography condensed +It comprises 8 years of experience in the field of ptychography condensed to a versatile python package. The package covers the whole path of ptychographic analysis after the actual experiment - from data management to reconstruction to visualization. @@ -25,14 +25,17 @@ Get started quickly :ref:`here ` or with one of the examples in Highlights ---------- -* **Difference Map** [#dm]_ algorithm engine with power bound constraint +* **Difference Map** [#dm]_ algorithm engine with power bound constraint [#power]_. * **Maximum Likelihood** [#ml]_ engine with preconditioners and regularizers. +* A few more engines (RAAR, sDR, ePIE, ...). -* **Fully parallelized** (CPU only) using the Massage Passing Interface +* **Fully parallelized** using the Massage Passing Interface (`MPI `_). Simply execute your script with:: - $ mpiexec -n [nodes] python .py + $ mpiexec/mpirun -n [nodes] python .py + +* **GPU acceleration** based on custom kernels, pycuda, and reikna. * A **client-server** approach for visualization and control based on `ZeroMQ `_ . @@ -72,6 +75,9 @@ Quicklinks .. [#Thi2013] P.Thibault and A.Menzel, **Nature** 494, 68 (2013), `doi `__ -.. [#ml] P.Thibault and M.Guizar-Sicairos, **New J. of Phys.** 14, 6 (2012), `doi `__ +.. [#ml] P.Thibault and M.Guizar-Sicairos, **New J. of Phys. 14**, 6 (2012), `doi `__ + +.. [#dm] P.Thibault, M.Dierolf *et al.*, **Ultramicroscopy 109**, 4 (2009), `doi `__ + +.. [#power] K.Giewekemeyer *et al.*, **PNAS 108**, 2 (2007), `suppl. material `__, `doi `__ -.. [#dm] P.Thibault, M.Dierolf *et al.*, **New J. of Phys. 14**, 6 (2012), `doi `__ diff --git a/doc/parameters2rst.py b/doc/parameters2rst.py index be8928777..8058e6a96 100644 --- a/doc/parameters2rst.py +++ b/doc/parameters2rst.py @@ -1,61 +1,139 @@ from ptypy import defaults_tree -prst = open('rst/parameters.rst','w') -Header= '.. _parameters:\n\n' -Header+= '************************\n' -Header+= 'Parameter tree structure\n' -Header+= '************************\n\n' -prst.write(Header) +def write_desc_recursive(prst, tree): + for path, desc in tree.children.items(): + print(path) + types = desc.type + default = desc.default + lowlim, uplim = desc.limits + is_wildcard = (desc.name == '*') + + if is_wildcard: + path = path.replace('*', desc.parent.name[:-1] + '_00') + + if path == '': + continue + + if desc.children or desc.is_symlink: + if desc.parent is desc.root: + prst.write('\n' + path + '\n') + prst.write('=' * len(path) + '\n\n') + if desc.parent.parent is desc.root: + prst.write('\n' + path + '\n') + prst.write('-' * len(path) + '\n\n') + + prst.write('.. py:data:: ' + path) + + if desc.is_symlink: + tp = 'Param' + else: + tp = ', '.join([str(t) for t in types]) + prst.write(' (' + tp + ')') + prst.write('\n\n') -for path, desc in defaults_tree.descendants: - - types = desc.type - default = desc.default - lowlim, uplim = desc.limits - is_wildcard = (desc.name == '*') - - if is_wildcard: - path = path.replace('*', desc.parent.name[:-1] + '_00') - - if path == '': - continue - if desc.children and desc.parent is desc.root: - prst.write('\n'+path+'\n') - prst.write('='*len(path)+'\n\n') - if desc.children and desc.parent.parent is desc.root: - prst.write('\n'+path+'\n') - prst.write('-'*len(path)+'\n\n') - - prst.write('.. py:data:: '+path) - - if desc.is_symlink: - tp = 'Param' - else: - tp = ', '.join([str(t) for t in types]) - prst.write(' ('+tp+')') - prst.write('\n\n') - - if is_wildcard: - prst.write(' *Wildcard*: multiple entries with arbitrary names are accepted.\n\n') - - # prst.write(' '+desc.help+'\n\n') - prst.write(' ' + desc.help.replace('', '\n').replace('\n', '\n ') + '\n\n') - prst.write(' '+desc.doc.replace('','\n').replace('\n', '\n ')+'\n\n') - - if desc.is_symlink: - prst.write(' *default* = '+':py:data:`'+desc.path+'`\n') - else: - prst.write(' *default* = ``'+repr(default)) - if lowlim is not None and uplim is not None: - prst.write(' (>'+str(lowlim)+', <'+str(uplim)+')``\n') - elif lowlim is not None and uplim is None: - prst.write(' (>'+str(lowlim)+')``\n') - elif lowlim is None and uplim is not None: - prst.write(' (<'+str(uplim)+')``\n') + if is_wildcard: + prst.write(' *Wildcard*: multiple entries with arbitrary names are accepted.\n\n') + + # prst.write(' '+desc.help+'\n\n') + prst.write(' ' + desc.help.replace('', '\n').replace('\n', '\n ') + '\n\n') + prst.write(' ' + desc.doc.replace('', '\n').replace('\n', '\n ') + '\n\n') + + if desc.children: + print('recursion ' + path) + prst.write('\n') + write_desc_recursive(prst, desc) + elif desc.is_symlink: + print('following symlink ' + path) + prst.write('\n') + write_desc_recursive(prst, desc.type[0]) else: - prst.write('``\n') - - prst.write('\n') + prst.write(' *default* = ``' + repr(default)) + if lowlim is not None and uplim is not None: + prst.write(' (>' + str(lowlim) + ', <' + str(uplim) + ')``\n') + elif lowlim is not None and uplim is None: + prst.write(' (>' + str(lowlim) + ')``\n') + elif lowlim is None and uplim is not None: + prst.write(' (<' + str(uplim) + ')``\n') + else: + prst.write('``\n') + + prst.write('\n') + + +def write_desc_tree(prst, tree): + for path, desc in tree.descendants: + + types = desc.type + default = desc.default + lowlim, uplim = desc.limits + is_wildcard = (desc.name == '*') + + if is_wildcard: + path = path.replace('*', desc.parent.name[:-1] + '_00') + + if path == '': + continue + if desc.children and desc.parent is desc.root: + prst.write('\n' + path + '\n') + prst.write('=' * len(path) + '\n\n') + if desc.children and desc.parent.parent is desc.root: + prst.write('\n' + path + '\n') + prst.write('-' * len(path) + '\n\n') + + prst.write('.. py:data:: ' + path) + + if desc.is_symlink: + tp = 'Param' + else: + tp = ', '.join([str(t) for t in types]) + prst.write(' (' + tp + ')') + prst.write('\n\n') + + if is_wildcard: + prst.write(' *Wildcard*: multiple entries with arbitrary names are accepted.\n\n') + + # prst.write(' '+desc.help+'\n\n') + prst.write(' ' + desc.help.replace('', '\n').replace('\n', '\n ') + '\n\n') + prst.write(' ' + desc.doc.replace('', '\n').replace('\n', '\n ') + '\n\n') + + if desc.is_symlink: + prst.write(' *default* = ' + ':py:data:`' + desc.type[0].path + '`\n') + else: + prst.write(' *default* = ``' + repr(default)) + if lowlim is not None and uplim is not None: + prst.write(' (>' + str(lowlim) + ', <' + str(uplim) + ')``\n') + elif lowlim is not None and uplim is None: + prst.write(' (>' + str(lowlim) + ')``\n') + elif lowlim is None and uplim is not None: + prst.write(' (<' + str(uplim) + ')``\n') + else: + prst.write('``\n') + + prst.write('\n') + + +prst = open('rst/parameters.rst','w') + +Header = """\ +.. _parameters: + +************************ +Parameter tree structure +************************ + +.. note:: + This section needs to be reworked to account for the + recursive nature for |ptypy|_'s parameter tree. + Please use the examples in the templates folder to + craft your own scripts. +""" + +prst.write(Header) +write_desc_tree(prst, defaults_tree) +# write_desc_recursive(prst, defaults_tree['ptycho']) +# write_desc_tree(prst, defaults_tree['io']) +# write_desc_tree(prst, defaults_tree['scan']) +# write_desc_tree(prst, defaults_tree['scandata']) prst.close() diff --git a/doc/rst/ptypy.engines.rst b/doc/rst/ptypy.engines.rst index 0ff64b012..0e4128c3d 100644 --- a/doc/rst/ptypy.engines.rst +++ b/doc/rst/ptypy.engines.rst @@ -4,10 +4,10 @@ ptypy.engines package Submodules ---------- -ptypy.engines.DM module ------------------------ +ptypy.engines.projectional module +--------------------------------- -.. automodule:: ptypy.engines.DM +.. automodule:: ptypy.engines.projectional :members: :undoc-members: :show-inheritance: @@ -21,6 +21,14 @@ ptypy.engines.ML module :undoc-members: :show-inheritance: +ptypy.engines.stochastic module +------------------------------- + +.. automodule:: ptypy.engines.stochastic + :members: + :undoc-members: + :show-inheritance: + ptypy.engines.base module ------------------------- diff --git a/doc/rst/ptypy.experiment.rst b/doc/rst/ptypy.experiment.rst index 40e4ca02a..fef2a7ded 100644 --- a/doc/rst/ptypy.experiment.rst +++ b/doc/rst/ptypy.experiment.rst @@ -12,6 +12,14 @@ ptypy.experiment.ID16Anfp module :undoc-members: :show-inheritance: +ptypy.experiment.hdf5_loader module +----------------------------------- + +.. automodule:: ptypy.experiment.hdf5_loader + :members: + :undoc-members: + :show-inheritance: + ptypy.experiment.optiklabor module ---------------------------------- diff --git a/doc/rst_templates/getting_started.tmp b/doc/rst_templates/getting_started.tmp index 0b5d7850e..db4a6a19c 100644 --- a/doc/rst_templates/getting_started.tmp +++ b/doc/rst_templates/getting_started.tmp @@ -12,42 +12,65 @@ Installation General installation instructions --------------------------------- -Being a python package, |ptypy|_ depends on a number of other -packages which are listed below. -Once its dependecies are met, ptypy should work *out-of-the-box*. +Download and unpack |ptypy|_ from the sources. For example, on Linux +systems you can do:: -.. note:: - |ptypy| is developed for Python 2.7.x and is currently incompatible - with Python 3.x. Python 3 support is planned in future releases. - + $ wget https://github.com/ptycho/ptypy/archive/master.zip + $ unzip master.zip -d /tmp/; rm master.zip + +or make a clone of the |ptypy|_ github repository:: + + $ git clone https://github.com/ptycho/ptypy.git /tmp/ptypy-master + +which will unpack the master branch of ptypy into `/tmp/ptypy-master` but +you are of course free to place the software wherever it is convenient for you. +In principle, next you only have to navigate to that directory and install with +:: + + $ pip install . + +However, since |ptypy|_ depends on a number of other packages, we recommend +installing it in a virtual environment using the +`anaconda `_ package manager. Conveniently, this +installation route allows you to install |ptypy|_ across platforms. -Essential packages -^^^^^^^^^^^^^^^^^^ -* `NumPy `_ +Essential install +^^^^^^^^^^^^^^^^^ + +Only three Python packages are essential for |ptypy|_ to work: + +* `NumPy `_ (homepage: ``_) -* `SciPy `_ +* `SciPy `_ (homepage: ``_) -* `h5py `_ +* `h5py `_ (homepage: ``_) -Recommended packages for additional functionality -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Please note that |ptypy| is an alpha release and lacks rigorous import +Install the essential version like so +:: + + $ conda env create -f dependencies_core.yml + $ conda activate ptypy_core + (ptypy_core)$ pip install . + + +Recommended install for additional functionality +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Please note that |ptypy|_ is an alpha release and lacks rigorous import checking. There may be parts of code that implicitly ask for any of the packages listed here. Therefore, we recommend to install these packages. -* `Matplotlib `_ +* `Matplotlib `_ for any kind of plotting or image generation - (homepage: ``_) and python bindings for - `QT4 `_ + (homepage: ``_) -* `mpi4py `_ - for CPU-node parallelisation, contains python bindings for the +* `mpi4py `_ + for CPU-node parallelization, contains python bindings for the `Message Passaging Interface `_, (homepage: ``_) -* `pyzmq `_ +* `pyzmq `_ for a threaded server on the main node to perfrom asynchronous client-server communication, contains python bindings for the ZeroMQ protocol, @@ -55,139 +78,48 @@ packages listed here. Therefore, we recommend to install these packages. This package is needed for non-blocking plots of the reconstruction run (e.g. for :ref:`plotclient`). -Optional packages -^^^^^^^^^^^^^^^^^ -Other very useful packages are - -* `Ipython `_ - (homepage: ``_) - -Installation on Debian/Ubuntu ------------------------------ -|ptypy| is developed on the current LTS version of Ubuntu which is the -recommended operating system for |ptypy| - -Prerequisites -^^^^^^^^^^^^^ -Many of the required python packages are -available in the repositories. Just type (with sudo rights) +Install the recommended version like so :: - $ sudo apt-get install python-numpy python-scipy python-h5py\ - $ python-matplotlib python-mpi4py python-pyzmq + $ conda env create -f dependencies_full.yml + $ conda activate ptypy_full + (ptypy_full)$ pip install . -Get ptypy -^^^^^^^^^ -Download and unpack |ptypy|_ from the sources:: - $ wget https://github.com/ptycho/ptypy/archive/master.zip - $ unzip master.zip -d /tmp/; rm master.zip +Recommended install for GPU support +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -or make a clone of the |ptypy|_ github repository:: - - $ git clone https://github.com/ptycho/ptypy.git /tmp/ptypy-master +We support an accelerated version of |ptypy|_ for CUDA-capable +GPUs based on our own kernels and the +`PyCUDA `_ package. -CD into the download directory (e.g. /tmp/ptypy-master) and install |ptypy| with +Install the dependencies for this version like so. :: - $ python setup.py install --user + $ conda env create -f accelerate/cuda_pycuda/dependencies.yml + $ conda activate ptypy_pycuda + (ptypy_pycuda)$ pip install . -The local ``--user`` install is recommended instead of a system-wide -``sudo`` install. It makes it easier to quickly apply fixes yourself. -However, for an all-user system-wide install, type + +While we use `Reikna `_ to +provide a filtered FFT, i.e. a FFT that is fused with a pointwise +matrix multiplication, you can optionally also install a version +based on cufft and callbacks. Due to the nature of this extension it +needs to be built for fixed array sizes externally. :: - $ sudo python setup.py install + $ conda activate ptypy_pycuda + (ptypy_pycuda)$ cd cufft + (ptypy_pycuda)$ conda env update --file dependencies.yml --name ptypy_pycuda + (ptypy_pycuda)$ pip install . -Installation on Windows ------------------------ -These installation instructions were contributed by M. Stockmar and tested -with *Windows 8.1 Enterprise (64 bit)* on a core i7 Thinkpad w520 -in February 2015. -No python was installed before on that machine. +Optional packages +^^^^^^^^^^^^^^^^^ +Other very useful packages are -.. note:: - There might be also simpler ways to get a full scientific python - suite for 32 bit Windows. Please let us know if you managed to get - |ptypy|_ running on a system not listed here. - -Prerequisites -^^^^^^^^^^^^^ - -* Download and install python 2.7.x from ``_ - (Make sure you have the 64 bit version) - Click *yes* when asked to add python to the path environment variable. - -* Go to the command line and install wheel:: - - $ pip install wheel - -* Install Microsoft's implementation for MPI to use multiple CPUs from - ``_ - Other MPI implementations may work as well but were not tested. - -* Install the QT-Framework if you don't have it already. - ``_ - - -Download all other binaries -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The binaries will be downloaded from a `website `_ -which offers various builds for different python versions for Windows (both 32 and 64 bit) in -the form of a `python wheel `_. -Make sure you choose the correct version for your windows and system. - -Numpy and some other builds are linked against the -Intel Math Kernel Library (MKL) which is supposed to be fast. - -* First make sure you have the Microsoft Visual C++ 2010 redistributable package (maybe also the 2008 version). - - * | 2010 (64 bit, for 32 bit version check the website): - | ``_ - * | 2008 (64 bit, for 32 bit check the website): - | ``_ - -* Now, go to to ``_ - and download the latest pip binaries and update pip:: - - $ python.exe pip-6.0.8-py2.py3-none-any.whl/pip install pip-6.0.8-py2.py3-none-any.whl. - - (The file name might change slightly if a newer version of pip is available) - -* Then download all the other binaries as whl-files. - A whl-file can be installed via command line according to:: - - $ pip install filename.whl - - Download and install the binaries in the following order: - - #. ``numpy`` - #. ``pillow`` (replacement for PIL) - #. ``matplotlib`` - #. ``ipython`` - #. ``h5py`` - #. ``scipy`` (may install additional packages as well) - #. ``mpi4py`` (choose the one for MS MPI if you have installed MS MPI) - #. ``pyzmq`` - #. ``pyqt4`` (QT framework was installed before on the testing machine) - #. ``pyside`` - -.. - pip install pyqt4 (QT framework was installed before on the testing machine) - pip install pyside - - In case these instructions are not sufficient, refer to the website by C. Gohlke. - -Get ptypy -^^^^^^^^^ -Download |ptypy| from ``_ -and unzip to any directory, for example ``C:\Temp``. -Change into that directly and install from commandline:: - - $ cd C:\Temp\ptypy-master - $ python setup.py install +* `Ipython `_ + (homepage: ``_) .. _quickstart: @@ -205,8 +137,7 @@ Utilies/Binaries for convenience |ptypy| provides a few utility scripts to make life easier for you, the user. They are located in ``[ptypy_root]/scripts``. -In case of a user install on -Ubuntu Linux, they are copied to ``~/.local/bin`` +In case of an install with conda, these are copied to ``/bin`` .. note:: Due to the early stage of developmnet, @@ -248,13 +179,14 @@ that was created by |ptypy|. For such cases, we can use ``ptypy.inspect``. For example, a quick view at the top level can be realized with :: - $ ptypy.inspect /tmp/ptypy/recons/minimal/minimal_DM.ptyr -d 1 + $ ptypy.inspect /tmp/ptypy/recons/minimal/minimal_DM_0040.ptyr -d 1 which has the following the output:: - * content [Param 4]: + * content [Param 5]: * obj [dict 1]: * pars [Param 9]: + * positions [dict 1]: * probe [dict 1]: * runtime [Param 8]: * header [dict 2]: @@ -267,20 +199,27 @@ If we are interested solely in the probe we could use :: which has the following the output:: - * S00G00 [dict 13]: - * DataTooSmall [scalar = False] - * ID [string = "S00G00"] - * _center [array = [64 64]] - * _origin [array = [ -4.07172778e-06 -4.07172778e-06]] - * _pool [Param 0]: - * _psize [array = [ 6.36207466e-08 6.36207466e-08]] - * data [1x128x128 complex64 array] - * fill_value [scalar = 0.0] - * layermap [list = [0]] - * model_initialized [scalar = True] - * nlayers [scalar = 1] - * padonly [scalar = False] - * shape [tuple = (1, 128, 128)] + * SMF [dict 20]: + * DataTooSmall [scalar = False] + * ID [string = "SMF"] + * _center [array = [64. 64.]] + * _energy [scalar = 7.2] + * _origin [array = [-3.59918584e-06 -3.59918584e-06]] + * _pool [dict 0]: + * _psize [array = [5.62372787e-08 5.62372787e-08]] + * _record [scalar = (b'SMF',)] + * _recs [dict 0]: + * data [1x128x128 complex64 array] + * distributed [scalar = False] + * fill_value [scalar = 0.0] + * grids [tuple = 2x[1x128x128 float64 array]] + * layermap [list = [0]] + * model_initialized [scalar = True] + * nlayers [scalar = 1] + * numID [scalar = 1] + * padding [scalar = 0] + * padonly [scalar = False] + * shape [tuple = (1, 128, 128)] We omitted the result for the complete file to save some space but you are encouraged to try:: @@ -292,26 +231,8 @@ are encouraged to try:: Create a new template for a reconstruction script ------------------------------------------------- -In cases where we want to create a new reconstruction script from scratch, -it may be cumbersome to write each and every parameter that we want to -change. But, with the help of ``ptypy.new``, we can create a python -script which is prefilled with defaults:: - - $ ptypy.new [-h] [-u ULEVEL] [--short-doc] [--long-doc] pyfile - -In the folder ``[ptypy_root]/templates`` you find two scripts -that where auto-generated with the following calls:: - - $ ptypy.new -u 0 --long-doc pars_few_alldoc.py - $ ptypy.new -u 2 pars_all_nodoc.py - -And here is a quick view of the first one with much documentation -(only the first 50 lines). +[WIP] Due to wildcards and links in the parameter tree, this section will be reworked. -.. literalinclude:: ../../templates/pars_few_alldoc.py - :language: python - :linenos: - :lines: 1-50 .. _plotclient: @@ -341,29 +262,29 @@ you can temper with. All-in-one ---------- -We encourage you to use the script ``[ptypy_root]/templates/minimal_prep_and_run.py`` +We encourage you to use the script ``[ptypy_root]/templates/ptypy_minimal_prep_and_run.py`` and modify the *recipe* part of the data parameter branch. Observe what changes in the reconstruction when scan parameters change. -.. literalinclude:: ../../templates/minimal_prep_and_run.py +.. literalinclude:: ../../templates/ptypy_minimal_prep_and_run.py :language: python :linenos: - :emphasize-lines: 31,33,35 + :emphasize-lines: 41,43,45 Creating a .ptyd data-file -------------------------- -We encourage you to use this script ``[ptypy_root]/templates/make_sample_ptyd.py`` +We encourage you to use this script ``[ptypy_root]/templates/ptypy_make_sample_ptyd.py`` to create various different samples and see what happens if the data processing parameters are changed. If you have become curious, move -forward to :ref:`ptypy_data` and take a look at |ptypy|'s data management. +forward to :ref:`ptypy_data` and take a look at |ptypy|_'s data management. Check out the data parameter branch :py:data:`.scan.data` for detailed parameter descriptions. -.. literalinclude:: ../../templates/make_sample_ptyd.py +.. literalinclude:: ../../templates/ptypy_make_sample_ptyd.py :language: python :linenos: - :emphasize-lines: 15-26 + :emphasize-lines: 12-19 Loading a data file to run a reconstruction ------------------------------------------- @@ -375,6 +296,6 @@ and alter the reconstruction parameters and algorithms to find out if you can make the recontruction converge. Check out the engine parameter branch :py:data:`.engine` for detailed parameter descriptions. -.. literalinclude:: ../../templates/minimal_load_and_run.py +.. literalinclude:: ../../templates/ptypy_minimal_load_and_run.py :language: python :linenos: diff --git a/doc/script2rst.py b/doc/script2rst.py index 206a3f07d..b05148e46 100644 --- a/doc/script2rst.py +++ b/doc/script2rst.py @@ -137,10 +137,14 @@ def stdoutIO(stdout=None): sout.buf = '' if len(wline) > 0: - if line.startswith('# '): + if line.startswith('#'): # This was '# ' before, but pycharm eats trailing whitespaces so the marker wline = line[2:] was_comment = True - frst.write(wline) + if not wline: + # just in case there is an empty line on purpose + frst.write('\n') + else: + frst.write(wline) else: wline = ' >>> '+wline if was_comment: diff --git a/ptypy/__init__.py b/ptypy/__init__.py index 2f1633878..33da60ac8 100644 --- a/ptypy/__init__.py +++ b/ptypy/__init__.py @@ -72,8 +72,57 @@ # Import core modules from . import utils from . import io -from . import experiment from . import core from . import simulations from . import resources +# Convenience loader for GPU engines +def load_gpu_engines(arch='cuda'): + if arch=='cuda': + from .accelerate.cuda_pycuda.engines import projectional_pycuda + from .accelerate.cuda_pycuda.engines import projectional_pycuda_stream + from .accelerate.cuda_pycuda.engines import stochastic + from .accelerate.cuda_pycuda.engines import ML_pycuda + if arch=='serial': + from .accelerate.base.engines import projectional_serial + from .accelerate.base.engines import projectional_serial_stream + from .accelerate.base.engines import stochastic + from .accelerate.base.engines import ML_serial + if arch=='ocl': + from .accelerate.ocl_pyopencl.engines import DM_ocl, DM_ocl_npy + +from importlib import import_module +from .utils.verbose import log +ptyscan_modules = ['hdf5_loader', + 'cSAXS', + 'savu', + 'plugin', + 'ID16Anfp', + 'AMO_LCLS', + 'DiProI_FERMI', + 'optiklabor', + 'UCL', + 'nanomax', + 'nanomax_streaming', + 'ALS_5321', + 'Bragg3dSim'] + +# Convenience loader for ptyscan modules +def load_ptyscan_module(module): + try: + lib = import_module("."+module, 'ptypy.experiment') + except ImportError as exception: + log(2, 'Could not import ptyscan module %s, Reason: %s' % (module, exception)) + pass + +# Convenience loader for all ptyscan modules +def load_all_ptyscan_modules(): + for m in ptyscan_modules: + load_ptyscan_module(m) + +# Convenience loader for all ptyscan modules and all gpu engines +def load_all(): + load_gpu_engines("cuda") + load_gpu_engines("serial") + #load_gpu_engines("ocl") + load_all_ptyscan_modules() \ No newline at end of file diff --git a/templates/__init__.py b/ptypy/accelerate/__init__.py similarity index 100% rename from templates/__init__.py rename to ptypy/accelerate/__init__.py diff --git a/ptypy/accelerate/base/__init__.py b/ptypy/accelerate/base/__init__.py new file mode 100644 index 000000000..cfacc2aa2 --- /dev/null +++ b/ptypy/accelerate/base/__init__.py @@ -0,0 +1,3 @@ +import numpy as np +COMPLEX_TYPE= np.complex64 +FLOAT_TYPE = np.float32 \ No newline at end of file diff --git a/ptypy/accelerate/base/address_manglers.py b/ptypy/accelerate/base/address_manglers.py new file mode 100644 index 000000000..8ac8b8d1e --- /dev/null +++ b/ptypy/accelerate/base/address_manglers.py @@ -0,0 +1,87 @@ +''' +utils to help with position refinement +''' + +import numpy as np +np.random.seed(0) + +class BaseMangler(object): + ''' + Assumes integer pixel shift. + ''' + def __init__(self, max_step_per_shift, start, stop, nshifts, decay=True, max_bound=None, randomseed=None): + # can be initialised in the engine.init + + # maximum distance from the starting positions + self.max_bound = max_bound + # maximum step per iteration, decreases with progression + self.max_step = lambda it: np.ceil(max_step_per_shift * (stop - it) / (stop - start)) if decay else max_step_per_shift + self.nshifts = nshifts + self.delta = 0 + + def get_address(self, index, addr_current, mangled_addr, max_oby, max_obx): + ''' + Mangles with the address given a delta shift + ''' + old_positions = np.zeros((addr_current.shape[0], 2)) + old_positions[:] = addr_current[:, 0, 1, 1:] + new_positions = np.zeros((addr_current.shape[0],2)) + new_positions[:] = old_positions + self.delta[index] # first mode is same as all of them. + # now update the main matrix (Same for all modes) + for idx in range(addr_current.shape[1]): + mangled_addr[:, idx, 1, 1:] = new_positions + self.apply_bounding_box(mangled_addr[:,:,1,1], 0, max_oby) + self.apply_bounding_box(mangled_addr[:,:,1,2], 0, max_obx) + + def apply_bounding_box(self, addr, min, max): + ''' + Check if the mangled addresses are within valid bounds + ''' + addr[addrmax] = max + + def setup_shifts(self, current_iteration, nframes=1): + ''' + Arrange an array of shifts + ''' + raise NotImplementedError("This method needs to be overwritten in order to position correct") + + +class RandomIntMangler(BaseMangler): + + def __init__(self, *args, **kwargs): + super(RandomIntMangler, self).__init__(*args, **kwargs) + + def setup_shifts(self, current_iteration, nframes=1): + ''' + Calculates random integer shifts + ''' + max_step = self.max_step(current_iteration) + self.delta = np.random.randint(0, max_step + 1, (self.nshifts, nframes, 2)) + for index in range(self.nshifts): + self.delta[index, :, 0] *= (-1)**index + self.delta[index, :, 1] *= (-1)**(index//2) + # check if the shifts are within the maximum bound + norms = np.linalg.norm(self.delta, axis=-1) + self.delta[norms > self.max_bound] = 0 + +class GridSearchMangler(BaseMangler): + def __init__(self, *args, **kwargs): + super(GridSearchMangler, self).__init__(*args, **kwargs) + + def setup_shifts(self, current_iteration, nframes=1): + ''' + Calculates integer shifts on a grid + ''' + max_step = self.max_step(current_iteration) + delta = np.mgrid[-max_step:max_step+1:1, + -max_step:max_step+1:1] + within_bound = (delta[0]**2 + delta[1]**2) < (self.max_bound**2) + self.delta = np.tile(delta[:,within_bound].T.reshape(within_bound.sum(),1,2), (1,nframes,1)) + self.nshifts = self.delta.shape[0] + + + + + + diff --git a/ptypy/accelerate/base/array_utils.py b/ptypy/accelerate/base/array_utils.py new file mode 100644 index 000000000..839b08e70 --- /dev/null +++ b/ptypy/accelerate/base/array_utils.py @@ -0,0 +1,150 @@ +''' +useful utilities from ptypy that should be ported to gpu. These don't ahve external dependencies +''' +import numpy as np +from scipy import ndimage as ndi + + +def dot(A, B, acc_dtype=np.float64): + assert A.dtype == B.dtype, "Input arrays must of same data type" + if np.iscomplexobj(B): + out = np.sum(np.multiply(A, B.conj()).real, dtype=acc_dtype) + else: + out = np.sum(np.multiply(A, B), dtype=acc_dtype) + return out + + +def norm2(A): + return dot(A, A) + +def max_abs2(A): + ''' + A has ndim = 3. + compute abs2, sum along first dimension and take maximum along last two dims + ''' + return np.max(np.sum(np.abs(A)**2,axis=0),axis=(-2,-1)) + +def abs2(input): + ''' + + :param input. An array that we want to take the absolute value of and square. Can be inplace. Can be complex or real. + :return: The real valued abs**2 array + ''' + return np.multiply(input, input.conj()).real + + +def sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype): + ''' + :param in1. An array . Can be inplace. Can be complex or real. + :param outshape. An array. + :param in1_addr. An array . Can be inplace. Can be complex or real. + :param out1_addr. An array . Can be inplace. Can be complex or real. + :return: The real valued abs**2 array + ''' + out1 = np.zeros(outshape, dtype=dtype) + inshape = in1.shape + for i1, o1 in zip(in1_addr, out1_addr): + out1[o1[0], o1[1]:(o1[1] + inshape[1]), o1[2]:(o1[2] + inshape[2])] += in1[i1[0]] + return out1 + + +def norm2(input): + ''' + Input here could be a variety of 1D, 2D, 3D complex or real. all will be single precision at least. + return should be real + ''' + return np.sum(abs2(input)) + + +def complex_gaussian_filter(input, mfs): + ''' + takes 2D and 3D arrays. Complex input, complex output. mfs has len 0 2: + raise NotImplementedError("Only batches of 2D arrays allowed!") + + if input.ndim == 3: + mfs = np.insert(mfs, 0, 0) + + return (ndi.gaussian_filter(np.real(input), mfs) + 1j * ndi.gaussian_filter(np.imag(input), mfs)).astype( + input.dtype) + + +def mass_center(A): + ''' + Input will always be real, and 2d or 3d, single precision here + ''' + return np.array(ndi.measurements.center_of_mass(A), dtype=A.dtype) + + +def interpolated_shift(c, shift, do_linear=False): + ''' + complex bicubic interpolated shift. + complex output. This shift should be applied to 2D arrays. shift should have len=c.ndims + + ''' + if not do_linear: + return ndi.interpolation.shift(np.real(c), shift, order=3, prefilter=True) + 1j * ndi.interpolation.shift( + np.imag(c), shift, order=3, prefilter=True) + else: + return ndi.interpolation.shift(np.real(c), shift, order=1, mode='constant', cval=0, + prefilter=False) + 1j * ndi.interpolation.shift(np.imag(c), shift, order=1, + mode='constant', cval=0, + prefilter=False) + + +def clip_complex_magnitudes_to_range(complex_input, clip_min, clip_max): + ''' + This takes a single precision 2D complex input, clips the absolute magnitudes to be within a range, but leaves the phase untouched. + ''' + ampl = np.abs(complex_input) + phase = np.exp(1j * np.angle(complex_input)) + ampl = np.clip(ampl, clip_min, clip_max) + complex_input[:] = ampl * phase + + +def fill3D(A, B, offset=[0, 0, 0]): + """ + Fill 3-dimensional array A with B. + """ + if A.ndim < 3 or B.ndim < 3: + raise ValueError('Input arrays must each be at least 3D') + assert A.ndim == B.ndim, "Input and Output must have the same number of dimensions." + ash = A.shape + bsh = B.shape + misfit = np.array(bsh) - np.array(ash) + assert not misfit[:-3].any(), "Input and Output must have the same shape everywhere but the last three axes." + + Alim = np.array(A.shape[-3:]) + Blim = np.array(B.shape[-3:]) + off = np.array(offset) + Ao = off.copy() + Ao[Ao < 0] = 0 + Bo = -off.copy() + Bo[Bo < 0] = 0 + assert (Bo < Blim).all() and (Ao < Alim).all(), "At least one dimension lacks overlap" + A[..., Ao[0]:min(off[0] + Blim[0], Alim[0]), + Ao[1]:min(off[1] + Blim[1], Alim[1]), + Ao[2]:min(off[2] + Blim[2], Alim[2])] \ + = B[..., Bo[0]:min(Alim[0] - off[0], Blim[0]), + Bo[1]:min(Alim[1] - off[1], Blim[1]), + Bo[2]:min(Alim[2] - off[2], Blim[2])] + + +def crop_pad_2d_simple(A, B): + """ + Places B in A centered around the last two axis. A and B must be of the same shape + anywhere but the last two dims. + """ + assert A.ndim >= 2, "Arrays must have more than 2 dimensions." + assert A.ndim == B.ndim, "Input and Output must have the same number of dimensions." + misfit = np.array(A.shape) - np.array(B.shape) + assert not misfit[:-2].any(), "Input and Output must have the same shape everywhere but the last two axes." + if A.ndim == 2: + A = A.reshape((1,) + A.shape) + if B.ndim == 2: + B = B.reshape((1,) + B.shape) + a1, a2 = A.shape[-2:] + b1, b2 = B.shape[-2:] + offset = [0, a1 // 2 - b1 // 2, a2 // 2 - b2 // 2] + fill3D(A, B, offset) diff --git a/ptypy/accelerate/base/engines/ML_serial.py b/ptypy/accelerate/base/engines/ML_serial.py new file mode 100644 index 000000000..b779a6667 --- /dev/null +++ b/ptypy/accelerate/base/engines/ML_serial.py @@ -0,0 +1,541 @@ +# -*- coding: utf-8 -*- +""" +Maximum Likelihood reconstruction engine. + +TODO. + + * Implement other regularizers + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import numpy as np +import time + +from ptypy.engines.ML import ML, BaseModel +from .projectional_serial import serialize_array_access +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines.utils import Cnorm2, Cdot +from ptypy.engines import register +from ptypy.accelerate.base.kernels import GradientDescentKernel, AuxiliaryWaveKernel, PoUpdateKernel, PositionCorrectionKernel +from ptypy.accelerate.base import address_manglers + + +__all__ = ['ML_serial'] + +@register() +class ML_serial(ML): + + def __init__(self, ptycho_parent, pars=None): + """ + Maximum likelihood reconstruction engine. + """ + super(ML_serial, self).__init__(ptycho_parent, pars) + + self.kernels = {} + self.diff_info = {} + self.cn2_ob_grad = 0. + self.cn2_pr_grad = 0. + + def engine_initialize(self): + """ + Prepare for ML reconstruction. + """ + super(ML_serial, self).engine_initialize() + self._setup_kernels() + + def _initialize_model(self): + + # Create noise model + if self.p.ML_type.lower() == "gaussian": + self.ML_model = GaussianModel(self) + elif self.p.ML_type.lower() == "poisson": + raise NotImplementedError('Poisson norm model not yet implemented') + elif self.p.ML_type.lower() == "euclid": + raise NotImplementedError('Euclid norm model not yet implemented') + else: + raise RuntimeError("Unsupported ML_type: '%s'" % self.p.ML_type) + + def _setup_kernels(self): + """ + Setup kernels, one for each scan. Derive scans from ptycho class + """ + # get the scans + for label, scan in self.ptycho.model.scans.items(): + + kern = u.Param() + kern.scanmodel = type(scan).__name__ + self.kernels[label] = kern + + # TODO: needs to be adapted for broad bandwidth + geo = scan.geometries[0] + + # Get info to shape buffer arrays + fpc = scan.max_frames_per_block + + # TODO : make this more foolproof + try: + nmodes = scan.p.coherence.num_probe_modes + except: + nmodes = 1 + + # create buffer arrays + ash = (fpc * nmodes,) + tuple(geo.shape) + aux = np.zeros(ash, dtype=np.complex64) + kern.aux = aux + kern.a = np.zeros(ash, dtype=np.complex64) + kern.b = np.zeros(ash, dtype=np.complex64) + + # setup kernels, one for each SCAN. + kern.GDK = GradientDescentKernel(aux, nmodes) + kern.GDK.allocate() + + kern.POK = PoUpdateKernel() + kern.POK.allocate() + + kern.AWK = AuxiliaryWaveKernel() + kern.AWK.allocate() + + kern.FW = geo.propagator.fw + kern.BW = geo.propagator.bw + kern.resolution = geo.resolution[0] + + if self.do_position_refinement: + kern.PCK = PositionCorrectionKernel(aux, nmodes, self.p.position_refinement, geo.resolution) + kern.PCK.allocate() + + def engine_prepare(self): + + ## Serialize new data ## + + for label, d in self.ptycho.new_data: + prep = u.Param() + prep.label = label + self.diff_info[d.ID] = prep + prep.err_phot = np.zeros((d.data.shape[0],), dtype=np.float32) + # set floating intensity coefficients to 1.0 + # they get overridden if self.p.floating_intensities=True + prep.float_intens_coeff = np.ones((d.data.shape[0],), dtype=np.float32) + + # Unfortunately this needs to be done for all pods, since + # the shape of the probe / object was modified. + # TODO: possible scaling issue + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + prep.view_IDs, prep.poe_IDs, prep.addr = serialize_array_access(d) + # Re-create exit addresses when gradient models (single exit buffer per view) are used + # TODO: this should not be necessary, kernels should not use exit wave information + if self.kernels[prep.label].scanmodel in ("GradFull", "BlockGradFull"): + for i,addr in enumerate(prep.addr): + nmodes = len(addr[:,2,0]) + for j,ea in enumerate(addr[:,2,0]): + prep.addr[i,j,2,0] = i*nmodes+j + prep.I = d.data + if self.do_position_refinement: + prep.original_addr = np.zeros_like(prep.addr) + prep.original_addr[:] = prep.addr + prep.ma = self.ma.S[d.ID].data + + self.ML_model.prepare() + + def _get_smooth_gradient(self, data, sigma): + return self.smooth_gradient(data) + + def _replace_ob_grad(self): + new_ob_grad = self.ob_grad_new + # Smoothing preconditioner + if self.smooth_gradient: + self.smooth_gradient.sigma *= (1. - self.p.smooth_gradient_decay) + for name, s in new_ob_grad.storages.items(): + s.data[:] = self._get_smooth_gradient(s.data, self.smooth_gradient.sigma) + + norm = Cnorm2(new_ob_grad) + dot = np.real(Cdot(new_ob_grad, self.ob_grad)) + self.ob_grad << new_ob_grad + return norm, dot + + def _replace_pr_grad(self): + new_pr_grad = self.pr_grad_new + # probe support + if self.p.probe_update_start <= self.curiter: + # Apply probe support if needed + for name, s in new_pr_grad.storages.items(): + self.support_constraint(s) + else: + new_pr_grad.fill(0.) + + norm = Cnorm2(new_pr_grad) + dot = np.real(Cdot(new_pr_grad, self.pr_grad)) + self.pr_grad << new_pr_grad + return norm, dot + + def engine_iterate(self, num=1): + """ + Compute `num` iterations. + """ + ######################## + # Compute new gradient + ######################## + tg = 0. + tc = 0. + ta = time.time() + for it in range(num): + t1 = time.time() + error_dct = self.ML_model.new_grad() + tg += time.time() - t1 + + cn2_new_pr_grad, cdotr_pr_grad = self._replace_pr_grad() + cn2_new_ob_grad, cdotr_ob_grad = self._replace_ob_grad() + + # probe/object rescaling + if self.p.scale_precond: + if cn2_new_pr_grad > 1e-5: + scale_p_o = (self.p.scale_probe_object * cn2_new_ob_grad + / cn2_new_pr_grad) + else: + scale_p_o = self.p.scale_probe_object + if self.scale_p_o is None: + self.scale_p_o = scale_p_o + else: + self.scale_p_o = self.scale_p_o ** self.scale_p_o_memory + self.scale_p_o *= scale_p_o ** (1 - self.scale_p_o_memory) + logger.debug('Scale P/O: %6.3g' % scale_p_o) + else: + self.scale_p_o = self.p.scale_probe_object + + ############################ + # Compute next conjugate + ############################ + if self.curiter == 0: + bt = 0. + else: + bt_num = (self.scale_p_o * (cn2_new_pr_grad - cdotr_pr_grad) + (cn2_new_ob_grad - cdotr_ob_grad)) + + bt_denom = self.scale_p_o * self.cn2_pr_grad + self.cn2_ob_grad + + bt = max(0, bt_num / bt_denom) + + # logger.info('Polak-Ribiere coefficient: %f ' % bt) + + self.cn2_ob_grad = cn2_new_ob_grad + self.cn2_pr_grad = cn2_new_pr_grad + + dt = self.ptycho.FType + # 3. Next conjugate + self.ob_h *= dt(bt / self.tmin) + + # Smoothing preconditioner + if self.smooth_gradient: + for name, s in self.ob_h.storages.items(): + s.data[:] -= self._get_smooth_gradient(self.ob_grad.storages[name].data, self.smooth_gradient.sigma) + else: + self.ob_h -= self.ob_grad + + self.pr_h *= dt(bt / self.tmin) + self.pr_grad *= dt(self.scale_p_o) + self.pr_h -= self.pr_grad + + # In principle, the way things are now programmed this part + # could be iterated over in a real Newton-Raphson style. + t2 = time.time() + B = self.ML_model.poly_line_coeffs(self.ob_h, self.pr_h) + tc += time.time() - t2 + + if np.isinf(B).any() or np.isnan(B).any(): + logger.warning( + 'Warning! inf or nan found! Trying to continue...') + B[np.isinf(B)] = 0. + B[np.isnan(B)] = 0. + + self.tmin = dt(-.5 * B[1] / B[2]) + self.ob_h *= self.tmin + self.pr_h *= self.tmin + self.ob += self.ob_h + self.pr += self.pr_h + # Newton-Raphson loop would end here + + # Refine the scan positions + self.position_update() + + # Allow for customized modifications at the end of each iteration + self._post_iterate_update() + + # increase iteration counter + self.curiter += 1 + + logger.info('Time spent in gradient calculation: %.2f' % tg) + logger.info(' .... in coefficient calculation: %.2f' % tc) + return error_dct # np.array([[self.ML_model.LL[0]] * 3]) + + def position_update(self): + """ + Position refinement + """ + if not self.do_position_refinement: + return + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + log(4, "----------- START POS REF -------------") + for dID in self.di.S.keys(): + + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + ob = self.ob.S[oID].data + pr = self.pr.S[pID].data + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr + original_addr = prep.original_addr + mangled_addr = addr.copy() + w = prep.weights + I = prep.I + err_phot = prep.err_phot + + PCK = kern.PCK + FW = kern.FW + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We need to re-calculate the current error + PCK.build_aux(aux, addr, ob, pr) + aux[:] = FW(aux) + PCK.log_likelihood_ml(aux, addr, I, w, err_phot) + error_state = np.zeros_like(err_phot) + error_state[:] = err_phot + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + + log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + for i in range(PCK.mangler.nshifts): + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + aux[:] = FW(aux) + PCK.log_likelihood_ml(aux, mangled_addr, I, w, err_phot) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_phot) + + prep.err_phot = error_state + prep.addr = addr + + def engine_finalize(self): + """ + try deleting ever helper contianer + """ + if self.do_position_refinement and self.p.position_refinement.record: + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + res = self.kernels[prep.label].resolution + for i,view in enumerate(d.views): + for j,(pname, pod) in enumerate(view.pods.items()): + delta = (prep.addr[i][j][1][1:] - prep.original_addr[i][j][1][1:]) * res + pod.ob_view.coord += delta + pod.ob_view.storage.update_views(pod.ob_view) + self.ptycho.record_positions = True + + # Save floating intensities into runtime + float_intens_coeff = {} + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + float_intens_coeff[label] = prep.float_intens_coeff + self.ptycho.runtime["float_intens"] = parallel.gather_dict(float_intens_coeff) + + +class BaseModelSerial(BaseModel): + """ + Base class for log-likelihood models. + """ + + def __del__(self): + """ + Clean up routine + """ + pass + + +class GaussianModel(BaseModelSerial): + """ + Gaussian noise model. + TODO: feed actual statistical weights instead of using the Poisson statistic heuristic. + """ + + def __init__(self, MLengine): + """ + Core functions for ML computation using a Gaussian model. + """ + super(GaussianModel, self).__init__(MLengine) + + def prepare(self): + + super(GaussianModel, self).prepare() + + for label, d in self.engine.ptycho.new_data: + prep = self.engine.diff_info[d.ID] + prep.weights = (self.Irenorm * self.engine.ma.S[d.ID].data + / (1. / self.Irenorm + d.data)).astype(d.data.dtype) + + def __del__(self): + """ + Clean up routine + """ + super(GaussianModel, self).__del__() + + def new_grad(self): + """ + Compute a new gradient direction according to a Gaussian noise model. + + Note: The negative log-likelihood and local errors are also computed + here. + """ + ob_grad = self.engine.ob_grad_new + pr_grad = self.engine.pr_grad_new + ob_grad << 0. + pr_grad << 0. + + # We need an array for MPI + LL = np.array([0.]) + error_dct = {} + + for dID in self.di.S.keys(): + prep = self.engine.diff_info[dID] + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.engine.kernels[prep.label] + GDK = kern.GDK + AWK = kern.AWK + POK = kern.POK + + aux = kern.aux + + FW = kern.FW + BW = kern.BW + + # get addresses and auxilliary array + addr = prep.addr + w = prep.weights + err_phot = prep.err_phot + fic = prep.float_intens_coeff + + # local references + ob = self.engine.ob.S[oID].data + obg = ob_grad.S[oID].data + pr = self.engine.pr.S[pID].data + prg = pr_grad.S[pID].data + I = prep.I + + # make propagated exit (to buffer) + AWK.build_aux_no_ex(aux, addr, ob, pr, add=False) + + # forward prop + aux[:] = FW(aux) + + GDK.make_model(aux, addr) + + if self.p.floating_intensities: + GDK.floating_intensity(addr, w, I, fic) + + GDK.main(aux, addr, w, I) + GDK.error_reduce(addr, err_phot) + aux[:] = BW(aux) + + POK.ob_update_ML(addr, obg, pr, aux) + POK.pr_update_ML(addr, prg, ob, aux) + + for dID, prep in self.engine.diff_info.items(): + err_phot = prep.err_phot + LL += err_phot.sum() + err_phot /= np.prod(prep.weights.shape[-2:]) + err_fourier = np.zeros_like(err_phot) + err_exit = np.zeros_like(err_phot) + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error_dct.update(zip(prep.view_IDs, errs)) + + # MPI reduction of gradients + ob_grad.allreduce() + pr_grad.allreduce() + parallel.allreduce(LL) + + # Object regularizer + if self.regularizer: + for name, s in self.engine.ob.storages.items(): + ob_grad.storages[name].data += self.regularizer.grad(s.data) + LL += self.regularizer.LL + + self.LL = LL / self.tot_measpts + return error_dct + + def poly_line_coeffs(self, c_ob_h, c_pr_h): + """ + Compute the coefficients of the polynomial for line minimization + in direction h + """ + + B = np.zeros((3,), dtype=np.longdouble) + Brenorm = 1. / self.LL[0] ** 2 + + # Outer loop: through diffraction patterns + for dID in self.di.S.keys(): + prep = self.engine.diff_info[dID] + + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.engine.kernels[prep.label] + GDK = kern.GDK + AWK = kern.AWK + + f = kern.aux + a = kern.a + b = kern.b + + FW = kern.FW + + # get addresses and auxilliary array + addr = prep.addr + w = prep.weights + fic = prep.float_intens_coeff + + # local references + ob = self.ob.S[oID].data + ob_h = c_ob_h.S[oID].data + pr = self.pr.S[pID].data + pr_h = c_pr_h.S[pID].data + I = self.di.S[dID].data + + # make propagated exit (to buffer) + AWK.build_aux_no_ex(f, addr, ob, pr, add=False) + AWK.build_aux_no_ex(a, addr, ob_h, pr, add=False) + AWK.build_aux_no_ex(a, addr, ob, pr_h, add=True) + AWK.build_aux_no_ex(b, addr, ob_h, pr_h, add=False) + + # forward prop + f[:] = FW(f) + a[:] = FW(a) + b[:] = FW(b) + + GDK.make_a012(f, a, b, addr, I, fic) + GDK.fill_b(addr, Brenorm, w, B) + + parallel.allreduce(B) + + # Object regularizer + if self.regularizer: + for name, s in self.ob.storages.items(): + B += Brenorm * self.regularizer.poly_line_coeffs( + c_ob_h.storages[name].data, s.data) + + self.B = B + + return B diff --git a/ptypy/accelerate/base/engines/__init__.py b/ptypy/accelerate/base/engines/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ptypy/accelerate/base/engines/projectional_serial.py b/ptypy/accelerate/base/engines/projectional_serial.py new file mode 100644 index 000000000..0816c7049 --- /dev/null +++ b/ptypy/accelerate/base/engines/projectional_serial.py @@ -0,0 +1,614 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import numpy as np +import time + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines import register +from ptypy.engines.projectional import _ProjectionEngine, DMMixin, RAARMixin +from ptypy.accelerate.base.kernels import FourierUpdateKernel, AuxiliaryWaveKernel, PoUpdateKernel, PositionCorrectionKernel +from ptypy.accelerate.base import array_utils as au + + +### TODOS +# +# - The Propagator needs to be made somewhere else +# - Get it running faster with MPI (partial sync) +# - implement "batching" when processing frames to lower the pressure on memory +# - Be smarter about the engine.prepare() part +# - Propagator needs to be reconfigurable for a certain batch size, gpyfft hates that. +# - Fourier_update_kernel needs to allow batched execution + + +__all__ = ['DM_serial', 'RAAR_serial'] + +parallel = u.parallel + + +def gaussian_kernel(sigma, size=None, sigma_y=None, size_y=None): + size = int(size) + sigma = np.float(sigma) + if not size_y: + size_y = size + if not sigma_y: + sigma_y = sigma + + x, y = np.mgrid[-size:size + 1, -size_y:size_y + 1] + + g = np.exp(-(x ** 2 / (2 * sigma ** 2) + y ** 2 / (2 * sigma_y ** 2))) + return g / g.sum() + + +def serialize_array_access(diff_storage): + # Sort views according to layer in diffraction stack + views = diff_storage.views + dlayers = [view.dlayer for view in views] + views = [views[i] for i in np.argsort(dlayers)] + view_IDs = [view.ID for view in views] + + # Master pod + mpod = views[0].pod + + # Determine linked storages for probe, object and exit waves + pr = mpod.pr_view.storage + ob = mpod.ob_view.storage + ex = mpod.ex_view.storage + + poe_ID = (pr.ID, ob.ID, ex.ID) + + addr = [] + for view in views: + address = [] + + for pname, pod in view.pods.items(): + ## store them for each pod + # create addresses + a = np.array( + [(pod.pr_view.dlayer, pod.pr_view.dlow[0], pod.pr_view.dlow[1]), + (pod.ob_view.dlayer, pod.ob_view.dlow[0], pod.ob_view.dlow[1]), + (pod.ex_view.dlayer, pod.ex_view.dlow[0], pod.ex_view.dlow[1]), + (pod.di_view.dlayer, pod.di_view.dlow[0], pod.di_view.dlow[1]), + (pod.ma_view.dlayer, pod.ma_view.dlow[0], pod.ma_view.dlow[1])]) + + address.append(a) + + if pod.pr_view.storage.ID != pr.ID: + log(1, "Splitting probes for one diffraction stack is not supported in " + __name__) + if pod.ob_view.storage.ID != ob.ID: + log(1, "Splitting objects for one diffraction stack is not supported in " + __name__) + if pod.ex_view.storage.ID != ex.ID: + log(1, "Splitting exit stacks for one diffraction stack is not supported in " + __name__) + + ## store data for each view + # adresses + addr.append(address) + + # store them for each storage + return view_IDs, poe_ID, np.array(addr).astype(np.int32) + + +class _ProjectionEngine_serial(_ProjectionEngine): + """ + A full-fledged Difference Map engine that uses numpy arrays instead of iteration. + + """ + + def __init__(self, ptycho_parent, pars=None): + """ + Difference map reconstruction engine. + """ + + super().__init__(ptycho_parent, pars) + + ## gaussian filter + # dummy kernel + """ + if not self.p.obj_smooth_std: + gauss_kernel = gaussian_kernel(1,1).astype(np.float32) + else: + gauss_kernel = gaussian_kernel(self.p.obj_smooth_std,self.p.obj_smooth_std).astype(np.float32) + + kernel_pars = {'kernel_sh_x' : gauss_kernel.shape[0], 'kernel_sh_y': gauss_kernel.shape[1]} + """ + self.benchmark = u.Param() + + # Stores all information needed with respect to the diffraction storages. + self.diff_info = {} + self.ob_cfact = {} + self.pr_cfact = {} + self.kernels = {} + + def engine_initialize(self): + """ + Prepare for reconstruction. + """ + + super().engine_initialize() + self._reset_benchmarks() + self._setup_kernels() + + def _reset_benchmarks(self): + self.benchmark.A_Build_aux = 0. + self.benchmark.B_Prop = 0. + self.benchmark.C_Fourier_update = 0. + self.benchmark.D_iProp = 0. + self.benchmark.E_Build_exit = 0. + self.benchmark.F_LLerror = 0. + self.benchmark.probe_update = 0. + self.benchmark.object_update = 0. + self.benchmark.calls_fourier = 0 + self.benchmark.calls_object = 0 + self.benchmark.calls_probe = 0 + + def _setup_kernels(self): + """ + Setup kernels, one for each scan. Derive scans from ptycho class + """ + # get the scans + for label, scan in self.ptycho.model.scans.items(): + + kern = u.Param() + kern.scanmodel = type(scan).__name__ + self.kernels[label] = kern + + # TODO: needs to be adapted for broad bandwidth + geo = scan.geometries[0] + + # Get info to shape buffer arrays + fpc = scan.max_frames_per_block + + # TODO : make this more foolproof + try: + nmodes = scan.p.coherence.num_probe_modes * \ + scan.p.coherence.num_object_modes + except: + nmodes = 1 + + # create buffer arrays + ash = (fpc * nmodes,) + tuple(geo.shape) + aux = np.zeros(ash, dtype=np.complex64) + kern.aux = aux + + # setup kernels, one for each SCAN. + kern.FUK = FourierUpdateKernel(aux, nmodes) + kern.FUK.allocate() + + kern.POK = PoUpdateKernel() + kern.POK.allocate() + + kern.AWK = AuxiliaryWaveKernel() + kern.AWK.allocate() + + kern.FW = geo.propagator.fw + kern.BW = geo.propagator.bw + kern.resolution = geo.resolution[0] + + if self.do_position_refinement: + kern.PCK = PositionCorrectionKernel(aux, nmodes, self.p.position_refinement, geo.resolution) + kern.PCK.allocate() + + def engine_prepare(self): + + super().engine_prepare() + + ## Serialize new data ## + + for label, d in self.ptycho.new_data: + prep = u.Param() + + prep.label = label + self.diff_info[d.ID] = prep + prep.mag = np.sqrt(np.abs(d.data)) + prep.ma = self.ma.S[d.ID].data.astype(np.float32) + # self.ma.S[d.ID].data = prep.ma + prep.ma_sum = prep.ma.sum(-1).sum(-1) + prep.err_phot = np.zeros_like(prep.ma_sum) + prep.err_fourier = np.zeros_like(prep.ma_sum) + prep.err_exit = np.zeros_like(prep.ma_sum) + + # Unfortunately this needs to be done for all pods, since + # the shape of the probe / object was modified. + # TODO: possible scaling issue, remove the need for padding + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + prep.view_IDs, prep.poe_IDs, prep.addr = serialize_array_access(d) + if self.do_position_refinement: + prep.original_addr = np.zeros_like(prep.addr) + prep.original_addr[:] = prep.addr + pID, oID, eID = prep.poe_IDs + + # calculate c_facts + cfact = self.p.object_inertia * self.mean_power + self.ob_cfact[oID] = cfact / u.parallel.size + + pr = self.pr.S[pID] + cfact = self.p.probe_inertia * len(pr.views) / pr.data.shape[0] + self.pr_cfact[pID] = cfact / u.parallel.size + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + + for it in range(num): + + error = {} + + for dID in self.di.S.keys(): + + # find probe, object and exit ID in dependence of dID + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + FW = kern.FW + BW = kern.BW + + # get addresses and buffers + addr = prep.addr + mag = prep.mag + ma_sum = prep.ma_sum + err_phot = prep.err_phot + err_fourier = prep.err_fourier + err_exit = prep.err_exit + pbound = self.pbound_scan[prep.label] + aux = kern.aux + + # local references + ma = prep.ma + ob = self.ob.S[oID].data + pr = self.pr.S[pID].data + ex = self.ex.S[eID].data + + ## compute log-likelihood + if self.p.compute_log_likelihood: + t1 = time.time() + AWK.build_aux_no_ex(aux, addr, ob, pr) + aux[:] = FW(aux) + FUK.log_likelihood(aux, addr, mag, ma, err_phot) + self.benchmark.F_LLerror += time.time() - t1 + + ## build auxilliary wave + t1 = time.time() + AWK.make_aux(aux, addr, ob, pr, ex, c_po=self._c, c_e=1-self._c) + self.benchmark.A_Build_aux += time.time() - t1 + + ## forward FFT + t1 = time.time() + aux[:] = FW(aux) + self.benchmark.B_Prop += time.time() - t1 + + ## Deviation from measured data + t1 = time.time() + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + FUK.fmag_all_update(aux, addr, mag, ma, err_fourier, pbound) + self.benchmark.C_Fourier_update += time.time() - t1 + + ## backward FFT + t1 = time.time() + aux[:] = BW(aux) + self.benchmark.D_iProp += time.time() - t1 + + ## build exit wave + t1 = time.time() + AWK.make_exit(aux, addr, ob, pr, ex, c_a=self._b, c_po=self._a, c_e=-(self._a+self._b)) + FUK.exit_error(aux,addr) + FUK.error_reduce(addr, err_exit) + self.benchmark.E_Build_exit += time.time() - t1 + + # update errors + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error.update(zip(prep.view_IDs, errs)) + + self.benchmark.calls_fourier += 1 + + parallel.barrier() + + sync = (self.curiter % 1 == 0) + self.overlap_update(MPI=True) + + # Recenter the probe + self.center_probe() + + parallel.barrier() + + self.position_update() + + self.curiter += 1 + + self.error = error + return error + + def position_update(self): + """ + Position refinement + """ + if not self.do_position_refinement: + return + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + log(4, "----------- START POS REF -------------") + for dID in self.di.S.keys(): + + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + ma = self.ma.S[dID].data + ob = self.ob.S[oID].data + pr = self.pr.S[pID].data + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr + original_addr = prep.original_addr + mangled_addr = addr.copy() + mag = prep.mag + ma_sum = prep.ma_sum + err_fourier = prep.err_fourier + + PCK = kern.PCK + FW = kern.FW + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We need to re-calculate the current error + PCK.build_aux(aux, addr, ob, pr) + aux[:] = FW(aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, addr, mag, ma, ma_sum) + PCK.error_reduce(addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, addr, mag, ma, err_fourier) + error_state = np.zeros_like(err_fourier) + error_state[:] = err_fourier + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + + log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + for i in range(PCK.mangler.nshifts): + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + aux[:] = FW(aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, mangled_addr, mag, ma, ma_sum) + PCK.error_reduce(mangled_addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, mangled_addr, mag, ma, err_fourier) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_fourier) + + prep.err_fourier = error_state + prep.addr = addr + + + def overlap_update(self, MPI=True): + """ + DM overlap constraint update. + """ + change = 1. + # Condition to update probe + do_update_probe = (self.p.probe_update_start <= self.curiter) + + for inner in range(self.p.overlap_max_iterations): + prestr = '%d Iteration (Overlap) #%02d: ' % (parallel.rank, inner) + # Update object first + if self.p.update_object_first or (inner > 0): + # Update object + log(4, prestr + '----- object update -----', True) + self.object_update(MPI=(parallel.size > 1 and MPI)) + + # Exit if probe should not yet be updated + if not do_update_probe: break + + # Update probe + log(4, prestr + '----- probe update -----', True) + change = self.probe_update(MPI=(parallel.size > 1 and MPI)) + log(4, prestr + 'change in probe is %.3f' % change, True) + + # stop iteration if probe change is small + if change < self.p.overlap_converge_factor: break + + + ## object update + def object_update(self, MPI=False): + t1 = time.time() + + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + cfact = self.p.object_inertia * self.mean_power + + if self.p.obj_smooth_std is not None: + log(4, 'Smoothing object, cfact is %.2f' % cfact) + smooth_mfs = [self.p.obj_smooth_std, self.p.obj_smooth_std] + ob.data = cfact * au.complex_gaussian_filter(ob.data, smooth_mfs) + else: + ob.data *= cfact + + obn.data[:] = cfact + + # storage for-loop + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + POK = self.kernels[prep.label].POK + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for loop + ev = POK.ob_update(prep.addr, + self.ob.S[oID].data, + self.ob_nrm.S[oID].data, + self.pr.S[pID].data, + self.ex.S[eID].data) + + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + # MPI test + if MPI: + parallel.allreduce(ob.data) + parallel.allreduce(obn.data) + ob.data /= obn.data + else: + ob.data /= obn.data + + # Clip object (This call takes like one ms. Not time critical) + if self.p.clip_object is not None: + clip_min, clip_max = self.p.clip_object + ampl_obj = np.abs(ob.data) + phase_obj = np.exp(1j * np.angle(ob.data)) + too_high = (ampl_obj > clip_max) + too_low = (ampl_obj < clip_min) + ob.data[too_high] = clip_max * phase_obj[too_high] + ob.data[too_low] = clip_min * phase_obj[too_low] + + self.benchmark.object_update += time.time() - t1 + self.benchmark.calls_object += 1 + + ## probe update + def probe_update(self, MPI=False): + t1 = time.time() + + # storage for-loop + change = 0 + + for pID, pr in self.pr.storages.items(): + prn = self.pr_nrm.S[pID] + cfact = self.pr_cfact[pID] + pr.data *= cfact + prn.data.fill(cfact) + + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + POK = self.kernels[prep.label].POK + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for-loop + ev = POK.pr_update(prep.addr, + self.pr.S[pID].data, + self.pr_nrm.S[pID].data, + self.ob.S[oID].data, + self.ex.S[eID].data) + + self.benchmark.probe_update += time.time() - t1 + self.benchmark.calls_probe += 1 + + for pID, pr in self.pr.storages.items(): + + buf = self.pr_buf.S[pID] + prn = self.pr_nrm.S[pID] + + # MPI test + if MPI: + # if False: + parallel.allreduce(pr.data) + parallel.allreduce(prn.data) + pr.data /= prn.data + else: + pr.data /= prn.data + + self.support_constraint(pr) + + change += u.norm2(pr.data - buf.data) / u.norm2(pr.data) + buf.data[:] = pr.data + if MPI: + change = parallel.allreduce(change) / parallel.size + + return np.sqrt(change) + + def engine_finalize(self, benchmark=True): + """ + try deleting ever helper contianer + """ + if parallel.master and benchmark: + print("----- BENCHMARKS ----") + acc = 0. + for name in sorted(self.benchmark.keys()): + t = self.benchmark[name] + if name[0] in 'ABCDEFGHI': + print('%20s : %1.3f ms per iteration' % (name, t / self.benchmark.calls_fourier * 1000)) + acc += t + elif str(name) == 'probe_update': + print('%20s : %1.3f ms per call. %d calls' % ( + name, t / self.benchmark.calls_probe * 1000, self.benchmark.calls_probe)) + elif str(name) == 'object_update': + print('%20s : %1.3f ms per call. %d calls' % ( + name, t / self.benchmark.calls_object * 1000, self.benchmark.calls_object)) + + print('%20s : %1.3f ms per iteration. %d calls' % ( + 'Fourier_total', acc / self.benchmark.calls_fourier * 1000, self.benchmark.calls_fourier)) + + self._reset_benchmarks() + + if self.do_position_refinement and self.p.position_refinement.record: + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + res = self.kernels[prep.label].resolution + for i,view in enumerate(d.views): + for j,(pname, pod) in enumerate(view.pods.items()): + delta = (prep.addr[i][j][1][1:] - prep.original_addr[i][j][1][1:]) * res + pod.ob_view.coord += delta + pod.ob_view.storage.update_views(pod.ob_view) + self.ptycho.record_positions = True + + super().engine_finalize() + + +@register() +class DM_serial(_ProjectionEngine_serial, DMMixin): + """ + A full-fledged Difference Map engine serialized. + + Defaults: + + [name] + default = DM_serial + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _ProjectionEngine_serial.__init__(self, ptycho_parent, pars) + DMMixin.__init__(self, self.p.alpha) + ptycho_parent.citations.add_article(**self.article) + + +@register() +class RAAR_serial(_ProjectionEngine_serial, RAARMixin): + """ + A RAAR engine. + + Defaults: + + [name] + default = RAAR_serial + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + + _ProjectionEngine_serial.__init__(self, ptycho_parent, pars) + RAARMixin.__init__(self, self.p.beta) diff --git a/ptypy/accelerate/base/engines/projectional_serial_stream.py b/ptypy/accelerate/base/engines/projectional_serial_stream.py new file mode 100644 index 000000000..1d5784700 --- /dev/null +++ b/ptypy/accelerate/base/engines/projectional_serial_stream.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +# from .. import core +from __future__ import division + +import numpy as np +import time + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines import register +from .projectional_serial import DM_serial + +### TODOS +# +# - The Propagator needs to be made somewhere else +# - Get it running faster with MPI (partial sync) +# - implement "batching" when processing frames to lower the pressure on memory +# - Be smarter about the engine.prepare() part +# - Propagator needs to be reconfigurable for a certain batch size, gpyfft hates that. +# - Fourier_update_kernel needs to allow batched execution + + +__all__ = ['DM_serial_stream'] + +parallel = u.parallel +MPI = (parallel.size > 1) + + +@register() +class DM_serial_stream(DM_serial): + """ + A full-fledged Difference Map engine that uses numpy arrays instead of iteration. + """ + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + + for it in range(num): + + error = {} + + for inner in range(self.p.overlap_max_iterations): + + change = 0 + + do_update_probe = (self.curiter >= self.p.probe_update_start) + do_update_object = (self.p.update_object_first or (inner > 0) or not do_update_probe) + do_update_fourier = (inner == 0) + + # initialize probe and object buffer to receive an update + if do_update_object: + for oID, ob in self.ob.storages.items(): + cfact = self.ob_cfact[oID] + obn = self.ob_nrm.S[oID] + obb = self.ob_buf.S[oID] + """ + if self.p.obj_smooth_std is not None: + logger.info('Smoothing object, cfact is %.2f' % cfact) + t2 = time.time() + self.prg.gaussian_filter(queue, (info[3],info[4]), None, obj_gpu.data, self.gauss_kernel_gpu.data) + queue.finish() + obj_gpu *= cfact + print 'gauss: ' + str(time.time()-t2) + else: + obj_gpu *= cfact + """ + obb.data[:] = ob.data + obb.data *= cfact + obn.data[:] = cfact + + # First cycle: Fourier + object update + for dID in self.di.S.keys(): + t1 = time.time() + + prep = self.diff_info[dID] + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + POK = kern.POK + + pbound = self.pbound_scan[prep.label] + aux = kern.aux + FW = kern.FW + BW = kern.BW + + # get addresses and auxilliary array + addr = prep.addr + mag = prep.mag + ma_sum = prep.ma_sum + err_fourier = prep.err_fourier + + # local references + ma = self.ma.S[dID].data + ob = self.ob.S[oID].data + obn = self.ob_nrm.S[oID].data + obb = self.ob_buf.S[oID].data + pr = self.pr.S[pID].data + ex = self.ex.S[eID].data + + # Fourier update. + if do_update_fourier: + log(4, '----- Fourier update -----', True) + t1 = time.time() + AWK.make_aux(aux, addr, ob, pr, ex, c_po=self._c, c_e=1-self._c) + self.benchmark.A_Build_aux += time.time() - t1 + + ## FFT + t1 = time.time() + aux[:] = FW(aux) + self.benchmark.B_Prop += time.time() - t1 + + ## Deviation from measured data + t1 = time.time() + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + FUK.fmag_all_update(aux, addr, mag, ma, err_fourier, pbound) + self.benchmark.C_Fourier_update += time.time() - t1 + + t1 = time.time() + aux[:] = BW(aux) + self.benchmark.D_iProp += time.time() - t1 + + ## apply changes #2 + t1 = time.time() + AWK.make_exit(aux, addr, ob, pr, ex, c_a=self._b, c_po=self._a, c_e=-(self._a+self._b)) + self.benchmark.E_Build_exit += time.time() - t1 + + err_phot = np.zeros_like(err_fourier) + err_exit = np.zeros_like(err_fourier) + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error.update(zip(prep.view_IDs, errs)) + + self.benchmark.calls_fourier += 1 + + parallel.barrier() + + prestr = '%d Iteration (Overlap) #%02d: ' % (parallel.rank, inner) + + # Update object + if do_update_object: + # Update object + log(4, prestr + '----- object update -----', True) + t1 = time.time() + + # scan for loop + ev = POK.ob_update(addr, obb, obn, pr, ex) + + self.benchmark.object_update += time.time() - t1 + self.benchmark.calls_object += 1 + + if do_update_object: + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + obb = self.ob_buf.S[oID] + # MPI test + if MPI: + parallel.allreduce(obb.data) + parallel.allreduce(obn.data) + obb.data /= obn.data + else: + obb.data /= obn.data + + self.clip_object(obb) + ob.data[:] = obb.data + + # Exit if probe should not yet be updated + if not do_update_probe: + break + + # Update probe + log(4, prestr + '----- probe update -----', True) + change = self.probe_update(MPI=MPI) + # change = self.probe_update(MPI=(parallel.size>1 and MPI)) + + log(4, prestr + 'change in probe is %.3f' % change, True) + + # stop iteration if probe change is small + if change < self.p.overlap_converge_factor: break + """ + # Exit if probe should not yet be updated + if do_update_probe: + # Update probe + log(4, prestr + '----- probe update -----', True) + + # init probe + for pID, pr in self.pr.storages.items(): + prn = self.pr_nrm.S[pID] + cfact = self.pr_cfact[pID] + pr.data[:] = pr.data + prn.data.fill(cfact) + + # second cycle + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + POK = kern.POK + + # get addresses and auxilliary array + addr = prep.addr + + # local references + ob = self.ob.S[oID].data + pr = self.pr.S[pID].data + ex = self.ex.S[eID].data + prn = self.pr_nrm.S[pID].data + + t1 = time.time() + + # scan for-loop + ev = POK.pr_update(addr, pr, prn, ob, ex) + + self.benchmark.probe_update += time.time() - t1 + self.benchmark.calls_probe += 1 + + # synchronize + for pID, pr in self.pr.storages.items(): + + prn = self.pr_nrm.S[pID] + buf = self.pr_buf.S[pID] + # MPI test + if MPI: + # if False: + parallel.allreduce(pr.data) + parallel.allreduce(prn.data) + pr.data /= prn.data + else: + pr.data /= prn.data + + self.support_constraint(pr) + + change += u.norm2(pr.data - buf.data) / u.norm2(buf.data) + buf.data[:] = pr.data + if MPI: + change = parallel.allreduce(change) / parallel.size + + change = np.sqrt(change) + + log(4, prestr + 'change in probe is %.3f' % change, True) + + # stop iteration if probe change is small + if change < self.p.overlap_converge_factor: break + """ + + parallel.barrier() + self.curiter += 1 + + self.error = error + return error diff --git a/ptypy/accelerate/base/engines/stochastic.py b/ptypy/accelerate/base/engines/stochastic.py new file mode 100644 index 000000000..f3b7f897e --- /dev/null +++ b/ptypy/accelerate/base/engines/stochastic.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- +""" +Serialized stochastic reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import numpy as np +import time + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy import defaults_tree +from ptypy.engines import register +from ptypy.engines.stochastic import _StochasticEngine, EPIEMixin, SDRMixin +#from ptypy.core.manager import Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull +from ptypy.accelerate.base.engines import projectional_serial +from ptypy.accelerate.base.kernels import FourierUpdateKernel, AuxiliaryWaveKernel, PoUpdateKernel, PositionCorrectionKernel +from ptypy.accelerate.base import address_manglers +from ptypy.accelerate.base import array_utils as au + +__all__ = ["EPIE_serial", "SDR_serial"] + +class _StochasticEngineSerial(_StochasticEngine): + """ + A serialized base implementation of a stochastic algorithm for ptychography + + Defaults: + + [compute_exit_error] + default = False + type = bool + help = A switch for computing the exitwave error (this can impact the performance of the engine) + + [compute_fourier_error] + default = False + type = bool + help = A switch for computing the fourier error (this can impact the performance of the engine) + + """ + + #SUPPORTED_MODELS = [Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull] + + def __init__(self, ptycho_parent, pars=None): + """ + Stochastic reconstruction engine. + """ + super().__init__(ptycho_parent, pars) + + # keep track of timings + self.benchmark = u.Param() + + # Stores all information needed with respect to the diffraction storages. + self.diff_info = {} + self.ob_cfact = {} + self.pr_cfact = {} + self.kernels = {} + + def engine_initialize(self): + """ + Prepare for reconstruction. + """ + super().engine_initialize() + + self.error = [] + self._reset_benchmarks() + self._setup_kernels() + + def _reset_benchmarks(self): + self.benchmark.A_Build_aux = 0. + self.benchmark.B_Prop = 0. + self.benchmark.C_Fourier_update = 0. + self.benchmark.D_iProp = 0. + self.benchmark.E_Build_exit = 0. + self.benchmark.F_LLerror = 0. + self.benchmark.probe_update = 0. + self.benchmark.object_update = 0. + self.benchmark.calls_fourier = 0 + self.benchmark.calls_object = 0 + self.benchmark.calls_probe = 0 + + def _setup_kernels(self): + """ + Setup kernels, one for each scan. Derive scans from ptycho class + """ + # get the scans + for label, scan in self.ptycho.model.scans.items(): + + kern = u.Param() + kern.scanmodel = type(scan).__name__ + self.kernels[label] = kern + + # TODO: needs to be adapted for broad bandwidth + geo = scan.geometries[0] + + # TODO : make this more foolproof + try: + nmodes = scan.p.coherence.num_probe_modes * \ + scan.p.coherence.num_object_modes + except: + nmodes = 1 + + # create buffer arrays + ash = (nmodes,) + tuple(geo.shape) + aux = np.zeros(ash, dtype=np.complex64) + kern.aux = aux + + # setup kernels, one for each SCAN. + kern.FUK = FourierUpdateKernel(aux, nmodes) + kern.FUK.allocate() + + kern.POK = PoUpdateKernel() + kern.POK.allocate() + + kern.AWK = AuxiliaryWaveKernel() + kern.AWK.allocate() + + kern.FW = geo.propagator.fw + kern.BW = geo.propagator.bw + kern.resolution = geo.resolution[0] + + if self.do_position_refinement: + kern.PCK = PositionCorrectionKernel(aux, nmodes, self.p.position_refinement, geo.resolution) + kern.PCK.allocate() + + def engine_prepare(self): + """ + Last minute initialization. + + Everything that needs to be recalculated when new data arrives. + """ + if self.ptycho.new_data: + + # recalculate everything + mean_power = 0. + for s in self.di.storages.values(): + mean_power += s.mean_power + self.mean_power = mean_power / len(self.di.storages) + + ## Serialize new data ## + for label, d in self.ptycho.new_data: + prep = u.Param() + prep.label = label + self.diff_info[d.ID] = prep + prep.mag = np.sqrt(np.abs(d.data)) + prep.ma = self.ma.S[d.ID].data.astype(np.float32) + prep.ma_sum = prep.ma.sum(-1).sum(-1) + prep.err_phot = np.zeros_like(prep.ma_sum) + prep.err_fourier = np.zeros_like(prep.ma_sum) + prep.err_exit = np.zeros_like(prep.ma_sum) + + # Unfortunately this needs to be done for all pods, since + # the shape of the probe / object was modified. + # TODO: possible scaling issue, remove the need for padding + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + prep.view_IDs, prep.poe_IDs, prep.addr = projectional_serial.serialize_array_access(d) + if self.do_position_refinement: + prep.original_addr = np.zeros_like(prep.addr) + prep.original_addr[:] = prep.addr + pID, oID, eID = prep.poe_IDs + + # Keep a list of view indices + prep.rng = np.random.default_rng() + prep.vieworder = np.arange(prep.addr.shape[0]) + + # Modify addresses, copy pa into ea and remove da/ma + prep.addr_ex = np.vstack([prep.addr[:,0,2,0], prep.addr[:,-1,2,0]+1]).T + prep.addr[:,:,2] = prep.addr[:,:,0] + prep.addr[:,:,3:,0] = 0 + + # Reference to ex + prep.ex = self.ex.S[eID].data + + # Object / probe norm + prep.obn = np.zeros_like(prep.mag[0,None], dtype=np.float32) + prep.prn = np.zeros_like(prep.mag[0,None], dtype=np.float32) + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + for it in range(num): + + error_dct = {} + + for dID in self.di.S.keys(): + + # find probe, object and exit ID in dependence of dID + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + POK = kern.POK + FW = kern.FW + BW = kern.BW + + # global aux buffer + aux = kern.aux + + # references for ob, pr + ob = self.ob.S[oID].data + pr = self.pr.S[pID].data + + # shuffle view order + vieworder = prep.vieworder + prep.rng.shuffle(vieworder) + + # Iterate through views + for i in vieworder: + + # Get local adress and arrays + addr = prep.addr[i,None] + ex_from, ex_to = prep.addr_ex[i] + ex = prep.ex[ex_from:ex_to] + mag = prep.mag[i,None] + ma = prep.ma[i,None] + ma_sum = prep.ma_sum[i,None] + obn = prep.obn + prn = prep.prn + err_phot = prep.err_phot[i,None] + err_fourier = prep.err_fourier[i,None] + err_exit = prep.err_exit[i,None] + + # position update + self.position_update_local(prep,i) + + ## build auxilliary wave + t1 = time.time() + AWK.make_aux(aux, addr, ob, pr, ex, c_po=self._c, c_e=1-self._c) + self.benchmark.A_Build_aux += time.time() - t1 + + ## forward FFT + t1 = time.time() + aux[:] = FW(aux) + self.benchmark.B_Prop += time.time() - t1 + + ## Deviation from measured data + t1 = time.time() + if self.p.compute_fourier_error: + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + else: + FUK.fourier_deviation(aux, addr, mag) + FUK.fmag_update_nopbound(aux, addr, mag, ma) + self.benchmark.C_Fourier_update += time.time() - t1 + + ## backward FFT + t1 = time.time() + aux[:] = BW(aux) + self.benchmark.D_iProp += time.time() - t1 + + ## build exit wave + t1 = time.time() + AWK.make_exit(aux, addr, ob, pr, ex, c_a=self._b, c_po=self._a, c_e=-(self._a+self._b)) + if self.p.compute_exit_error: + FUK.exit_error(aux,addr) + FUK.error_reduce(addr, err_exit) + self.benchmark.E_Build_exit += time.time() - t1 + self.benchmark.calls_fourier += 1 + + ## build auxilliary wave (ob * pr product) + t1 = time.time() + AWK.build_aux_no_ex(aux, addr, ob, pr) + self.benchmark.A_Build_aux += time.time() - t1 + + # object update + t1 = time.time() + POK.pr_norm_local(addr, pr, prn) + POK.ob_update_local(addr, ob, pr, ex, aux, prn, a=self._ob_a, b=self._ob_b) + self.benchmark.object_update += time.time() - t1 + self.benchmark.calls_object += 1 + + # probe update + t1 = time.time() + if self._object_norm_is_global and self._pr_a == 0: + obn_max = au.max_abs2(ob) + obn[:] = 0 + else: + POK.ob_norm_local(addr, ob, obn) + obn_max = obn.max() + if self.p.probe_update_start <= self.curiter: + POK.pr_update_local(addr, pr, ob, ex, aux, obn, obn_max, a=self._pr_a, b=self._pr_b) + self.benchmark.probe_update += time.time() - t1 + self.benchmark.calls_probe += 1 + + ## compute log-likelihood + if self.p.compute_log_likelihood: + t1 = time.time() + aux[:] = FW(aux) + FUK.log_likelihood(aux, addr, mag, ma, err_phot) + self.benchmark.F_LLerror += time.time() - t1 + + + # update errors + errs = np.ascontiguousarray(np.vstack([np.hstack(prep.err_fourier), + np.hstack(prep.err_phot), + np.hstack(prep.err_exit)]).T) + error_dct.update(zip(prep.view_IDs, errs)) + + # Re-center the probe + self.center_probe() + + self.curiter += 1 + + #error = parallel.gather_dict(error_dct) + return error_dct + + def position_update_local(self, prep, i): + """ + Position refinement update for current view. + """ + if not self.do_position_refinement: + return + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + #log(4, "----------- START POS REF -------------") + pID, oID, eID = prep.poe_IDs + mag = prep.mag[i,None] + ma = prep.ma[i,None] + ma_sum = prep.ma_sum[i,None] + ob = self.ob.S[oID].data + pr = self.pr.S[pID].data + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr[i,None] + original_addr = prep.original_addr[i,None] + mangled_addr = addr.copy() + err_fourier = prep.err_fourier[i,None] + + PCK = kern.PCK + FW = kern.FW + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We first need to calculate the current error + PCK.build_aux(aux, addr, ob, pr) + aux[:] = FW(aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, addr, mag, ma, ma_sum) + PCK.error_reduce(addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, addr, mag, ma, err_fourier) + error_state = np.zeros_like(err_fourier) + error_state[:] = err_fourier + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + + #log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + for i in range(PCK.mangler.nshifts): + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + aux[:] = FW(aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, mangled_addr, mag, ma, ma_sum) + PCK.error_reduce(mangled_addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, mangled_addr, mag, ma, err_fourier) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_fourier) + + prep.err_fourier[i,None] = error_state + prep.addr[i,None] = addr + + def engine_finalize(self): + """ + try deleting ever helper contianer + """ + if parallel.master and self.benchmark.calls_fourier: + print("----- BENCHMARKS ----") + acc = 0. + for name in sorted(self.benchmark.keys()): + t = self.benchmark[name] + if name[0] in 'ABCDEFGHI': + print('%20s : %1.3f ms per iteration' % (name, t / self.benchmark.calls_fourier * 1000)) + acc += t + elif str(name) == 'probe_update': + print('%20s : %1.3f ms per call. %d calls' % ( + name, t / self.benchmark.calls_probe * 1000, self.benchmark.calls_probe)) + elif str(name) == 'object_update': + print('%20s : %1.3f ms per call. %d calls' % ( + name, t / self.benchmark.calls_object * 1000, self.benchmark.calls_object)) + + print('%20s : %1.3f ms per iteration. %d calls' % ( + 'Fourier_total', acc / self.benchmark.calls_fourier * 1000, self.benchmark.calls_fourier)) + + self._reset_benchmarks() + + if self.do_position_refinement: + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + res = self.kernels[prep.label].resolution + for i,view in enumerate(d.views): + for j,(pname, pod) in enumerate(view.pods.items()): + delta = (prep.original_addr[i][j][1][1:] - prep.addr[i][j][1][1:]) * res + pod.ob_view.coord += delta + pod.ob_view.storage.update_views(pod.ob_view) + + +@register() +class EPIE_serial(_StochasticEngineSerial, EPIEMixin): + """ + A serialized implementation of the EPIE algorithm. + + Defaults: + + [name] + default = EPIE_serial + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _StochasticEngineSerial.__init__(self, ptycho_parent, pars) + EPIEMixin.__init__(self, self.p.alpha, self.p.beta) + ptycho_parent.citations.add_article(**self.article) + +@register() +class SDR_serial(_StochasticEngineSerial, SDRMixin): + """ + A serialized implemnentation of the semi-implicit relaxed Douglas-Rachford (SDR) algorithm. + + Defaults: + + [name] + default = SDR_serial + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _StochasticEngineSerial.__init__(self, ptycho_parent, pars) + SDRMixin.__init__(self, self.p.sigma, self.p.tau, self.p.beta_probe, self.p.beta_object) + ptycho_parent.citations.add_article(**self.article) diff --git a/ptypy/accelerate/base/kernels.py b/ptypy/accelerate/base/kernels.py new file mode 100644 index 000000000..f3a13bad5 --- /dev/null +++ b/ptypy/accelerate/base/kernels.py @@ -0,0 +1,815 @@ +import numpy as np +from ptypy.utils.verbose import logger, log +from .array_utils import max_abs2, abs2 + +class Adict(object): + + def __init__(self): + pass + + +class BaseKernel(object): + + def __init__(self): + self.verbose = False + self.npy = Adict() + self.benchmark = {} + + def log(self, x): + if self.verbose: + print(x) + + +class FourierUpdateKernel(BaseKernel): + + def __init__(self, aux, nmodes=1): + + super(FourierUpdateKernel, self).__init__() + self.denom = 1e-7 + self.nmodes = np.int32(nmodes) + ash = aux.shape + self.fshape = (ash[0] // nmodes, ash[1], ash[2]) + + # temporary buffer arrays + self.npy.fdev = None + self.npy.ferr = None + + self.kernels = [ + 'fourier_error', + 'error_reduce', + 'fmag_all_update' + ] + + def allocate(self): + """ + Allocate memory according to the number of modes and + shape of the diffraction stack. + """ + # temporary buffer arrays + self.npy.fdev = np.zeros(self.fshape, dtype=np.float32) + self.npy.ferr = np.zeros(self.fshape, dtype=np.float32) + + def fourier_error(self, b_aux, addr, mag, mask, mask_sum): + # reference shape (write-to shape) + sh = self.fshape + # stopper + maxz = mag.shape[0] + + # batch buffers + fdev = self.npy.fdev[:maxz] + ferr = self.npy.ferr[:maxz] + aux = b_aux[:maxz * self.nmodes] + + ## Actual math ## + + # build model from complex fourier magnitudes, summing up + # all modes incoherently + tf = aux.reshape(maxz, self.nmodes, sh[1], sh[2]) + af = np.sqrt((np.abs(tf) ** 2).sum(1)) + + # calculate difference to real data (g_mag) + fdev[:] = af - mag + + # Calculate error on fourier magnitudes on a per-pixel basis + ferr[:] = mask * np.abs(fdev) ** 2 / mask_sum.reshape((maxz, 1, 1)) + return + + def fourier_deviation(self, b_aux, addr, mag): + # reference shape (write-to shape) + sh = self.fshape + # stopper + maxz = mag.shape[0] + + # batch buffers + fdev = self.npy.fdev[:maxz] + aux = b_aux[:maxz * self.nmodes] + + ## Actual math ## + + # build model from complex fourier magnitudes, summing up + # all modes incoherently + tf = aux.reshape(maxz, self.nmodes, sh[1], sh[2]) + af = np.sqrt((np.abs(tf) ** 2).sum(1)) + + # calculate difference to real data (g_mag) + fdev[:] = af - mag + + return + + def error_reduce(self, addr, err_sum): + # reference shape (write-to shape) + sh = self.fshape + + # stopper + maxz = err_sum.shape[0] + + # batch buffers + ferr = self.npy.ferr[:maxz] + + ## Actual math ## + + # Reduces the Fourier error along the last 2 dimensions.fd + #err_sum[:] = ferr.astype(np.double).sum(-1).sum(-1).astype(np.float) + err_sum[:] = ferr.sum(-1).sum(-1) + return + + def fmag_all_update(self, b_aux, addr, mag, mask, err_sum, pbound=0.0): + + sh = self.fshape + nmodes = self.nmodes + + # stopper + maxz = mag.shape[0] + + # batch buffers + fdev = self.npy.fdev[:maxz] + aux = b_aux[:maxz * nmodes] + + # write-to shape + ish = aux.shape + + ## Actual math ## + + # local values + fm = np.ones((maxz, sh[1], sh[2]), np.float32) + renorm = np.ones((maxz,), np.float32) + + ## As opposed to DM we use renorm to differentiate the cases. + + # pbound >= g_err_sum + # fm = 1.0 (as renorm = 1, i.e. renorm[~ind]) + # pbound < g_err_sum : + # fm = (1 - g_mask) + g_mask * (g_mag + fdev * renorm) / (af + 1e-10) + # (as renorm in [0,1]) + # pbound == 0.0 + # fm = (1 - g_mask) + g_mask * g_mag / (af + 1e-10) (as renorm=0) + + ind = err_sum > pbound + renorm[ind] = np.sqrt(pbound / err_sum[ind]) + renorm = renorm.reshape((renorm.shape[0], 1, 1)) + + af = fdev + mag + fm[:] = (1 - mask) + mask * (mag + fdev * renorm) / (af + self.denom) + + #fm[:] = mag / (af + 1e-6) + # upcasting + aux[:] = (aux.reshape(ish[0] // nmodes, nmodes, ish[1], ish[2]) * fm[:, np.newaxis, :, :]).reshape(ish) + return + + def fmag_update_nopbound(self, b_aux, addr, mag, mask): + + sh = self.fshape + nmodes = self.nmodes + + # stopper + maxz = mag.shape[0] + + # batch buffers + fdev = self.npy.fdev[:maxz] + aux = b_aux[:maxz * nmodes] + + # write-to shape + ish = aux.shape + + ## Actual math ## + + # local values + fm = np.ones((maxz, sh[1], sh[2]), np.float32) + + af = fdev + mag + fm[:] = (1 - mask) + mask * mag / (af + self.denom) + + # upcasting + aux[:] = (aux.reshape(ish[0] // nmodes, nmodes, ish[1], ish[2]) * fm[:, np.newaxis, :, :]).reshape(ish) + return + + def log_likelihood(self, b_aux, addr, mag, mask, err_phot): + # reference shape (write-to shape) + sh = self.fshape + # stopper + maxz = mag.shape[0] + + # batch buffers + aux = b_aux[:maxz * self.nmodes] + + # build model from complex fourier magnitudes, summing up + # all modes incoherently + tf = aux.reshape(maxz, self.nmodes, sh[1], sh[2]) + LL = (np.abs(tf) ** 2).sum(1) + + # Intensity data + I = mag**2 + + # Calculate log likelihood error + err_phot[:] = ((mask * (LL - I)**2 / (I + 1.)).sum(-1).sum(-1) / np.prod(LL.shape[-2:])) + return + + def exit_error(self, aux, addr): + sh = addr.shape + maxz = sh[0] + + # batch buffers + ferr = self.npy.ferr[:maxz] + dex = aux[:maxz * self.nmodes] + fsh = dex.shape[-2:] + ferr[:] = (np.abs(dex.reshape((maxz,self.nmodes,fsh[0], fsh[1])))**2).sum(axis=1) / np.prod(fsh) + + +class GradientDescentKernel(BaseKernel): + + def __init__(self, aux, nmodes=1): + + super(GradientDescentKernel, self).__init__() + self.denom = 1e-7 + self.nmodes = np.int32(nmodes) + ash = aux.shape + self.bshape = ash + self.fshape = (ash[0] // nmodes, ash[1], ash[2]) + self.ctype = aux.dtype + self.ftype = np.float32 if self.ctype == np.complex64 else np.float64 + + self.npy.LLden = None + self.npy.LLerr = None + self.npy.Imodel = None + + self.npy.float_err1 = None + self.npy.float_err2 = None + + self.kernels = [ + 'make_model', + 'error_reduce', + 'make_a012', + 'fill_b', + 'main', + 'floating_intensity' + ] + + def allocate(self): + """ + Allocate memory according to the number of modes and + shape of the diffraction stack. + """ + # temporary buffer arrays + self.npy.LLden = np.zeros(self.fshape, dtype=self.ftype) + self.npy.LLerr = np.zeros(self.fshape, dtype=self.ftype) + self.npy.Imodel = np.zeros(self.fshape, dtype=self.ftype) + + self.npy.fic_tmp = np.ones((self.fshape[0],), dtype=self.ftype) + + def make_model(self, b_aux, addr): + + # reference shape (= GPU global dims) + sh = self.fshape + + # batch buffers + Imodel = self.npy.Imodel + aux = b_aux + + ## Actual math ## (subset of FUK.fourier_error) + tf = aux.reshape(sh[0], self.nmodes, sh[1], sh[2]) + Imodel[:] = ((tf * tf.conj()).real).sum(1) + + def make_a012(self, b_f, b_a, b_b, addr, I, fic): + + # reference shape (= GPU global dims) + sh = I.shape + + # stopper + maxz = I.shape[0] + + A0 = self.npy.Imodel + A1 = self.npy.LLerr + A2 = self.npy.LLden + + # batch buffers + f = b_f[:maxz * self.nmodes] + a = b_a[:maxz * self.nmodes] + b = b_b[:maxz * self.nmodes] + + ## Actual math ## (subset of FUK.fourier_error) + fc = fic.reshape((maxz,1,1)) + A0.fill(0.) + tf = np.real(f * f.conj()).astype(self.ftype) + A0[:maxz] = np.double(tf.reshape(maxz, self.nmodes, sh[1], sh[2]).sum(1) * fc) - I + + A1.fill(0.) + tf = 2. * np.real(f * a.conj()) + A1[:maxz] = tf.reshape(maxz, self.nmodes, sh[1], sh[2]).sum(1) * fc + + A2.fill(0.) + tf = 2. * np.real(f * b.conj()) + np.real(a * a.conj()) + A2[:maxz] = tf.reshape(maxz, self.nmodes, sh[1], sh[2]).sum(1) * fc + return + + def fill_b(self, addr, Brenorm, w, B): + + # don't know the best dims but this element wise anyway + + # stopper + maxz = w.shape[0] + + A0 = self.npy.Imodel[:maxz] + A1 = self.npy.LLerr[:maxz] + A2 = self.npy.LLden[:maxz] + + ## Actual math ## + + # maybe two kernel calls? + + B[0] += np.dot(w.flat, (A0 ** 2).flat) * Brenorm + B[1] += np.dot(w.flat, (2 * A0 * A1).flat) * Brenorm + B[2] += np.dot(w.flat, (A1 ** 2 + 2 * A0 * A2).flat) * Brenorm + return + + def error_reduce(self, addr, err_sum): + + # reference shape (= GPU global dims) + sh = err_sum.shape + + # stopper + maxz = err_sum.shape[0] + + # batch buffers + ferr = self.npy.LLerr[:maxz] + + ## Actual math ## + + # Reduces the LL error along the last 2 dimensions.fd + err_sum[:] = ferr.sum(-1).sum(-1) + return + + def floating_intensity(self, addr, w, I, fic): + + # reference shape (= GPU global dims) + sh = fic.shape + + # stopper + maxz = fic.shape[0] + + # internal buffers + num = self.npy.LLerr[:maxz] + den = self.npy.LLden[:maxz] + Imodel = self.npy.Imodel[:maxz] + fic_tmp = self.npy.fic_tmp[:maxz] + + ## math ## + num[:] = w * Imodel * I + den[:] = w * Imodel ** 2 + fic[:] = num.sum(-1).sum(-1) + fic_tmp[:]= den.sum(-1).sum(-1) + fic/=fic_tmp + Imodel *= fic.reshape(Imodel.shape[0], 1, 1) + + def main(self, b_aux, addr, w, I): + + nmodes = self.nmodes + # stopper + maxz = I.shape[0] + + # batch buffers + err = self.npy.LLerr[:maxz] + Imodel = self.npy.Imodel[:maxz] + aux = b_aux[:maxz*nmodes] + + # write-to shape (= GPU global dims) + ish = aux.shape + + ## math ## + DI = np.double(Imodel) - I + tmp = w * DI + err[:] = tmp * DI + + aux[:] = (aux.reshape(ish[0] // nmodes, nmodes, ish[1], ish[2]) * tmp[:, np.newaxis, :, :]).reshape(ish) + return + + +class AuxiliaryWaveKernel(BaseKernel): + + def __init__(self): + super(AuxiliaryWaveKernel, self).__init__() + self.kernels = [ + 'build_aux', + 'build_exit', + ] + + def allocate(self): + pass + + def build_aux(self, b_aux, addr, ob, pr, ex, alpha=1.0): + # DM only, legacy + self.make_aux(b_aux, addr, ob, pr, ex, 1.+alpha, -alpha) + + def _build_aux(self, b_aux, addr, ob, pr, ex, alpha=1.0): + + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + tmp = ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], :, :] * \ + (1. + alpha) - \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] * \ + alpha + aux[ind, :, :] = tmp + return + + def make_aux(self, b_aux, addr, ob, pr, ex, c_po=1.0, c_e=0.0): + + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + tmp = ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], :, :] * c_po + \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] * c_e + aux[ind, :, :] = tmp + return + + def build_exit(self, b_aux, addr, ob, pr, ex, alpha=1): + self.make_exit(b_aux, addr, ob, pr, ex, 1.0, -alpha, alpha-1) + + def build_exit_alpha_tau(self, b_aux, addr, ob, pr, ex, alpha=1, tau=1): + self.make_exit(b_aux, addr, ob, pr, ex, tau, 1 - tau * (1 + alpha), tau * alpha - 1) + + def make_exit(self, b_aux, addr, ob, pr, ex, c_a=1.0, c_po=0.0, c_e=-1.0): + + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + dex = c_a * aux[ind, :, :] + c_po * \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + c_e * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] += dex + aux[ind, :, :] = dex + return + + def _build_exit(self, b_aux, addr, ob, pr, ex, alpha=1): + + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + dex = aux[ind, :, :] - alpha * \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + (alpha - 1) * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] += dex + aux[ind, :, :] = dex + return + + def _build_exit_alpha_tau(self, b_aux, addr, ob, pr, ex, alpha=1, tau=1): + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + dex = tau * aux[ind, :, :] + (tau * alpha - 1) * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + \ + (1 - tau * (1 + alpha)) * \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] += dex + aux[ind, :, :] = dex + return + + def build_aux_no_ex(self, b_aux, addr, ob, pr, fac=1.0, add=False): + + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = b_aux.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + tmp = ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] * fac + if add: + aux[ind, :, :] += tmp + else: + aux[ind, :, :] = tmp + return + +class PoUpdateKernel(BaseKernel): + + def __init__(self): + + super(PoUpdateKernel, self).__init__() + self.kernels = [ + 'pr_update', + 'ob_update', + ] + + def allocate(self): + pass + + def ob_update(self, addr, ob, obn, pr, ex): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + obn[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + (pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols]).real + return + + def pr_update(self, addr, pr, prn, ob, ex): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + prn[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + (ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols]).real + return + + def ob_update_ML(self, addr, ob, pr, ex, fac=2.0): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] * fac + return + + def pr_update_ML(self, addr, pr, ob, ex, fac=2.0): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] * fac + return + + def ob_update_local(self, addr, ob, pr, ex, aux, prn, a=0., b=1.): + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + pr_norm = (1 - a) * prn.max() + a * prn + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + (a + b) * pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + (ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] - aux[ind,:,:]) / \ + pr_norm[dic[0], dic[1]:dic[1] + rows, dic[2]:dic[2] + cols] + return + + def pr_update_local(self, addr, pr, ob, ex, aux, obn, obn_max, a=0., b=1.): + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + ob_norm = (1 - a) * obn_max + a * obn + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + (a + b) * ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + (ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] - aux[ind,:,:]) / \ + ob_norm[dic[0], dic[1]:dic[1] + rows, dic[2]:dic[2] + cols] + return + + def ob_norm_local(self, addr, ob, obn): + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = obn.shape[-2:] + obn[:] = 0. + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + # each object mode should only be counted once + if prc[0] > 0: + continue + obn[dic[0],dic[1]:dic[1] + rows, dic[2]:dic[2] + cols] += \ + (ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols]).real + return + + def pr_norm_local(self, addr, pr, prn): + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = prn.shape[-2:] + prn[:] = 0. + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + # each probe mode should only be counted once + if obc[0] > 0: + continue + prn[dic[0],dic[1]:dic[1] + rows, dic[2]:dic[2] + cols] += \ + (pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols]).real + return + + +class PositionCorrectionKernel(BaseKernel): + from ptypy.accelerate.base import address_manglers + + MANGLERS = { + 'Annealing': address_manglers.RandomIntMangler, + 'GridSearch': address_manglers.GridSearchMangler + } + + def __init__(self, aux, nmodes, parameters, resolution): + super(PositionCorrectionKernel, self).__init__() + ash = aux.shape + self.fshape = (ash[0] // nmodes, ash[1], ash[2]) + self.npy.ferr = None + self.npy.fdev = None + self.addr = None + self.nmodes = nmodes + self.param = parameters + self.nshifts = parameters.nshifts + self.resolution = resolution + self.kernels = ['build_aux', + 'fourier_error', + 'error_reduce', + 'update_addr'] + self.setup() + + def setup(self): + Mangler = self.MANGLERS[self.param.method] + amplitude = int(np.ceil(self.param.amplitude / self.resolution[0])) + max_shift = int(np.ceil(self.param.max_shift / self.resolution[0])) + self.mangler = Mangler(amplitude, self.param.start, self.param.stop, + self.param.nshifts, decay=self.param.amplitude_decay, + max_bound=max_shift, randomseed=0) + + def allocate(self): + self.npy.fdev = np.zeros(self.fshape, dtype=np.float32) # we won't use this again but preallocate for speed + self.npy.ferr = np.zeros(self.fshape, dtype=np.float32) + + def build_aux(self, b_aux, addr, ob, pr): + """ + different to the AWK, no alpha subtraction. It would be the same, but with alpha permanentaly set to 0. + """ + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = aux.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + dex = ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * pr[prc[0], :, :] + aux[ind, :, :] = dex + + def fourier_error(self, b_aux, addr, mag, mask, mask_sum): + """ + Should be identical to that of the FUK, but we don't need fdev out. + """ + # reference shape (write-to shape) + sh = self.fshape + # stopper + maxz = mag.shape[0] + + # batch buffers + ferr = self.npy.ferr[:maxz] + fdev = self.npy.fdev[:maxz] + aux = b_aux[:maxz * self.nmodes] + + ## Actual math ## + + # build model from complex fourier magnitudes, summing up + # all modes incoherently + tf = aux.reshape(maxz, self.nmodes, sh[1], sh[2]) + af = np.sqrt((np.abs(tf) ** 2).sum(1)) + + # calculate difference to real data (g_mag) + fdev[:] = af - mag # we won't reuse this so don't need to keep a persistent buffer + + # Calculate error on fourier magnitudes on a per-pixel basis + ferr[:] = mask * np.abs(fdev) ** 2 / mask_sum.reshape((maxz, 1, 1)) + return + + def error_reduce(self, addr, err_sum): + """ + This should the exact same tree reduction as the FUK. + """ + # reference shape (write-to shape) + sh = self.fshape + + # stopper + maxz = err_sum.shape[0] + + # batch buffers + ferr = self.npy.ferr[:maxz] + + ## Actual math ## + + # Reduceses the Fourier error along the last 2 dimensions.fd + #err_sum[:] = ferr.astype(np.double).sum(-1).sum(-1).astype(np.float) + err_sum[:] = ferr.sum(-1).sum(-1) + return + + def log_likelihood(self, b_aux, addr, mag, mask, err_sum): + # reference shape (write-to shape) + sh = self.fshape + # stopper + maxz = mag.shape[0] + + # batch buffers + aux = b_aux[:maxz * self.nmodes] + + # build model from complex fourier magnitudes, summing up + # all modes incoherently + tf = aux.reshape(maxz, self.nmodes, sh[1], sh[2]) + LL = (np.abs(tf) ** 2).sum(1) + + # Intensity data + I = mag**2 + + # Calculate log likelihood error + err_sum[:] = ((mask * (LL - I)**2 / (I + 1.)).sum(-1).sum(-1) / np.prod(LL.shape[-2:])) + return + + def log_likelihood_ml(self, b_aux, addr, I, weights, err_sum): + # reference shape (write-to shape) + sh = self.fshape + # stopper + maxz = I.shape[0] + + # batch buffers + aux = b_aux[:maxz * self.nmodes] + + # build model from complex fourier magnitudes, summing up + # all modes incoherently + tf = aux.reshape(maxz, self.nmodes, sh[1], sh[2]) + LL = (np.abs(tf) ** 2).sum(1) + + # Calculate log likelihood error + err_sum[:] = ((weights * (LL - I)**2).sum(-1).sum(-1) / np.prod(LL.shape[-2:])) + return + + def update_addr_and_error_state(self, addr, error_state, mangled_addr, err_sum): + """ + updates the addresses and err state vector corresponding to the smallest error. I think this can be done on the cpu + """ + update_indices = err_sum < error_state + #log(4, "Position correction: updating %s indices" % np.sum(update_indices)) + addr[update_indices] = mangled_addr[update_indices] + error_state[update_indices] = err_sum[update_indices] diff --git a/ptypy/accelerate/cuda_pycuda/__init__.py b/ptypy/accelerate/cuda_pycuda/__init__.py new file mode 100644 index 000000000..e6c51d49f --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/__init__.py @@ -0,0 +1,68 @@ +import pycuda.driver as cuda +from pycuda.compiler import SourceModule +import numpy as np +import os +# debug_options = [] +# debug_options = ['-O0', '-G', '-g'] +debug_options = ['-O3', '-DNDEBUG', '-lineinfo'] # release mode flags + +# C++14 support was added with CUDA 9, so we only enable the flag there +if cuda.get_version()[0] >= 9: + debug_options += ['-std=c++14'] +else: + debug_options += ['-std=c++11'] + +context = None +queue = None + +def get_context(new_context=False, new_queue=False): + + from ptypy.utils import parallel + + global context + global queue + + if context is None or new_context: + cuda.init() + if parallel.rank_local >= cuda.Device.count(): + raise Exception('Local rank must be smaller than total device count, \ + rank={}, rank_local={}, device_count={}'.format( + parallel.rank, parallel.rank_local, cuda.Device.count() + )) + context = cuda.Device(parallel.rank_local).make_context() + context.push() + # print("made context %s on rank %s" % (str(context), str(parallel.rank))) + # print("The cuda device count on %s is:%s" % (str(parallel.rank), + # str(cuda.Device.count()))) + # print("parallel.rank:%s, parallel.rank_local:%s" % (str(parallel.rank), + # str(parallel.rank_local))) + if queue is None or new_queue: + queue = cuda.Stream() + + return context, queue + + +def load_kernel(name, subs={}, file=None): + + if file is None: + if isinstance(name, str): + fn = "%s/cuda/%s.cu" % (os.path.dirname(__file__), name) + else: + raise ValueError("name parameter must be a string if not filename is given") + else: + fn = "%s/cuda/%s" % (os.path.dirname(__file__), file) + + with open(fn, 'r') as f: + kernel = f.read() + for k,v in list(subs.items()): + kernel = kernel.replace(k, str(v)) + # insert a preprocessor line directive to assist compiler errors + escaped = fn.replace("\\", "\\\\") + kernel = '#line 1 "{}"\n'.format(escaped) + kernel + mod = SourceModule(kernel, include_dirs=[np.get_include()], no_extern_c=True, options=debug_options) + + if isinstance(name, str): + return mod.get_function(name) + else: # tuple + return tuple(mod.get_function(n) for n in name) + diff --git a/ptypy/accelerate/cuda_pycuda/address_manglers.py b/ptypy/accelerate/cuda_pycuda/address_manglers.py new file mode 100644 index 000000000..d19a77fa4 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/address_manglers.py @@ -0,0 +1,74 @@ +from ptypy.accelerate.cuda_pycuda import load_kernel +import numpy as np +from ptypy.accelerate.base import address_manglers as npam +from pycuda import gpuarray +import pycuda.driver as cuda + +class BaseMangler(npam.BaseMangler): + + def __init__(self, *args, queue_thread=None, **kwargs): + super().__init__(*args, **kwargs) + self.queue = queue_thread + self.get_address_cuda = load_kernel("get_address") + self.delta = None + self.delta_gpu = None + + def _setup_delta_gpu(self): + assert self.delta is not None, "Setup delta using the setup_shifts method first" + self.delta = np.ascontiguousarray(self.delta, dtype=np.int32) + + if self.delta_gpu is None or self.delta_gpu.shape[0] < self.delta.shape[0]: + self.delta_gpu = gpuarray.empty(self.delta.shape, dtype=np.int32) + # in case self.delta is smaller than delta_gpu, this will only copy the + # relevant part + cuda.memcpy_htod(dest=self.delta_gpu.ptr, + src=self.delta) + + def get_address(self, index, addr_current, mangled_addr, max_oby, max_obx): + assert addr_current.dtype == np.int32, "addresses must be int32" + assert mangled_addr.dtype == np.int32, "addresses must be int32" + assert len(addr_current.shape) == 4, "addresses must be 4 dimensions" + assert addr_current.shape == mangled_addr.shape, "output addresses must be pre-allocated" + assert self.delta_gpu is not None, "Deltas are not set yet - call setup_shifts first" + assert index < self.delta_gpu.shape[0], "Index out of range for deltas" + assert isinstance(self.delta_gpu, gpuarray.GPUArray), "Only GPU arrays are supported for delta" + + # only using a single thread block here as it's not enough work + # otherwise + self.get_address_cuda( + addr_current, + mangled_addr, + np.int32(addr_current.shape[0] * addr_current.shape[1]), + self.delta_gpu[index,None], + np.int32(max_oby), + np.int32(max_obx), + block=(64,1,1), + grid=(1, 1, 1), + stream=self.queue) + +# with multiple inheritance, we have to be explicit which super class +# we are calling in the methods +class RandomIntMangler(BaseMangler, npam.RandomIntMangler): + + def __init__(self, *args, **kwargs): + BaseMangler.__init__(self, *args, **kwargs) + + def setup_shifts(self, *args, **kwargs): + npam.RandomIntMangler.setup_shifts(self, *args, **kwargs) + self._setup_delta_gpu() + + def get_address(self, *args, **kwargs): + BaseMangler.get_address(self, *args, **kwargs) + + +class GridSearchMangler(BaseMangler, npam.GridSearchMangler): + + def __init__(self, *args, **kwargs): + BaseMangler.__init__(self, *args, **kwargs) + + def setup_shifts(self, *args, **kwargs): + npam.GridSearchMangler.setup_shifts(self, *args, **kwargs) + self._setup_delta_gpu() + + def get_address(self, *args, **kwargs): + BaseMangler.get_address(self, *args, **kwargs) \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/array_utils.py b/ptypy/accelerate/cuda_pycuda/array_utils.py new file mode 100644 index 000000000..7c2de8f3f --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/array_utils.py @@ -0,0 +1,698 @@ +from . import load_kernel +from pycuda import gpuarray +import pycuda.driver as cuda +from ptypy.utils import gaussian +import numpy as np + +# maps a numpy dtype to the corresponding C type +def map2ctype(dt): + if dt == np.float32: + return 'float' + elif dt == np.float64: + return 'double' + elif dt == np.complex64: + return 'complex' + elif dt == np.complex128: + return 'complex' + elif dt == np.int32: + return 'int' + elif dt == np.int64: + return 'long long' + else: + raise ValueError('No mapping for {}'.format(dt)) + + +class ArrayUtilsKernel: + def __init__(self, acc_dtype=np.float64, queue=None): + self.queue = queue + self.acc_dtype = acc_dtype + self.cdot_cuda = load_kernel("dot", { + 'IN_TYPE': 'complex', + 'ACC_TYPE': 'double' if acc_dtype==np.float64 else 'float' + }) + self.dot_cuda = load_kernel("dot", { + 'IN_TYPE': 'float', + 'ACC_TYPE': 'double' if acc_dtype==np.float64 else 'float' + }) + self.full_reduce_cuda = load_kernel("full_reduce", { + 'IN_TYPE': 'double' if acc_dtype==np.float64 else 'float', + 'OUT_TYPE': 'double' if acc_dtype==np.float64 else 'float', + 'ACC_TYPE': 'double' if acc_dtype==np.float64 else 'float', + 'BDIM_X': 1024 + }) + self.Ctmp = None + + def dot(self, A, B, out=None): + assert A.dtype == B.dtype, "Input arrays must be of same data type" + assert A.size == B.size, "Input arrays must be of the same size" + + if out is None: + out = gpuarray.zeros((1,), dtype=self.acc_dtype) + + block = (1024, 1, 1) + grid = (int((B.size + 1023) // 1024), 1, 1) + if self.acc_dtype == np.float32: + elsize = 4 + elif self.acc_dtype == np.float64: + elsize = 8 + if self.Ctmp is None or self.Ctmp.size < grid[0]: + self.Ctmp = gpuarray.zeros((grid[0],), dtype=self.acc_dtype) + Ctmp = self.Ctmp + if grid[0] == 1: + Ctmp = out + if np.iscomplexobj(B): + self.cdot_cuda(A, B, np.int32(A.size), Ctmp, + block=block, grid=grid, + shared=1024 * elsize, + stream=self.queue) + else: + self.dot_cuda(A, B, np.int32(A.size), Ctmp, + block=block, grid=grid, + shared=1024 * elsize, + stream=self.queue) + if grid[0] > 1: + self.full_reduce_cuda(self.Ctmp, out, np.int32(grid[0]), + block=(1024, 1, 1), grid=(1,1,1), shared=elsize*1024, + stream=self.queue) + + return out + + def norm2(self, A, out=None): + return self.dot(A, A, out) + +class TransposeKernel: + + def __init__(self, queue=None): + self.queue = queue + self.transpose_cuda = load_kernel("transpose", { + 'DTYPE': 'int', + 'BDIM': 16 + }) + + def transpose(self, input, output): + # only for int at the moment (addr array), and 2D (reshape pls) + if len(input.shape) != 2: + raise ValueError("Only 2D tranpose is supported - reshape as desired") + if input.shape[0] != output.shape[1] or input.shape[1] != output.shape[0]: + raise ValueError("Input/Output must be of flipped shape") + if input.dtype != np.int32 or output.dtype != np.int32: + raise ValueError("Only int types are supported at the moment") + + width = input.shape[1] + height = input.shape[0] + blk = (16, 16, 1) + grd = ( + int((input.shape[1] + 15)// 16), + int((input.shape[0] + 15)// 16), + 1 + ) + self.transpose_cuda(input, output, np.int32(width), np.int32(height), + block=blk, grid=grd, stream=self.queue) + +class MaxAbs2Kernel: + + def __init__(self, queue=None): + self.queue = queue + # we lazy-load this depending on the data types we get + self.max_abs2_cuda = {} + + def max_abs2(self, X, out): + """ Calculate max(abs(x)**2) across the final 2 dimensions""" + rows = np.int32(X.shape[-2]) + cols = np.int32(X.shape[-1]) + firstdims = np.int32(np.prod(X.shape[:-2])) + gy = int(rows) + # lazy-loading, keeping scratch memory and both kernels in the same dictionary + bx = int(64) + version = '{},{},{}'.format(map2ctype(X.dtype), map2ctype(out.dtype), gy) + if version not in self.max_abs2_cuda: + step1, step2 = load_kernel( + ("max_abs2_step1", "max_abs2_step2"), + { + 'IN_TYPE': map2ctype(X.dtype), + 'OUT_TYPE': map2ctype(out.dtype), + 'BDIM_X': bx, + }, "max_abs2.cu") + self.max_abs2_cuda[version] = { + 'step1': step1, + 'step2': step2, + 'scratchmem': gpuarray.empty((gy,), dtype=out.dtype) + } + + # if self.max_abs2_cuda[version]['scratchmem'] is None \ + # or self.max_abs2_cuda[version]['scratchmem'].shape[0] != gy: + # self.max_abs2_cuda[version]['scratchmem'] = + scratch = self.max_abs2_cuda[version]['scratchmem'] + + + self.max_abs2_cuda[version]['step1'](X, firstdims, rows, cols, scratch, + block=(bx, 1, 1), grid=(1, gy, 1), + stream=self.queue) + self.max_abs2_cuda[version]['step2'](scratch, np.int32(gy), out, + block=(bx, 1, 1), grid=(1, 1, 1), + stream=self.queue + ) + + +class CropPadKernel: + + def __init__(self, queue=None): + self.queue = queue + # we lazy-load this depending on the data types we get + self.fill3D_cuda = {} + + def fill3D(self, A, B, offset=[0, 0, 0]): + """ + Fill 3-dimensional array A with B. + """ + if A.ndim < 3 or B.ndim < 3: + raise ValueError('Input arrays must each be at least 3D') + assert A.ndim == B.ndim, "Input and Output must have the same number of dimensions." + ash = A.shape + bsh = B.shape + misfit = np.array(bsh) - np.array(ash) + assert not misfit[:-3].any(), "Input and Output must have the same shape everywhere but the last three axes." + + Alim = np.array(A.shape[-3:]) + Blim = np.array(B.shape[-3:]) + off = np.array(offset) + Ao = off.copy() + Ao[Ao < 0] = 0 + Bo = -off.copy() + Bo[Bo < 0] = 0 + assert (Bo < Blim).all() and (Ao < Alim).all(), "At least one dimension lacks overlap" + Ao = Ao.astype(np.int32) + Bo = Bo.astype(np.int32) + lengths = np.array([ + min(off[0] + Blim[0], Alim[0]) - Ao[0], + min(off[1] + Blim[1], Alim[1]) - Ao[1], + min(off[2] + Blim[2], Alim[2]) - Ao[2], + ], dtype=np.int32) + lengths2 = np.array([ + min(Alim[0] - off[0], Blim[0]) - Bo[0], + min(Alim[1] - off[1], Blim[1]) - Bo[1], + min(Alim[2] - off[2], Blim[2]) - Bo[2], + ], dtype=np.int32) + assert (lengths == lengths2).all(), "left and right lenghts are not matching" + batch = int(np.prod(A.shape[:-3])) + + # lazy loading depending on data type + version = '{},{}'.format(map2ctype(B.dtype), map2ctype(A.dtype)) + if version not in self.fill3D_cuda: + self.fill3D_cuda[version] = load_kernel("fill3D", { + 'IN_TYPE': map2ctype(B.dtype), + 'OUT_TYPE': map2ctype(A.dtype) + }) + bx = by = 32 + self.fill3D_cuda[version]( + A, B, + np.int32(A.shape[-3]), np.int32(A.shape[-2]), np.int32(A.shape[-1]), + np.int32(B.shape[-3]), np.int32(B.shape[-2]), np.int32(B.shape[-1]), + Ao[0], Ao[1], Ao[2], + Bo[0], Bo[1], Bo[2], + lengths[0], lengths[1], lengths[2], + block=(int(bx), int(by), int(1)), + grid=( + int((lengths[2] + bx - 1)//bx), + int((lengths[1] + by - 1)//by), + int(batch)), + stream=self.queue + ) + + + def crop_pad_2d_simple(self, A, B): + """ + Places B in A centered around the last two axis. A and B must be of the same shape + anywhere but the last two dims. + """ + assert A.ndim >= 2, "Arrays must have more than 2 dimensions." + assert A.ndim == B.ndim, "Input and Output must have the same number of dimensions." + misfit = np.array(A.shape) - np.array(B.shape) + assert not misfit[:-2].any(), "Input and Output must have the same shape everywhere but the last two axes." + if A.ndim == 2: + A = A.reshape((1,) + A.shape) + if B.ndim == 2: + B = B.reshape((1,) + B.shape) + a1, a2 = A.shape[-2:] + b1, b2 = B.shape[-2:] + offset = [0, a1 // 2 - b1 // 2, a2 // 2 - b2 // 2] + self.fill3D(A, B, offset) + + +class DerivativesKernel: + def __init__(self, dtype, queue=None): + if dtype == np.float32: + stype = "float" + elif dtype == np.complex64: + stype = "complex" + else: + raise NotImplementedError( + "delxf is only implemented for float32 and complex64") + + self.queue = queue + self.dtype = dtype + self.last_axis_block = (256, 4, 1) + self.mid_axis_block = (256, 4, 1) + + self.delxf_last, self.delxf_mid = load_kernel( + ("delx_last", "delx_mid"), + file="delx.cu", + subs={ + 'IS_FORWARD': 'true', + 'BDIM_X': str(self.last_axis_block[0]), + 'BDIM_Y': str(self.last_axis_block[1]), + 'IN_TYPE': stype, + 'OUT_TYPE': stype + }) + self.delxb_last, self.delxb_mid = load_kernel( + ("delx_last", "delx_mid"), + file="delx.cu", + subs={ + 'IS_FORWARD': 'false', + 'BDIM_X': str(self.last_axis_block[0]), + 'BDIM_Y': str(self.last_axis_block[1]), + 'IN_TYPE': stype, + 'OUT_TYPE': stype + }) + + + def delxf(self, input, out, axis=-1): + if input.dtype != self.dtype: + raise ValueError('Invalid input data type') + + if axis < 0: + axis = input.ndim + axis + axis = np.int32(axis) + + if axis == input.ndim - 1: + flat_dim = np.int32(np.product(input.shape[0:-1])) + self.delxf_last(input, out, flat_dim, np.int32(input.shape[axis]), + block=self.last_axis_block, + grid=( + int((flat_dim + + self.last_axis_block[1] - 1) // self.last_axis_block[1]), + 1, 1), + stream=self.queue + ) + else: + lower_dim = np.int32(np.product(input.shape[(axis+1):])) + higher_dim = np.int32(np.product(input.shape[:axis])) + gx = int( + (lower_dim + self.mid_axis_block[0] - 1) // self.mid_axis_block[0]) + gy = 1 + gz = int(higher_dim) + self.delxf_mid(input, out, lower_dim, higher_dim, np.int32(input.shape[axis]), + block=self.mid_axis_block, + grid=(gx, gy, gz), + stream=self.queue + ) + + def delxb(self, input, out, axis=-1): + if input.dtype != self.dtype: + raise ValueError('Invalid input data type') + + if axis < 0: + axis = input.ndim + axis + axis = np.int32(axis) + + if axis == input.ndim - 1: + flat_dim = np.int32(np.product(input.shape[0:-1])) + self.delxb_last(input, out, flat_dim, np.int32(input.shape[axis]), + block=self.last_axis_block, + grid=( + int((flat_dim + + self.last_axis_block[1] - 1) // self.last_axis_block[1]), + 1, 1), + stream=self.queue + ) + else: + lower_dim = np.int32(np.product(input.shape[(axis+1):])) + higher_dim = np.int32(np.product(input.shape[:axis])) + gx = int( + (lower_dim + self.mid_axis_block[0] - 1) // self.mid_axis_block[0]) + gy = 1 + gz = int(higher_dim) + self.delxb_mid(input, out, lower_dim, higher_dim, np.int32(input.shape[axis]), + block=self.mid_axis_block, + grid=(gx, gy, gz), + stream=self.queue + ) + + +class GaussianSmoothingKernel: + def __init__(self, queue=None, num_stdevs=4, kernel_type='float'): + if kernel_type not in ['float', 'double']: + raise ValueError('Invalid data type for kernel') + self.kernel_type = kernel_type + self.dtype = np.complex64 + self.stype = "complex" + self.queue = queue + self.num_stdevs = num_stdevs + self.blockdim_x = 4 + self.blockdim_y = 16 + + + # At least 2 blocks per SM + self.max_shared_per_block = 48 * 1024 // 2 + self.max_shared_per_block_complex = self.max_shared_per_block / 2 * np.dtype(np.float32).itemsize + self.max_kernel_radius = int(self.max_shared_per_block_complex / self.blockdim_y) + + self.convolution_row = load_kernel( + "convolution_row", file="convolution.cu", subs={ + 'BDIM_X': self.blockdim_x, + 'BDIM_Y': self.blockdim_y, + 'DTYPE': self.stype, + 'MATH_TYPE': self.kernel_type + }) + self.convolution_col = load_kernel( + "convolution_col", file="convolution.cu", subs={ + 'BDIM_X': self.blockdim_y, # NOTE: we swap x and y in this columns + 'BDIM_Y': self.blockdim_x, + 'DTYPE': self.stype, + 'MATH_TYPE': self.kernel_type + }) + # pre-allocate kernel memory on gpu, with max-radius to accomodate + dtype=np.float32 if self.kernel_type == 'float' else np.float64 + self.kernel_gpu = gpuarray.empty((self.max_kernel_radius,), dtype=dtype) + # keep track of previus radius and std to determine if we need to transfer again + self.r = 0 + self.std = 0 + + + def convolution(self, data, mfs, tmp=None): + """ + Calculates a stacked 2D convolution for smoothing, with the standard deviations + given in mfs (stdx, stdy). It works in-place in the data array, + and tmp is a gpu-allocated array of the same size and type as data, + used internally for temporary storage + """ + ndims = data.ndim + shape = data.shape + + # Create temporary array (if not given) + if tmp is None: + tmp = gpuarray.empty(shape, dtype=data.dtype) + assert shape == tmp.shape and data.dtype == tmp.dtype + + # Check input dimensions + if ndims == 3: + batches,y,x = shape + stdy, stdx = mfs + elif ndims == 2: + batches = 1 + y,x = shape + stdy, stdx = mfs + elif ndims == 1: + batches = 1 + y,x = shape[0],1 + stdy, stdx = mfs[0], 0.0 + else: + raise NotImplementedError("input needs to be of dimensions 0 < ndims <= 3") + + input = data + output = tmp + + # Row convolution kernel + # TODO: is this threshold acceptable in all cases? + if stdx > 0.1: + r = int(self.num_stdevs * stdx + 0.5) + if r > self.max_kernel_radius: + raise ValueError("Size of Gaussian kernel too large") + if r != self.r or stdx != self.std: + # recalculate + transfer + g = gaussian(np.arange(-r,r+1), stdx) + g /= g.sum() + k = np.ascontiguousarray(g[r:].astype(np.float32 if self.kernel_type == 'float' else np.float64)) + self.kernel_gpu[:r+1] = k[:] + self.r = r + self.std = stdx + + bx = self.blockdim_x + by = self.blockdim_y + + shared = (bx + 2*r) * by * np.dtype(np.complex64).itemsize + if shared > self.max_shared_per_block: + raise MemoryError("Cannot run kernel in shared memory") + + blk = (bx, by, 1) + grd = (int((y + bx -1)// bx), int((x + by-1)// by), batches) + self.convolution_row(input, output, np.int32(y), np.int32(x), self.kernel_gpu, np.int32(r), + block=blk, grid=grd, shared=shared, stream=self.queue) + + input = output + output = data + + # Column convolution kernel + # TODO: is this threshold acceptable in all cases? + if stdy > 0.1: + r = int(self.num_stdevs * stdy + 0.5) + if r > self.max_kernel_radius: + raise ValueError("Size of Gaussian kernel too large") + if r != self.r or stdy != self.std: + # recalculate + transfer + g = gaussian(np.arange(-r,r+1), stdy) + g /= g.sum() + k = np.ascontiguousarray(g[r:].astype(np.float32 if self.kernel_type == 'float' else np.float64)) + self.kernel_gpu[:r+1] = k[:] + self.r = r + self.std = stdy + + + bx = self.blockdim_y + by = self.blockdim_x + + shared = (by + 2*r) * bx * np.dtype(np.complex64).itemsize + if shared > self.max_shared_per_block: + raise MemoryError("Cannot run kernel in shared memory") + + blk = (bx, by, 1) + grd = (int((y + bx -1)// bx), int((x + by-1)// by), batches) + self.convolution_col(input, output, np.int32(y), np.int32(x), self.kernel_gpu, np.int32(r), + block=blk, grid=grd, shared=shared, stream=self.queue) + + # TODO: is this threshold acceptable in all cases? + if (stdx <= 0.1 and stdy <= 0.1): + return # nothing to do + elif (stdx > 0.1 and stdy > 0.1): + return # both parts have run, output is back in data + else: + data[:] = tmp[:] # only one of them has run, output is in tmp + +class ClipMagnitudesKernel: + + def __init__(self, queue=None): + self.queue = queue + self.clip_magnitudes_cuda = load_kernel("clip_magnitudes", { + 'IN_TYPE': 'complex', + }) + + def clip_magnitudes_to_range(self, array, clip_min, clip_max): + + cmin = np.float32(clip_min) + cmax = np.float32(clip_max) + + npixel = np.int32(np.prod(array.shape)) + bx = 256 + gx = int((npixel + bx - 1) // bx) + self.clip_magnitudes_cuda(array, cmin, cmax, + npixel, + block=(bx, 1, 1), + grid=(gx, 1, 1), + stream=self.queue) + +class MassCenterKernel: + + def __init__(self, queue=None): + self.queue = queue + self.threadsPerBlock = 256 + + self.indexed_sum_middim_cuda = load_kernel("indexed_sum_middim", + file="mass_center.cu", subs={ + 'IN_TYPE': 'float', + 'BDIM_X' : self.threadsPerBlock, + 'BDIM_Y' : 1, + } + ) + + self.indexed_sum_lastdim_cuda = load_kernel("indexed_sum_lastdim", + file="mass_center.cu", subs={ + 'IN_TYPE': 'float', + 'BDIM_X' : 32, + 'BDIM_Y' : 32, + } + ) + + self.final_sums_cuda = load_kernel("final_sums", + file="mass_center.cu", subs={ + 'IN_TYPE': 'float', + 'BDIM_X' : 256, + 'BDIM_Y' : 1, + } + ) + + def mass_center(self, array): + if array.dtype != np.float32: + raise NotImplementedError("mass_center is only implemented for float32") + + i = np.int32(array.shape[0]) + m = np.int32(array.shape[1]) + if array.ndim >= 3: + n = np.int32(array.shape[2]) + else: + n = np.int32(1) + + total_sum = gpuarray.sum(array, dtype=np.float32, stream=self.queue).get() + sc = np.float32(1. / total_sum.item()) + + i_sum = gpuarray.empty(array.shape[0], dtype=np.float32) + m_sum = gpuarray.empty(array.shape[1], dtype=np.float32) + n_sum = gpuarray.empty(int(n), dtype=np.float32) + out = gpuarray.empty(3 if n>1 else 2, dtype=np.float32) + + # sum all dims except the first, multiplying by the index and scaling factor + block_ = (self.threadsPerBlock, 1, 1) + grid_ = (int(i), 1, 1) + self.indexed_sum_middim_cuda(array, i_sum, np.int32(1), i, n*m, sc, + block=block_, + grid=grid_, + stream=self.queue, + shared=self.threadsPerBlock*4) + + if array.ndim >= 3: + # 3d case + # sum all dims, except the middle, multiplying by the index and scaling factor + block_ = (self.threadsPerBlock, 1, 1) + grid_ = (int(m), 1, 1) + self.indexed_sum_middim_cuda(array, m_sum, i, n, m, sc, + block=block_, + grid=grid_, + stream=self.queue, + shared=self.threadsPerBlock*4) + + # sum the all dims except the last, multiplying by the index and scaling factor + block_ = (32, 32, 1) + grid_ = (1, int(n + 32 - 1) // 32, 1) + self.indexed_sum_lastdim_cuda(array, n_sum, i*m, n, sc, + block=block_, + grid=grid_, + stream=self.queue, + shared=32*32*4) + else: + # 2d case + # sum the all dims except the last, multiplying by the index and scaling factor + block_ = (32, 32, 1) + grid_ = (1, int(m + 32 - 1) // 32, 1) + self.indexed_sum_lastdim_cuda(array, m_sum, i, m, sc, + block=block_, + grid=grid_, + stream=self.queue, + shared=32*32*4) + + block_ = (256, 1, 1) + grid_ = (3 if n>1 else 2, 1, 1) + self.final_sums_cuda(i_sum, i, m_sum, m, n_sum, n, out, + block=block_, + grid=grid_, + stream=self.queue, + shared=256*4) + + return out + +class Abs2SumKernel: + + def __init__(self, dtype, queue=None): + self.in_stype = map2ctype(dtype) + if self.in_stype == 'complex': + self.out_stype = 'float' + self.out_dtype = np.float32 + elif self.in_stype == 'copmlex': + self.out_stype = 'double' + self.out_dtype = np.float64 + else: + self.out_stype = self.in_stype + self.out_dtype = dtype + + self.queue = queue + self.threadsPerBlock = 32 + + self.abs2sum_cuda = load_kernel("abs2sum", subs={ + 'IN_TYPE': self.in_stype, + 'OUT_TYPE' : self.out_stype, + 'BDIM_X' : 32, + } + ) + + def abs2sum(self, array): + nmodes = np.int32(array.shape[0]) + row, col = array.shape[1:] + out = gpuarray.empty(array.shape[1:], dtype=self.out_dtype) + + block_ = (32, 1, 1) + grid_ = (1, row, 1) + self.abs2sum_cuda(array, nmodes, np.int32(row), np.int32(col), out, + block=block_, + grid=grid_, + stream=self.queue) + + return out + +class InterpolatedShiftKernel: + + def __init__(self, queue=None): + self.queue = queue + + self.integer_shift_cuda, self.linear_interpolate_cuda = load_kernel( + ("integer_shift_kernel", "linear_interpolate_kernel"), + file="interpolated_shift.cu", subs={ + 'IN_TYPE': 'complex', + 'OUT_TYPE': 'complex', + 'BDIM_X' : 32, + 'BDIM_Y' : 32, + } + ) + + def interpolate_shift(self, array, shift): + shift = np.asarray(shift, dtype=np.float32) + if len(shift) != 2: + raise NotImplementedError("Shift only applied to 2D array.") + if array.dtype != np.complex64: + raise NotImplementedError("Only complex single precision supported") + if array.ndim == 3: + items, rows, columns = array.shape + elif array.ndim == 2: + items, rows, columns = 1, *array.shape + else: + raise NotImplementedError("Only 2- or 3-dimensional arrays supported") + + offsetRow, offsetCol = shift + + offsetRowFrac, offsetRowInt = np.modf(offsetRow) + offsetColFrac, offsetColInt = np.modf(offsetCol) + + out = gpuarray.empty_like(array) + block_ = (32, 32, 1) + grid_ = ((rows + 31) // 32, (columns + 31) // 32, items) + + if np.abs(offsetRowFrac) < 1e-6 and np.abs(offsetColFrac) < 1e-6: + if offsetRowInt == 0 and offsetColInt == 0: + # no transformation at all + out = array + else: + # no fractional part, so we can just use a shifted copy + self.integer_shift_cuda(array, out, np.int32(rows), + np.int32(columns), np.int32(offsetRow), + np.int32(offsetCol), + block=block_, + grid=grid_, + stream=self.queue) + else: + self.linear_interpolate_cuda(array, out, np.int32(rows), + np.int32(columns), np.float32(offsetRow), + np.float32(offsetCol), + block=block_, + grid=grid_, + shared=(32+2)**2*8+32*(32+2)*8, + stream=self.queue) + + return out + diff --git a/ptypy/accelerate/cuda_pycuda/cuda/__init__.py b/ptypy/accelerate/cuda_pycuda/cuda/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ptypy/accelerate/cuda_pycuda/cuda/abs2sum.cu b/ptypy/accelerate/cuda_pycuda/cuda/abs2sum.cu new file mode 100644 index 000000000..475a228bb --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/abs2sum.cu @@ -0,0 +1,29 @@ +/** abs2sum kernel, calculating the sum of abs(x)**2 value in the first dimension + * + * Data types: + * - IN_TYPE: can be float/double or complex/complex + * - OUT_TYPE: can be float/double + */ + +#include +using thrust::complex; + +extern "C" __global__ void abs2sum(const IN_TYPE* a, + const int n, + const int rows, + const int cols, + OUT_TYPE* out) +{ + int tx = threadIdx.x; + const int iy = blockIdx.y; + + for (int ix = tx; ix < cols; ix += BDIM_X) { + OUT_TYPE acc = OUT_TYPE(0); + for (int in = 0; in < n; ++in) { + OUT_TYPE tmp = abs(a[in * rows * cols + iy * cols + ix]); + acc += tmp * tmp; + } + out[iy * cols + ix] = acc; + } +} + diff --git a/ptypy/accelerate/cuda_pycuda/cuda/batched_multiply.cu b/ptypy/accelerate/cuda_pycuda/cuda/batched_multiply.cu new file mode 100644 index 000000000..1263841b6 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/batched_multiply.cu @@ -0,0 +1,37 @@ +/** This kernel was used for FFT pre- and post-scaling, + to test if cuFFT via python is worthwhile. + It turned out it wasn't. + * + * Data types: + * - IN_TYPE: the data type for the inputs + * - OUT_TYPE: the data type for the outputs + * - MATH_TYPE: the data type used for computation (filter) + */ + +#include +using thrust::complex; + +extern "C" __global__ void batched_multiply(const complex* input, + complex* output, + const complex* filter, + float scale, + int nBatches, + int rows, + int columns) +{ + int gx = threadIdx.x + blockIdx.x * blockDim.x; + int gy = threadIdx.y + blockIdx.y * blockDim.y; + int gz = threadIdx.z + blockIdx.z * blockDim.z; + + if (gx > columns || gy > rows || gz > nBatches) + return; + + auto val = input[gz * rows * columns + gy * rows + gx]; + if (MPY_DO_FILT) // set at compile-time + { + val *= filter[gy * rows + gx]; + } + if (MPY_DO_SCALE) // set at compile-time + val *= scale; + output[gz * rows * columns + gy * rows + gx] = val; +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/build_aux.cu b/ptypy/accelerate/cuda_pycuda/cuda/build_aux.cu new file mode 100644 index 000000000..e9ceeb80c --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/build_aux.cu @@ -0,0 +1,99 @@ +/** build_aux kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +// core calculation function - used by both kernels and inlined +inline __device__ complex calculate( + const complex& t_obj, + const complex& t_probe, + const complex& t_ex, + MATH_TYPE alpha) +{ + return t_obj * t_probe * (MATH_TYPE(1) + alpha) - t_ex * alpha; +} + +extern "C" __global__ void build_aux( + complex* auxiliary_wave, + const complex* __restrict__ exit_wave, + int B, + int C, + const complex* __restrict__ probe, + int E, + int F, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE alpha_) +{ + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + const MATH_TYPE alpha = alpha_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + exit_wave += ea[0] * B * C; + auxiliary_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { +#pragma unroll(4) // we use blockDim.x = 32, and C is typically more than 128 + // (it will work for less as well) + for (int c = tx; c < C; c += blockDim.x) + { + auxiliary_wave[b * C + c] = calculate( + obj[b * I + c], probe[b * F + c], exit_wave[b * C + c], alpha); + } + } +} + +extern "C" __global__ void build_aux2( + complex* auxiliary_wave, + const complex* __restrict__ exit_wave, + int B, + int C, + const complex* __restrict__ probe, + int E, + int F, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE alpha_) +{ + int bid = blockIdx.z; + int tx = threadIdx.x; + int b = threadIdx.y + blockIdx.y * blockDim.y; + if (b >= B) + return; + int addr_stride = 15; + const MATH_TYPE alpha = alpha_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + exit_wave += ea[0] * B * C; + auxiliary_wave += ea[0] * B * C; + + for (int c = tx; c < C; c += blockDim.x) + { + auxiliary_wave[b * C + c] = calculate( + obj[b * I + c], probe[b * F + c], exit_wave[b * C + c], alpha); + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/build_aux_no_ex.cu b/ptypy/accelerate/cuda_pycuda/cuda/build_aux_no_ex.cu new file mode 100644 index 000000000..ee091c58e --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/build_aux_no_ex.cu @@ -0,0 +1,103 @@ +/** build_aux without exit wave kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +extern "C" __global__ void build_aux_no_ex(complex* auxilliary_wave, + int aRows, + int aCols, + const complex* __restrict__ probe, + int pRows, + int pCols, + const complex* __restrict__ obj, + int oRows, + int oCols, + const int* __restrict__ addr, + IN_TYPE fac_, + int doAdd) +{ + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + const int addr_stride = 15; + const MATH_TYPE fac = fac_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + obj += oa[0] * oRows * oCols + oa[1] * oCols + oa[2]; + probe += pa[0] * pRows * pCols + pa[1] * pCols + pa[2]; + auxilliary_wave += ea[0] * aRows * aCols; + + for (int b = ty; b < aRows; b += blockDim.y) + { +# pragma unroll(4) + for (int c = tx; c < aCols; c += blockDim.x) + { + complex t_obj = obj[b * oCols + c]; + complex t_probe = probe[b * pCols + c]; + auto tmp = t_obj * t_probe * fac; + if (doAdd) + { + auxilliary_wave[b * aCols + c] += tmp; + } + else + { + auxilliary_wave[b * aCols + c] = tmp; + } + } + } +} + +extern "C" __global__ void build_aux2_no_ex(complex* auxilliary_wave, + int aRows, + int aCols, + const complex* __restrict__ probe, + int pRows, + int pCols, + const complex* __restrict__ obj, + int oRows, + int oCols, + const int* __restrict__ addr, + IN_TYPE fac_, + int doAdd) +{ + int bid = blockIdx.z; + int tx = threadIdx.x; + int b = threadIdx.y + blockIdx.y * blockDim.y; + if (b >= aRows) + return; + const int addr_stride = 15; + const MATH_TYPE fac = fac_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + obj += oa[0] * oRows * oCols + oa[1] * oCols + oa[2]; + probe += pa[0] * pRows * pCols + pa[1] * pCols + pa[2]; + auxilliary_wave += ea[0] * aRows * aCols; + + for (int c = tx; c < aCols; c += blockDim.x) + { + complex t_obj = obj[b * oCols + c]; + complex t_probe = probe[b * pCols + c]; + auto tmp = t_obj * t_probe * fac; + if (doAdd) + { + auxilliary_wave[b * aCols + c] += tmp; + } + else + { + auxilliary_wave[b * aCols + c] = tmp; + } + } + +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/build_aux_position_correction.cu b/ptypy/accelerate/cuda_pycuda/cuda/build_aux_position_correction.cu new file mode 100644 index 000000000..327040371 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/build_aux_position_correction.cu @@ -0,0 +1,46 @@ +/** build_aux for position correction. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +extern "C" __global__ void build_aux_position_correction( + complex* auxiliary_wave, + const complex* __restrict__ probe, + int B, + int C, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr) +{ + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * B * C + pa[1] * C + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + auxiliary_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { +#pragma unroll(4) // we use blockDim.x = 32, and C is typically more than 128 + // (it will work for less as well) + for (int c = tx; c < C; c += blockDim.x) + { + complex t_obj = obj[b * I + c]; + complex t_probe = probe[b * C + c]; + auxiliary_wave[b * C + c] = t_obj * t_probe; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/build_exit.cu b/ptypy/accelerate/cuda_pycuda/cuda/build_exit.cu new file mode 100644 index 000000000..2b98634dc --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/build_exit.cu @@ -0,0 +1,65 @@ +/** build_exit kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, complex y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, y.real()); + atomicAdd(xf + 1, y.imag()); +} + +extern "C" __global__ void build_exit(complex* auxiliary_wave, + complex* exit_wave, + int B, + int C, + const complex* __restrict__ probe, + int E, + int F, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE alpha_) +{ + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + const int addr_stride = 15; + const MATH_TYPE alpha = alpha_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + exit_wave += ea[0] * B * C; + auxiliary_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { +#pragma unroll(4) // we use blockDim.x = 32, and C is typically more than 128 + // (it will work for less as well) + for (int c = tx; c < C; c += blockDim.x) + { + complex auxv = auxiliary_wave[b * C + c]; + complex t_probe = probe[b * F + c]; + complex t_obj = obj[b * I + c]; + complex t_exit = exit_wave[b * C + c]; + auxv -= alpha * t_probe * t_obj; + auxv += (alpha - 1) * t_exit; + exit_wave[b * C + c] += auxv; + auxiliary_wave[b * C + c] = auxv; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/build_exit_alpha_tau.cu b/ptypy/accelerate/cuda_pycuda/cuda/build_exit_alpha_tau.cu new file mode 100644 index 000000000..8528f2e9c --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/build_exit_alpha_tau.cu @@ -0,0 +1,60 @@ +/** build_exit_alpha_tau kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + + +#include +using thrust::complex; + + +extern "C" __global__ void build_exit_alpha_tau( + complex* auxiliary_wave, + complex* exit_wave, + int B, + int C, + const complex* __restrict__ probe, + int E, + int F, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE alpha_, + IN_TYPE tau_) +{ + int bid = blockIdx.z; + int tx = threadIdx.x; + const int b = threadIdx.y + blockIdx.y * blockDim.y; + if (b >= B) + return; + const int addr_stride = 15; + MATH_TYPE alpha = alpha_; + MATH_TYPE tau = tau_; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + exit_wave += ea[0] * B * C; + auxiliary_wave += ea[0] * B * C; + + for (int c = tx; c < C; c += blockDim.x) + { + complex t_aux = auxiliary_wave[b * C + c]; + complex t_probe = probe[b * F + c]; + complex t_obj = obj[b * I + c]; + complex t_ex = exit_wave[b * C + c]; + + auto dex = tau * t_aux + (tau * alpha - MATH_TYPE(1)) * t_ex + + (MATH_TYPE(1) - tau * (MATH_TYPE(1) + alpha)) * t_obj * t_probe; + + exit_wave[b * C + c] += dex; + auxiliary_wave[b * C + c] = dex; + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/clip_magnitudes.cu b/ptypy/accelerate/cuda_pycuda/cuda/clip_magnitudes.cu new file mode 100644 index 000000000..8128091f9 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/clip_magnitudes.cu @@ -0,0 +1,30 @@ +/** clip_magnitudes. + * + */ + #include + #include + #include + using thrust::complex; + + extern "C" __global__ void clip_magnitudes(IN_TYPE *arr, + float clip_min, + float clip_max, + int N) +{ + int id = threadIdx.x + blockIdx.x * blockDim.x; + + if (id >= N) + return; + + auto v = arr[id]; + auto mag = abs(v); + auto theta = arg(v); + + if (mag > clip_max) + mag = clip_max; + if (mag < clip_min) + mag = clip_min; + + v = thrust::polar(mag, theta); + arr[id] = v; +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/convolution.cu b/ptypy/accelerate/cuda_pycuda/cuda/convolution.cu new file mode 100644 index 000000000..ae42ecba5 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/convolution.cu @@ -0,0 +1,190 @@ +/** + * Data types: + * - DTYPE (float/double/complex/complex) + * - MATH_TYPE (float/double) - used for the convolution kernel itself + * + * A symmetric convolution kernel is assumed here + */ + +#include +using thrust::complex; + +/** Implements reflect-mode index wrapping + * + * maps indexes like this (reflects on both ends): + * Extension | Input | Extension + * 5 6 6 5 4 3 2 | 2 3 4 5 6 | 6 5 4 3 2 2 3 4 5 6 6 + * + */ + class IndexReflect + { + public: + /** Create index range. maxX is not included in the valid range, + * i.e., the range is [minX, maxX) + */ + __device__ IndexReflect(int minX, int maxX) : maxX_(maxX), minX_(minX) {} + + /// Map given index to the valid range using reflect mode + __device__ int operator()(int idx) const + { + if (idx < maxX_ && idx >= minX_) + return idx; + auto ddd = (idx - minX_) / (maxX_ - minX_); + auto mmm = (idx - minX_) % (maxX_ - minX_); + if (mmm < 0) + mmm = -mmm - 1; + // if odd it goes backwards from max + // if even it goes upwards from min + return ddd % 2 == 0 ? minX_ + mmm : maxX_ - mmm - 1; + } + + private: + int maxX_, minX_; + }; + + +/* +Row convolution kernel +*/ +extern "C" __global__ void convolution_row(const DTYPE *__restrict__ input, + DTYPE *output, + int height, + int width, + const MATH_TYPE* kernel, + int kernel_radius) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int bx = blockIdx.x; + int by = blockIdx.y; + + // offset for batch + input += width * height * blockIdx.z; + output += width * height * blockIdx.z; + + // reinterpret to avoid compiler warning that + // constructor of complex() cannot be called if it's + // shared memory - polluting the outputs + extern __shared__ char shr[]; + auto shm = reinterpret_cast(shr); + + // Offset to block start of core area + int gbx = bx * BDIM_X; + int gby = by * BDIM_Y; + int start = gbx * width + gby; + input += start; + output += start; + + // width of shared memory + int shwidth = BDIM_Y + 2 * kernel_radius; + + // only do this if row index is in range + // (need to keep threads with us, so that synchthreads below doesn't deadlock) + if (gbx + tx < height) + { + // main data (center point for each thread) - reflecting as needed + IndexReflect ind(-gby, width - gby); + shm[tx * shwidth + (kernel_radius + ty)] = input[tx * width + ind(ty)]; + + // left halo (kernel radius before) + for (int i = ty - kernel_radius; i < 0; i += BDIM_Y) + { + shm[tx * shwidth + (i + kernel_radius)] = input[tx * width + ind(i)]; + } + + // right halo (kernel radius after) + for (int i = ty + BDIM_Y; i < BDIM_Y + kernel_radius; i += BDIM_Y) + { + shm[tx * shwidth + (i + kernel_radius)] = input[tx * width + ind(i)]; + } + } + + __syncthreads(); + + // safe to return now, after syncing + if (gby + ty >= width || gbx + tx >= height) + return; + + // compute - will be complex if kernel is double + auto sum = shm[tx * shwidth + (ty + kernel_radius)] * kernel[0]; + for (int i = 1; i <= kernel_radius; ++i) + { + sum += (shm[tx * shwidth + (ty + i + kernel_radius)] + + shm[tx * shwidth + (ty - i + kernel_radius)]) * + kernel[i]; + } + + output[tx * width + ty] = sum; +} + + +/* +Column convolution kernel +*/ +extern "C" __global__ void convolution_col(const DTYPE *__restrict__ input, + DTYPE *output, + int height, + int width, + const MATH_TYPE* kernel, + int kernel_radius) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int bx = blockIdx.x; + int by = blockIdx.y; + + // offset for batch + input += width * height * blockIdx.z; + output += width * height * blockIdx.z; + + // reinterpret to avoid compiler warning that + // constructor of complex() cannot be called if it's + // shared memory - polluting the outputs + extern __shared__ char shr[]; + auto shm = reinterpret_cast(shr); + + // Offset to block start of core area + int gbx = bx * BDIM_X; + int gby = by * BDIM_Y; + int start = gbx * width + gby; + input += start; + output += start; + + // only do this if column index is in range + // (need to keep threads with us, so that synchthreads below doesn't deadlock) + if (gby + ty < width) + { + // main data (center point for each thread) - reflecting if needed + IndexReflect ind(-gbx, height - gbx); + shm[(kernel_radius + tx) * BDIM_Y + ty] = input[ind(tx) * width + ty]; + + // upper halo (kernel radius before) + for (int i = tx - kernel_radius; i < 0; i += BDIM_X) + { + shm[(i + kernel_radius) * BDIM_Y + ty] = input[ind(i) * width + ty]; + } + + // lower halo (kernel radius after) + for (int i = tx + BDIM_X; i < BDIM_X + kernel_radius; i += BDIM_X) + { + shm[(i + kernel_radius) * BDIM_Y + ty] = input[ind(i) * width + ty]; + } + } + + __syncthreads(); + + // safe to return now, after syncing + if (gby + ty >= width || gbx + tx >= height) + return; + + // compute - will be complex if kernel is double + auto sum = shm[(tx + kernel_radius) * BDIM_Y + ty] * kernel[0]; + for (int i = 1; i <= kernel_radius; ++i) + { + sum += (shm[(tx + i + kernel_radius) * BDIM_Y + ty] + + shm[(tx - i + kernel_radius) * BDIM_Y + ty]) * + kernel[i]; + } + + output[tx * width + ty] = sum; +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/delx.cu b/ptypy/accelerate/cuda_pycuda/cuda/delx.cu new file mode 100644 index 000000000..f2e8a934e --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/delx.cu @@ -0,0 +1,210 @@ +/** difference along axes (last and mid axis kernels) + * + * Data types: + * - IN_TYPE: the data type for the inputs + * - OUT_TYPE: the data type for the outputs + */ + +#include +using thrust::complex; + + +/** Finite difference for forward/backward for any axis that is not the + * last one, assuring that the reads and writes are coalesced. + * + * The idea is that arrays of any number of dimensions can be reshaped + * (or just treated) as 3D, with the higher dimensions and lower dimensions + * multiplied together and the axis along which we differentiate in the middle. + * The higher dim might also be 1, capturing the case when we compute along + * the zero axis. + * + * We use the following variables: + * - higher_dim: sizes of the axes left of the diff axis multiplied together + * - axis_dim: the size along the axis we diff over + * - lower_dim: the sizes of the axes right of the diff axis multiplied together + * + * Examples: + * - 5x3x10, axis=1: higher_dim=5, axis_dim=3, lower_dim=10 + * - 10x5x4x3, axis=1: higher_dim=10, axis_dim=5, lower_dim=12 + * - 10x5x4x3, axis=0: higher_dim=1, axis_dim=10, lower_dim=60 + * - 30x40, axis=0: higher_dim=1, axis_dim=30, lower_dim=40 + * + * The thread/block dimensions are mapped as: + * z = high_dim, + * y = axis_dim, + * x = lower_dim + * + * We read tiles of the input into BDIM_Y x BDIM_X elements of shared memory, + * always using a single thread block along the y dimension (axis_dim), + * which iterates over the full axis in a loop. The other 2 dimensions are + * fully parallelised in different thread blocks. + * Data reads/writes into shared mem are coalesced since the ix index + * corresponding to the threadIdx.x is used to read the lower_dim, with no + * multiplier on the index. + * + * Once the tile is in shared memory, the difference is calculated - + * depending on forward/backward diffs, and with special cases at the + * end of the tile - either overlapping with next block or ensuring a + * zero if it's the end of the input. + * + */ +extern "C" __global__ void delx_mid(const IN_TYPE *__restrict__ input, + OUT_TYPE *output, + int lower_dim, // x for 3D + int higher_dim, // z for 3D + int axis_dim) +{ + // reinterpret to avoid compiler warning that + // constructor of complex() cannot be called if it's + // shared memory - polluting the outputs + __shared__ char shr[BDIM_X * BDIM_Y * sizeof(IN_TYPE)]; + auto shared_data = reinterpret_cast(shr); + + unsigned int tx = threadIdx.x; + unsigned int ty = threadIdx.y; + unsigned int tz = threadIdx.z; // only 0 here + + unsigned int ix = tx + blockIdx.x * BDIM_X; + unsigned int iy = ty; + unsigned int iz = tz + blockIdx.z * blockDim.z; + + // offset pointers for z dimension (higher-dim) + input += iz * axis_dim * lower_dim; + output += iz * axis_dim * lower_dim; + + // now read x/y tiles coalesced and perform difference along y, + // letting this thread block iterate along the full y axis + // to give a thread a bit more substantial work to do. + auto maxblocks = (axis_dim + BDIM_Y - 1) / BDIM_Y; + for (int bidx = 0; bidx < maxblocks; ++bidx) + { + iy = ty + bidx * BDIM_Y; + + if (iy < axis_dim && ix < lower_dim) + { + shared_data[ty * BDIM_X + tx] = input[iy * lower_dim + ix]; + } + __syncthreads(); + + if (iy < axis_dim && ix < lower_dim) + { + if (IS_FORWARD) + { + IN_TYPE plus1; + if (ty < BDIM_Y - 1 && + iy < axis_dim - 1) // we have a next element in shared data + { + plus1 = shared_data[(ty + 1) * BDIM_X + tx]; + } + else if (iy == axis_dim - 1) // end of axis + { + plus1 = shared_data[ty * BDIM_X + tx]; // make sure it's zero + } + else // end of block, but nore input is there + { + plus1 = input[(iy + 1) * lower_dim + ix]; + } + output[iy * lower_dim + ix] = plus1 - shared_data[ty * BDIM_X + tx]; + } + else + { + IN_TYPE minus1; + if (ty > 0) // we have a previous element in shared + { + minus1 = shared_data[(ty - 1) * BDIM_X + tx]; + } + else if (iy == 0) // use same as next to get zero + { + minus1 = shared_data[ty * BDIM_X + tx]; + } + else // read previous input (ty == 0 but iy > 0) + { + minus1 = input[(iy - 1) * lower_dim + ix]; + } + output[iy * lower_dim + ix] = shared_data[ty * BDIM_X + tx] - minus1; + } + } + } +} + + + +/** This is the special case for when we diff along the last axis. + * + * Here, flat_dim is all other dims multiplied together, and axis_dim + * is the dimension along which we diff. + * To ensure that we stay coalesced (compared to delx_mid), + * we use the x index to iterate within each thread block (the loop). + * Otherwise it follows the same ideas as delx_mid - please read the + * description there. + */ +extern "C" __global__ void delx_last(const IN_TYPE *__restrict__ input, + OUT_TYPE *output, + int flat_dim, + int axis_dim) +{ + // reinterpret to avoid constructor of complex() + compiler warning + __shared__ char shr[BDIM_X * BDIM_Y * sizeof(IN_TYPE)]; + auto shared_data = reinterpret_cast(shr); + + unsigned int tx = threadIdx.x; + unsigned int ty = threadIdx.y; + + unsigned int ix = tx; + unsigned int iy = ty + blockIdx.x * BDIM_Y; // we always use x in grid + + int stride_y = axis_dim; + + auto maxblocks = (axis_dim + BDIM_X - 1) / BDIM_X; + for (int bidx = 0; bidx < maxblocks; ++bidx) + { + ix = tx + bidx * BDIM_X; + + if (iy < flat_dim && ix < axis_dim) + { + shared_data[ty * BDIM_X + tx] = input[iy * stride_y + ix]; + } + + __syncthreads(); + + if (iy < flat_dim && ix < axis_dim) + { + if (IS_FORWARD) + { + IN_TYPE plus1; + if (tx < BDIM_X - 1 && + ix < axis_dim - 1) // we have a next element in shared data + { + plus1 = shared_data[ty * BDIM_X + tx + 1]; + } + else if (ix == axis_dim - 1) // end of axis - same as current to get 0 + { + plus1 = shared_data[ty * BDIM_X + tx]; + } + else // end of block, but nore input is there + { + plus1 = input[iy * stride_y + ix + 1]; + } + + output[iy * stride_y + ix] = plus1 - shared_data[ty * BDIM_X + tx]; + } + else + { + IN_TYPE minus1; + if (tx > 0) // we have a previous element in shared + { + minus1 = shared_data[ty * BDIM_X + tx - 1]; + } + else if (ix == 0) // use same as next to get zero + { + minus1 = shared_data[ty * BDIM_X + tx]; + } + else // read previous input (ty == 0 but iy > 0) + { + minus1 = input[iy * stride_y + ix - 1]; + } + output[iy * stride_y + ix] = shared_data[ty * BDIM_X + tx] - minus1; + } + } + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/dot.cu b/ptypy/accelerate/cuda_pycuda/cuda/dot.cu new file mode 100644 index 000000000..21087abe3 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/dot.cu @@ -0,0 +1,56 @@ +#include +#include +using thrust::complex; + +template +__device__ inline T dotmul(const T& a, const T& b) +{ + return a * b; +} + +template +__device__ inline T dotmul(const complex& a, const complex& b) +{ + //return (a * conj(b)).real(); + return a.real() * b.real() + a.imag() * b.imag(); +} + +extern "C" __global__ void dot(const IN_TYPE* a, + const IN_TYPE* b, + int size, + ACC_TYPE* out) +{ + int tx = threadIdx.x; + int ix = tx + blockIdx.x * blockDim.x; + + __shared__ ACC_TYPE sh[1024]; + + if (ix < size) + { + sh[tx] = dotmul(a[ix], b[ix]); + } + else + { + sh[tx] = ACC_TYPE(0); + } + __syncthreads(); + + int nt = blockDim.x; + int c = nt; + + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + sh[tx] += sh[c - tx - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tx == 0 && ix < size) + { + out[blockIdx.x] = sh[0]; + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/error_reduce.cu b/ptypy/accelerate/cuda_pycuda/cuda/error_reduce.cu new file mode 100644 index 000000000..91b5357b4 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/error_reduce.cu @@ -0,0 +1,56 @@ +/** error_reduce kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - ACC_TYPE: the data type used for computation + */ + +extern "C" __global__ void error_reduce(const IN_TYPE* ferr, + OUT_TYPE* err_fmag, + int M, + int N) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int batch = blockIdx.x; + __shared__ ACC_TYPE sum_v[BDIM_X*BDIM_Y]; + + int shidx = + ty * blockDim.x + tx; // shidx: index in shared memory for this block + ACC_TYPE sum = ACC_TYPE(0.0); + + for (int m = ty; m < M; m += blockDim.y) + { +#pragma unroll(4) + for (int n = tx; n < N; n += blockDim.x) + { + int idx = batch * M * N + m * N + + n; // idx is index qwith respect to the full stack + sum += ACC_TYPE(ferr[idx]); + } + } + + sum_v[shidx] = sum; + + __syncthreads(); + + int nt = BDIM_X * BDIM_Y; + int c = nt; + + while (c > 1) + { + int half = c / 2; + if (shidx < half) + { + sum_v[shidx] += sum_v[c - shidx - 1]; + } + __syncthreads(); + c = c - half; + } + + if (shidx == 0) + { + err_fmag[batch] = OUT_TYPE(sum_v[0]); + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/exit_error.cu b/ptypy/accelerate/cuda_pycuda/cuda/exit_error.cu new file mode 100644 index 000000000..fdac52e46 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/exit_error.cu @@ -0,0 +1,47 @@ +#include +#include +#include +using std::sqrt; +using thrust::abs; +using thrust::complex; + +// specify max number of threads/block and min number of blocks per SM, +// to assist the compiler in register optimisations. +// We achieve a higher occupancy in this case, as less registers are used +// (guided by profiler) +extern "C" __global__ void __launch_bounds__(1024, 2) + exit_error(int nmodes, + const complex * __restrict aux, + OUT_TYPE *ferr, + const int * __restrict addr, + int A, + int B) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + MATH_TYPE denom = A * B; + + const int *ea = addr + 6 + (blockIdx.x * nmodes) * addr_stride; + const int *da = addr + 9 + (blockIdx.x * nmodes) * addr_stride; + + aux += ea[0] * A * B; + ferr += da[0] * A * B; + + for (int a = ty; a < A; a += blockDim.y) + { + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE acc = 0.0; + for (int idx = 0; idx < nmodes; ++idx) + { + complex t_aux = aux[a * B + b + idx * A * B]; + MATH_TYPE abs_exit_wave = abs(t_aux); + acc += abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + ferr[a * B + b] = OUT_TYPE(acc / denom); + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fill3D.cu b/ptypy/accelerate/cuda_pycuda/cuda/fill3D.cu new file mode 100644 index 000000000..c3f03d8ca --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fill3D.cu @@ -0,0 +1,60 @@ +/** fill3D kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs + * - OUT_TYPE: data type for outputs + */ + +#include +#include +using thrust::complex; + +extern "C" __global__ void fill3D( + OUT_TYPE* A, + const IN_TYPE* B, + // final dimensions of A/B in [z, y, x] + int A_Z, + int A_Y, + int A_X, + int B_Z, + int B_Y, + int B_X, + // offsets to start reading/writing + int Ao_z, + int Ao_y, + int Ao_x, + int Bo_z, + int Bo_y, + int Bo_x, + // lengths to copy + int len_z, + int len_y, + int len_x + ) +{ + // We use the following strategy: + // - BlockIdx.z for the batch (first dims combined if 4D+) + // - blockDim.z = 1 + // - multiple blocks are used across y and x dimensions + // - we loop over z dimension within the thread block + int batch = blockIdx.z; + int ix = threadIdx.x + blockIdx.x * blockDim.x; + int iy = threadIdx.y + blockIdx.y * blockDim.y; + + if (ix >= len_x || iy >= len_y) + return; + + // offset for current batch (4D+ dimension) + A += batch * A_X * A_Y * A_Z; + B += batch * B_X * B_Y * B_Z; + + // offset for start position in each dimension of the last 3 + A += Ao_z * A_Y * A_X + Ao_y * A_X + Ao_x; + B += Bo_z * B_Y * B_X + Bo_y * B_X + Bo_x; + + // copy data + for (int iz = 0; iz < len_z; ++iz) { + A[iz * A_Y * A_X + iy * A_X + ix] = + B[iz * B_Y * B_X + iy * B_X + ix]; + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fill_b.cu b/ptypy/accelerate/cuda_pycuda/cuda/fill_b.cu new file mode 100644 index 000000000..46d0d09f1 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fill_b.cu @@ -0,0 +1,105 @@ +/** fill_b kernels. + * Data types: + * - IN_TYPE: the data type for the inputs + * - OUT_TYPE: the data type for the outputs + * - MATH_TYPE: the data type used for computation + * - ACC_TYPE: the accumulator type for summing + */ + +extern "C" __global__ void fill_b(const IN_TYPE* A0, + const IN_TYPE* A1, + const IN_TYPE* A2, + const IN_TYPE* w, + IN_TYPE Brenorm, + int size, + ACC_TYPE* out) +{ + int tx = threadIdx.x; + int ix = tx + blockIdx.x * blockDim.x; + __shared__ ACC_TYPE smem[3][BDIM_X]; + + if (ix < size) + { + // MATHTYPE(2) to make sure it's float in single precision and doesn't + // accidentally promote the equation to double + MATH_TYPE t_a0 = A0[ix]; + MATH_TYPE t_a1 = A1[ix]; + MATH_TYPE t_a2 = A2[ix]; + MATH_TYPE t_w = w[ix]; + smem[0][tx] = t_w * t_a0 * t_a0; + smem[1][tx] = t_w * MATH_TYPE(2) * t_a0 * t_a1; + smem[2][tx] = t_w * (t_a1 * t_a1 + MATH_TYPE(2) * t_a0 * t_a2); + } + else + { + smem[0][tx] = ACC_TYPE(0); + smem[1][tx] = ACC_TYPE(0); + smem[2][tx] = ACC_TYPE(0); + } + __syncthreads(); + + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + smem[0][tx] += smem[0][c - tx - 1]; + smem[1][tx] += smem[1][c - tx - 1]; + smem[2][tx] += smem[2][c - tx - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tx == 0) + { + out[blockIdx.x * 3 + 0] = MATH_TYPE(smem[0][0]) * MATH_TYPE(Brenorm); + out[blockIdx.x * 3 + 1] = MATH_TYPE(smem[1][0]) * MATH_TYPE(Brenorm); + out[blockIdx.x * 3 + 2] = MATH_TYPE(smem[2][0]) * MATH_TYPE(Brenorm); + } +} + +extern "C" __global__ void fill_b_reduce(const ACC_TYPE* in, OUT_TYPE* B, int blocks) +{ + // always a single thread block for 2nd stage + assert(gridDim.x == 1); + int tx = threadIdx.x; + + __shared__ ACC_TYPE smem[3][BDIM_X]; + + double sum0 = 0.0, sum1 = 0.0, sum2 = 0.0; + for (int ix = tx; ix < blocks; ix += blockDim.x) + { + sum0 += in[ix * 3 + 0]; + sum1 += in[ix * 3 + 1]; + sum2 += in[ix * 3 + 2]; + } + smem[0][tx] = sum0; + smem[1][tx] = sum1; + smem[2][tx] = sum2; + __syncthreads(); + + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + smem[0][tx] += smem[0][c - tx - 1]; + smem[1][tx] += smem[1][c - tx - 1]; + smem[2][tx] += smem[2][c - tx - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tx == 0) + { + B[0] += OUT_TYPE(smem[0][0]); + B[1] += OUT_TYPE(smem[1][0]); + B[2] += OUT_TYPE(smem[2][0]); + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fmag_all_update.cu b/ptypy/accelerate/cuda_pycuda/cuda/fmag_all_update.cu new file mode 100644 index 000000000..f8f695ca5 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fmag_all_update.cu @@ -0,0 +1,62 @@ +/** fmag_all_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +#include +using std::sqrt; +using thrust::complex; + +extern "C" __global__ void fmag_all_update(complex* f, + const IN_TYPE* fmask, + const IN_TYPE* fmag, + const IN_TYPE* fdev, + const IN_TYPE* err_fmag, + const int* addr_info, + IN_TYPE pbound_, + int A, + int B) +{ + int batch = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + MATH_TYPE pbound = pbound_; + + const int* ea = addr_info + batch * addr_stride + 6; + const int* da = addr_info + batch * addr_stride + 9; + const int* ma = addr_info + batch * addr_stride + 12; + + fmask += ma[0] * A * B; + MATH_TYPE err = err_fmag[da[0]]; + fdev += da[0] * A * B; + fmag += da[0] * A * B; + f += ea[0] * A * B; + MATH_TYPE renorm = sqrt(pbound / err); + + for (int a = ty; a < A; a += blockDim.y) + { + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE m = fmask[a * A + b]; + if (renorm < 1.0f) + { + /* + // assuming this is actually a mask, i.e. 0 or 1 --> this is slower + float fm = m < 0.5f ? 1.0f : + ((fmag[a * A + b] + fdev[a * A + b] * renorm) / (fdev[a * A + b] + + fmag[a * A + b] + 1e-7f)) ; + */ + MATH_TYPE fmagv = fmag[a * A + b]; + MATH_TYPE fdevv = fdev[a * A + b]; + MATH_TYPE fm = (MATH_TYPE(1) - m) + + m * ((fmagv + fdevv * renorm) / (fmagv + fdevv + MATH_TYPE(1e-7))); + f[a * A + b] *= fm; + } + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fmag_update_nopbound.cu b/ptypy/accelerate/cuda_pycuda/cuda/fmag_update_nopbound.cu new file mode 100644 index 000000000..40a65c172 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fmag_update_nopbound.cu @@ -0,0 +1,53 @@ +/** fmag_all_update_nopbound. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +#include +using std::sqrt; +using thrust::complex; + +extern "C" __global__ void fmag_update_nopbound(complex* f, + const IN_TYPE* fmask, + const IN_TYPE* fmag, + const IN_TYPE* fdev, + const int* addr_info, + int A, + int B) +{ + const int bid = blockIdx.z; + const int tx = threadIdx.x; + const int a = threadIdx.y + blockIdx.y * blockDim.y; + if (a >= A) + return; + int addr_stride = 15; + + const int* ea = addr_info + bid * addr_stride + 6; + const int* da = addr_info + bid * addr_stride + 9; + const int* ma = addr_info + bid * addr_stride + 12; + + fmask += ma[0] * A * B; + fdev += da[0] * A * B; + fmag += da[0] * A * B; + f += ea[0] * A * B; + + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE m = fmask[a * A + b]; + /* + // assuming this is actually a mask, i.e. 0 or 1 --> this is slower + float fm = m < 0.5f ? 1.0f : + ((fmag[a * A + b] + fdev[a * A + b] * renorm) / (fdev[a * A + b] + + fmag[a * A + b] + 1e-7f)) ; + */ + MATH_TYPE fmagv = fmag[a * A + b]; + MATH_TYPE fdevv = fdev[a * A + b]; + MATH_TYPE fm = + (MATH_TYPE(1) - m) + m * (fmagv / (fmagv + fdevv + MATH_TYPE(1e-7))); + f[a * A + b] *= fm; + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fourier_deviation.cu b/ptypy/accelerate/cuda_pycuda/cuda/fourier_deviation.cu new file mode 100644 index 000000000..3427222c3 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fourier_deviation.cu @@ -0,0 +1,58 @@ +/** fourier_deviation. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +#include +#include +using std::sqrt; +using thrust::abs; +using thrust::complex; + +// specify max number of threads/block and min number of blocks per SM, +// to assist the compiler in register optimisations. +// We achieve a higher occupancy in this case, as less registers are used +// (guided by profiler) +extern "C" __global__ void __launch_bounds__(1024, 2) + fourier_deviation(int nmodes, + const complex *f, + const IN_TYPE *fmag, + OUT_TYPE *fdev, + const int *addr, + int A, + int B) +{ + const int bid = blockIdx.z; + const int tx = threadIdx.x; + const int a = threadIdx.y + blockIdx.y * blockDim.y; + const int addr_stride = 15; + + const int *ea = addr + 6 + (bid * nmodes) * addr_stride; + const int *da = addr + 9 + (bid * nmodes) * addr_stride; + + f += ea[0] * A * B; + fdev += da[0] * A * B; + fmag += da[0] * A * B; + + if (a >= A) + return; + + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE acc = MATH_TYPE(0); + for (int idx = 0; idx < nmodes; ++idx) + { + complex t_f = f[a * B + b + idx * A * B]; + MATH_TYPE abs_exit_wave = abs(t_f); + acc += abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + auto fdevv = sqrt(acc) - MATH_TYPE(fmag[a * B + b]); + fdev[a * B + b] = fdevv; + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fourier_error.cu b/ptypy/accelerate/cuda_pycuda/cuda/fourier_error.cu new file mode 100644 index 000000000..ad483c870 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fourier_error.cu @@ -0,0 +1,65 @@ +/** fourier_error. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + + +#include +#include +#include +using std::sqrt; +using thrust::abs; +using thrust::complex; + +// specify max number of threads/block and min number of blocks per SM, +// to assist the compiler in register optimisations. +// We achieve a higher occupancy in this case, as less registers are used +// (guided by profiler) +extern "C" __global__ void __launch_bounds__(1024, 2) + fourier_error(int nmodes, + const complex *f, + const IN_TYPE *fmask, + const IN_TYPE *fmag, + OUT_TYPE *fdev, + OUT_TYPE *ferr, + const IN_TYPE *mask_sum, + const int *addr, + int A, + int B) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + + const int *ea = addr + 6 + (blockIdx.x * nmodes) * addr_stride; + const int *da = addr + 9 + (blockIdx.x * nmodes) * addr_stride; + const int *ma = addr + 12 + (blockIdx.x * nmodes) * addr_stride; + + f += ea[0] * A * B; + fdev += da[0] * A * B; + fmag += da[0] * A * B; + fmask += ma[0] * A * B; + ferr += da[0] * A * B; + + for (int a = ty; a < A; a += blockDim.y) + { + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE acc = MATH_TYPE(0); + for (int idx = 0; idx < nmodes; ++idx) + { + complex t_f = f[a * B + b + idx * A * B]; + MATH_TYPE abs_exit_wave = abs(t_f); + acc += abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + auto fdevv = sqrt(acc) - MATH_TYPE(fmag[a * B + b]); + ferr[a * B + b] = (fmask[a * B + b] * fdevv * fdevv) / mask_sum[ma[0]]; + fdev[a * B + b] = fdevv; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fourier_error2.cu b/ptypy/accelerate/cuda_pycuda/cuda/fourier_error2.cu new file mode 100644 index 000000000..86dddf549 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fourier_error2.cu @@ -0,0 +1,84 @@ +/** This kernel was an experiment to use shared memory reduction across + * the modes. It turned out to run about 2x slower than the one without + * shared memory, so it's not used at this stage. + */ +#include +#include +#include +using thrust::complex; + +extern "C" __global__ void fourier_error2(int nmodes, + complex *f, + const float *fmask, + const float *fmag, + float *fdev, + float *ferr, + const float *mask_sum, + const int *addr, + int A, + int B) +{ + // block in x/y are across the full tile (ix and iy go from 0 to A/B) + // might go beyond if not divisible + // blockDim.z is nmodes - we use z index to go over the modes accumulation + + int ix = threadIdx.x + blockIdx.x * blockDim.x; + int iy = threadIdx.y + blockIdx.y * blockDim.y; + int tz = threadIdx.z; // for all modes within block + int addr_stride = 15; + assert(tz < nmodes); + + const int *ea = addr + 6 + (blockIdx.z * nmodes) * addr_stride; + const int *da = addr + 9 + (blockIdx.z * nmodes) * addr_stride; + const int *ma = addr + 12 + (blockIdx.z * nmodes) * addr_stride; + + // full offset for this thread + f += ea[0] * A * B + iy * B + ix; + fdev += da[0] * A * B + iy * B + ix; + fmag += da[0] * A * B + iy * B + ix; + fmask += ma[0] * A * B + iy * B + ix; + ferr += da[0] * A * B + iy * B + ix; + + extern __shared__ float shm[]; // BX * BY * nmodes + + // offset so we have shmt[0..nmodes] to reduce in + auto shmt = + shm + threadIdx.x * blockDim.y * blockDim.z + threadIdx.y * blockDim.z; + + // modes values + if (ix < B && iy < A) + { + float abs_exit_wave = abs(f[tz * A * B]); + shmt[tz] = abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + else + { + shmt[tz] = 0.0f; + } + __syncthreads(); + + // accumulate across modes + assert(nmodes == blockDim.z); + int c = nmodes; + while (c > 1) + { + int half = c / 2; + if (tz < half) + { + shmt[tz] += shmt[c - tz - 1]; + } + __syncthreads(); + c = c - half; + } + + // now write outputs if we're the first thread in the block + if (tz == 0 && iy < A && ix < B) + { + auto acc = shmt[0]; + auto fdevv = sqrt(acc) - *fmag; + *ferr = (*fmask * fdevv * fdevv) / mask_sum[ma[0]]; + *fdev = fdevv; + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/fourier_update.cu b/ptypy/accelerate/cuda_pycuda/cuda/fourier_update.cu new file mode 100644 index 000000000..a713c4418 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/fourier_update.cu @@ -0,0 +1,138 @@ +/* +This was a test to join the fourier update kernels, +and use shared memory across the modes. +But the performance +is 2x slower than individual as we have many idle threads here. +It is not used at the moment. +*/ + +#include +#include +#include +using thrust::complex; + +extern "C" __global__ void fourier_update(int nmodes, + complex *f_d, + const float *fmask_d, + const float *fmag_d, + float *fdev_d, + float *ferr_d, + const float *mask_sum, + const int *addr, + float *err_fmag, + float pbound, + int A, + int B) +{ + // block in x/y are across the full tile (ix and iy go from 0 to A/B) + // might go beyond if not divisible + // blockDim.z is nmodes - we use z index to go over the modes accumulation + int tx = threadIdx.x; + int ty = threadIdx.y; + int tz = threadIdx.z; + + int ix = tx + blockIdx.x * blockDim.x; + int iy = ty + blockIdx.y * blockDim.y; + int addr_stride = 15; + assert(tz < nmodes); + + const int *ea = addr + 6 + (blockIdx.z * nmodes) * addr_stride; + const int *da = addr + 9 + (blockIdx.z * nmodes) * addr_stride; + const int *ma = addr + 12 + (blockIdx.z * nmodes) * addr_stride; + + // full offset for this thread + auto f = f_d + ea[0] * A * B + iy * B + ix; + auto fdev = fdev_d + da[0] * A * B + iy * B + ix; + auto fmag = fmag_d + da[0] * A * B + iy * B + ix; + auto fmask = fmask_d + ma[0] * A * B + iy * B + ix; + auto ferr = ferr_d + da[0] * A * B + iy * B + ix; + + extern __shared__ float shm[]; // BX * BY * nmodes + + // offset so we have shmt[0..nmodes] to reduce in + auto shmt = + shm + threadIdx.x * blockDim.y * blockDim.z + threadIdx.y * blockDim.z; + + // modes values + if (ix < B && iy < A) + { + float abs_exit_wave = abs(f[tz * A * B]); + shmt[tz] = abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get bad rounding errors + } + else + { + shmt[tz] = 0.0f; + } + __syncthreads(); + + // accumulate across modes + assert(nmodes == blockDim.z); + int c = nmodes; + while (c > 1) + { + int half = c / 2; + if (tz < half) + { + shmt[tz] += shmt[c - tz - 1]; + } + __syncthreads(); + c = c - half; + } + + // now write outputs if we're the first thread in the block + int tyrem = (A - iy) < int(blockDim.y) ? (A - iy) : blockDim.y; + int txrem = (B - ix) < int(blockDim.x) ? (B - ix) : blockDim.x; + int nt = tyrem * txrem; + int shidx = ty * txrem + tx; + if (tz == 0 && iy < A && ix < B) + { + auto acc = shmt[0]; + auto fdevv = sqrt(acc) - *fmag; + *ferr = (*fmask * fdevv * fdevv) / mask_sum[ma[0]]; + *fdev = fdevv; + + shm[shidx] = *ferr; + } + + ////////////// error reduce + __syncthreads(); + c = nt; + while (c > 1) + { + int half = c / 2; + if (shidx < half && tz == 0) + { + shm[shidx] += shm[c - shidx - 1]; + } + __syncthreads(); + c = c - half; + } + if (shidx == 0 && tz == 0) + { + err_fmag[blockIdx.z] = shm[0]; + } + + ///////////// fmag_all_update + ea += tz * addr_stride; + da += tz * addr_stride; + ma += tz * addr_stride; + + fmask = fmask_d + ma[0] * A * B + iy * B + ix; + float err = err_fmag[da[0]]; // RACE CONDITION! + fdev = fdev_d + da[0] * A * B + iy * B + ix; + fmag = fmag_d + da[0] * A * B + iy * B + ix; + f = f_d + ea[0] * A * B + iy * B + ix; + float renorm = sqrt(pbound / err); + + if (ix < B && iy < A && renorm < 1.0f) + { + auto m = *fmask; + auto fmagv = *fmag; + auto fdevv = *fdev; + float fm = + (1.0f - m) + m * ((fmagv + fdevv * renorm) / (fmagv + fdevv + 1e-10f)); + *f *= fm; + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/full_reduce.cu b/ptypy/accelerate/cuda_pycuda/cuda/full_reduce.cu new file mode 100644 index 000000000..801204aaa --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/full_reduce.cu @@ -0,0 +1,44 @@ +/** full_reduce kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - ACC_TYPE: the data type used for internal accumulation + */ + + +#include + +extern "C" __global__ void full_reduce(const IN_TYPE* in, OUT_TYPE* out, int size) +{ + assert(gridDim.x == 1); + int tx = threadIdx.x; + + __shared__ ACC_TYPE smem[BDIM_X]; + + auto sum = ACC_TYPE(); + for (int ix = tx; ix < size; ix += blockDim.x) + { + sum = sum + ACC_TYPE(in[ix]); + } + smem[tx] = sum; + __syncthreads(); + + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + smem[tx] += smem[c - tx - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tx == 0) + { + out[0] = OUT_TYPE(smem[0]); + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/gd_main.cu b/ptypy/accelerate/cuda_pycuda/cuda/gd_main.cu new file mode 100644 index 000000000..1ab643c4c --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/gd_main.cu @@ -0,0 +1,36 @@ +/** gd_main kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +extern "C" __global__ void gd_main(const IN_TYPE* Imodel, + const IN_TYPE* I, + const IN_TYPE* w, + OUT_TYPE* err, + complex* aux, + int z, + int modes, + int x) +{ + int iz = blockIdx.z; + int ix = threadIdx.x + blockIdx.x * blockDim.x; + + if (iz >= z || ix >= x) + return; + + auto DI = MATH_TYPE(Imodel[iz * x + ix]) - MATH_TYPE(I[iz * x + ix]); + auto tmp = MATH_TYPE(w[iz * x + ix]) * MATH_TYPE(DI); + err[iz * x + ix] = tmp * DI; + + // now set this for all modes (promote) + for (int m = 0; m < modes; ++m) + { + aux[iz * x * modes + m * x + ix] *= tmp; + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/get_address.cu b/ptypy/accelerate/cuda_pycuda/cuda/get_address.cu new file mode 100644 index 000000000..dda9b45f1 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/get_address.cu @@ -0,0 +1,35 @@ +#include +#include +using thrust::complex; + +inline __device__ int minimum(int a, int b) { return a < b ? a : b; } + +inline __device__ int maximum(int a, int b) { return a < b ? b : a; } + +extern "C" __global__ void get_address(const int* addr_current, + int* mangled_addr, + int num_pods, + const int* __restrict delta, + int max_oby, + int max_obx) +{ + // we use only one thread block + const int tx = threadIdx.x; + const int idx = tx % 2; // even threads access y dim, odd threads x dim + const int maxval = (idx == 0) ? max_oby : max_obx; + + const int addr_stride = 15; + const int d = delta[idx]; + addr_current += 3 + idx + 1; + mangled_addr += 3 + idx + 1; + + for (int ix = tx; ix < num_pods * 2; ix += blockDim.x) + { + const int bid = ix / 2; + int cur = addr_current[bid * addr_stride] + d; + int bound = maximum(0, minimum(maxval, cur)); + assert(bound >= 0); + assert(bound <= maxval); + mangled_addr[bid * addr_stride] = bound; + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/intens_renorm.cu b/ptypy/accelerate/cuda_pycuda/cuda/intens_renorm.cu new file mode 100644 index 000000000..d0033f7f4 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/intens_renorm.cu @@ -0,0 +1,66 @@ +/** intens_renorm - with 2 steps as separate kernels. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +extern "C" __global__ void step1(const IN_TYPE* Imodel, + const IN_TYPE* I, + const IN_TYPE* w, + OUT_TYPE* num, + OUT_TYPE* den, + int n) +{ + int i = threadIdx.x + blockIdx.x * blockDim.x; + + if (i >= n) + return; + + auto tmp = MATH_TYPE(w[i]) * MATH_TYPE(Imodel[i]); + num[i] = tmp * MATH_TYPE(I[i]); + den[i] = tmp * MATH_TYPE(Imodel[i]); +} + +extern "C" __global__ void step2(const IN_TYPE* fic_tmp, + OUT_TYPE* fic, + OUT_TYPE* Imodel, + int X, + int Y) +{ + int iz = blockIdx.z; + int tx = threadIdx.x; + int ty = threadIdx.y; + + // one thread block per fic data point - we want the first thread to read this + // into shared memory and then sync the block, so we don't get into data races + // with writing it back to global memory in the end (and we read the value only + // once) + // + __shared__ MATH_TYPE shfic[1]; + if (tx == 0 && ty == 0) { + shfic[0] = MATH_TYPE(fic[iz]) / MATH_TYPE(fic_tmp[iz]); + } + __syncthreads(); + + // now all threads can access that value + auto tmp = shfic[0]; + + // offset Imodel for current z + Imodel += iz * X * Y; + + for (int iy = ty; iy < Y; iy += blockDim.y) { + #pragma unroll(4) + for (int ix = tx; ix < X; ix += blockDim.x) { + Imodel[iy * X + ix] *= tmp; + } + } + + // race condition if write is not restricted to one thread + if (tx==0 && ty == 0) + fic[iz] = tmp; +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/interpolated_shift.cu b/ptypy/accelerate/cuda_pycuda/cuda/interpolated_shift.cu new file mode 100644 index 000000000..49db445f7 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/interpolated_shift.cu @@ -0,0 +1,279 @@ +#include +#include +#include +#include +#include +#include +using thrust::complex; + +__device__ inline complex& ascomplex(float2& f2) +{ + return reinterpret_cast&>(f2); +} + +__device__ inline void calcWeights(float* weights, float fraction) +{ + if (fraction < 0.0) + { + weights[2] = -fraction; + weights[1] = 1.0f + fraction; + weights[0] = 0.0f; + } + else + { + weights[2] = 0.0f; + weights[1] = 1.0f - fraction; + weights[0] = fraction; + } +} + +extern "C" __global__ void integer_shift_kernel(const IN_TYPE* in, + OUT_TYPE* out, + int rows, + int columns, + int rowOffset, + int colOffset) +{ + int tx = threadIdx.x + blockIdx.x * BDIM_X; + int ty = threadIdx.y + blockIdx.y * BDIM_Y; + if (tx >= rows || ty >= columns) + return; + + int item = blockIdx.z; + in += item * rows * columns; + out += item * rows * columns; + + int gid_old = tx * columns + ty; + assert(gid_old < columns * rows); + assert(gid_old >= 0); + + auto val = in[gid_old]; + + int gid_new_x = tx + rowOffset; + int gid_new_y = ty + colOffset; + + // write zero on the other end + while (gid_new_x >= rows) + { + val = complex(); + gid_new_x -= rows; + } + while (gid_new_x < 0) + { + val = complex(); + gid_new_x += rows; + } + while (gid_new_y >= columns) + { + val = complex(); + gid_new_y -= columns; + } + while (gid_new_y < 0) + { + val = complex(); + gid_new_y += columns; + } + // do we need to do something with the corners? + + int gid_new = gid_new_x * columns + gid_new_y; + assert(gid_new < rows * columns); + assert(gid_new >= 0); + + out[gid_new] = val; +} + +extern "C" __global__ void linear_interpolate_kernel(const IN_TYPE* in, + OUT_TYPE* out, + int rows, + int columns, + float offsetRow, + float offsetColumn) +{ + int offsetRowInt = int(offsetRow); + int offsetColInt = int(offsetColumn); + float offsetRowFrac = offsetRow - offsetRowInt; // positive or negative + float offsetColFrac = offsetColumn - offsetColInt; + + // calculate convolutional weights + float wx[3]; + calcWeights(wx, offsetRowFrac); + float wy[3]; + calcWeights(wy, offsetColFrac); + + // indices + int tx = threadIdx.x; + int ty = threadIdx.y; + int bx = blockIdx.x; + int by = blockIdx.y; + int gx = tx + bx * BDIM_X; + int gy = ty + by * BDIM_Y; + int gx_old = gx - offsetRowInt; + int gy_old = gy - offsetColInt; + + // items index is blockIdx.z + // we just advance the data + int item = blockIdx.z; + in += item * rows * columns; + out += item * rows * columns; + + __shared__ float2 shr[BDIM_X + 2][BDIM_Y + 2]; + + // read top Halo + if (tx == 0) + { + if (gx_old - 1 >= 0 && gx_old - 1 < rows && gy_old >= 0 && gy_old < columns) + { + ascomplex(shr[0][ty + 1]) = in[(gx_old - 1) * columns + gy_old]; + } + else + { + ascomplex(shr[0][ty + 1]) = complex(); + } + } + // read bottom Halo + if (tx == BDIM_X - 1) + { + if (gx_old + 1 >= 0 && gx_old + 1 < rows && gy_old >= 0 && gy_old < columns) + { + ascomplex(shr[BDIM_X + 1][ty + 1]) = in[(gx_old + 1) * columns + gy_old]; + } + else + { + ascomplex(shr[BDIM_X + 1][ty + 1]) = complex(); + } + } + // read left Halo + if (ty == 0) + { + if (gx_old >= 0 && gx_old < rows && gy_old - 1 >= 0 && gy_old - 1 < columns) + { + ascomplex(shr[tx + 1][0]) = in[gx_old * columns + gy_old - 1]; + } + else + { + ascomplex(shr[tx + 1][0]) = complex(); + } + } + // read right Halo + if (ty == BDIM_Y - 1) + { + if (gx_old >= 0 && gx_old < rows && gy_old + 1 >= 0 && gy_old + 1 < columns) + { + ascomplex(shr[tx + 1][BDIM_Y + 1]) = in[gx_old * columns + gy_old + 1]; + } + else + { + ascomplex(shr[tx + 1][BDIM_Y + 1]) = complex(); + } + } + // read top-left Halo + if ((tx == 0) && (ty == 0)) + { + if (gx_old - 1 >= 0 && gx_old - 1 < rows && gy_old - 1 >= 0 && gy_old - 1 < columns) + { + ascomplex(shr[0][0]) = in[(gx_old - 1) * columns + gy_old - 1]; + } + else + { + ascomplex(shr[0][0]) = complex(); + } + } + // read bottom-right Halo + if ((tx == BDIM_X - 1) && (ty == BDIM_Y - 1)) + { + if (gx_old + 1 >= 0 && gx_old + 1 < rows && gy_old + 1 >= 0 && gy_old + 1 < columns) + { + ascomplex(shr[BDIM_X + 1][BDIM_Y + 1]) = in[(gx_old + 1) * columns + gy_old + 1]; + } + else + { + ascomplex(shr[BDIM_X + 1][BDIM_Y + 1]) = complex(); + } + } + // read bottom-left Halo + if ((ty == 0) && (tx == BDIM_X - 1)) + { + if (gx_old + 1 >= 0 && gx_old + 1 < rows && gy_old - 1 >= 0 && gy_old - 1 < columns) + { + ascomplex(shr[BDIM_X + 1][0]) = in[(gx_old + 1) * columns + gy_old - 1]; + } + else + { + ascomplex(shr[BDIM_X + 1][0]) = complex(); + } + } + // read top-right Halo + if ((ty == BDIM_Y - 1) && (tx == 0)) + { + if (gx_old - 1 >= 0 && gx_old - 1 < rows && gy_old + 1 >= 0 && gy_old + 1 < columns) + { + ascomplex(shr[0][BDIM_Y + 1]) = in[(gx_old - 1) * columns + gy_old + 1]; + } + else + { + ascomplex(shr[0][BDIM_Y + 1]) = complex(); + } + } + // read the rest + if (gx_old >= 0 && gx_old < rows && gy_old >= 0 && gy_old < columns) + { + ascomplex(shr[tx + 1][ty + 1]) = in[gx_old * columns + gy_old]; + } + else + { + ascomplex(shr[tx + 1][ty + 1]) = complex(); + } + + // now we have a block + halos in shared memory - do the interpolation + __syncthreads(); + + // interpolate rows in x + __shared__ float2 shry[BDIM_X][BDIM_Y + 2]; + + ascomplex(shry[tx][ty + 1]) = wx[0] * ascomplex(shr[tx][ty + 1]) + + wx[1] * ascomplex(shr[tx + 1][ty + 1]) + + wx[2] * ascomplex(shr[tx + 2][ty + 1]); + if (ty == 0) + { + ascomplex(shry[tx][0]) = wx[0] * ascomplex(shr[tx][0]) + + wx[1] * ascomplex(shr[tx + 1][0]) + + wx[2] * ascomplex(shr[tx + 2][0]); + } + if (ty == BDIM_Y - 1) + { + ascomplex(shry[tx][BDIM_Y + 1]) = + wx[0] * ascomplex(shr[tx][BDIM_Y + 1]) + + wx[1] * ascomplex(shr[tx + 1][BDIM_Y + 1]) + + wx[2] * ascomplex(shr[tx + 2][BDIM_Y + 1]); + } + + __syncthreads(); + + if (gx >= rows || gy >= columns) + { + return; + } + + auto intv = wy[0] * ascomplex(shry[tx][ty]) + + wy[1] * ascomplex(shry[tx][ty + 1]) + + wy[2] * ascomplex(shry[tx][ty + 2]); + + // write back + + // if the point lies outside of the original frame and we're shifting in + // that direction, it gets a zero value in any case + // otherwise we take the interpolated value + bool rightzero = offsetColFrac < 0.0f; + bool leftzero = offsetColFrac > 0.0f; + bool topzero = offsetRowFrac > 0.0f; + bool bottomzero = offsetRowFrac < 0.0f; + if ((gx_old == 0 && topzero) || (gx_old == rows - 1 && bottomzero) || + (gy_old == 0 && leftzero) || (gy_old == columns - 1 && rightzero)) + { + out[gx * columns + gy] = complex(); + } + else + { + out[gx * columns + gy] = intv; + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/log_likelihood.cu b/ptypy/accelerate/cuda_pycuda/cuda/log_likelihood.cu new file mode 100644 index 000000000..075d59f0a --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/log_likelihood.cu @@ -0,0 +1,153 @@ +/** log_likelihood kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +#include +#include +using std::sqrt; +using thrust::abs; +using thrust::complex; + +// specify max number of threads/block and min number of blocks per SM, +// to assist the compiler in register optimisations. +// We achieve a higher occupancy in this case, as less registers are used +// (guided by profiler) +extern "C" __global__ void __launch_bounds__(1024, 2) + log_likelihood(int nmodes, + complex *aux, + const IN_TYPE *fmask, + const IN_TYPE *fmag, + const int *addr, + IN_TYPE *llerr, + int A, + int B) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + + const int *ea = addr + 6 + (blockIdx.x * nmodes) * addr_stride; + const int *da = addr + 9 + (blockIdx.x * nmodes) * addr_stride; + const int *ma = addr + 12 + (blockIdx.x * nmodes) * addr_stride; + + aux += ea[0] * A * B; + fmag += da[0] * A * B; + fmask += ma[0] * A * B; + llerr += da[0] * A * B; + MATH_TYPE norm = A * B; + + for (int a = ty; a < A; a += blockDim.y) + { + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE acc = 0.0; + for (int idx = 0; idx < nmodes; ++idx) + { + complex t_aux = aux[a * B + b + idx * A * B]; + MATH_TYPE abs_exit_wave = abs(t_aux); + acc += abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + auto I = MATH_TYPE(fmag[a * B + b]) * MATH_TYPE(fmag[a * B + b]); + llerr[a * B + b] = + MATH_TYPE(fmask[a * B + b]) * (acc - I) * (acc - I) / (I + 1) / norm; + } + } +} + + +extern "C" __global__ void + log_likelihood2(int nmodes, + complex *aux, + const IN_TYPE *fmask, + const IN_TYPE *fmag, + const int *addr, + IN_TYPE *llerr, + int A, + int B) +{ + int bid = blockIdx.z; + int tx = threadIdx.x; + int a = threadIdx.y + blockIdx.y * blockDim.y; + if (a >= A) + return; + int addr_stride = 15; + + const int *ea = addr + 6 + (bid * nmodes) * addr_stride; + const int *da = addr + 9 + (bid * nmodes) * addr_stride; + const int *ma = addr + 12 + (bid * nmodes) * addr_stride; + + aux += ea[0] * A * B; + fmag += da[0] * A * B; + fmask += ma[0] * A * B; + llerr += da[0] * A * B; + MATH_TYPE norm = A * B; + + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE acc = 0.0; + for (int idx = 0; idx < nmodes; ++idx) + { + complex t_aux = aux[a * B + b + idx * A * B]; + MATH_TYPE abs_exit_wave = abs(t_aux); + acc += abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + auto I = MATH_TYPE(fmag[a * B + b]) * MATH_TYPE(fmag[a * B + b]); + llerr[a * B + b] = + MATH_TYPE(fmask[a * B + b]) * (acc - I) * (acc - I) / (I + 1) / norm; + } +} + +// ML variant which uses weights and intensity directly. +// Based of log_likelihood +extern "C" __global__ void __launch_bounds__(1024, 2) + log_likelihood_ml(int nmodes, + complex *aux, + const IN_TYPE *weights, + const IN_TYPE *I, + const int *addr, + IN_TYPE *llerr, + int A, + int B) +{ + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + + const int *ea = addr + 6 + (blockIdx.x * nmodes) * addr_stride; + const int *da = addr + 9 + (blockIdx.x * nmodes) * addr_stride; + const int *ma = addr + 12 + (blockIdx.x * nmodes) * addr_stride; + + aux += ea[0] * A * B; + I += da[0] * A * B; + weights += ma[0] * A * B; + llerr += da[0] * A * B; + MATH_TYPE norm = A * B; + + for (int a = ty; a < A; a += blockDim.y) + { + for (int b = tx; b < B; b += blockDim.x) + { + MATH_TYPE acc = 0.0; + MATH_TYPE i = I[a * B + b]; + for (int idx = 0; idx < nmodes; ++idx) + { + complex t_aux = aux[a * B + b + idx * A * B]; + MATH_TYPE abs_exit_wave = abs(t_aux); + acc += abs_exit_wave * + abs_exit_wave; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + llerr[a * B + b] = + MATH_TYPE(weights[a * B + b]) * (acc - i) * (acc - i) / norm; + } + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/make_a012.cu b/ptypy/accelerate/cuda_pycuda/cuda/make_a012.cu new file mode 100644 index 000000000..11ba29f62 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/make_a012.cu @@ -0,0 +1,67 @@ +/** fmag_all_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + * - ACC_TYPE: data type used for accumulation + */ + +#include +using thrust::complex; + +extern "C" __global__ void make_a012(const complex* f, + const complex* a, + const complex* b, + const IN_TYPE* I, + const IN_TYPE* fic, + OUT_TYPE* A0, + OUT_TYPE* A1, + OUT_TYPE* A2, + int z, + int y, + int x, + int maxz) +{ + int ix = threadIdx.x + blockIdx.x * blockDim.x; + int iz = blockIdx.z; + + if (ix >= x) + return; + + if (iz >= maxz) + { + A0[iz * x + ix] = OUT_TYPE(0); // make sure it's the right type (double/float) + A1[iz * x + ix] = OUT_TYPE(0); + A2[iz * x + ix] = OUT_TYPE(0); + return; + } + + // we sum across y directly, as this is the number of modes, + // which is typically small + auto sumtf0 = ACC_TYPE(0); + auto sumtf1 = ACC_TYPE(0); + auto sumtf2 = ACC_TYPE(0); + for (auto iy = 0; iy < y; ++iy) + { + complex fv = f[iz * y * x + iy * x + ix]; + sumtf0 += fv.real() * fv.real() + fv.imag() * fv.imag(); + + complex av = a[iz * y * x + iy * x + ix]; + // 2 * real(f * conj(a)) + sumtf1 += MATH_TYPE(2) * (fv.real() * av.real() + fv.imag() * av.imag()); + + // use FTYPE(2) to make sure double creeps into a float calculation + // as 2.0 * would make everything double. + complex bv = b[iz * y * x + iy * x + ix]; + // 2 * real(f * conj(b)) + abs(a)^2 + sumtf2 += MATH_TYPE(2) * (fv.real() * bv.real() + fv.imag() * bv.imag()) + + (av.real() * av.real() + av.imag() * av.imag()); + } + + MATH_TYPE Iv = I[iz * x + ix]; + MATH_TYPE ficv = fic[iz]; + A0[iz * x + ix] = OUT_TYPE(MATH_TYPE(sumtf0) * ficv - Iv); + A1[iz * x + ix] = OUT_TYPE(MATH_TYPE(sumtf1) * ficv); + A2[iz * x + ix] = OUT_TYPE(MATH_TYPE(sumtf2) * ficv); +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/make_aux.cu b/ptypy/accelerate/cuda_pycuda/cuda/make_aux.cu new file mode 100644 index 000000000..b2f64ba1d --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/make_aux.cu @@ -0,0 +1,104 @@ +/** build_aux kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +// core calculation function - used by both kernels and inlined +inline __device__ complex calculate( + const complex& t_obj, + const complex& t_probe, + const complex& t_ex, + MATH_TYPE a, + MATH_TYPE b) +{ + return t_obj * t_probe * a + t_ex * b; +} + +extern "C" __global__ void make_aux( + complex* auxiliary_wave, + const complex* __restrict__ exit_wave, + int B, + int C, + const complex* __restrict__ probe, + int E, + int F, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE coeff_po_, + IN_TYPE coeff_e_) +{ + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + int addr_stride = 15; + const MATH_TYPE coeff_po = coeff_po_; // type conversion + const MATH_TYPE coeff_e = coeff_e_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + exit_wave += ea[0] * B * C; + auxiliary_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { +#pragma unroll(4) // we use blockDim.x = 32, and C is typically more than 128 + // (it will work for less as well) + for (int c = tx; c < C; c += blockDim.x) + { + auxiliary_wave[b * C + c] = calculate( + obj[b * I + c], probe[b * F + c], exit_wave[b * C + c], coeff_po, coeff_e); + } + } +} + +extern "C" __global__ void make_aux2( + complex* auxiliary_wave, + const complex* __restrict__ exit_wave, + int B, + int C, + const complex* __restrict__ probe, + int E, + int F, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE coeff_po_, + IN_TYPE coeff_e_) +{ + int bid = blockIdx.z; + int tx = threadIdx.x; + int b = threadIdx.y + blockIdx.y * blockDim.y; + if (b >= B) + return; + int addr_stride = 15; + const MATH_TYPE coeff_po = coeff_po_; // type conversion + const MATH_TYPE coeff_e = coeff_e_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + exit_wave += ea[0] * B * C; + auxiliary_wave += ea[0] * B * C; + + for (int c = tx; c < C; c += blockDim.x) + { + auxiliary_wave[b * C + c] = calculate( + obj[b * I + c], probe[b * F + c], exit_wave[b * C + c], coeff_po, coeff_e); + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/make_exit.cu b/ptypy/accelerate/cuda_pycuda/cuda/make_exit.cu new file mode 100644 index 000000000..956b292dc --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/make_exit.cu @@ -0,0 +1,70 @@ +/** build_exit kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double - for aux wave) + * - MATH_TYPE: the data type used for computation + */ + + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, complex y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, y.real()); + atomicAdd(xf + 1, y.imag()); +} + +extern "C" __global__ void make_exit(complex* auxiliary_wave, + complex* exit_wave, + int B, + int C, + const complex* __restrict__ probe, + int E, + int F, + const complex* __restrict__ obj, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE coeff_a_, + IN_TYPE coeff_po_, + IN_TYPE coeff_e_) +{ + int bid = blockIdx.x; + int tx = threadIdx.x; + int ty = threadIdx.y; + const int addr_stride = 15; + const MATH_TYPE coeff_a = coeff_a_; // type conversion + const MATH_TYPE coeff_po = coeff_po_; // type conversion + const MATH_TYPE coeff_e = coeff_e_; // type conversion + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + exit_wave += ea[0] * B * C; + auxiliary_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { +#pragma unroll(4) // we use blockDim.x = 32, and C is typically more than 128 + // (it will work for less as well) + for (int c = tx; c < C; c += blockDim.x) + { + complex auxv = auxiliary_wave[b * C + c]; + complex t_probe = probe[b * F + c]; + complex t_obj = obj[b * I + c]; + complex t_exit = exit_wave[b * C + c]; + auxv *= coeff_a; + auxv += coeff_po * t_probe * t_obj; + auxv += coeff_e * t_exit; + exit_wave[b * C + c] += auxv; + auxiliary_wave[b * C + c] = auxv; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/make_model.cu b/ptypy/accelerate/cuda_pycuda/cuda/make_model.cu new file mode 100644 index 000000000..22bf7d4ab --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/make_model.cu @@ -0,0 +1,30 @@ +/** make_model - with 2 steps as separate kernels. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +extern "C" __global__ void make_model( + const complex* in, OUT_TYPE* out, int z, int y, int x) +{ + int ix = threadIdx.x + blockIdx.x * blockDim.x; + int iz = blockIdx.z; + + if (ix >= x) + return; + + // we sum accross y directly, as this is the number of modes, + // which is typically small + auto sum = MATH_TYPE(); + for (auto iy = 0; iy < y; ++iy) + { + complex v = in[iz * y * x + iy * x + ix]; + sum += v.real() * v.real() + v.imag() * v.imag(); + } + out[iz * x + ix] = OUT_TYPE(sum); +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/mass_center.cu b/ptypy/accelerate/cuda_pycuda/cuda/mass_center.cu new file mode 100644 index 000000000..3261495d2 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/mass_center.cu @@ -0,0 +1,138 @@ +extern "C" __global__ void indexed_sum_middim( + const IN_TYPE* data, + IN_TYPE* sums, + int i, + int m, // dim we work on - 1 block per output + int n, + float scale) +{ + int bid = blockIdx.x; + int tid = threadIdx.x; + + data += bid * n; + + auto val = 0.0f; + for (int x = 0; x < i; ++x) + { + auto d_inner = data + x * n * m; + for (int z = tid; z < n; z += BDIM_X) + { + val += d_inner[z]; + } + } + + __shared__ float sumshr[BDIM_X]; + sumshr[tid] = val; + + __syncthreads(); + int c = BDIM_X; + while (c > 1) + { + int half = c / 2; + if (tid < half) + { + sumshr[tid] += sumshr[c - tid - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tid == 0) + { + sums[bid] = sumshr[0] * float(bid) * scale; + } +} + + +extern "C" __global__ void indexed_sum_lastdim( + const IN_TYPE* data, IN_TYPE* sums, int n, int i, float scale) +{ + int ty = threadIdx.y + blockIdx.y * BDIM_Y; + int tx = threadIdx.x; + + auto val = 0.0f; + if (ty < i) + { + data += ty; // column to work on + + // we collaborate along the x axis (columns) to get more threads in case i + // is small + for (int r = tx; r < n; r += BDIM_X) + { + val += data[r * i]; + } + } + + // reduce along X dimension in shared memory (column sum) + __shared__ float blocksums[BDIM_X][BDIM_Y]; + blocksums[tx][threadIdx.y] = val; + + __syncthreads(); + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + blocksums[tx][threadIdx.y] += blocksums[c - tx - 1][threadIdx.y]; + } + __syncthreads(); + c = c - half; + } + + if (ty >= i) + { + return; + } + + if (tx == 0) + { + sums[ty] = blocksums[0][threadIdx.y] * float(ty) * scale; + } +} + + +extern "C" __global__ void final_sums(const IN_TYPE* sum_i, + int i, + const IN_TYPE* sum_m, + int m, + const IN_TYPE* sum_n, + int n, + IN_TYPE* output) +{ + int bid = blockIdx.x; + int tid = threadIdx.x; + // each block works on a single dimension + int nn = bid == 0 ? i : (bid == 1 ? m : n); + const float* data = bid == 0 ? sum_i : (bid == 1 ? sum_m : sum_n); + + __shared__ float shared[BDIM_X]; + auto val = 0.0f; + for (int i = tid; i < nn; i += blockDim.x) + { + val += data[i]; + } + shared[tid] = val; + + // now add up sumbuffer in shared memory + __syncthreads(); + int nt = blockDim.x; + int c = nt; + while (c > 1) + { + int half = c / 2; + if (tid < half) + { + shared[tid] += shared[c - tid - 1]; + } + __syncthreads(); + c = c - half; + } + + if (tid == 0) + { + output[bid] = shared[0]; + } +} + diff --git a/ptypy/accelerate/cuda_pycuda/cuda/max_abs2.cu b/ptypy/accelerate/cuda_pycuda/cuda/max_abs2.cu new file mode 100644 index 000000000..4da8efb3e --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/max_abs2.cu @@ -0,0 +1,115 @@ +/** max_abs2 kernel, calculating the sum of abs(x)**2 value in the first dimension + * and then the maximum across the last 2 dimensions + * + * Data types: + * - IN_TYPE: can be float/double or complex/complex + */ + +#include +#include +using thrust::complex; +using thrust::norm; + +inline __device__ OUT_TYPE norm(const float& in) { + return in*in; +} + +inline __device__ OUT_TYPE norm(const double& in) { + return in*in; +} + +extern "C" __global__ void max_abs2_step1(const IN_TYPE* a, + int n, + int rows, + int cols, + OUT_TYPE* out) +{ + int tx = threadIdx.x; + const int iy = blockIdx.y; + + __shared__ OUT_TYPE sh[BDIM_X]; + + OUT_TYPE maxv = OUT_TYPE(0); + + for (int ix = tx; ix < cols; ix += BDIM_X) { + OUT_TYPE v = OUT_TYPE(0); + for (int in = 0; in < n; ++in) { + v += norm(a[in * rows * cols + iy * cols + ix]); + } + if (v > maxv) + maxv = v; + } + + + sh[tx] = maxv; + + __syncthreads(); + + // reduce: + const int nt = BDIM_X; + int c = nt; + + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + auto v = sh[c - tx - 1]; + if (maxv < v) { + sh[tx] = v; + maxv = v; + } + } + __syncthreads(); + c = c - half; + } + + if (tx == 0) + { + out[iy] = sh[0]; + } +} + +extern "C" __global__ void max_abs2_step2(const OUT_TYPE* in, + int n, + OUT_TYPE* out) +{ + int tx = threadIdx.x; + + in += blockIdx.x * n; + + __shared__ OUT_TYPE sh[BDIM_X]; + + OUT_TYPE maxv = OUT_TYPE(0); + for (int i = tx; i < n; ++i) { + auto v = in[i]; + if (v > maxv) + maxv = v; + } + sh[tx] = maxv; + __syncthreads(); + + // reduce: + const int nt = BDIM_X; + int c = nt; + + while (c > 1) + { + int half = c / 2; + if (tx < half) + { + auto v = sh[c - tx - 1]; + if (maxv < v) { + sh[tx] = v; + maxv = v; + } + } + __syncthreads(); + c = c - half; + } + + if (tx == 0) + { + out[0] = sh[0]; + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/ob_norm_local.cu b/ptypy/accelerate/cuda_pycuda/cuda/ob_norm_local.cu new file mode 100644 index 000000000..3969ea6e9 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/ob_norm_local.cu @@ -0,0 +1,59 @@ +/** ob_norm_local. +* +* Data types: +* - IN_TYPE: the data type for the inputs (float or double) +* - OUT_TYPE: the data type for the outputs (float or double) +* - MATH_TYPE: the data type used for computation +*/ + +#include +#include +#include +using std::sqrt; +using thrust::abs; +using thrust::complex; + +// specify max number of threads/block and min number of blocks per SM, +// to assist the compiler in register optimisations. +// We achieve a higher occupancy in this case, as less registers are used +// (guided by profiler) +extern "C" __global__ void __launch_bounds__(1024, 2) + ob_norm_local(OUT_TYPE *ob_norm, + int A, + int B, + int C, + const complex* __restrict__ obj, + int D, + int E, + int F, + const int* __restrict__ addr) +{ + const int bid = blockIdx.z; + const int tx = threadIdx.x; + const int b = threadIdx.y + blockIdx.y * blockDim.y; + const int addr_stride = 15; + + const int* oa = addr + 3 + (bid * D) * addr_stride; + const int* da = addr + 9 + (bid * D) * addr_stride; + + obj += oa[0] * E * F + oa[1] * F + oa[2]; + ob_norm += da[0] * B * C; + + if (b >= B) + return; + + for (int c = tx; c < C; c += blockDim.x) + { + MATH_TYPE acc = MATH_TYPE(0); + for (int idx = 0; idx < D; ++idx) + { + complex obj_val = obj[b * F + c + idx * E * F]; + MATH_TYPE abs_obj_val = abs(obj_val); + acc += abs_obj_val * + abs_obj_val; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + ob_norm[b * C + c] = acc; + } + +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/ob_update.cu b/ptypy/accelerate/cuda_pycuda/cuda/ob_update.cu new file mode 100644 index 000000000..29b993fb0 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/ob_update.cu @@ -0,0 +1,68 @@ +/** ob_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, const complex& y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, y.real()); + atomicAdd(xf + 1, y.imag()); +} + +extern "C" __global__ void ob_update( + const complex* __restrict__ exit_wave, + int A, + int B, + int C, + const complex* __restrict__ probe, + int D, + int E, + int F, + complex* obj, + int G, + int H, + int I, + const int* __restrict__ addr, + OUT_TYPE* denominator) +{ + const int bid = blockIdx.x; + const int tx = threadIdx.x; + const int ty = threadIdx.y; + const int addr_stride = 15; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + denominator += oa[0] * H * I + oa[1] * I + oa[2]; + + assert(oa[0] * H * I + oa[1] * I + oa[2] + (B - 1) * I + C - 1 < G * H * I); + + exit_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { + for (int c = tx; c < C; c += blockDim.x) + { + complex probe_val = probe[b * F + c]; + complex exit_val = exit_wave[b * C + c]; + auto add_val_m = conj(probe_val) * exit_val; + complex add_val = add_val_m; + atomicAdd(&obj[b * I + c], add_val); + + auto upd_probe = probe_val.real() * probe_val.real() + + probe_val.imag() * probe_val.imag(); + atomicAdd(&denominator[b * I + c], upd_probe); + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/ob_update2.cu b/ptypy/accelerate/cuda_pycuda/cuda/ob_update2.cu new file mode 100644 index 000000000..821c04a6d --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/ob_update2.cu @@ -0,0 +1,128 @@ +/** ob_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + * - ACC_TYPE: accumulator type for the local ob accumulation + * + * NOTE: This version of ob_update goes over all tiles that need to be accumulated + * in a single thread block to avoid global atomic additions (as in ob_update.cu). + * This requires a local array of NUM_MODES size to store the local updates. + * GPU registers per thread are limited (255 32bit registers on V100), + * and at some point the registers will spill into shared or global memory + * and the kernel will get considerably slower. + */ + + +#include +#include +using thrust::complex; + +#define pr_dlayer(k) addr[(k)] +#define ex_dlayer(k) addr[6 * num_pods + (k)] +#define obj_dlayer(k) addr[3 * num_pods + (k)] +#define obj_roi_row(k) addr[4 * num_pods + (k)] +#define obj_roi_column(k) addr[5 * num_pods + (k)] + + +extern "C" __global__ void ob_update2( + int pr_sh, + int ob_modes, + int num_pods, + int ob_sh_rows, + int ob_sh_cols, + int pr_modes, + int ex_0, + int ex_1, + int ex_2, + complex* ob_g, + OUT_TYPE* obn_g, + const complex* __restrict__ pr_g, // 2, 5, 5 + const complex* __restrict__ ex_g, // 16, 5, 5 + const int* addr) +{ + int y = blockIdx.y * BDIM_Y + threadIdx.y; + int dy = ob_sh_rows; + int z = blockIdx.x * BDIM_X + threadIdx.x; + int dz = ob_sh_cols; + complex ob[NUM_MODES]; + ACC_TYPE obn[NUM_MODES]; + + int txy = threadIdx.y * BDIM_X + threadIdx.x; + assert(ob_modes <= NUM_MODES); + + if (y < dy && z < dz) + { +#pragma unroll + for (int i = 0; i < NUM_MODES; ++i) + { + auto idx = i * dy * dz + y * dz + z; + assert(idx < ob_modes * ob_sh_rows * ob_sh_cols); + ob[i] = ob_g[idx]; + obn[i] = obn_g[idx]; + } + } + + __shared__ int addresses[BDIM_X * BDIM_Y * 5]; + + for (int p = 0; p < num_pods; p += BDIM_X * BDIM_Y) + { + int mi = BDIM_X * BDIM_Y; + if (mi > num_pods - p) + mi = num_pods - p; + + if (p > 0) + __syncthreads(); + + if (txy < mi) + { + assert(p + txy < num_pods); + assert(txy < BDIM_X * BDIM_Y); + addresses[txy * 5 + 0] = pr_dlayer(p + txy); + addresses[txy * 5 + 1] = ex_dlayer(p + txy); + addresses[txy * 5 + 2] = obj_dlayer(p + txy); + assert(obj_dlayer(p + txy) < NUM_MODES); + assert(addresses[txy * 5 + 2] < NUM_MODES); + addresses[txy * 5 + 3] = obj_roi_row(p + txy); + addresses[txy * 5 + 4] = obj_roi_column(p + txy); + } + + __syncthreads(); + + if (y >= dy || z >= dz) + continue; + +#pragma unroll 4 + for (int i = 0; i < mi; ++i) + { + int* ad = addresses + i * 5; + int v1 = y - ad[3]; + int v2 = z - ad[4]; + if (v1 >= 0 && v1 < pr_sh && v2 >= 0 && v2 < pr_sh) + { + auto pridx = ad[0] * pr_sh * pr_sh + v1 * pr_sh + v2; + assert(pridx < pr_modes * pr_sh * pr_sh); + complex pr = pr_g[pridx]; + int idx = ad[2]; + assert(idx < NUM_MODES); + auto cpr = conj(pr); + auto exidx = ad[1] * pr_sh * pr_sh + v1 * pr_sh + v2; + assert(exidx < ex_0 * ex_1 * ex_2); + complex t_ex_g = ex_g[exidx]; + complex add_val = cpr * t_ex_g; + ob[idx] += add_val; + obn[idx] += pr.real() * pr.real() + pr.imag() * pr.imag(); + } + } + } + + if (y < dy && z < dz) + { + for (int i = 0; i < NUM_MODES; ++i) + { + ob_g[i * dy * dz + y * dz + z] = ob[i]; + obn_g[i * dy * dz + y * dz + z] = obn[i]; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/ob_update2_ML.cu b/ptypy/accelerate/cuda_pycuda/cuda/ob_update2_ML.cu new file mode 100644 index 000000000..b62e66006 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/ob_update2_ML.cu @@ -0,0 +1,124 @@ +/** ob_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + * - ACC_TYPE: accumulator for the ob field + * + * NOTE: This version of ob_update goes over all tiles that need to be accumulated + * in a single thread block to avoid global atomic additions (as in ob_update_ML.cu). + * This requires a local array of NUM_MODES size to store the local updates. + * GPU registers per thread are limited (255 32bit registers on V100), + * and at some point the registers will spill into shared or global memory + * and the kernel will get considerably slower. + */ + + +#include +#include +using thrust::complex; + +#define pr_dlayer(k) addr[(k)] +#define ex_dlayer(k) addr[6 * num_pods + (k)] +#define obj_dlayer(k) addr[3 * num_pods + (k)] +#define obj_roi_row(k) addr[4 * num_pods + (k)] +#define obj_roi_column(k) addr[5 * num_pods + (k)] + +extern "C" __global__ void ob_update2_ML(int pr_sh, + int ob_modes, + int num_pods, + int ob_sh_rows, + int ob_sh_cols, + int pr_modes, + int ex_0, + int ex_1, + int ex_2, + complex* ob_g, + const complex* __restrict__ pr_g, + const complex* __restrict__ ex_g, + const int* addr, + IN_TYPE fac_) +{ + int y = blockIdx.y * BDIM_Y + threadIdx.y; + int dy = ob_sh_rows; + int z = blockIdx.x * BDIM_X + threadIdx.x; + int dz = ob_sh_cols; + MATH_TYPE fac = fac_; + complex ob[NUM_MODES]; + + + int txy = threadIdx.y * BDIM_X + threadIdx.x; + assert(ob_modes <= NUM_MODES); + + if (y < dy && z < dz) + { +#pragma unroll + for (int i = 0; i < NUM_MODES; ++i) + { + auto idx = i * dy * dz + y * dz + z; + assert(idx < ob_modes * ob_sh_rows * ob_sh_cols); + ob[i] = ob_g[idx]; + } + } + + __shared__ int addresses[BDIM_X * BDIM_Y * 5]; + + for (int p = 0; p < num_pods; p += BDIM_X * BDIM_Y) + { + int mi = BDIM_X * BDIM_Y; + if (mi > num_pods - p) + mi = num_pods - p; + + if (p > 0) + __syncthreads(); + + if (txy < mi) + { + assert(p + txy < num_pods); + assert(txy < BDIM_X * BDIM_Y); + addresses[txy * 5 + 0] = pr_dlayer(p + txy); + addresses[txy * 5 + 1] = ex_dlayer(p + txy); + addresses[txy * 5 + 2] = obj_dlayer(p + txy); + assert(obj_dlayer(p + txy) < NUM_MODES); + assert(addresses[txy * 5 + 2] < NUM_MODES); + addresses[txy * 5 + 3] = obj_roi_row(p + txy); + addresses[txy * 5 + 4] = obj_roi_column(p + txy); + } + + __syncthreads(); + + if (y >= dy || z >= dz) + continue; + +#pragma unroll 4 + for (int i = 0; i < mi; ++i) + { + int* ad = addresses + i * 5; + int v1 = y - ad[3]; + int v2 = z - ad[4]; + if (v1 >= 0 && v1 < pr_sh && v2 >= 0 && v2 < pr_sh) + { + auto pridx = ad[0] * pr_sh * pr_sh + v1 * pr_sh + v2; + assert(pridx < pr_modes * pr_sh * pr_sh); + complex pr = pr_g[pridx]; + int idx = ad[2]; + assert(idx < NUM_MODES); + auto cpr = conj(pr); + auto exidx = ad[1] * pr_sh * pr_sh + v1 * pr_sh + v2; + assert(exidx < ex_0 * ex_1 * ex_2); + complex t_ex_g = ex_g[exidx]; + complex add_val = cpr * t_ex_g * fac; + ob[idx] += add_val; + } + } + } + + if (y < dy && z < dz) + { + for (int i = 0; i < NUM_MODES; ++i) + { + ob_g[i * dy * dz + y * dz + z] = ob[i]; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/ob_update_ML.cu b/ptypy/accelerate/cuda_pycuda/cuda/ob_update_ML.cu new file mode 100644 index 000000000..84e678ebb --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/ob_update_ML.cu @@ -0,0 +1,67 @@ +/** ob_update_ML. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, const complex& y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, y.real()); + atomicAdd(xf + 1, y.imag()); +} + +extern "C" +{ + __global__ void ob_update_ML(const complex* __restrict__ exit_wave, + int A, + int B, + int C, + const complex* __restrict__ probe, + int D, + int E, + int F, + complex* obj, + int G, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE fac_) + { + const int bid = blockIdx.x; + const int tx = threadIdx.x; + const int ty = threadIdx.y; + const int addr_stride = 15; + MATH_TYPE fac = fac_; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + + assert(oa[0] * H * I + oa[1] * I + oa[2] + (B - 1) * I + C - 1 < G * H * I); + + exit_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { + for (int c = tx; c < C; c += blockDim.x) + { + complex probe_val = probe[b * F + c]; + complex exit_val = exit_wave[b * C + c]; + complex add_val_m = conj(probe_val) * exit_val * fac; + complex add_val(add_val_m); + + atomicAdd(&obj[b * I + c], add_val); + } + } + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/ob_update_local.cu b/ptypy/accelerate/cuda_pycuda/cuda/ob_update_local.cu new file mode 100644 index 000000000..b3a955868 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/ob_update_local.cu @@ -0,0 +1,73 @@ +/** ob_update_local - in DR algorithm. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, const complex& y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, y.real()); + atomicAdd(xf + 1, y.imag()); +} + +extern "C" __global__ void ob_update_local( + const complex* __restrict__ exit_wave, + const complex* __restrict__ aux, + int A, + int B, + int C, + const complex* __restrict__ probe, + int D, + int E, + int F, + const IN_TYPE* __restrict__ pr_norm, + complex* obj, + int G, + int H, + int I, + const int* __restrict__ addr, + const IN_TYPE* pr_norm_max, + const IN_TYPE A_, + const IN_TYPE B_) +{ + const int bid = blockIdx.z; + const int tx = threadIdx.x; + const int b = threadIdx.y + blockIdx.y * blockDim.y; + if (b >= B) + return; + const int addr_stride = 15; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + aux += bid * B * C; + const MATH_TYPE pr_norm_max_val = pr_norm_max[0]; + const MATH_TYPE A_val = A_; + const MATH_TYPE B_val = B_; + + assert(oa[0] * H * I + oa[1] * I + oa[2] + (B - 1) * I + C - 1 < G * H * I); + + exit_wave += ea[0] * B * C; + + for (int c = tx; c < C; c += blockDim.x) + { + complex probe_val = probe[b * F + c]; + complex exit_val = exit_wave[b * C + c]; + complex aux_val = aux[b * C + c]; + MATH_TYPE norm_val = (MATH_TYPE(1) - A_val) * pr_norm_max_val + A_val * pr_norm[b * F + c]; + + auto add_val_m = (A_val + B_val) * conj(probe_val) * (exit_val - aux_val) / norm_val; + complex add_val = add_val_m; + atomicAdd(&obj[b * I + c], add_val); + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/pr_norm_local.cu b/ptypy/accelerate/cuda_pycuda/cuda/pr_norm_local.cu new file mode 100644 index 000000000..6e9a8ea76 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/pr_norm_local.cu @@ -0,0 +1,59 @@ +/** pr_norm_local. +* +* Data types: +* - IN_TYPE: the data type for the inputs (float or double) +* - OUT_TYPE: the data type for the outputs (float or double) +* - MATH_TYPE: the data type used for computation +*/ + +#include +#include +#include +using std::sqrt; +using thrust::abs; +using thrust::complex; + +// specify max number of threads/block and min number of blocks per SM, +// to assist the compiler in register optimisations. +// We achieve a higher occupancy in this case, as less registers are used +// (guided by profiler) +extern "C" __global__ void __launch_bounds__(1024, 2) + pr_norm_local(OUT_TYPE *pr_norm, + int A, + int B, + int C, + const complex* __restrict__ probe, + int D, + int E, + int F, + const int* __restrict__ addr) +{ + const int bid = blockIdx.z; + const int tx = threadIdx.x; + const int b = threadIdx.y + blockIdx.y * blockDim.y; + const int addr_stride = 15; + + const int* pa = addr + 1 + (bid * D) * addr_stride; + const int* da = addr + 9 + (bid * D) * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + pr_norm += da[0] * B * C; + + if (b >= B) + return; + + for (int c = tx; c < C; c += blockDim.x) + { + MATH_TYPE acc = MATH_TYPE(0); + for (int idx = 0; idx < D; ++idx) + { + complex probe_val = probe[b * F + c + idx * E * F]; + MATH_TYPE abs_probe_val = abs(probe_val); + acc += abs_probe_val * + abs_probe_val; // if we do this manually (real*real +imag*imag) + // we get differences to numpy due to rounding + } + pr_norm[b * C + c] = acc; + } + +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/pr_update.cu b/ptypy/accelerate/cuda_pycuda/cuda/pr_update.cu new file mode 100644 index 000000000..180cf8f14 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/pr_update.cu @@ -0,0 +1,69 @@ +/** pr_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, const complex& y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, T(y.real())); + atomicAdd(xf + 1, T(y.imag())); +} + +extern "C" __global__ void pr_update( + const complex* __restrict__ exit_wave, + int A, + int B, + int C, + complex* probe, + int D, + int E, + int F, + const complex* __restrict__ obj, + int G, + int H, + int I, + const int* __restrict__ addr, + OUT_TYPE* denominator) +{ + assert(B == E); // prsh[1] + assert(C == F); // prsh[2] + const int bid = blockIdx.x; + const int tx = threadIdx.x; + const int ty = threadIdx.y; + const int addr_stride = 15; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + denominator += pa[0] * E * F + pa[1] * F + pa[2]; + + assert(oa[0] * H * I + oa[1] * I + oa[2] + (B - 1) * I + C - 1 < G * H * I); + + exit_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { + for (int c = tx; c < C; c += blockDim.x) + { + complex obj_val = obj[b * I + c]; + complex exit_val = exit_wave[b * C + c]; + complex add_val_m = conj(obj_val) * exit_val; + complex add_val = add_val_m; + atomicAdd(&probe[b * F + c], add_val); + MATH_TYPE upd_obj = + obj_val.real() * obj_val.real() + obj_val.imag() * obj_val.imag(); + atomicAdd(&denominator[b * F + c], upd_obj); + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/pr_update2.cu b/ptypy/accelerate/cuda_pycuda/cuda/pr_update2.cu new file mode 100644 index 000000000..e5417cc01 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/pr_update2.cu @@ -0,0 +1,124 @@ +/** pr_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + * - ACC_TYPE: accumulator type for local pr array + * + * NOTE: This version of ob_update goes over all tiles that need to be accumulated + * in a single thread block to avoid global atomic additions (as in pr_update.cu). + * This requires a local array of NUM_MODES size to store the local updates. + * GPU registers per thread are limited (255 32bit registers on V100), + * and at some point the registers will spill into shared or global memory + * and the kernel will get considerably slower. + */ + +#include +#include +using thrust::complex; + +#define pr_dlayer(k) addr[(k)] +#define pr_roi_row(k) addr[1 * num_pods + (k)] +#define pr_roi_column(k) addr[2 * num_pods + (k)] +#define ex_dlayer(k) addr[6 * num_pods + (k)] +#define obj_dlayer(k) addr[3 * num_pods + (k)] +#define obj_roi_row(k) addr[4 * num_pods + (k)] +#define obj_roi_column(k) addr[5 * num_pods + (k)] + + +extern "C" __global__ void pr_update2(int pr_sh, + int ob_sh_row, + int ob_sh_col, + int pr_modes, + int ob_modes, + int num_pods, + complex* pr_g, + OUT_TYPE* prn_g, + const complex* __restrict__ ob_g, + const complex* __restrict__ ex_g, + const int* addr) +{ + int y = blockIdx.y * BDIM_Y + threadIdx.y; + int dy = pr_sh; + int z = blockIdx.x * BDIM_X + threadIdx.x; + int dz = pr_sh; + complex pr[NUM_MODES]; + ACC_TYPE prn[NUM_MODES]; + + int txy = threadIdx.y * BDIM_X + threadIdx.x; + assert(pr_modes <= NUM_MODES); + + if (y < pr_sh && z < pr_sh) + { +#pragma unroll + for (int i = 0; i < NUM_MODES; ++i) + { + auto idx = i * dy * dz + y * dz + z; + assert(idx < pr_modes * pr_sh * pr_sh); + pr[i] = pr_g[idx]; + prn[i] = prn_g[idx]; + } + } + + __shared__ int addresses[BDIM_X * BDIM_Y * 5]; + + for (int p = 0; p < num_pods; p += BDIM_X * BDIM_Y) + { + int mi = BDIM_X * BDIM_Y; + if (mi > num_pods - p) + mi = num_pods - p; + + if (p > 0) + __syncthreads(); + + if (txy < mi) + { + assert(p + txy < num_pods); + assert(txy < BDIM_X * BDIM_Y); + addresses[txy * 5 + 0] = pr_dlayer(p + txy); + addresses[txy * 5 + 1] = ex_dlayer(p + txy); + addresses[txy * 5 + 2] = obj_dlayer(p + txy); + assert(obj_dlayer(p + txy) < NUM_MODES); + assert(addresses[txy * 5 + 2] < NUM_MODES); + addresses[txy * 5 + 3] = obj_roi_row(p + txy); + addresses[txy * 5 + 4] = obj_roi_column(p + txy); + } + + __syncthreads(); + + if (y >= pr_sh || z >= pr_sh) + continue; + +#pragma unroll 4 + for (int i = 0; i < mi; ++i) + { + int* ad = addresses + i * 5; + int v1 = y + ad[3]; + int v2 = z + ad[4]; + if (v1 >= 0 && v1 < ob_sh_row && v2 >= 0 && v2 < ob_sh_col) + { + auto obidx = ad[2] * ob_sh_row * ob_sh_col + v1 * ob_sh_col + v2; + assert(obidx < ob_modes * ob_sh_row * ob_sh_col); + complex ob = ob_g[obidx]; + + int idx = ad[0]; + assert(idx < NUM_MODES); + auto cob = conj(ob); + complex ex_val = ex_g[ad[1] * pr_sh * pr_sh + y * pr_sh + z]; + complex add_val = cob * ex_val; + pr[idx] += add_val; + prn[idx] += ob.real() * ob.real() + ob.imag() * ob.imag(); + } + } + } + + if (y < pr_sh && z < pr_sh) + { + for (int i = 0; i < NUM_MODES; ++i) + { + pr_g[i * dy * dz + y * dz + z] = pr[i]; + prn_g[i * dy * dz + y * dz + z] = prn[i]; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/pr_update2_ML.cu b/ptypy/accelerate/cuda_pycuda/cuda/pr_update2_ML.cu new file mode 100644 index 000000000..8a45891c5 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/pr_update2_ML.cu @@ -0,0 +1,121 @@ +/** pr_update. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + * - ACC_TYPE: accumulator type for local pr array + * + * NOTE: This version of ob_update goes over all tiles that need to be accumulated + * in a single thread block to avoid global atomic additions (as in pr_update_ML.cu). + * This requires a local array of NUM_MODES size to store the local updates. + * GPU registers per thread are limited (255 32bit registers on V100), + * and at some point the registers will spill into shared or global memory + * and the kernel will get considerably slower. + */ + +#include +#include +using thrust::complex; + +#define pr_dlayer(k) addr[(k)] +#define pr_roi_row(k) addr[1 * num_pods + (k)] +#define pr_roi_column(k) addr[2 * num_pods + (k)] +#define ex_dlayer(k) addr[6 * num_pods + (k)] +#define obj_dlayer(k) addr[3 * num_pods + (k)] +#define obj_roi_row(k) addr[4 * num_pods + (k)] +#define obj_roi_column(k) addr[5 * num_pods + (k)] + +extern "C" __global__ void pr_update2_ML(int pr_sh, + int ob_sh_row, + int ob_sh_col, + int pr_modes, + int ob_modes, + int num_pods, + complex* pr_g, + const complex* __restrict__ ob_g, + const complex* __restrict__ ex_g, + const int* addr, + IN_TYPE fac_) +{ + int y = blockIdx.y * BDIM_Y + threadIdx.y; + int dy = pr_sh; + int z = blockIdx.x * BDIM_X + threadIdx.x; + int dz = pr_sh; + MATH_TYPE fac = fac_; + complex pr[NUM_MODES]; + + int txy = threadIdx.y * BDIM_X + threadIdx.x; + assert(pr_modes <= NUM_MODES); + + if (y < pr_sh && z < pr_sh) + { +#pragma unroll + for (int i = 0; i < NUM_MODES; ++i) + { + auto idx = i * dy * dz + y * dz + z; + assert(idx < pr_modes * pr_sh * pr_sh); + pr[i] = pr_g[idx]; + } + } + + __shared__ int addresses[BDIM_X * BDIM_Y * 5]; + + for (int p = 0; p < num_pods; p += BDIM_X * BDIM_Y) + { + int mi = BDIM_X * BDIM_Y; + if (mi > num_pods - p) + mi = num_pods - p; + + if (p > 0) + __syncthreads(); + + if (txy < mi) + { + assert(p + txy < num_pods); + assert(txy < BDIM_X * BDIM_Y); + addresses[txy * 5 + 0] = pr_dlayer(p + txy); + addresses[txy * 5 + 1] = ex_dlayer(p + txy); + addresses[txy * 5 + 2] = obj_dlayer(p + txy); + assert(obj_dlayer(p + txy) < NUM_MODES); + assert(addresses[txy * 5 + 2] < NUM_MODES); + addresses[txy * 5 + 3] = obj_roi_row(p + txy); + addresses[txy * 5 + 4] = obj_roi_column(p + txy); + } + + __syncthreads(); + + if (y >= pr_sh || z >= pr_sh) + continue; + +#pragma unroll 4 + for (int i = 0; i < mi; ++i) + { + int* ad = addresses + i * 5; + int v1 = y + ad[3]; + int v2 = z + ad[4]; + if (v1 >= 0 && v1 < ob_sh_row && v2 >= 0 && v2 < ob_sh_col) + { + auto obidx = ad[2] * ob_sh_row * ob_sh_col + v1 * ob_sh_col + v2; + assert(obidx < ob_modes * ob_sh_row * ob_sh_col); + complex ob = ob_g[obidx]; + + int idx = ad[0]; + assert(idx < NUM_MODES); + auto cob = conj(ob); + complex ex_val = ex_g[ad[1] * pr_sh * pr_sh + y * pr_sh + z]; + complex add_val_m = cob * ex_val * fac; + complex add_val = add_val_m; + pr[idx] += add_val; + } + } + } + + if (y < pr_sh && z < pr_sh) + { + for (int i = 0; i < NUM_MODES; ++i) + { + pr_g[i * dy * dz + y * dz + z] = pr[i]; + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/pr_update_ML.cu b/ptypy/accelerate/cuda_pycuda/cuda/pr_update_ML.cu new file mode 100644 index 000000000..3fa24137d --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/pr_update_ML.cu @@ -0,0 +1,66 @@ +/** pr_update_ML. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + */ + + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, const complex& y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, y.real()); + atomicAdd(xf + 1, y.imag()); +} + +extern "C" __global__ void pr_update_ML(const complex* __restrict__ exit_wave, + int A, + int B, + int C, + complex* probe, + int D, + int E, + int F, + const complex* __restrict__ obj, + int G, + int H, + int I, + const int* __restrict__ addr, + IN_TYPE fac_) +{ + assert(B == E); // prsh[1] + assert(C == F); // prsh[2] + const int bid = blockIdx.x; + const int tx = threadIdx.x; + const int ty = threadIdx.y; + const int addr_stride = 15; + MATH_TYPE fac = fac_; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + + assert(oa[0] * H * I + oa[1] * I + oa[2] + (B - 1) * I + C - 1 < G * H * I); + + exit_wave += ea[0] * B * C; + + for (int b = ty; b < B; b += blockDim.y) + { + for (int c = tx; c < C; c += blockDim.x) + { + complex obj_val = obj[b * I + c]; + complex exit_val = exit_wave[b * C + c]; + complex add_val_m = conj(obj_val) * exit_val * fac; + complex add_val = add_val_m; + atomicAdd(&probe[b * F + c], add_val); + } + } +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/pr_update_local.cu b/ptypy/accelerate/cuda_pycuda/cuda/pr_update_local.cu new file mode 100644 index 000000000..d515afd55 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/pr_update_local.cu @@ -0,0 +1,77 @@ +/** pr_update_local - for DR algorithm. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + * - MATH_TYPE: the data type used for computation + * - ACC_TYPE: data type used in norm calculation (input here) + */ + +#include +using thrust::complex; + +template +__device__ inline void atomicAdd(complex* x, const complex& y) +{ + auto xf = reinterpret_cast(x); + atomicAdd(xf, T(y.real())); + atomicAdd(xf + 1, T(y.imag())); +} + +extern "C" __global__ void pr_update_local( + const complex* __restrict__ exit_wave, + const complex* __restrict__ aux, + int A, + int B, + int C, + complex* probe, + int D, + int E, + int F, + const IN_TYPE* __restrict__ ob_norm, + const complex* __restrict__ obj, + int G, + int H, + int I, + const int* __restrict__ addr, + const IN_TYPE* ob_norm_max, + const IN_TYPE A_, + const IN_TYPE B_) +{ + assert(B == E); // prsh[1] + assert(C == F); // prsh[2] + const int bid = blockIdx.z; + const int tx = threadIdx.x; + const int b = threadIdx.y + blockIdx.y * blockDim.y; + if (b >= B) + return; + const int addr_stride = 15; + + const int* oa = addr + 3 + bid * addr_stride; + const int* pa = addr + bid * addr_stride; + const int* ea = addr + 6 + bid * addr_stride; + + probe += pa[0] * E * F + pa[1] * F + pa[2]; + obj += oa[0] * H * I + oa[1] * I + oa[2]; + aux += bid * B * C; + const MATH_TYPE ob_norm_max_val = ob_norm_max[0]; + const MATH_TYPE A_val = A_; + const MATH_TYPE B_val = B_; + + assert(oa[0] * H * I + oa[1] * I + oa[2] + (B - 1) * I + C - 1 < G * H * I); + + exit_wave += ea[0] * B * C; + + for (int c = tx; c < C; c += blockDim.x) + { + complex obj_val = obj[b * I + c]; + complex exit_val = exit_wave[b * C + c]; + complex aux_val = aux[b * C + c]; + MATH_TYPE norm_val = (MATH_TYPE(1) - A_val) * ob_norm_max_val + A_val * ob_norm[b * C + c]; + + complex add_val_m = (A_val + B_val) * conj(obj_val) * (exit_val - aux_val) / norm_val; + complex add_val = add_val_m; + atomicAdd(&probe[b * F + c], add_val); + } + +} diff --git a/ptypy/accelerate/cuda_pycuda/cuda/transpose.cu b/ptypy/accelerate/cuda_pycuda/cuda/transpose.cu new file mode 100644 index 000000000..8de4e7ad7 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/transpose.cu @@ -0,0 +1,45 @@ +/** Implementation taken from + * https://github.com/JonathanWatkins/CUDA/blob/master/NvidiaCourse/Exercises/transpose/transpose.cu + * + * Kernel optimised to ensure all global memory reads and writes are coalesced, + * and shared memory access has no bank conflicts. + */ + +/** + * Data types: + * - DTYPE - any pod type + */ + +#include +using thrust::complex; + +extern "C" __global__ void transpose(const DTYPE* idata, + DTYPE* odata, + int width, + int height) +{ + __shared__ DTYPE block[BDIM][BDIM + 1]; + + // read the matrix tile into shared memory + // load one element per thread from device memory (idata) and + // store it in transposed order in block[][] + unsigned int xIndex = blockIdx.x * BDIM + threadIdx.x; + unsigned int yIndex = blockIdx.y * BDIM + threadIdx.y; + if (xIndex < width && yIndex < height) + { + unsigned int index_in = yIndex * width + xIndex; + block[threadIdx.y][threadIdx.x] = idata[index_in]; + } + + // synchronise to ensure all writes to block[][] are complete + __syncthreads(); + + // write transposed matrix back to global memory (odata) in linear order + xIndex = blockIdx.y * BDIM + threadIdx.x; + yIndex = blockIdx.x * BDIM + threadIdx.y; + if (xIndex < height && yIndex < width) + { + unsigned int index_out = yIndex * height + xIndex; + odata[index_out] = block[threadIdx.x][threadIdx.y]; + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cuda/update_addr_error_state.cu b/ptypy/accelerate/cuda_pycuda/cuda/update_addr_error_state.cu new file mode 100644 index 000000000..1220a0986 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cuda/update_addr_error_state.cu @@ -0,0 +1,43 @@ +/** update_addr_error_state kernel. + * + * Data types: + * - IN_TYPE: the data type for the inputs (float or double) + * - OUT_TYPE: the data type for the outputs (float or double) + */ + +#include +#include +using thrust::complex; + +extern "C" __global__ void update_addr_error_state(int* __restrict addr, + const int* __restrict mangled_addr, + OUT_TYPE* error_state, + const IN_TYPE* __restrict error_sum, + int nmodes) +{ + int tx = threadIdx.x; + int row = blockIdx.y * blockDim.y + threadIdx.y; + + // we're using one warp only in x direction, to get implicit + // intra-warp sync between reading err_st and writing it + assert(blockDim.x <= 32); + + addr += row * nmodes * 15; + mangled_addr += row * nmodes * 15; + + auto err_sum = error_sum[row]; + auto err_st = error_state[row]; + + if (err_sum < err_st) + { + for (int i = tx, e = nmodes * 15; i < e; i += blockDim.x) + { + addr[i] = mangled_addr[i]; + } + } + + if (tx == 0 && err_sum < err_st) + { + error_state[row] = error_sum[row]; + } +} \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/cufft.py b/ptypy/accelerate/cuda_pycuda/cufft.py new file mode 100644 index 000000000..d10e82b1a --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/cufft.py @@ -0,0 +1,176 @@ +import skcuda.fft as cu_fft +from skcuda.fft import cufft as cufftlib +from pycuda import gpuarray +from . import load_kernel +import numpy as np + +class FFT_cuda(object): + + def __init__(self, array, queue=None, + inplace=False, + pre_fft=None, + post_fft=None, + symmetric=True, + forward=True): + self._queue = queue + dims = array.ndim + if dims < 2: + raise AssertionError('Input array must be at least 2-dimensional') + self.arr_shape = (array.shape[-2], array.shape[-1]) + rows = self.arr_shape[0] + columns = self.arr_shape[1] + if rows != columns or rows not in [16, 32, 64, 128, 256, 512, 1024, 2048]: + raise ValueError("CUDA FFT only supports powers of 2 for rows/columns, from 16 to 2048") + self.batches = int(np.product(array.shape[0:dims-2]) if dims > 2 else 1) + self.forward = forward + + self._load(array, pre_fft, post_fft, symmetric, forward) + + def _load(self, array, pre_fft, post_fft, symmetric, forward): + if pre_fft is not None: + self.pre_fft = gpuarray.to_gpu(pre_fft) + self.pre_fft_ptr = self.pre_fft.gpudata + else: + self.pre_fft_ptr = 0 + if post_fft is not None: + self.post_fft = gpuarray.to_gpu(post_fft) + self.post_fft_ptr = self.post_fft.gpudata + else: + self.post_fft_ptr = 0 + + import filtered_cufft + self.fftobj = filtered_cufft.FilteredFFT( + self.batches, + self.arr_shape[0], + self.arr_shape[1], + symmetric, + forward, + self.pre_fft_ptr, + self.post_fft_ptr, + self._queue.handle) + + self.ft = self._ft + self.ift = self._ift + + @property + def queue(self): + return self._queue + + @queue.setter + def queue(self, queue): + self._queue = queue + self.fftobj.queue = queue.handle + + def _ft(self, input, output): + self.fftobj.fft(input.gpudata, output.gpudata) + + def _ift(self, input, output): + self.fftobj.ifft(input.gpudata, output.gpudata) + + +class FFT_skcuda(FFT_cuda): + + @property + def queue(self): + return self._queue + + @queue.setter + def queue(self, queue): + self._queue = queue + cufftlib.cufftSetStream(self.plan.handle, queue.handle) + + def _load(self, array, pre_fft, post_fft, symmetric, forward): + assert(array.dtype in [np.complex64, np.complex128]) + assert(pre_fft.dtype in [np.complex64, np.complex128] if pre_fft is not None else True) + assert(post_fft.dtype in [np.complex64, np.complex128] if post_fft is not None else True) + + math_type = 'float' if array.dtype == np.complex64 else 'double' + if pre_fft is not None: + math_type = 'float' if pre_fft.dtype == np.complex64 else 'double' + self.pre_fft_knl = load_kernel("batched_multiply", { + 'MPY_DO_SCALE': 'false', + 'MPY_DO_FILT': 'true', + 'IN_TYPE': 'float' if array.dtype == np.complex64 else 'double', + 'OUT_TYPE': 'float' if array.dtype == np.complex64 else 'double', + 'MATH_TYPE': math_type + }) if pre_fft is not None else None + + math_type = 'float' if array.dtype == np.complex64 else 'double' + if post_fft is not None: + math_type = 'float' if post_fft.dtype == np.complex64 else 'double' + self.post_fft_knl = load_kernel("batched_multiply", { + 'MPY_DO_SCALE': 'true' if (not forward and not symmetric) or symmetric else 'false', + 'MPY_DO_FILT': 'true' if post_fft is not None else 'false', + 'IN_TYPE': 'float' if array.dtype == np.complex64 else 'double', + 'OUT_TYPE': 'float' if array.dtype == np.complex64 else 'double', + 'MATH_TYPE': math_type + }) if (not (forward and not symmetric) or post_fft is not None) else None + + self.block = (32, 32, 1) + self.grid = ( + int((self.arr_shape[0] + 31) // 32), + int((self.arr_shape[1] + 31) // 32), + int(self.batches) + ) + self.plan = cu_fft.Plan( + self.arr_shape, + array.dtype, + array.dtype, + self.batches, + self._queue + ) + # with cuFFT, we need to scale ifft + if not symmetric and not forward: + self.scale = 1 / np.product(self.arr_shape) + elif forward and not symmetric: + self.scale = 1.0 + else: + self.scale = 1 / np.sqrt(np.product(self.arr_shape)) + + if pre_fft is not None: + self.pre_fft = gpuarray.to_gpu(pre_fft) + else: + self.pre_fft = np.intp(0) # NULL + if post_fft is not None: + self.post_fft = gpuarray.to_gpu(post_fft) + else: + self.post_fft = np.intp(0) + + self.ft = self._ft + self.ift = self._ift + + def _prefilt(self, x, y): + if self.pre_fft_knl: + self.pre_fft_knl(x, y, self.pre_fft, + np.float32(self.scale), + np.int32(self.batches), + np.int32(self.arr_shape[0]), + np.int32(self.arr_shape[1]), + block=self.block, + grid=self.grid, + stream=self._queue) + return y + else: + return x + + def _postfilt(self, y): + if self.post_fft_knl: + assert self.post_fft is not None + assert self.scale is not None + self.post_fft_knl(y, y, self.post_fft, np.float32(self.scale), + np.int32(self.batches), + np.int32(self.arr_shape[0]), + np.int32(self.arr_shape[1]), + block=self.block, grid=self.grid, + stream=self._queue) + + def _ft(self, x, y): + d = self._prefilt(x, y) + cu_fft.fft(d, y, self.plan) + self._postfilt(y) + + def _ift(self, x, y): + d = self._prefilt(x, y) + cu_fft.ifft(d, y, self.plan) + self._postfilt(y) + diff --git a/ptypy/accelerate/cuda_pycuda/dependencies.yml b/ptypy/accelerate/cuda_pycuda/dependencies.yml new file mode 100644 index 000000000..93ac5cd68 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/dependencies.yml @@ -0,0 +1,20 @@ +name: ptypy_pycuda +channels: + - conda-forge +dependencies: + - python=3.9 + - numpy + - scipy + - matplotlib + - h5py + - pyzmq + - mpi4py + - pillow + - pyfftw + - reikna + - pycuda + - cudatoolkit-dev + - pip + - compilers + - pip: + - scikit-cuda \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/engines/ML_pycuda.py b/ptypy/accelerate/cuda_pycuda/engines/ML_pycuda.py new file mode 100644 index 000000000..2022a2bdf --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/engines/ML_pycuda.py @@ -0,0 +1,789 @@ +# -*- coding: utf-8 -*- +""" +Maximum Likelihood reconstruction engine. + +TODO. + + * Implement other regularizers + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import numpy as np +from pycuda import gpuarray +import pycuda.driver as cuda +import pycuda.cumath +from pycuda.tools import DeviceMemoryPool + +from ptypy.engines import register +from ptypy.accelerate.base.engines.ML_serial import ML_serial, BaseModelSerial +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from .. import get_context +from ..kernels import PropagationKernel, RealSupportKernel, FourierSupportKernel +from ..kernels import GradientDescentKernel, AuxiliaryWaveKernel, PoUpdateKernel, PositionCorrectionKernel +from ..array_utils import ArrayUtilsKernel, DerivativesKernel, GaussianSmoothingKernel, TransposeKernel + +from ..mem_utils import GpuDataManager +from ptypy.accelerate.base import address_manglers + +__all__ = ['ML_pycuda'] + +MAX_BLOCKS = 99999 # can be used to limit the number of blocks, simulating that they don't fit +#MAX_BLOCKS = 3 # can be used to limit the number of blocks, simulating that they don't fit + +@register() +class ML_pycuda(ML_serial): + + """ + Defaults: + + [probe_update_cuda_atomics] + default = False + type = bool + help = For GPU, use the atomics version for probe update kernel + + [object_update_cuda_atomics] + default = True + type = bool + help = For GPU, use the atomics version for object update kernel + + [use_cuda_device_memory_pool] + default = True + type = bool + help = For GPU, use a device memory pool + + [fft_lib] + default = reikna + type = str + help = Choose the pycuda-compatible FFT module. + doc = One of: + - ``'reikna'`` : the reikna packaga (fast load, competitive compute for streaming) + - ``'cuda'`` : ptypy's cuda wrapper (delayed load, but fastest compute if all data is on GPU) + - ``'skcuda'`` : scikit-cuda (fast load, slowest compute due to additional store/load stages) + choices = 'reikna','cuda','skcuda' + userlevel = 2 + + """ + + def __init__(self, ptycho_parent, pars=None): + """ + Maximum likelihood reconstruction engine. + """ + super().__init__(ptycho_parent, pars) + + def engine_initialize(self): + """ + Prepare for ML reconstruction. + """ + self.context, self.queue = get_context(new_context=True, new_queue=True) + + if self.p.use_cuda_device_memory_pool: + self._dmp = DeviceMemoryPool() + self.allocate = self._dmp.allocate + else: + self._dmp = None + self.allocate = cuda.mem_alloc + + self.qu_htod = cuda.Stream() + self.qu_dtoh = cuda.Stream() + + self.GSK = GaussianSmoothingKernel(queue=self.queue) + self.GSK.tmp = None + + # Real/Fourier Support Kernel + self.RSK = {} + self.FSK = {} + + super().engine_initialize() + #self._setup_kernels() + + def _setup_kernels(self): + """ + Setup kernels, one for each scan. Derive scans from ptycho class + """ + AUK = ArrayUtilsKernel(queue=self.queue) + self._dot_kernel = AUK.dot + # get the scans + for label, scan in self.ptycho.model.scans.items(): + + kern = u.Param() + kern.scanmodel = type(scan).__name__ + self.kernels[label] = kern + + # TODO: needs to be adapted for broad bandwidth + geo = scan.geometries[0] + + # Get info to shape buffer arrays + fpc = scan.max_frames_per_block + + # TODO : make this more foolproof + try: + nmodes = scan.p.coherence.num_probe_modes * \ + scan.p.coherence.num_object_modes + except: + nmodes = 1 + + # create buffer arrays + ash = (fpc * nmodes,) + tuple([int(s) for s in geo.shape]) + aux = gpuarray.zeros(ash, dtype=np.complex64) + kern.aux = aux + kern.a = gpuarray.zeros(ash, dtype=np.complex64) + kern.b = gpuarray.zeros(ash, dtype=np.complex64) + + # setup kernels, one for each SCAN. + kern.GDK = GradientDescentKernel(aux, nmodes, queue=self.queue, math_type="double") + kern.GDK.allocate() + + kern.POK = PoUpdateKernel(queue_thread=self.queue) + kern.POK.allocate() + + kern.AWK = AuxiliaryWaveKernel(queue_thread=self.queue) + kern.AWK.allocate() + + kern.TK = TransposeKernel(queue=self.queue) + + kern.PROP = PropagationKernel(aux, geo.propagator, queue_thread=self.queue, fft=self.p.fft_lib) + kern.PROP.allocate() + kern.resolution = geo.resolution[0] + + if self.do_position_refinement: + kern.PCK = PositionCorrectionKernel(aux, nmodes, self.p.position_refinement, geo.resolution, queue_thread=self.queue) + kern.PCK.allocate() + + mag_mem = 0 + for scan, kern in self.kernels.items(): + mag_mem = max(kern.aux.nbytes // 2, mag_mem) + ma_mem = mag_mem + mem = cuda.mem_get_info()[0] + blk = ma_mem + mag_mem + fit = int(mem - 200 * 1024 * 1024) // blk # leave 200MB room for safety + if not fit: + log(1,"Cannot fit memory into device, if possible reduce frames per block. Exiting...") + self.context.pop() + self.context.detach() + raise SystemExit("ptypy has been exited.") + + # TODO grow blocks dynamically + nma = min(fit, MAX_BLOCKS) + log(4, 'Free memory on device: %.2f GB' % (float(mem)/1e9)) + log(4, 'PyCUDA max blocks fitting on GPU: ma_arrays={}'.format(nma)) + # reset memory or create new + self.w_data = GpuDataManager(ma_mem, 0, nma, False) + self.I_data = GpuDataManager(mag_mem, 0, nma, False) + + def engine_prepare(self): + + super().engine_prepare() + ## Serialize new data ## + use_tiles = (not self.p.probe_update_cuda_atomics) or (not self.p.object_update_cuda_atomics) + + # recursive copy to gpu for probe and object + for _cname, c in self.ptycho.containers.items(): + if c.original != self.pr and c.original != self.ob: + continue + for _sname, s in c.S.items(): + # convert data here + s.gpu = gpuarray.to_gpu(s.data) + s.cpu = cuda.pagelocked_empty(s.data.shape, s.data.dtype, order="C") + s.cpu[:] = s.data + + for label, d in self.ptycho.new_data: + prep = self.diff_info[d.ID] + prep.err_phot_gpu = gpuarray.to_gpu(prep.err_phot) + prep.fic_gpu = gpuarray.ones_like(prep.err_phot_gpu) + + if use_tiles: + prep.addr2 = np.ascontiguousarray(np.transpose(prep.addr, (2, 3, 0, 1))) + + prep.addr_gpu = gpuarray.to_gpu(prep.addr) + if self.do_position_refinement: + prep.original_addr_gpu = gpuarray.to_gpu(prep.original_addr) + prep.error_state_gpu = gpuarray.empty_like(prep.err_phot_gpu) + prep.mangled_addr_gpu = prep.addr_gpu.copy() + + # Todo: Which address to pick? + if use_tiles: + prep.addr2_gpu = gpuarray.to_gpu(prep.addr2) + + prep.I = cuda.pagelocked_empty(d.data.shape, d.data.dtype, order="C", mem_flags=4) + prep.I[:] = d.data + + # Todo: avoid that extra copy of data + if self.do_position_refinement: + ma = self.ma.S[d.ID].data.astype(np.float32) + prep.ma = cuda.pagelocked_empty(ma.shape, ma.dtype, order="C", mem_flags=4) + prep.ma[:] = ma + + log(4, 'Free memory on device: %.2f GB' % (float(cuda.mem_get_info()[0])/1e9)) + self.w_data.add_data_block() + self.I_data.add_data_block() + + self.dID_list = list(self.di.S.keys()) + + def _initialize_model(self): + + # Create noise model + if self.p.ML_type.lower() == "gaussian": + self.ML_model = GaussianModel(self) + elif self.p.ML_type.lower() == "poisson": + raise NotImplementedError('Poisson norm model not yet implemented') + elif self.p.ML_type.lower() == "euclid": + raise NotImplementedError('Euclid norm model not yet implemented') + else: + raise RuntimeError("Unsupported ML_type: '%s'" % self.p.ML_type) + + def _set_pr_ob_ref_for_data(self, dev='gpu', container=None, sync_copy=False): + """ + Overloading the context of Storage.data here, to allow for in-place math on Container instances: + """ + if container is not None: + if container.original==self.pr or container.original==self.ob: + for s in container.S.values(): + # convert data here + if dev == 'gpu': + s.data = s.gpu + if sync_copy: s.gpu.set(s.cpu) + elif dev == 'cpu': + s.data = s.cpu + if sync_copy: + s.gpu.get(s.cpu) + #print('%s to cpu' % s.ID) + else: + for container in self.ptycho.containers.values(): + self._set_pr_ob_ref_for_data(dev=dev, container=container, sync_copy=sync_copy) + + def _get_smooth_gradient(self, data, sigma): + if self.GSK.tmp is None: + self.GSK.tmp = gpuarray.empty(data.shape, dtype=np.complex64) + self.GSK.convolution(data, [sigma, sigma], tmp=self.GSK.tmp) + return data + + def _replace_ob_grad(self): + new_ob_grad = self.ob_grad_new + # Smoothing preconditioner + if self.smooth_gradient: + self.smooth_gradient.sigma *= (1. - self.p.smooth_gradient_decay) + for name, s in new_ob_grad.storages.items(): + s.gpu = self._get_smooth_gradient(s.gpu, self.smooth_gradient.sigma) + + return self._replace_grad(self.ob_grad, new_ob_grad) + + def _replace_pr_grad(self): + new_pr_grad = self.pr_grad_new + # probe support + if self.p.probe_update_start <= self.curiter: + # Apply probe support if needed + for name, s in new_pr_grad.storages.items(): + self.support_constraint(s) + else: + new_pr_grad.fill(0.) + + return self._replace_grad(self.pr_grad , new_pr_grad) + + def _replace_grad(self, grad, new_grad): + norm = np.double(0.) + dot = np.double(0.) + for name, new in new_grad.storages.items(): + old = grad.storages[name] + norm += self._dot_kernel(new.gpu,new.gpu).get()[0] + dot += self._dot_kernel(new.gpu,old.gpu).get()[0] + old.gpu[:] = new.gpu + return norm, dot + + def engine_iterate(self, num=1): + err = super().engine_iterate(num) + # copy all data back to cpu + self._set_pr_ob_ref_for_data(dev='cpu', container=None, sync_copy=True) + return err + + def position_update(self): + """ + Position refinement + """ + if not self.do_position_refinement or (not self.curiter): + return + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + use_tiles = (not self.p.probe_update_cuda_atomics) or (not self.p.object_update_cuda_atomics) + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + log(4, "----------- START POS REF -------------") + for dID in self.dID_list: + + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr_gpu + original_addr = prep.original_addr + mangled_addr = prep.mangled_addr_gpu + err_phot = prep.err_phot_gpu + error_state = prep.error_state_gpu + + # copy intensities and weights to GPU + ev_w, w, data_w = self.w_data.to_gpu(prep.weights, dID, self.qu_htod) + ev, I, data_I = self.I_data.to_gpu(prep.I, dID, self.qu_htod) + + PCK = kern.PCK + TK = kern.TK + PROP = kern.PROP + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We need to re-calculate the current error + PCK.build_aux(aux, addr, ob, pr) + PROP.fw(aux, aux) + PCK.queue.wait_for_event(ev) + # w & I now on device + PCK.log_likelihood_ml(aux, addr, I, w, err_phot) + cuda.memcpy_dtod(dest=error_state.ptr, + src=err_phot.ptr, + size=err_phot.nbytes) + + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + + log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + for i in range(PCK.mangler.nshifts): + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + PROP.fw(aux, aux) + PCK.log_likelihood_ml(aux, mangled_addr, I, w, err_phot) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_phot) + + data_w.record_done(self.queue, 'compute') + data_I.record_done(self.queue, 'compute') + cuda.memcpy_dtod(dest=err_phot.ptr, + src=error_state.ptr, + size=err_phot.nbytes) + if use_tiles: + s1 = addr.shape[0] * addr.shape[1] + s2 = addr.shape[2] * addr.shape[3] + TK.transpose(addr.reshape(s1, s2), prep.addr2_gpu.reshape(s2, s1)) + + self.dID_list.reverse() + + def support_constraint(self, storage=None): + """ + Enforces 2D support constraint on probe. + """ + if storage is None: + for s in self.pr.storages.values(): + self.support_constraint(s) + + # Fourier space + support = self._probe_fourier_support.get(storage.ID) + if support is not None: + if storage.ID not in self.FSK: + supp = support.astype(np.complex64) + self.FSK[storage.ID] = FourierSupportKernel(supp, self.queue, self.p.fft_lib) + self.FSK[storage.ID].allocate() + self.FSK[storage.ID].apply_fourier_support(storage.gpu) + + # Real space + support = self._probe_support.get(storage.ID) + if support is not None: + if storage.ID not in self.RSK: + self.RSK[storage.ID] = RealSupportKernel(support.astype(np.complex64)) + self.RSK[storage.ID].allocate() + self.RSK[storage.ID].apply_real_support(storage.gpu) + + def engine_finalize(self): + """ + Clear all GPU data, pinned memory, etc + """ + self.w_data = None + self.I_data = None + + for name, s in self.pr.S.items(): + s.data = s.gpu.get() # need this, otherwise getting segfault once context is detached + # no longer need those + del s.gpu + del s.cpu + for name, s in self.ob.S.items(): + s.data = s.gpu.get() # need this, otherwise getting segfault once context is detached + # no longer need those + del s.gpu + del s.cpu + for dID, prep in self.diff_info.items(): + prep.addr = prep.addr_gpu.get() + prep.float_intens_coeff = prep.fic_gpu.get() + + + #self.queue.synchronize() + self.context.pop() + self.context.detach() + super().engine_finalize() + +class GaussianModel(BaseModelSerial): + """ + Gaussian noise model. + TODO: feed actual statistical weights instead of using the Poisson statistic heuristic. + """ + + def __init__(self, MLengine): + """ + Core functions for ML computation using a Gaussian model. + """ + super(GaussianModel, self).__init__(MLengine) + + if self.p.reg_del2: + self.regularizer = Regul_del2_pycuda( + self.p.reg_del2_amplitude, + queue=self.engine.queue, + allocator=self.engine.allocate + ) + else: + self.regularizer = None + + def prepare(self): + + super(GaussianModel, self).prepare() + + for label, d in self.engine.ptycho.new_data: + prep = self.engine.diff_info[d.ID] + w = (self.Irenorm * self.engine.ma.S[d.ID].data + / (1. / self.Irenorm + d.data)).astype(d.data.dtype) + prep.weights = cuda.pagelocked_empty(w.shape, w.dtype, order="C", mem_flags=4) + prep.weights[:] = w + + def __del__(self): + """ + Clean up routine + """ + super(GaussianModel, self).__del__() + + def new_grad(self): + """ + Compute a new gradient direction according to a Gaussian noise model. + + Note: The negative log-likelihood and local errors are also computed + here. + """ + ob_grad = self.engine.ob_grad_new + pr_grad = self.engine.pr_grad_new + qu_htod = self.engine.qu_htod + queue = self.engine.queue + + self.engine._set_pr_ob_ref_for_data('gpu') + ob_grad << 0. + pr_grad << 0. + + # We need an array for MPI + LL = np.array([0.]) + error_dct = {} + + for dID in self.engine.dID_list: + prep = self.engine.diff_info[dID] + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.engine.kernels[prep.label] + GDK = kern.GDK + AWK = kern.AWK + POK = kern.POK + aux = kern.aux + + FW = kern.PROP.fw + BW = kern.PROP.bw + + # get addresses and auxilliary array + addr = prep.addr_gpu + fic = prep.fic_gpu + + err_phot = prep.err_phot_gpu + # local references + ob = self.engine.ob.S[oID].data + obg = ob_grad.S[oID].data + pr = self.engine.pr.S[pID].data + prg = pr_grad.S[pID].data + + # Schedule w & I to device + ev_w, w, data_w = self.engine.w_data.to_gpu(prep.weights, dID, qu_htod) + ev, I, data_I = self.engine.I_data.to_gpu(prep.I, dID, qu_htod) + + # make propagated exit (to buffer) + AWK.build_aux_no_ex(aux, addr, ob, pr, add=False) + + # forward prop + FW(aux, aux) + GDK.make_model(aux, addr) + + queue.wait_for_event(ev) + + if self.p.floating_intensities: + GDK.floating_intensity(addr, w, I, fic) + + GDK.main(aux, addr, w, I) + data_w.record_done(queue, 'compute') + data_I.record_done(queue, 'compute') + + GDK.error_reduce(addr, err_phot) + + BW(aux, aux) + + use_atomics = self.p.object_update_cuda_atomics + addr = prep.addr_gpu if use_atomics else prep.addr2_gpu + POK.ob_update_ML(addr, obg, pr, aux, atomics=use_atomics) + + use_atomics = self.p.probe_update_cuda_atomics + addr = prep.addr_gpu if use_atomics else prep.addr2_gpu + POK.pr_update_ML(addr, prg, ob, aux, atomics=use_atomics) + + queue.synchronize() + self.engine.dID_list.reverse() + + # TODO we err_phot.sum, but not necessarily this error_dct until the end of contiguous iteration + for dID, prep in self.engine.diff_info.items(): + err_phot = prep.err_phot_gpu.get() + LL += err_phot.sum() + err_phot /= np.prod(prep.weights.shape[-2:]) + err_fourier = np.zeros_like(err_phot) + err_exit = np.zeros_like(err_phot) + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error_dct.update(zip(prep.view_IDs, errs)) + + # MPI reduction of gradients + + # DtoH copies + for s in ob_grad.S.values(): + s.gpu.get(s.cpu) + for s in pr_grad.S.values(): + s.gpu.get(s.cpu) + self.engine._set_pr_ob_ref_for_data('cpu') + + ob_grad.allreduce() + pr_grad.allreduce() + parallel.allreduce(LL) + + # HtoD cause we continue on gpu + for s in ob_grad.S.values(): + s.gpu.set(s.cpu) + for s in pr_grad.S.values(): + s.gpu.set(s.cpu) + self.engine._set_pr_ob_ref_for_data('gpu') + + # Object regularizer + if self.regularizer: + for name, s in self.engine.ob.storages.items(): + ob_grad.storages[name].data += self.regularizer.grad(s.data) + LL += self.regularizer.LL + + self.LL = LL / self.tot_measpts + + return error_dct + + def poly_line_coeffs(self, c_ob_h, c_pr_h): + """ + Compute the coefficients of the polynomial for line minimization + in direction h + """ + self.engine._set_pr_ob_ref_for_data('gpu') + qu_htod = self.engine.qu_htod + queue = self.engine.queue + + B = gpuarray.zeros((3,), dtype=np.float32) # does not accept np.longdouble + Brenorm = 1. / self.LL[0] ** 2 + + # Outer loop: through diffraction patterns + for dID in self.engine.dID_list: + prep = self.engine.diff_info[dID] + + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.engine.kernels[prep.label] + GDK = kern.GDK + AWK = kern.AWK + + f = kern.aux + a = kern.a + b = kern.b + + FW = kern.PROP.fw + + # get addresses and auxiliary arrays + addr = prep.addr_gpu + fic = prep.fic_gpu + + # Schedule w & I to device + ev_w, w, data_w = self.engine.w_data.to_gpu(prep.weights, dID, qu_htod) + ev, I, data_I = self.engine.I_data.to_gpu(prep.I, dID, qu_htod) + + # local references + ob = self.ob.S[oID].data + ob_h = c_ob_h.S[oID].data + pr = self.pr.S[pID].data + pr_h = c_pr_h.S[pID].data + + # make propagated exit (to buffer) + AWK.build_aux_no_ex(f, addr, ob, pr, add=False) + AWK.build_aux_no_ex(a, addr, ob_h, pr, add=False) + AWK.build_aux_no_ex(a, addr, ob, pr_h, add=True) + AWK.build_aux_no_ex(b, addr, ob_h, pr_h, add=False) + + # forward prop + FW(f,f) + FW(a,a) + FW(b,b) + + queue.wait_for_event(ev) + + GDK.make_a012(f, a, b, addr, I, fic) + GDK.fill_b(addr, Brenorm, w, B) + + data_w.record_done(queue, 'compute') + data_I.record_done(queue, 'compute') + + queue.synchronize() + self.engine.dID_list.reverse() + + B = B.get() + parallel.allreduce(B) + + # Object regularizer + if self.regularizer: + for name, s in self.ob.storages.items(): + B += Brenorm * self.regularizer.poly_line_coeffs( + c_ob_h.storages[name].data, s.data) + + self.B = B + + return B + +class Regul_del2_pycuda(object): + """\ + Squared gradient regularizer (Gaussian prior). + + This class applies to any numpy array. + """ + def __init__(self, amplitude, axes=[-2, -1], queue=None, allocator=None): + # Regul.__init__(self, axes) + self.axes = axes + self.amplitude = amplitude + self.delxy = None + self.g = None + self.LL = None + self.queue = queue + self.AUK = ArrayUtilsKernel(queue=queue) + self.DELK_c = DerivativesKernel(np.complex64, queue=queue) + self.DELK_f = DerivativesKernel(np.float32, queue=queue) + + if allocator is None: + self._dmp = DeviceMemoryPool() + self.allocator=self._dmp.allocate + else: + self.allocator = allocator + self._dmp= None + + empty = lambda x: gpuarray.empty(x.shape, x.dtype, allocator=self.allocator) + + def delxb(x, axis=-1): + out = empty(x) + if x.dtype == np.float32: + self.DELK_f.delxb(x, out, axis) + elif x.dtype == np.complex64: + self.DELK_c.delxb(x, out, axis) + else: + raise TypeError("Type %s invalid for derivatives" % x.dtype) + return out + + self.delxb = delxb + + def delxf(x, axis=-1): + out = empty(x) + if x.dtype == np.float32: + self.DELK_f.delxf(x, out, axis) + elif x.dtype == np.complex64: + self.DELK_c.delxf(x, out, axis) + else: + raise TypeError("Type %s invalid for derivatives" % x.dtype) + return out + + self.delxf = delxf + self.norm = lambda x : self.AUK.norm2(x).get().item() + self.dot = lambda x, y : self.AUK.dot(x,y).get().item() + + from pycuda.elementwise import ElementwiseKernel + self._grad_reg_kernel = ElementwiseKernel( + "pycuda::complex *g, float fac, \ + pycuda::complex *py, pycuda::complex *px, \ + pycuda::complex *my, pycuda::complex *mx", + "g[i] = (px[i]+py[i]-my[i]-mx[i]) * fac", + "grad_reg", + ) + def grad(amp, px,py, mx, my): + out = empty(px) + self._grad_reg_kernel(out, amp, py, px, mx, my, stream=self.queue) + return out + self.reg_grad = grad + + def grad(self, x): + """ + Compute and return the regularizer gradient given the array x. + """ + ax0, ax1 = self.axes + del_xf = self.delxf(x, axis=ax0) + del_yf = self.delxf(x, axis=ax1) + del_xb = self.delxb(x, axis=ax0) + del_yb = self.delxb(x, axis=ax1) + + self.delxy = [del_xf, del_yf, del_xb, del_yb] + + # TODO this one might be slow, maybe try with elementwise kernel + #self.g = (del_xb + del_yb - del_xf - del_yf) * 2. * self.amplitude + self.g = self.reg_grad(2. * self.amplitude, del_xb, del_yb, del_xf, del_yf) + + + self.LL = self.amplitude * (self.norm(del_xf) + + self.norm(del_yf) + + self.norm(del_xb) + + self.norm(del_yb)) + + return self.g + + def poly_line_coeffs(self, h, x=None): + ax0, ax1 = self.axes + if x is None: + del_xf, del_yf, del_xb, del_yb = self.delxy + else: + del_xf = self.delxf(x, axis=ax0) + del_yf = self.delxf(x, axis=ax1) + del_xb = self.delxb(x, axis=ax0) + del_yb = self.delxb(x, axis=ax1) + + hdel_xf = self.delxf(h, axis=ax0) + hdel_yf = self.delxf(h, axis=ax1) + hdel_xb = self.delxb(h, axis=ax0) + hdel_yb = self.delxb(h, axis=ax1) + + c0 = self.amplitude * (self.norm(del_xf) + + self.norm(del_yf) + + self.norm(del_xb) + + self.norm(del_yb)) + + c1 = 2 * self.amplitude * (self.dot(del_xf, hdel_xf) + + self.dot(del_yf, hdel_yf) + + self.dot(del_xb, hdel_xb) + + self.dot(del_yb, hdel_yb)) + + c2 = self.amplitude * (self.norm(hdel_xf) + + self.norm(hdel_yf) + + self.norm(hdel_xb) + + self.norm(hdel_yb)) + + self.coeff = np.array([c0, c1, c2]) + return self.coeff diff --git a/ptypy/accelerate/cuda_pycuda/engines/__init__.py b/ptypy/accelerate/cuda_pycuda/engines/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ptypy/accelerate/cuda_pycuda/engines/projectional_pycuda.py b/ptypy/accelerate/cuda_pycuda/engines/projectional_pycuda.py new file mode 100644 index 000000000..aa88a380f --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/engines/projectional_pycuda.py @@ -0,0 +1,603 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import numpy as np +import time +from pycuda import gpuarray +import pycuda.driver as cuda + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines import register +from ptypy.engines.projectional import DMMixin, RAARMixin +from ptypy.accelerate.base.engines import projectional_serial +from .. import get_context +from ..kernels import FourierUpdateKernel, AuxiliaryWaveKernel, PoUpdateKernel, PositionCorrectionKernel +from ..kernels import PropagationKernel, RealSupportKernel, FourierSupportKernel +from ..array_utils import ArrayUtilsKernel, GaussianSmoothingKernel,\ +TransposeKernel, ClipMagnitudesKernel, MassCenterKernel, Abs2SumKernel,\ +InterpolatedShiftKernel +from ..mem_utils import make_pagelocked_paired_arrays as mppa +from ..multi_gpu import get_multi_gpu_communicator + +__all__ = ['DM_pycuda', 'RAAR_pycuda'] + +class _ProjectionEngine_pycuda(projectional_serial._ProjectionEngine_serial): + + """ + Defaults: + + [probe_update_cuda_atomics] + default = False + type = bool + help = For GPU, use the atomics version for probe update kernel + + [object_update_cuda_atomics] + default = True + type = bool + help = For GPU, use the atomics version for object update kernel + + [fft_lib] + default = reikna + type = str + help = Choose the pycuda-compatible FFT module. + doc = One of: + - ``'reikna'`` : the reikna packaga (fast load, competitive compute for streaming) + - ``'cuda'`` : ptypy's cuda wrapper (delayed load, but fastest compute if all data is on GPU) + - ``'skcuda'`` : scikit-cuda (fast load, slowest compute due to additional store/load stages) + choices = 'reikna','cuda','skcuda' + userlevel = 2 + + """ + def __init__(self, ptycho_parent, pars=None): + """ + Difference map reconstruction engine. + """ + super().__init__(ptycho_parent, pars) + self.multigpu = None + + def engine_initialize(self): + """ + Prepare for reconstruction. + """ + # Context, Multi GPU communicator and Stream (needs to be in this order) + self.context, self.queue = get_context(new_context=True, new_queue=False) + self.multigpu = get_multi_gpu_communicator() + self.context, self.queue = get_context(new_context=False, new_queue=True) + + # Gaussian Smoothing Kernel + self.GSK = GaussianSmoothingKernel(queue=self.queue) + + # Real/Fourier Support Kernel + self.RSK = {} + self.FSK = {} + + # Clip Magnitudes Kernel + self.CMK = ClipMagnitudesKernel(queue=self.queue) + + # initialise kernels for centring probe if required + if self.p.probe_center_tol is not None: + # mass center kernel + self.MCK = MassCenterKernel(queue=self.queue) + # absolute sum kernel + self.A2SK = Abs2SumKernel(dtype=self.pr.dtype, queue=self.queue) + # interpolated shift kernel + self.ISK = InterpolatedShiftKernel(queue=self.queue) + + super().engine_initialize() + + def _setup_kernels(self): + """ + Setup kernels, one for each scan. Derive scans from ptycho class + """ + # get the scans + for label, scan in self.ptycho.model.scans.items(): + + kern = u.Param() + kern.scanmodel = type(scan).__name__ + self.kernels[label] = kern + # TODO: needs to be adapted for broad bandwidth + geo = scan.geometries[0] + + # Get info to shape buffer arrays + fpc = scan.max_frames_per_block + + # TODO : make this more foolproof + try: + nmodes = scan.p.coherence.num_probe_modes * \ + scan.p.coherence.num_object_modes + except: + nmodes = 1 + + # create buffer arrays + ash = (fpc * nmodes,) + tuple(geo.shape) + aux = np.zeros(ash, dtype=np.complex64) + kern.aux = gpuarray.to_gpu(aux) + + # setup kernels, one for each SCAN. + log(4, "Setting up FourierUpdateKernel") + kern.FUK = FourierUpdateKernel(aux, nmodes, queue_thread=self.queue) + kern.FUK.allocate() + + log(4, "Setting up PoUpdateKernel") + kern.POK = PoUpdateKernel(queue_thread=self.queue) + kern.POK.allocate() + + log(4, "Setting up AuxiliaryWaveKernel") + kern.AWK = AuxiliaryWaveKernel(queue_thread=self.queue) + kern.AWK.allocate() + + log(4, "Setting up ArrayUtilsKernel") + kern.AUK = ArrayUtilsKernel(queue=self.queue) + + log(4, "Setting up TransposeKernel") + kern.TK = TransposeKernel(queue=self.queue) + + log(4, "Setting up PropagationKernel") + kern.PROP = PropagationKernel(aux, geo.propagator, self.queue, self.p.fft_lib) + kern.PROP.allocate() + kern.resolution = geo.resolution[0] + + if self.do_position_refinement: + log(4, "Setting up PositionCorrectionKernel") + kern.PCK = PositionCorrectionKernel(aux, nmodes, self.p.position_refinement, geo.resolution, queue_thread=self.queue) + kern.PCK.allocate() + log(4, "Kernel setup completed") + + def engine_prepare(self): + + super().engine_prepare() + + for name, s in self.ob.S.items(): + s.gpu = gpuarray.to_gpu(s.data) + for name, s in self.ob_buf.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.ob_nrm.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.pr.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.pr_buf.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.pr_nrm.S.items(): + s.gpu, s.data = mppa(s.data) + + use_tiles = (not self.p.probe_update_cuda_atomics) or (not self.p.object_update_cuda_atomics) + + # TODO : like the serialization this one is needed due to object reformatting + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + prep.addr_gpu = gpuarray.to_gpu(prep.addr) + if use_tiles: + prep.addr2 = np.ascontiguousarray(np.transpose(prep.addr, (2, 3, 0, 1))) + prep.addr2_gpu = gpuarray.to_gpu(prep.addr2) + if self.do_position_refinement: + prep.mangled_addr_gpu = prep.addr_gpu.copy() + + for label, d in self.ptycho.new_data: + prep = self.diff_info[d.ID] + pID, oID, eID = prep.poe_IDs + s = self.ex.S[eID] + s.gpu = gpuarray.to_gpu(s.data) + s = self.ma.S[d.ID] + s.gpu = gpuarray.to_gpu(s.data.astype(np.float32)) + + prep.mag = gpuarray.to_gpu(prep.mag) + prep.ma_sum = gpuarray.to_gpu(prep.ma_sum) + prep.err_fourier_gpu = gpuarray.to_gpu(prep.err_fourier) + prep.err_phot_gpu = gpuarray.to_gpu(prep.err_phot) + prep.err_exit_gpu = gpuarray.to_gpu(prep.err_exit) + if self.do_position_refinement: + prep.error_state_gpu = gpuarray.empty_like(prep.err_fourier_gpu) + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + queue = self.queue + + for it in range(num): + error = {} + for dID in self.di.S.keys(): + + # find probe, object and exit ID in dependence of dID + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + PROP = kern.PROP + + # get addresses and buffers + addr = prep.addr_gpu + mag = prep.mag + ma_sum = prep.ma_sum + err_fourier = prep.err_fourier_gpu + err_phot = prep.err_phot_gpu + err_exit = prep.err_exit_gpu + pbound = self.pbound_scan[prep.label] + aux = kern.aux + + # local references + ma = self.ma.S[dID].gpu + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + ex = self.ex.S[eID].gpu + + ## compute log-likelihood + if self.p.compute_log_likelihood: + AWK.build_aux_no_ex(aux, addr, ob, pr) + PROP.fw(aux, aux) + FUK.log_likelihood(aux, addr, mag, ma, err_phot) + + ## build auxilliary wave + #AWK.build_aux(aux, addr, ob, pr, ex, alpha=self.p.alpha) + AWK.make_aux(aux, addr, ob, pr, ex, c_po=self._c, c_e=1-self._c) + + ## forward FFT + PROP.fw(aux, aux) + + ## Deviation from measured data + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + FUK.fmag_all_update(aux, addr, mag, ma, err_fourier, pbound) + + ## backward FFT + PROP.bw(aux, aux) + + ## build exit wave + #AWK.build_exit(aux, addr, ob, pr, ex, alpha=self.p.alpha) + AWK.make_exit(aux, addr, ob, pr, ex, c_a=self._b, c_po=self._a, c_e=-(self._a + self._b)) + FUK.exit_error(aux, addr) + FUK.error_reduce(addr, err_exit) + + parallel.barrier() + + sync = (self.curiter % 1 == 0) + self.overlap_update() + + self.center_probe() + + parallel.barrier() + self.position_update() + + self.curiter += 1 + queue.synchronize() + + for name, s in self.ob.S.items(): + s.data[:] = s.gpu.get() + for name, s in self.pr.S.items(): + s.data[:] = s.gpu.get() + + # costly but needed to sync back with + # for name, s in self.ex.S.items(): + # s.data[:] = s.gpu.get() + for dID, prep in self.diff_info.items(): + err_fourier = prep.err_fourier_gpu.get() + err_phot = prep.err_phot_gpu.get() + err_exit = prep.err_exit_gpu.get() + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error.update(zip(prep.view_IDs, errs)) + + self.error = error + return error + + def position_update(self): + """ + Position refinement + """ + if not self.do_position_refinement or (not self.curiter): + return + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + use_tiles = (not self.p.probe_update_cuda_atomics) or (not self.p.object_update_cuda_atomics) + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + log(4, "----------- START POS REF -------------") + for dID in self.di.S.keys(): + + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + ma = self.ma.S[dID].gpu + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr_gpu + original_addr = prep.original_addr + mangled_addr = prep.mangled_addr_gpu + mag = prep.mag + ma_sum = prep.ma_sum + err_fourier = prep.err_fourier_gpu + error_state = prep.error_state_gpu + + PCK = kern.PCK + TK = kern.TK + PROP = kern.PROP + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We need to re-calculate the current error + PCK.build_aux(aux, addr, ob, pr) + PROP.fw(aux, aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, addr, mag, ma, ma_sum) + PCK.error_reduce(addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, addr, mag, ma, err_fourier) + cuda.memcpy_dtod(dest=error_state.ptr, + src=err_fourier.ptr, + size=err_fourier.nbytes) + + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + + log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + for i in range(PCK.mangler.nshifts): + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + PROP.fw(aux, aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, mangled_addr, mag, ma, ma_sum) + PCK.error_reduce(mangled_addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, mangled_addr, mag, ma, err_fourier) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_fourier) + + cuda.memcpy_dtod(dest=err_fourier.ptr, + src=error_state.ptr, + size=err_fourier.nbytes) + if use_tiles: + s1 = addr.shape[0] * addr.shape[1] + s2 = addr.shape[2] * addr.shape[3] + TK.transpose(addr.reshape(s1, s2), prep.addr2_gpu.reshape(s2, s1)) + + + def center_probe(self): + if self.p.probe_center_tol is not None: + for name, pr_s in self.pr.storages.items(): + psum_d = self.A2SK.abs2sum(pr_s.gpu) + c1 = self.MCK.mass_center(psum_d).get() + c2 = (np.asarray(pr_s.shape[-2:]) // 2).astype(c1.dtype) + + shift = c2 - c1 + # exit if the current center of mass is within the tolerance + if u.norm(shift) < self.p.probe_center_tol: + break + + # shift the probe + pr_s.gpu = self.ISK.interpolate_shift(pr_s.gpu, shift) + + # shift the object + ob_s = pr_s.views[0].pod.ob_view.storage + ob_s.gpu = self.ISK.interpolate_shift(ob_s.gpu, shift) + + # shift the exit waves + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + if pID == name: + self.ex.S[eID].gpu = self.ISK.interpolate_shift( + self.ex.S[eID].gpu, shift) + + log(4,'Probe recentered from %s to %s' + % (str(tuple(c1)), str(tuple(c2)))) + + + ## object update + def object_update(self, MPI=False): + use_atomics = self.p.object_update_cuda_atomics + queue = self.queue + queue.synchronize() + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + cfact = self.ob_cfact[oID] + + if self.p.obj_smooth_std is not None: + log(4, 'Smoothing object, cfact is %.2f' % cfact) + obb = self.ob_buf.S[oID] + smooth_mfs = [self.p.obj_smooth_std, self.p.obj_smooth_std] + self.GSK.convolution(ob.gpu, smooth_mfs, tmp=obb.gpu) + + ob.gpu *= cfact + obn.gpu.fill(cfact) + queue.synchronize() + + # storage for-loop + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + POK = self.kernels[prep.label].POK + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for loop + addr = prep.addr_gpu if use_atomics else prep.addr2_gpu + ev = POK.ob_update(addr, + self.ob.S[oID].gpu, + self.ob_nrm.S[oID].gpu, + self.pr.S[pID].gpu, + self.ex.S[eID].gpu, + atomics = use_atomics) + queue.synchronize() + + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + self.multigpu.allReduceSum(ob.gpu) + self.multigpu.allReduceSum(obn.gpu) + ob.gpu /= obn.gpu + + self.clip_object(ob.gpu) + queue.synchronize() + + ## probe update + def probe_update(self, MPI=False): + queue = self.queue + + # storage for-loop + change_gpu = gpuarray.zeros((1,), dtype=np.float32) + cfact = self.p.probe_inertia + use_atomics = self.p.probe_update_cuda_atomics + for pID, pr in self.pr.storages.items(): + prn = self.pr_nrm.S[pID] + cfact = self.pr_cfact[pID] + pr.gpu *= cfact + prn.gpu.fill(cfact) + + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + POK = self.kernels[prep.label].POK + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for-loop + addr = prep.addr_gpu if use_atomics else prep.addr2_gpu + ev = POK.pr_update(addr, + self.pr.S[pID].gpu, + self.pr_nrm.S[pID].gpu, + self.ob.S[oID].gpu, + self.ex.S[eID].gpu, + atomics=use_atomics) + queue.synchronize() + + for pID, pr in self.pr.storages.items(): + + buf = self.pr_buf.S[pID] + prn = self.pr_nrm.S[pID] + + self.multigpu.allReduceSum(pr.gpu) + self.multigpu.allReduceSum(prn.gpu) + pr.gpu /= prn.gpu + self.support_constraint(pr) + + ## calculate change on GPU + queue.synchronize() + AUK = self.kernels[list(self.kernels)[0]].AUK + buf.gpu -= pr.gpu + change_gpu += (AUK.norm2(buf.gpu) / AUK.norm2(pr.gpu)) + buf.gpu[:] = pr.gpu + self.multigpu.allReduceSum(change_gpu) + change = change_gpu.get().item() / parallel.size + + return np.sqrt(change) + + def support_constraint(self, storage=None): + """ + Enforces 2D support constraint on probe. + """ + if storage is None: + for s in self.pr.storages.values(): + self.support_constraint(s) + + # Fourier space + support = self._probe_fourier_support.get(storage.ID) + if support is not None: + if storage.ID not in self.FSK: + supp = support.astype(np.complex64) + self.FSK[storage.ID] = FourierSupportKernel(supp, self.queue, self.p.fft_lib) + self.FSK[storage.ID].allocate() + self.FSK[storage.ID].apply_fourier_support(storage.gpu) + + # Real space + support = self._probe_support.get(storage.ID) + if support is not None: + if storage.ID not in self.RSK: + self.RSK[storage.ID] = RealSupportKernel(support.astype(np.complex64)) + self.RSK[storage.ID].allocate() + self.RSK[storage.ID].apply_real_support(storage.gpu) + + def clip_object(self, ob): + """ + Clips magnitudes of object into given range. + """ + if self.p.clip_object is not None: + cmin, cmax = self.p.clip_object + self.CMK.clip_magnitudes_to_range(ob, cmin, cmax) + + def engine_finalize(self): + """ + clear GPU data and destroy context. + """ + for name, s in self.ob.S.items(): + del s.gpu + for name, s in self.ob_buf.S.items(): + del s.gpu + for name, s in self.ob_nrm.S.items(): + del s.gpu + for name, s in self.pr.S.items(): + del s.gpu + for name, s in self.pr_buf.S.items(): + del s.gpu + for name, s in self.pr_nrm.S.items(): + del s.gpu + + # copy addr to cpu + for dID, prep in self.diff_info.items(): + prep.addr = prep.addr_gpu.get() + + # copy data to cpu + # this kills the pagelock memory (otherwise we get segfaults in h5py) + for name, s in self.pr.S.items(): + s.data = np.copy(s.data) + + self.context.pop() + self.context.detach() + + # we don't need the "benchmarking" in DM_serial + super().engine_finalize(benchmark=False) + + +@register(name="DM_pycuda_nostream") +class DM_pycuda(_ProjectionEngine_pycuda, DMMixin): + """ + A full-fledged Difference Map engine accelerated with pycuda. + + Defaults: + + [name] + default = DM_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _ProjectionEngine_pycuda.__init__(self, ptycho_parent, pars) + DMMixin.__init__(self, self.p.alpha) + ptycho_parent.citations.add_article(**self.article) + + +@register(name="RAAR_pycuda_nostream") +class RAAR_pycuda(_ProjectionEngine_pycuda, RAARMixin): + """ + A RAAR engine in accelerated with pycuda. + + Defaults: + + [name] + default = RAAR_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _ProjectionEngine_pycuda.__init__(self, ptycho_parent, pars) + RAARMixin.__init__(self, self.p.beta) diff --git a/ptypy/accelerate/cuda_pycuda/engines/projectional_pycuda_stream.py b/ptypy/accelerate/cuda_pycuda/engines/projectional_pycuda_stream.py new file mode 100644 index 000000000..c6e5adda8 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/engines/projectional_pycuda_stream.py @@ -0,0 +1,512 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine for NVIDIA GPUs. + +This engine uses three streams, one for the compute queue and one for each I/O queue. +Events are used to synchronize download / compute/ upload. we cannot manipulate memory +for each loop over the state vector, a certain number of memory sections is preallocated +and reused. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import numpy as np +import time +from pycuda import gpuarray +import pycuda.driver as cuda +from pycuda.tools import DeviceMemoryPool + +from ptypy import utils as u +from ptypy.utils.verbose import log, logger +from ptypy.utils import parallel +from ptypy.engines import register +from ptypy.engines.projectional import DMMixin, RAARMixin +from . import projectional_pycuda + +from ..mem_utils import make_pagelocked_paired_arrays as mppa +from ..mem_utils import GpuDataManager + +EX_MA_BLOCKS_RATIO = 2 +MAX_BLOCKS = 99999 # can be used to limit the number of blocks, simulating that they don't fit +#MAX_BLOCKS = 3 # can be used to limit the number of blocks, simulating that they don't fit + +__all__ = ['DM_pycuda_stream', 'RAAR_pycuda_stream'] + + +class _ProjectionEngine_pycuda_stream(projectional_pycuda._ProjectionEngine_pycuda): + + def __init__(self, ptycho_parent, pars=None): + + super().__init__(ptycho_parent, pars) + self.ma_data = None + self.mag_data = None + self.ex_data = None + + def engine_initialize(self): + super().engine_initialize() + self.qu_htod = cuda.Stream() + self.qu_dtoh = cuda.Stream() + + def _setup_kernels(self): + + super()._setup_kernels() + ex_mem = 0 + mag_mem = 0 + for scan, kern in self.kernels.items(): + ex_mem = max(kern.aux.nbytes, ex_mem) + mag_mem = max(kern.FUK.gpu.fdev.nbytes, mag_mem) + ma_mem = mag_mem + mem = cuda.mem_get_info()[0] + blk = ex_mem * EX_MA_BLOCKS_RATIO + ma_mem + mag_mem + fit = int(mem - 200 * 1024 * 1024) // blk # leave 200MB room for safety + if not fit: + log(1,"Cannot fit memory into device, if possible reduce frames per block. Exiting...") + self.context.pop() + self.context.detach() + raise SystemExit("ptypy has been exited.") + + # TODO grow blocks dynamically + nex = min(fit * EX_MA_BLOCKS_RATIO, MAX_BLOCKS) + nma = min(fit, MAX_BLOCKS) + log(4, 'Free memory on device: %.2f GB' % (float(mem)/1e9)) + log(4, 'PyCUDA max blocks fitting on GPU: exit arrays={}, ma_arrays={}'.format(nex, nma)) + # reset memory or create new + self.ex_data = GpuDataManager(ex_mem, 0, nex, True) + self.ma_data = GpuDataManager(ma_mem, 0, nma, False) + self.mag_data = GpuDataManager(mag_mem, 0, nma, False) + + def engine_prepare(self): + + super(projectional_pycuda._ProjectionEngine_pycuda, self).engine_prepare() + + for name, s in self.ob.S.items(): + s.gpu = gpuarray.to_gpu(s.data) + for name, s in self.ob_buf.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.ob_nrm.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.pr.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.pr_buf.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.pr_nrm.S.items(): + s.gpu, s.data = mppa(s.data) + + use_tiles = (not self.p.probe_update_cuda_atomics) or (not self.p.object_update_cuda_atomics) + + # Extra object buffer for smoothing kernel + if self.p.obj_smooth_std is not None: + for name, s in self.ob_buf.S.items(): + s.tmp = gpuarray.empty(s.gpu.shape, s.gpu.dtype) + + # TODO : like the serialization this one is needed due to object reformatting + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + prep.addr_gpu = gpuarray.to_gpu(prep.addr) + if use_tiles: + prep.addr2 = np.ascontiguousarray(np.transpose(prep.addr, (2, 3, 0, 1))) + prep.addr2_gpu = gpuarray.to_gpu(prep.addr2) + if self.do_position_refinement: + prep.mangled_addr_gpu = prep.addr_gpu.copy() + + for label, d in self.ptycho.new_data: + dID = d.ID + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + + prep.ma_sum_gpu = gpuarray.to_gpu(prep.ma_sum) + # prepare page-locked mems: + prep.err_fourier_gpu = gpuarray.to_gpu(prep.err_fourier) + prep.err_phot_gpu = gpuarray.to_gpu(prep.err_phot) + prep.err_exit_gpu = gpuarray.to_gpu(prep.err_exit) + if self.do_position_refinement: + prep.error_state_gpu = gpuarray.empty_like(prep.err_fourier_gpu) + ma = self.ma.S[dID].data.astype(np.float32) + prep.ma = cuda.pagelocked_empty(ma.shape, ma.dtype, order="C", mem_flags=4) + prep.ma[:] = ma + ex = self.ex.S[eID].data + prep.ex = cuda.pagelocked_empty(ex.shape, ex.dtype, order="C", mem_flags=4) + prep.ex[:] = ex + mag = prep.mag + prep.mag = cuda.pagelocked_empty(mag.shape, mag.dtype, order="C", mem_flags=4) + prep.mag[:] = mag + + log(4, 'Free memory on device: %.2f GB' % (float(cuda.mem_get_info()[0])/1e9)) + self.ex_data.add_data_block() + self.ma_data.add_data_block() + self.mag_data.add_data_block() + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + # ma_buf = ma_c = np.zeros(FUK.fshape, dtype=np.float32) + self.dID_list = list(self.di.S.keys()) + atomics_probe = self.p.probe_update_cuda_atomics + atomics_object = self.p.object_update_cuda_atomics + use_tiles = (not atomics_object) or (not atomics_probe) + + for it in range(num): + + error = {} + + for inner in range(self.p.overlap_max_iterations): + + change = 0 + + do_update_probe = (self.curiter >= self.p.probe_update_start) + do_update_object = (self.p.update_object_first or (inner > 0) or not do_update_probe) + do_update_fourier = (inner == 0) + + # initialize probe and object buffer to receive an update + if do_update_object: + for oID, ob in self.ob.storages.items(): + cfact = self.ob_cfact[oID] + obn = self.ob_nrm.S[oID] + obb = self.ob_buf.S[oID] + + if self.p.obj_smooth_std is not None: + log(4, 'Smoothing object, cfact is %.2f' % cfact) + smooth_mfs = [self.p.obj_smooth_std, self.p.obj_smooth_std] + # We need a third copy, because we still need ob.gpu for the fourier update + obb.gpu[:] = ob.gpu[:] + self.GSK.convolution(obb.gpu, smooth_mfs, tmp=obb.tmp) + obb.gpu *= np.complex64(cfact) + else: + # obb.gpu[:] = ob.gpu * np.complex64(cfact) + ob.gpu._axpbz(np.complex64(cfact), 0, obb.gpu) + obn.gpu.fill(np.float32(cfact)) + + # First cycle: Fourier + object update + for iblock, dID in enumerate(self.dID_list): + prep = self.diff_info[dID] + + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + POK = kern.POK + + pbound = self.pbound_scan[prep.label] + aux = kern.aux + PROP = kern.PROP + + # get addresses and auxilliary array + addr = prep.addr_gpu + addr2 = prep.addr2_gpu if use_tiles else None + err_fourier = prep.err_fourier_gpu + err_phot = prep.err_phot_gpu + err_exit = prep.err_exit_gpu + ma_sum = prep.ma_sum_gpu + + # local references + ob = self.ob.S[oID].gpu + obn = self.ob_nrm.S[oID].gpu + obb = self.ob_buf.S[oID].gpu + pr = self.pr.S[pID].gpu + + # Schedule ex to device + ev_ex, ex, data_ex = self.ex_data.to_gpu(prep.ex, dID, self.qu_htod) + + # Fourier update. + if do_update_fourier: + self.ex_data.syncback = True + log(4, '----- Fourier update -----', True) + + # Schedule ma & mag to device + ev_ma, ma, data_ma = self.ma_data.to_gpu(prep.ma, dID, self.qu_htod) + ev_mag, mag, data_mag = self.mag_data.to_gpu(prep.mag, dID, self.qu_htod) + + ## compute log-likelihood + if self.p.compute_log_likelihood: + AWK.build_aux_no_ex(aux, addr, ob, pr) + PROP.fw(aux, aux) + # synchronize h2d stream with compute stream + self.queue.wait_for_event(ev_mag) + FUK.log_likelihood(aux, addr, mag, ma, err_phot) + + # synchronize h2d stream with compute stream + self.queue.wait_for_event(ev_ex) + #AWK.build_aux(aux, addr, ob, pr, ex, alpha=self.p.alpha) + AWK.make_aux(aux, addr, ob, pr, ex, c_po=self._c, c_e=1-self._c) + + ## FFT + PROP.fw(aux, aux) + + ## Deviation from measured data + # synchronize h2d stream with compute stream + self.queue.wait_for_event(ev_mag) + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + FUK.fmag_all_update(aux, addr, mag, ma, err_fourier, pbound) + + data_mag.record_done(self.queue, 'compute') + data_ma.record_done(self.queue, 'compute') + + PROP.bw(aux, aux) + ## apply changes + #AWK.build_exit(aux, addr, ob, pr, ex, alpha=self.p.alpha) + AWK.make_exit(aux, addr, ob, pr, ex, c_a=self._b, c_po=self._a, c_e=-(self._a + self._b)) + FUK.exit_error(aux, addr) + FUK.error_reduce(addr, err_exit) + + prestr = '%d Iteration (Overlap) #%02d: ' % (parallel.rank, inner) + + # Update object + if do_update_object: + log(4, prestr + '----- object update -----', True) + addrt = addr if atomics_object else addr2 + self.queue.wait_for_event(ev_ex) + POK.ob_update(addrt, obb, obn, pr, ex, atomics=atomics_object) + + data_ex.record_done(self.queue, 'compute') + if iblock + len(self.ex_data) < len(self.dID_list): + data_ex.from_gpu(self.qu_dtoh) + + # swap direction + if do_update_fourier or do_update_object: + self.dID_list.reverse() + + # wait for compute stream to finish + self.queue.synchronize() + + if do_update_object: + + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + obb = self.ob_buf.S[oID] + self.multigpu.allReduceSum(obb.gpu) + self.multigpu.allReduceSum(obn.gpu) + obb.gpu /= obn.gpu + + self.clip_object(obb.gpu) + ob.gpu[:] = obb.gpu + + # Exit if probe should not yet be updated + if not do_update_probe: + break + self.ex_data.syncback = False + + # Update probe + log(4, prestr + '----- probe update -----', True) + change = self.probe_update() + log(4, prestr + 'change in probe is %.3f' % change, True) + + # stop iteration if probe change is small + if change < self.p.overlap_converge_factor: break + + self.queue.synchronize() + parallel.barrier() + + if self.do_position_refinement and (self.curiter): + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + log(4, "----------- START POS REF -------------") + for dID in self.di.S.keys(): + + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr_gpu + original_addr = prep.original_addr + mangled_addr = prep.mangled_addr_gpu + ma_sum = prep.ma_sum_gpu + err_fourier = prep.err_fourier_gpu + error_state = prep.error_state_gpu + + PCK = kern.PCK + TK = kern.TK + PROP = kern.PROP + + # Make sure our data arrays are on device + ev_ma, ma, data_ma = self.ma_data.to_gpu(prep.ma, dID, self.qu_htod) + ev_mag, mag, data_mag = self.mag_data.to_gpu(prep.mag, dID, self.qu_htod) + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We need to re-calculate the current error + PCK.build_aux(aux, addr, ob, pr) + PROP.fw(aux, aux) + # wait for data to arrive + self.queue.wait_for_event(ev_mag) + + # We need to re-calculate the current error + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, addr, mag, ma, ma_sum) + PCK.error_reduce(addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, addr, mag, ma, err_fourier) + cuda.memcpy_dtod_async(dest=error_state.ptr, + src=err_fourier.ptr, + size=err_fourier.nbytes, stream=self.queue) + + log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + for i in range(PCK.mangler.nshifts): + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + PROP.fw(aux, aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, mangled_addr, mag, ma, ma_sum) + PCK.error_reduce(mangled_addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, mangled_addr, mag, ma, err_fourier) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_fourier) + + data_mag.record_done(self.queue, 'compute') + data_ma.record_done(self.queue, 'compute') + cuda.memcpy_dtod_async(dest=err_fourier.ptr, + src=error_state.ptr, + size=err_fourier.nbytes, stream=self.queue) + if use_tiles: + s1 = prep.addr_gpu.shape[0] * prep.addr_gpu.shape[1] + s2 = prep.addr_gpu.shape[2] * prep.addr_gpu.shape[3] + TK.transpose(prep.addr_gpu.reshape(s1, s2), prep.addr2_gpu.reshape(s2, s1)) + + self.curiter += 1 + self.queue.synchronize() + + for name, s in self.ob.S.items(): + s.data[:] = s.gpu.get() + for name, s in self.pr.S.items(): + s.data[:] = s.gpu.get() + + # costly but needed to sync back with + # for name, s in self.ex.S.items(): + # s.data[:] = s.gpu.get() + for dID, prep in self.diff_info.items(): + err_fourier = prep.err_fourier_gpu.get() + err_phot = prep.err_phot_gpu.get() + err_exit = prep.err_exit_gpu.get() + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error.update(zip(prep.view_IDs, errs)) + + self.error = error + return error + + ## probe update + def probe_update(self, MPI=False): + queue = self.queue + use_atomics = self.p.probe_update_cuda_atomics + # storage for-loop + change_gpu = gpuarray.zeros((1,), dtype=np.float32) + for pID, pr in self.pr.storages.items(): + prn = self.pr_nrm.S[pID] + cfact = self.pr_cfact[pID] + # pr.gpu *= np.float64(cfact) + pr.gpu._axpbz(np.complex64(cfact), 0, pr.gpu, stream=queue) + prn.gpu.fill(np.float32(cfact), stream=self.queue) + + for iblock, dID in enumerate(self.dID_list): + prep = self.diff_info[dID] + + POK = self.kernels[prep.label].POK + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + ev, ex, data_ex = self.ex_data.to_gpu(prep.ex, dID, self.qu_htod) + self.queue.wait_for_event(ev) + + addrt = prep.addr_gpu if use_atomics else prep.addr2_gpu + ev = POK.pr_update(addrt, + self.pr.S[pID].gpu, + self.pr_nrm.S[pID].gpu, + self.ob.S[oID].gpu, + ex, + atomics=use_atomics) + + data_ex.record_done(self.queue, 'compute') + if iblock + len(self.ex_data) < len(self.dID_list): + data_ex.from_gpu(self.qu_dtoh) + + self.dID_list.reverse() + + self.queue.synchronize() + for pID, pr in self.pr.storages.items(): + + buf = self.pr_buf.S[pID] + prn = self.pr_nrm.S[pID] + + self.multigpu.allReduceSum(pr.gpu) + self.multigpu.allReduceSum(prn.gpu) + pr.gpu /= prn.gpu + self.support_constraint(pr) + + ## calculate change on GPU + AUK = self.kernels[list(self.kernels)[0]].AUK + buf.gpu -= pr.gpu + change_gpu += (AUK.norm2(buf.gpu) / AUK.norm2(pr.gpu)) + buf.gpu[:] = pr.gpu + self.multigpu.allReduceSum(change_gpu) + change = change_gpu.get().item() / parallel.size + + return np.sqrt(change) + + def engine_finalize(self): + """ + Clear all GPU data, pinned memory, etc + """ + self.ex_data = None + self.ma_data = None + self.mag_data = None + + super().engine_finalize() + + +@register(name="DM_pycuda") +class DM_pycuda_stream(_ProjectionEngine_pycuda_stream, DMMixin): + """ + A full-fledged Difference Map engine accelerated with pycuda. + + Defaults: + + [name] + default = DM_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _ProjectionEngine_pycuda_stream.__init__(self, ptycho_parent, pars) + DMMixin.__init__(self, self.p.alpha) + ptycho_parent.citations.add_article(**self.article) + + +@register(name="RAAR_pycuda") +class RAAR_pycuda_stream(_ProjectionEngine_pycuda_stream, RAARMixin): + """ + A RAAR engine in accelerated with pycuda. + + Defaults: + + [name] + default = RAAR_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + + _ProjectionEngine_pycuda_stream.__init__(self, ptycho_parent, pars) + RAARMixin.__init__(self, self.p.beta) diff --git a/ptypy/accelerate/cuda_pycuda/engines/stochastic.py b/ptypy/accelerate/cuda_pycuda/engines/stochastic.py new file mode 100644 index 000000000..3ac819725 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/engines/stochastic.py @@ -0,0 +1,529 @@ +# -*- coding: utf-8 -*- +""" +Accelerated stochastic reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import numpy as np +import time +from pycuda import gpuarray +import pycuda.driver as cuda + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines import register +from ptypy.engines.stochastic import EPIEMixin, SDRMixin +from ptypy.accelerate.base.engines.stochastic import _StochasticEngineSerial +from ptypy.accelerate.base import address_manglers +from .. import get_context +from ..kernels import FourierUpdateKernel, AuxiliaryWaveKernel, PoUpdateKernel,\ + PositionCorrectionKernel, PropagationKernel +from ..array_utils import ArrayUtilsKernel, GaussianSmoothingKernel,\ + TransposeKernel, MaxAbs2Kernel, MassCenterKernel, Abs2SumKernel,\ + InterpolatedShiftKernel +from ..mem_utils import make_pagelocked_paired_arrays as mppa +from ..mem_utils import GpuDataManager + +MPI = False + +EX_MA_BLOCKS_RATIO = 2 +MAX_BLOCKS = 99999 # can be used to limit the number of blocks, simulating that they don't fit +#MAX_BLOCKS = 10 # can be used to limit the number of blocks, simulating that they don't fit + +class _StochasticEnginePycuda(_StochasticEngineSerial): + + """ + An accelerated implementation of a stochastic algorithm for ptychography + + Defaults: + + [fft_lib] + default = reikna + type = str + help = Choose the pycuda-compatible FFT module. + doc = One of: + - ``'reikna'`` : the reikna packaga (fast load, competitive compute for streaming) + - ``'cuda'`` : ptypy's cuda wrapper (delayed load, but fastest compute if all data is on GPU) + - ``'skcuda'`` : scikit-cuda (fast load, slowest compute due to additional store/load stages) + choices = 'reikna','cuda','skcuda' + userlevel = 2 + + """ + + def __init__(self, ptycho_parent, pars=None): + """ + Accelerated base engine for stochastic algorithms. + """ + super().__init__(ptycho_parent, pars) + self.ma_data = None + self.mag_data = None + self.ex_data = None + + def engine_initialize(self): + """ + Prepare for reconstruction. + """ + self.context, self.queue = get_context(new_context=True, new_queue=True) + + # initialise kernels for centring probe if required + if self.p.probe_center_tol is not None: + # mass center kernel + self.MCK = MassCenterKernel(queue=self.queue) + # absolute sum kernel + self.A2SK = Abs2SumKernel(dtype=self.pr.dtype, queue=self.queue) + # interpolated shift kernel + self.ISK = InterpolatedShiftKernel(queue=self.queue) + + super().engine_initialize() + self.qu_htod = cuda.Stream() + self.qu_dtoh = cuda.Stream() + + def _setup_kernels(self): + """ + Setup kernels, one for each scan. Derive scans from ptycho class + """ + fpc = 0 + + # get the scans + for label, scan in self.ptycho.model.scans.items(): + + kern = u.Param() + kern.scanmodel = type(scan).__name__ + self.kernels[label] = kern + # TODO: needs to be adapted for broad bandwidth + geo = scan.geometries[0] + + # Get info to shape buffer arrays + fpc = max(scan.max_frames_per_block, fpc) + + # TODO : make this more foolproof + try: + nmodes = scan.p.coherence.num_probe_modes * \ + scan.p.coherence.num_object_modes + except: + nmodes = 1 + + # create buffer arrays + ash = (nmodes,) + tuple(geo.shape) + aux = np.zeros(ash, dtype=np.complex64) + kern.aux = gpuarray.to_gpu(aux) + + # setup kernels, one for each SCAN. + log(4, "Setting up FourierUpdateKernel") + kern.FUK = FourierUpdateKernel(aux, nmodes, queue_thread=self.queue) + kern.FUK.fshape = (1,) + kern.FUK.fshape[1:] + kern.FUK.allocate() + + log(4, "Setting up PoUpdateKernel") + kern.POK = PoUpdateKernel(queue_thread=self.queue) + kern.POK.allocate() + + log(4, "Setting up AuxiliaryWaveKernel") + kern.AWK = AuxiliaryWaveKernel(queue_thread=self.queue) + kern.AWK.allocate() + + log(4, "Setting up ArrayUtilsKernel") + kern.AUK = ArrayUtilsKernel(queue=self.queue) + + #log(4, "Setting up TransposeKernel") + #kern.TK = TransposeKernel(queue=self.queue) + + log(4, "setting up MaxAbs2Kernel") + kern.MAK = MaxAbs2Kernel(queue=self.queue) + + log(4, "Setting up PropagationKernel") + kern.PROP = PropagationKernel(aux, geo.propagator, self.queue, self.p.fft_lib) + kern.PROP.allocate() + kern.resolution = geo.resolution[0] + + if self.do_position_refinement: + log(4, "Setting up position correction") + kern.PCK = PositionCorrectionKernel(aux, nmodes, self.p.position_refinement, geo.resolution, queue_thread=self.queue) + kern.PCK.allocate() + + ex_mem = 0 + mag_mem = 0 + for scan, kern in self.kernels.items(): + if kern.scanmodel in ("GradFull", "BlockGradFull"): + ex_mem = max(kern.aux.nbytes * 1, ex_mem) + else: + ex_mem = max(kern.aux.nbytes * fpc, ex_mem) + mag_mem = max(kern.FUK.gpu.fdev.nbytes * fpc, mag_mem) + ma_mem = mag_mem + mem = cuda.mem_get_info()[0] + blk = ex_mem * EX_MA_BLOCKS_RATIO + ma_mem + mag_mem + fit = int(mem - 200 * 1024 * 1024) // blk # leave 200MB room for safety + if not fit: + log(1,"Cannot fit memory into device, if possible reduce frames per block. Exiting...") + self.context.pop() + self.context.detach() + raise SystemExit("ptypy has been exited.") + + # TODO grow blocks dynamically + nex = min(fit * EX_MA_BLOCKS_RATIO, MAX_BLOCKS) + nma = min(fit, MAX_BLOCKS) + + log(3, 'PyCUDA max blocks fitting on GPU: exit arrays={}, ma_arrays={}'.format(nex, nma)) + # reset memory or create new + self.ex_data = GpuDataManager(ex_mem, 0, nex, True) + self.ma_data = GpuDataManager(ma_mem, 0, nma, False) + self.mag_data = GpuDataManager(mag_mem, 0, nma, False) + log(4, "Kernel setup completed") + + def engine_prepare(self): + super().engine_prepare() + + for name, s in self.ob.S.items(): + s.gpu, s.data = mppa(s.data) + for name, s in self.pr.S.items(): + s.gpu, s.data = mppa(s.data) + + for label, d in self.di.storages.items(): + prep = self.diff_info[d.ID] + prep.addr_gpu = gpuarray.to_gpu(prep.addr) + if self.do_position_refinement: + prep.mangled_addr_gpu = prep.addr_gpu.copy() + + for label, d in self.ptycho.new_data: + dID = d.ID + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + + prep.ma_sum_gpu = gpuarray.to_gpu(prep.ma_sum) + prep.err_fourier_gpu = gpuarray.to_gpu(prep.err_fourier) + prep.err_phot_gpu = gpuarray.to_gpu(prep.err_phot) + prep.err_exit_gpu = gpuarray.to_gpu(prep.err_exit) + if self.do_position_refinement: + prep.error_state_gpu = gpuarray.empty_like(prep.err_fourier_gpu) + prep.obn = gpuarray.to_gpu(prep.obn) + prep.prn = gpuarray.to_gpu(prep.prn) + # prepare page-locked mems: + ma = self.ma.S[dID].data.astype(np.float32) + prep.ma = cuda.pagelocked_empty(ma.shape, ma.dtype, order="C", mem_flags=4) + prep.ma[:] = ma + ex = self.ex.S[eID].data + prep.ex = cuda.pagelocked_empty(ex.shape, ex.dtype, order="C", mem_flags=4) + prep.ex[:] = ex + mag = prep.mag + prep.mag = cuda.pagelocked_empty(mag.shape, mag.dtype, order="C", mem_flags=4) + prep.mag[:] = mag + + self.ex_data.add_data_block() + self.ma_data.add_data_block() + self.mag_data.add_data_block() + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + self.dID_list = list(self.di.S.keys()) + error = {} + for it in range(num): + + for iblock, dID in enumerate(self.dID_list): + + # find probe, object and exit ID in dependence of dID + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + POK = kern.POK + MAK = kern.MAK + PROP = kern.PROP + + # get aux buffer + aux = kern.aux + + # local references + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + + # shuffle view order + vieworder = prep.vieworder + prep.rng.shuffle(vieworder) + + # Schedule ex, ma, mag to device + ev_ex, ex_full, data_ex = self.ex_data.to_gpu(prep.ex, dID, self.qu_htod) + ev_mag, mag_full, data_mag = self.mag_data.to_gpu(prep.mag, dID, self.qu_htod) + ev_ma, ma_full, data_ma = self.ma_data.to_gpu(prep.ma, dID, self.qu_htod) + + # Reference to ex, ma and mag + prep.ex_full = ex_full + prep.mag_full = mag_full + prep.ma_full = ma_full + + ## synchronize h2d stream with compute stream + self.queue.wait_for_event(ev_ex) + + # Iterate through views + for i in vieworder: + + # Get local adress and arrays + addr = prep.addr_gpu[i,None] + ex_from, ex_to = prep.addr_ex[i] + ex = prep.ex_full[ex_from:ex_to] + mag = prep.mag_full[i,None] + ma = prep.ma_full[i,None] + ma_sum = prep.ma_sum_gpu[i,None] + obn = prep.obn + prn = prep.prn + err_phot = prep.err_phot_gpu[i,None] + err_fourier = prep.err_fourier_gpu[i,None] + err_exit = prep.err_exit_gpu[i,None] + + # position update + self.position_update_local(prep,i) + + ## build auxilliary wave + AWK.make_aux(aux, addr, ob, pr, ex, c_po=self._c, c_e=1-self._c) + + ## forward FFT + PROP.fw(aux, aux) + + ## Deviation from measured data + self.queue.wait_for_event(ev_mag) + if self.p.compute_fourier_error: + self.queue.wait_for_event(ev_ma) + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + else: + FUK.fourier_deviation(aux, addr, mag) + self.queue.wait_for_event(ev_ma) + FUK.fmag_update_nopbound(aux, addr, mag, ma) + + ## backward FFT + PROP.bw(aux, aux) + + ## build exit wave + AWK.make_exit(aux, addr, ob, pr, ex, c_a=self._b, c_po=self._a, c_e=-(self._a + self._b)) + if self.p.compute_exit_error: + FUK.exit_error(aux,addr) + FUK.error_reduce(addr, err_exit) + + ## build auxilliary wave (ob * pr product) + AWK.build_aux2_no_ex(aux, addr, ob, pr) + + # object update + POK.pr_norm_local(addr, pr, prn) + POK.ob_update_local(addr, ob, pr, ex, aux, prn, a=self._ob_a, b=self._ob_b) + + # probe update + if self._object_norm_is_global and self._pr_a == 0: + obn_max = gpuarray.empty((1,), dtype=np.float32) + MAK.max_abs2(ob, obn_max) + obn.fill(np.float32(0.), stream=self.queue) + else: + POK.ob_norm_local(addr, ob, obn) + obn_max = gpuarray.max(obn, stream=self.queue) + if self.p.probe_update_start <= self.curiter: + POK.pr_update_local(addr, pr, ob, ex, aux, obn, obn_max, a=self._pr_a, b=self._pr_b) + + ## compute log-likelihood + if self.p.compute_log_likelihood: + PROP.fw(aux, aux) + FUK.log_likelihood2(aux, addr, mag, ma, err_phot) + + data_ex.record_done(self.queue, 'compute') + if iblock + len(self.ex_data) < len(self.dID_list): + data_ex.from_gpu(self.qu_dtoh) + + # swap direction + self.dID_list.reverse() + + # Re-center probe + self.center_probe() + + self.curiter += 1 + self.ex_data.syncback = False + + # finish all the compute + self.queue.synchronize() + + for name, s in self.ob.S.items(): + s.gpu.get_async(stream=self.qu_dtoh, ary=s.data) + for name, s in self.pr.S.items(): + s.gpu.get_async(stream=self.qu_dtoh, ary=s.data) + + for dID, prep in self.diff_info.items(): + err_fourier = prep.err_fourier_gpu.get() + err_phot = prep.err_phot_gpu.get() + err_exit = prep.err_exit_gpu.get() + errs = np.ascontiguousarray(np.vstack([err_fourier, err_phot, err_exit]).T) + error.update(zip(prep.view_IDs, errs)) + + # wait for the async transfers + self.qu_dtoh.synchronize() + + self.error = error + return error + + def position_update_local(self, prep, i): + if not self.do_position_refinement: + return + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + + # Update positions + if do_update_pos: + """ + Iterates through all positions and refines them by a given algorithm. + """ + #log(4, "----------- START POS REF -------------") + pID, oID, eID = prep.poe_IDs + mag = prep.mag_full[i,None] + ma = prep.ma_full[i,None] + ma_sum = prep.ma_sum_gpu[i,None] + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + kern = self.kernels[prep.label] + aux = kern.aux + addr = prep.addr_gpu[i,None] + mangled_addr = prep.mangled_addr_gpu[i,None] + err_fourier = prep.err_fourier_gpu[i,None] + error_state = prep.error_state_gpu[i,None] + + PCK = kern.PCK + PROP = kern.PROP + + # Keep track of object boundaries + max_oby = ob.shape[-2] - aux.shape[-2] - 1 + max_obx = ob.shape[-1] - aux.shape[-1] - 1 + + # We need to re-calculate the current error + PCK.build_aux(aux, addr, ob, pr) + PROP.fw(aux, aux) + #self.queue.wait_for_event(ev_mag) + #self.queue.wait_for_event(ev_ma) + + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, addr, mag, ma, ma_sum) + PCK.error_reduce(addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, addr, mag, ma, err_fourier) + cuda.memcpy_dtod_async(dest=error_state.ptr, + src=err_fourier.ptr, + size=err_fourier.nbytes, stream=self.queue) + + PCK.mangler.setup_shifts(self.curiter, nframes=addr.shape[0]) + + #log(4, 'Position refinement trial: iteration %s' % (self.curiter)) + for i in range(PCK.mangler.nshifts): + PCK.mangler.get_address(i, addr, mangled_addr, max_oby, max_obx) + PCK.build_aux(aux, mangled_addr, ob, pr) + PROP.fw(aux, aux) + if self.p.position_refinement.metric == "fourier": + PCK.fourier_error(aux, mangled_addr, mag, ma, ma_sum) + PCK.error_reduce(mangled_addr, err_fourier) + if self.p.position_refinement.metric == "photon": + PCK.log_likelihood(aux, mangled_addr, mag, ma, err_fourier) + PCK.update_addr_and_error_state(addr, error_state, mangled_addr, err_fourier) + + cuda.memcpy_dtod_async(dest=err_fourier.ptr, + src=error_state.ptr, + size=err_fourier.nbytes, stream=self.queue) + + + def center_probe(self): + if self.p.probe_center_tol is not None: + for name, pr_s in self.pr.storages.items(): + psum_d = self.A2SK.abs2sum(pr_s.gpu) + c1 = self.MCK.mass_center(psum_d).get() + c2 = (np.asarray(pr_s.shape[-2:]) // 2).astype(c1.dtype) + + shift = c2 - c1 + # exit if the current center of mass is within the tolerance + if u.norm(shift) < self.p.probe_center_tol: + break + + # shift the probe + pr_s.gpu = self.ISK.interpolate_shift(pr_s.gpu, shift) + + # shift the object + ob_s = pr_s.views[0].pod.ob_view.storage + ob_s.gpu = self.ISK.interpolate_shift(ob_s.gpu, shift) + + # shift the exit waves + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + pID, oID, eID = prep.poe_IDs + if pID == name: + prep.ex_full = self.ISK.interpolate_shift(prep.ex_full, + shift) + + log(4,'Probe recentered from %s to %s' + % (str(tuple(c1)), str(tuple(c2)))) + + + def engine_finalize(self): + """ + clear GPU data and destroy context. + """ + self.ex_data = None + self.ma_data = None + self.mag_data = None + + for name, s in self.ob.S.items(): + del s.gpu + for name, s in self.pr.S.items(): + del s.gpu + for dID, prep in self.diff_info.items(): + prep.addr = prep.addr_gpu.get() + + # copy data to cpu + # this kills the pagelock memory (otherwise we get segfaults in h5py) + for name, s in self.pr.S.items(): + s.data = np.copy(s.data) + for name, s in self.ob.S.items(): + s.data = np.copy(s.data) + + self.context.detach() + super().engine_finalize() + + +@register() +class EPIE_pycuda(_StochasticEnginePycuda, EPIEMixin): + """ + An accelerated implementation of the EPIE algorithm. + + Defaults: + + [name] + default = EPIE_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _StochasticEnginePycuda.__init__(self, ptycho_parent, pars) + EPIEMixin.__init__(self, self.p.alpha, self.p.beta) + ptycho_parent.citations.add_article(**self.article) + +@register() +class SDR_pycuda(_StochasticEnginePycuda, SDRMixin): + """ + An accelerated implementation of the semi-implicit relaxed Douglas-Rachford (SDR) algorithm. + + Defaults: + + [name] + default = SDR_pycuda + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _StochasticEnginePycuda.__init__(self, ptycho_parent, pars) + SDRMixin.__init__(self, self.p.sigma, self.p.tau, self.p.beta_probe, self.p.beta_object) + ptycho_parent.citations.add_article(**self.article) diff --git a/ptypy/accelerate/cuda_pycuda/fft.py b/ptypy/accelerate/cuda_pycuda/fft.py new file mode 100644 index 000000000..916ed7e54 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/fft.py @@ -0,0 +1,180 @@ +from pycuda.compiler import SourceModule + +import numpy as np + + +class FFT(object): + + def __init__(self, array, queue=None, + inplace=False, + pre_fft=None, + post_fft=None, + symmetric=True, + forward=True): + + self._queue = queue + from pycuda import gpuarray + ## reikna + from reikna import cluda + api = cluda.cuda_api() + thr = api.Thread(queue) + + dims = array.ndim + if dims < 2: + raise AssertionError('Input array must be at least 2-dimensional') + axes = (array.ndim - 2, array.ndim - 1) + + # build the fft + from reikna.fft import fft + ftreikna = fft.FFT(array, axes) + + # attach scaling + from reikna.transformations import mul_param + sc = mul_param(array, np.float32) + ftreikna.parameter.output.connect(sc, sc.input, out=sc.output, scale=sc.param) + iscale = np.sqrt(np.prod(array.shape[-2:])) if symmetric else 1.0 + scale = 1.0 / iscale + + # attach arbitrary multiplication + from reikna import core as rc + from reikna.cluda import functions + # get the IO type + T_io = ftreikna.parameter[0] + T_2d = rc.Type(T_io.dtype, T_io.shape[-2:]) + tr = rc.Transformation( + [ + rc.Parameter('output', rc.Annotation(T_io, 'o')), + rc.Parameter('fac', rc.Annotation(T_2d, 'i')), + rc.Parameter('input', rc.Annotation(T_io, 'i')), + ], + """ + const VSIZE_T x = ${idxs[%d]}; + const VSIZE_T y = ${idxs[%d]}; + + ${output.store_same}(${mul}(${input.load_same}, ${fac.load_idx}(x,y))); + """ % axes, + render_kwds={'mul': functions.mul(T_io.dtype, T_io.dtype)}, + connectors=['input', 'output'] + ) + + if pre_fft is None and post_fft is None: + self.pre_fft = self.post_fft = None + elif pre_fft is not None and post_fft is None: + self.pre_fft = thr.to_device(pre_fft) + self.post_fft = None + ftreikna.parameter.input.connect(tr, tr.output, pre_fft=tr.fac, data=tr.input) + elif pre_fft is None and post_fft is not None: + self.post_fft = thr.to_device(post_fft) + self.pre_fft = None + ftreikna.parameter.out.connect(tr, tr.input, post_fft=tr.fac, result=tr.output) + else: + self.pre_fft = thr.to_device(pre_fft) + self.post_fft = thr.to_device(post_fft) + ftreikna.parameter.input.connect(tr, tr.output, pre_fft=tr.fac, data=tr.input) + ftreikna.parameter.out.connect(tr, tr.input, post_fft=tr.fac, result=tr.output) + + self._ftreikna_raw = ftreikna + self._scale = scale + self._iscale = iscale + self._set_stream(thr) + + @property + def queue(self): + return self._queue + + @queue.setter + def queue(self, stream): + self._queue = stream + from reikna import cluda + api = cluda.cuda_api() + thr = api.Thread(stream) + self._set_stream(thr) + + def _set_stream(self, thr): + self._ftreikna = self._ftreikna_raw.compile(thr) + + if self.pre_fft is None and self.post_fft is None: + self.ft = lambda x, y: self._ftreikna(y, self._scale, x, 0) + self.ift = lambda x, y: self._ftreikna(y, self._iscale, x, 1) + elif self.pre_fft is not None and self.post_fft is None: + self.ft = lambda x, y: self._ftreikna(y, self._scale, self.pre_fft, x, 0) + self.ift = lambda x, y: self._ftreikna(y, self._iscale, self.pre_fft, x, 1) + elif self.pre_fft is None and self.post_fft is not None: + self.ft = lambda x, y: self._ftreikna(y, self.post_fft, self._scale, x, 0) + self.ift = lambda x, y: self._ftreikna(y, self.post_fft, self._iscale, x, 1) + else: + self.ft = lambda x, y: self._ftreikna(y, self.post_fft, self._scale, self.pre_fft, x, 0) + self.ift = lambda x, y: self._ftreikna(y, self.post_fft, self._iscale, self.pre_fft, x, 1) + + + # self.queue = queue + # apply_filter_code = """ + # #include + # #include + # #include + # #include + # using thrust::complex; + # + # extern "C"{ + # __global__ void apply_filter(complex *data, + # const complex *__restrict__ filter, + # float sc, + # int batchsize, + # int size) + # { + # int offset = threadIdx.x + blockIdx.x * blockDim.x; + # int total = batchsize * size; + # if (offset >= total) + # return; + # complex val = data[offset]; + # if (filter) + # { + # val = filter[offset % size] * val; + # } + # val *= sc; + # data[offset] = val; + # } + # } + # """ + # self.apply_filter = SourceModule(apply_filter_code, include_dirs=[np.get_include()], + # no_extern_c=True).get_function("apply_filter") + # sc = isc = 1.0 / np.sqrt(array.shape[-2:]) + # + # plan = cu_fft.Plan(array.shape[-2:], np.complex64, np.complex64, array.shape[0]) + # empty_filter = np.ones((array.shape[-2:])) + 1j * np.ones((array.shape[-2:])) + # if pre_fft is None and post_fft is None: + # pre_fft = gpuarray.to_gpu(empty_filter) + # post_fft = gpuarray.to_gpu(empty_filter) + # elif pre_fft is not None and post_fft is None: + # post_fft = gpuarray.to_gpu(empty_filter) + # elif pre_fft is None and post_fft is not None: + # pre_fft = gpuarray.to_gpu(empty_filter) + # + # batch_size = np.int32(array.shape[0]) + # block = 256 + # total = np.int32(np.prod(array.shape)) + # blocks = int((total + block - 1) // block) + # + # def ft(array, out_array): + # self.apply_filter(array, pre_fft, np.float32(1.0), batch_size, np.int32(np.prod(array.shape[-2:])), + # block=(block, 1, 1), + # grid=(blocks, 1, 1),) + # cu_fft.fft(array, out_array, plan) + # self.apply_filter(out_array, post_fft, np.float32(sc), batch_size, np.int32(np.prod(array.shape[-2:])), + # block=(block, 1, 1), + # grid=(blocks, 1, 1)) + # def ift(array, out_array): + # self.apply_filter(array, pre_fft, np.float32(1.0), batch_size, np.int32(np.prod(array.shape[-2:])), + # block=(block, 1, 1), + # grid=(blocks, 1, 1)) + # print("here") + # cu_fft.fft(array, out_array, plan, True) + # print("here now") + # self.apply_filter(out_array, post_fft, np.float32(isc), batch_size, np.int32(np.prod(array.shape[-2:])), + # block=(block, 1, 1), + # grid=(blocks, 1, 1)) + # print("done") + # + # self.ft = ft + # self.ift = ift + # print("Setup the fft") diff --git a/ptypy/accelerate/cuda_pycuda/kernels.py b/ptypy/accelerate/cuda_pycuda/kernels.py new file mode 100644 index 000000000..fcb1448bb --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/kernels.py @@ -0,0 +1,1270 @@ +import numpy as np +from inspect import getfullargspec +from pycuda import gpuarray +from ptypy.utils.verbose import log, logger +from . import load_kernel +from .array_utils import CropPadKernel +from .array_utils import MaxAbs2Kernel +from ..base import kernels as ab +from ..base.kernels import Adict + +def choose_fft(fft_type, arr_shape): + dims_are_powers_of_two = True + rows = arr_shape[0] + columns = arr_shape[1] + if rows != columns or rows not in [16, 32, 64, 128, 256, 512, 1024, 2048]: + dims_are_powers_of_two = False + if fft_type=='cuda' and not dims_are_powers_of_two: + logger.warning('cufft: array dimensions are not powers of two (16 to 2048) - using Reikna instead') + from ptypy.accelerate.cuda_pycuda.fft import FFT + elif fft_type=='cuda' and dims_are_powers_of_two: + try: + from ptypy.accelerate.cuda_pycuda.cufft import FFT_cuda as FFT + except: + logger.warning('Unable to import cufft version - using Reikna instead') + from ptypy.accelerate.cuda_pycuda.fft import FFT + elif fft_type=='skcuda': + try: + from ptypy.accelerate.cuda_pycuda.cufft import FFT_skcuda as FFT + except: + logger.warning('Unable to import skcuda.fft version - using Reikna instead') + from ptypy.accelerate.cuda_pycuda.fft import FFT + else: + from ptypy.accelerate.cuda_pycuda.fft import FFT + return FFT + +class PropagationKernel: + + def __init__(self, aux, propagator, queue_thread=None, fft='reikna'): + self.aux = aux + self._queue = queue_thread + self.prop_type = propagator.p.propagation + self.fw = None + self.bw = None + self._fft1 = None + self._fft2 = None + self._p = propagator + self._fft_type = fft + + def allocate(self): + + aux = self.aux + FFT = choose_fft(self._fft_type, aux.shape[-2:]) + + if self.prop_type == 'farfield': + + self._do_crop_pad = (self._p.crop_pad != 0).any() + if self._do_crop_pad: + aux_shape = tuple(np.array(aux.shape) + np.append([0],self._p.crop_pad)) + self._tmp = np.zeros(aux_shape, dtype=aux.dtype) + self._CPK = CropPadKernel(queue=self._queue) + else: + self._tmp = aux + + self._fft1 = FFT(self._tmp, self.queue, + pre_fft=self._p.pre_fft, + post_fft=self._p.post_fft, + symmetric=True, + forward=True) + self._fft2 = FFT(self._tmp, self.queue, + pre_fft=self._p.pre_ifft, + post_fft=self._p.post_ifft, + symmetric=True, + forward=False) + if self._do_crop_pad: + self._tmp = gpuarray.to_gpu(self._tmp) + + def _fw(x,y): + if self._do_crop_pad: + self._CPK.crop_pad_2d_simple(self._tmp, x) + self._fft1.ft(self._tmp, self._tmp) + self._CPK.crop_pad_2d_simple(y, self._tmp) + else: + self._fft1.ft(x,y) + + def _bw(x,y): + if self._do_crop_pad: + self._CPK.crop_pad_2d_simple(self._tmp, x) + self._fft2.ift(self._tmp, self._tmp) + self._CPK.crop_pad_2d_simple(y, self._tmp) + else: + self._fft2.ift(x,y) + + self.fw = _fw + self.bw = _bw + + elif self.prop_type == "nearfield": + self._fft1 = FFT(aux, self.queue, + post_fft=self._p.kernel, + symmetric=True, + forward=True) + self._fft2 = FFT(aux, self.queue, + post_fft=self._p.ikernel, + inplace=True, + symmetric=True, + forward=True) + self._fft3 = FFT(aux, self.queue, + symmetric=True, + forward=False) + + def _fw(x,y): + self._fft1.ft(x,y) + self._fft3.ift(y,y) + + def _bw(x,y): + self._fft2.ft(x,y) + self._fft3.ift(y,y) + + self.fw = _fw + self.bw = _bw + else: + logger.warning("Unable to select propagator %s, only nearfield and farfield are supported" %self.prop_type) + + @property + def queue(self): + return self._queue + + @queue.setter + def queue(self, queue): + self._queue = queue + self._fft1.queue = queue + self._fft2.queue = queue + if self.prop_type == "nearfield": + self._fft3.queue = queue + +class FourierSupportKernel: + def __init__(self, support, queue_thread=None, fft='reikna'): + self.support = support + self.queue = queue_thread + self._fft_type = fft + def allocate(self): + FFT = choose_fft(self._fft_type, self.support.shape[-2:]) + + self._fft1 = FFT(self.support, self.queue, + post_fft=self.support, + symmetric=True, + forward=True) + self._fft2 = FFT(self.support, self.queue, + symmetric=True, + forward=False) + def apply_fourier_support(self,x): + self._fft1.ft(x,x) + self._fft2.ift(x,x) + +class RealSupportKernel: + def __init__(self, support): + self.support = support + def allocate(self): + self.support = gpuarray.to_gpu(self.support) + def apply_real_support(self, x): + x *= self.support + +class FourierUpdateKernel(ab.FourierUpdateKernel): + + def __init__(self, aux, nmodes=1, queue_thread=None, accumulate_type='float', math_type='float'): + super(FourierUpdateKernel, self).__init__(aux, nmodes=nmodes) + + if accumulate_type not in ['float', 'double']: + raise ValueError('Only float or double types are supported') + if math_type not in ['float', 'double']: + raise ValueError('Only float or double types are supported') + self.accumulate_type = accumulate_type + self.math_type = math_type + self.queue = queue_thread + self.fmag_all_update_cuda = load_kernel("fmag_all_update", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.fmag_update_nopbound_cuda = None + self.fourier_deviation_cuda = None + self.fourier_error_cuda = load_kernel("fourier_error", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.fourier_error2_cuda = None + self.error_reduce_cuda = load_kernel("error_reduce", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'ACC_TYPE': self.accumulate_type, + 'BDIM_X': 32, + 'BDIM_Y': 32, + }) + self.fourier_update_cuda = None + self.log_likelihood_cuda, self.log_likelihood2_cuda = load_kernel( + ("log_likelihood", "log_likelihood2"), { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }, + "log_likelihood.cu") + self.exit_error_cuda = load_kernel("exit_error", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + + self.gpu = Adict() + self.gpu.fdev = None + self.gpu.ferr = None + + def allocate(self): + self.gpu.fdev = gpuarray.zeros(self.fshape, dtype=np.float32) + self.gpu.ferr = gpuarray.zeros(self.fshape, dtype=np.float32) + + def fourier_error(self, f, addr, fmag, fmask, mask_sum): + fdev = self.gpu.fdev + ferr = self.gpu.ferr + if True: + # version going over all modes in a single thread (faster) + self.fourier_error_cuda(np.int32(self.nmodes), + f, + fmask, + fmag, + fdev, + ferr, + mask_sum, + addr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(fmag.shape[0]), 1, 1), + stream=self.queue) + else: + # version using one thread per mode + shared mem reduction (slower) + if self.fourier_error2_cuda is None: + self.fourier_error2_cuda = load_kernel("fourier_error2") + bx = 16 + by = 16 + bz = int(self.nmodes) + blk = (bx, by, bz) + grd = (int((self.fshape[2] + bx-1) // bx), + int((self.fshape[1] + by-1) // by), + int(self.fshape[0])) + #print('block={}, grid={}, fshape={}'.format(blk, grd, self.fshape)) + self.fourier_error2_cuda(np.int32(self.nmodes), + f, + fmask, + fmag, + fdev, + ferr, + mask_sum, + addr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=blk, + grid=grd, + shared=int(bx*by*bz*4), + stream=self.queue) + + def fourier_deviation(self, f, addr, fmag): + fdev = self.gpu.fdev + if self.fourier_deviation_cuda is None: + self.fourier_deviation_cuda = load_kernel("fourier_deviation",{ + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + bx = 64 + by = 1 + self.fourier_deviation_cuda(np.int32(self.nmodes), + f, + fmag, + fdev, + addr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(bx, by, 1), + grid=(1, int((self.fshape[2] + by - 1)//by), int(fmag.shape[0])), + stream=self.queue) + + + def error_reduce(self, addr, err_sum): + self.error_reduce_cuda(self.gpu.ferr, + err_sum, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(err_sum.shape[0]), 1, 1), + stream=self.queue) + + def fmag_all_update(self, f, addr, fmag, fmask, err_fmag, pbound=0.0): + fdev = self.gpu.fdev + self.fmag_all_update_cuda(f, + fmask, + fmag, + fdev, + err_fmag, + addr, + np.float32(pbound), + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(fmag.shape[0]*self.nmodes), 1, 1), + stream=self.queue) + + def fmag_update_nopbound(self, f, addr, fmag, fmask): + fdev = self.gpu.fdev + bx = 64 + by = 1 + if self.fmag_update_nopbound_cuda is None: + self.fmag_update_nopbound_cuda = load_kernel("fmag_update_nopbound", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.fmag_update_nopbound_cuda(f, + fmask, + fmag, + fdev, + addr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(bx, by, 1), + grid=(1, + int((self.fshape[2] + by - 1) // by), + int(fmag.shape[0]*self.nmodes)), + stream=self.queue) + + # Note: this was a test to join the kernels, but it's > 2x slower! + def fourier_update(self, f, addr, fmag, fmask, mask_sum, err_fmag, pbound=0): + if self.fourier_update_cuda is None: + self.fourier_update_cuda = load_kernel("fourier_update") + fdev = self.gpu.fdev + ferr = self.gpu.ferr + + bx = 16 + by = 16 + bz = int(self.nmodes) + blk = (bx, by, bz) + grd = (int((self.fshape[2] + bx-1) // bx), + int((self.fshape[1] + by-1) // by), + int(self.fshape[0])) + smem = int(bx*by*bz*4) + self.fourier_update_cuda(np.int32(self.nmodes), + f, + fmask, + fmag, + fdev, + ferr, + mask_sum, + addr, + err_fmag, + np.float32(pbound), + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=blk, + grid=grd, + shared=smem, + stream=self.queue) + + def log_likelihood(self, b_aux, addr, mag, mask, err_phot): + ferr = self.gpu.ferr + self.log_likelihood_cuda(np.int32(self.nmodes), + b_aux, + mask, + mag, + addr, + ferr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(mag.shape[0]), 1, 1), + stream=self.queue) + # TODO: we might want to move this call outside of here + self.error_reduce(addr, err_phot) + + def log_likelihood2(self, b_aux, addr, mag, mask, err_phot): + ferr = self.gpu.ferr + bx = 64 + by = 1 + self.log_likelihood2_cuda(np.int32(self.nmodes), + b_aux, + mask, + mag, + addr, + ferr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(bx, by, 1), + grid=(1, int((self.fshape[1] + by - 1) // by), int(mag.shape[0])), + stream=self.queue) + # TODO: we might want to move this call outside of here + self.error_reduce(addr, err_phot) + + def exit_error(self, aux, addr): + sh = addr.shape + maxz = sh[0] + ferr = self.gpu.ferr + self.exit_error_cuda(np.int32(self.nmodes), + aux, + ferr, + addr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(maxz), 1, 1), + stream=self.queue) + + # DEPRECATED? + def execute(self, kernel_name=None, compare=False, sync=False): + + if kernel_name is None: + for kernel in self.kernels: + self.execute(kernel, compare, sync) + else: + self.log("KERNEL " + kernel_name) + meth = getattr(self, kernel_name) + kernel_args = getfullargspec(meth).args[1:] + args = [getattr(self.ocl, a) for a in kernel_args] + meth(*args) + + return self.ocl.err_fmag.get() + + +class AuxiliaryWaveKernel(ab.AuxiliaryWaveKernel): + + def __init__(self, queue_thread=None, math_type = 'float'): + super(AuxiliaryWaveKernel, self).__init__() + # and now initialise the cuda + self.queue = queue_thread + self._ob_shape = None + self._ob_id = None + self.math_type = math_type + if math_type not in ['float', 'double']: + raise ValueError('Only double or float math is supported') + self.make_aux_cuda, self.make_aux2_cuda = load_kernel( + ("make_aux", "make_aux2"), { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }, "make_aux.cu") + self.make_exit_cuda = load_kernel("make_exit", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.build_aux_no_ex_cuda, self.build_aux2_no_ex_cuda = load_kernel( + ("build_aux_no_ex", "build_aux2_no_ex"), { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }, "build_aux_no_ex.cu") + # self.build_exit_alpha_tau_cuda = load_kernel("build_exit_alpha_tau", { + # 'IN_TYPE': 'float', + # 'OUT_TYPE': 'float', + # 'MATH_TYPE': self.math_type + # }) + + # DEPRECATED? + def load(self, aux, ob, pr, ex, addr): + super(AuxiliaryWaveKernel, self).load(aux, ob, pr, ex, addr) + for key, array in self.npy.__dict__.items(): + self.ocl.__dict__[key] = gpuarray.to_gpu(array) + + def make_aux(self, b_aux, addr, ob, pr, ex, c_po=1.0, c_e=0.0): + obr, obc = self._cache_object_shape(ob) + sh = addr.shape + nmodes = sh[1] + maxz = sh[0] + self.make_aux_cuda(b_aux, + ex, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + pr, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + ob, + obr, obc, + addr, + np.float32(c_po) if ex.dtype == np.complex64 else np.float64(c_po), + np.float32(c_e) if ex.dtype == np.complex64 else np.float64(c_e), + block=(32, 32, 1), grid=(int(maxz * nmodes), 1, 1), stream=self.queue) + + def make_aux2(self, b_aux, addr, ob, pr, ex, c_po=1.0, c_e=0.0): + obr, obc = self._cache_object_shape(ob) + sh = addr.shape + nmodes = sh[1] + maxz = sh[0] + bx = 64 + by = 1 + self.make_aux2_cuda(b_aux, + ex, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + pr, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + ob, + obr, obc, + addr, + np.float32(c_po) if ex.dtype == np.complex64 else np.float64(c_po), + np.float32(c_e) if ex.dtype == np.complex64 else np.float64(c_e), + block=(bx, by, 1), + grid=( + 1, + int((ex.shape[1] + by - 1)//by), + int(maxz * nmodes)), + stream=self.queue) + + + def make_exit(self, b_aux, addr, ob, pr, ex, c_a=1.0, c_po=0.0, c_e=-1.0): + obr, obc = self._cache_object_shape(ob) + sh = addr.shape + nmodes = sh[1] + maxz = sh[0] + self.make_exit_cuda(b_aux, + ex, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + pr, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + ob, + obr, obc, + addr, + np.float32(c_a) if ex.dtype == np.complex64 else np.float64(c_a), + np.float32(c_po) if ex.dtype == np.complex64 else np.float64(c_po), + np.float32(c_e) if ex.dtype == np.complex64 else np.float64(c_e), + block=(32, 32, 1), grid=(int(maxz * nmodes), 1, 1), stream=self.queue) + + def build_aux2(self, b_aux, addr, ob, pr, ex, alpha=1.0): + # DM only, legacy. also make_aux2 does no exit in the parent + self.make_aux2(b_aux, addr, ob, pr, ex, 1.+alpha, -alpha) + + """ + def build_exit_alpha_tau(self, b_aux, addr, ob, pr, ex, alpha=1, tau=1): + obr, obc = self._cache_object_shape(ob) + sh = addr.shape + nmodes = sh[1] + maxz = sh[0] + bx = 64 + by = 1 + self.build_exit_alpha_tau_cuda(b_aux, + ex, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + pr, + np.int32(ex.shape[1]), np.int32(ex.shape[2]), + ob, + obr, obc, + addr, + np.float32(alpha), np.float32(tau), + block=(bx, by, 1), + grid=(1, int((ex.shape[1] + by - 1) // by), int(maxz * nmodes)), + stream=self.queue) + """ + def build_aux_no_ex(self, b_aux, addr, ob, pr, fac=1.0, add=False): + obr, obc = self._cache_object_shape(ob) + sh = addr.shape + nmodes = sh[1] + maxz = sh[0] + self.build_aux_no_ex_cuda(b_aux, + np.int32(b_aux.shape[-2]), + np.int32(b_aux.shape[-1]), + pr, + np.int32(pr.shape[-2]), + np.int32(pr.shape[-1]), + ob, + obr, obc, + addr, + np.float32(fac) if pr.dtype == np.complex64 else np.float64(fac), + np.int32(add), + block=(32, 32, 1), + grid=(int(maxz * nmodes), 1, 1), + stream=self.queue) + + + def build_aux2_no_ex(self, b_aux, addr, ob, pr, fac=1.0, add=False): + obr, obc = self._cache_object_shape(ob) + sh = addr.shape + nmodes = sh[1] + maxz = sh[0] + bx = 64 + by = 1 + self.build_aux2_no_ex_cuda(b_aux, + np.int32(b_aux.shape[-2]), + np.int32(b_aux.shape[-1]), + pr, + np.int32(pr.shape[-2]), + np.int32(pr.shape[-1]), + ob, + obr, obc, + addr, + np.float32(fac) if pr.dtype == np.complex64 else np.float64(fac), + np.int32(add), + block=(bx, by, 1), + grid=(1, int((b_aux.shape[-2] + by - 1)//by), int(maxz * nmodes)), + stream=self.queue) + + + def _cache_object_shape(self, ob): + oid = id(ob) + + if not oid == self._ob_id: + self._ob_id = oid + self._ob_shape = (np.int32(ob.shape[-2]), np.int32(ob.shape[-1])) + + return self._ob_shape + + +class GradientDescentKernel(ab.GradientDescentKernel): + + def __init__(self, aux, nmodes=1, queue=None, accumulate_type = 'double', math_type='float'): + super().__init__(aux, nmodes) + self.queue = queue + self.accumulate_type = accumulate_type + self.math_type = math_type + if (accumulate_type not in ['double', 'float']) or (math_type not in ['double', 'float']): + raise ValueError("accumulate and math types must be double for float") + + self.gpu = Adict() + self.gpu.LLden = None + self.gpu.LLerr = None + self.gpu.Imodel = None + + subs = { + 'IN_TYPE': 'float' if self.ftype == np.float32 else 'double', + 'OUT_TYPE': 'float' if self.ftype == np.float32 else 'double', + 'ACC_TYPE': self.accumulate_type, + 'MATH_TYPE': self.math_type + } + self.make_model_cuda = load_kernel('make_model', subs) + self.make_a012_cuda = load_kernel('make_a012', subs) + self.error_reduce_cuda = load_kernel('error_reduce', { + **subs, + 'OUT_TYPE': 'float' if self.ftype == np.float32 else 'double', + 'BDIM_X': 32, + 'BDIM_Y': 32 + }) + self.fill_b_cuda, self.fill_b_reduce_cuda = load_kernel( + ('fill_b', 'fill_b_reduce'), + { + **subs, + 'BDIM_X': 1024, + 'OUT_TYPE': 'float' if self.ftype == np.float32 else 'double' + }, + file="fill_b.cu") + self.main_cuda = load_kernel('gd_main', subs) + self.floating_intensity_cuda_step1, self.floating_intensity_cuda_step2 = \ + load_kernel(('step1', 'step2'), subs,'intens_renorm.cu') + + def allocate(self): + self.gpu.LLden = gpuarray.zeros(self.fshape, dtype=self.ftype) + self.gpu.LLerr = gpuarray.zeros(self.fshape, dtype=self.ftype) + self.gpu.Imodel = gpuarray.zeros(self.fshape, dtype=self.ftype) + tmp = np.ones((self.fshape[0],), dtype=self.ftype) + self.gpu.fic_tmp = gpuarray.to_gpu(tmp) + + # temporary array for the reduction in fill_b + sh = (3, int((np.prod(self.fshape)*self.nmodes + 1023) // 1024)) + self.gpu.Btmp = gpuarray.zeros(sh, dtype=np.float64 if self.accumulate_type == 'double' else np.float32) + + def make_model(self, b_aux, addr): + # reference shape + sh = self.fshape + + # batch buffers + Imodel = self.gpu.Imodel + aux = b_aux + + # dimensions / grid + z = np.int32(sh[0]) + y = np.int32(self.nmodes) + x = np.int32(sh[1] * sh[2]) + bx = 1024 + self.make_model_cuda(aux, Imodel, z, y, x, + block=(bx, 1, 1), + grid=(int((x + bx - 1) // bx), 1, int(z)), + stream=self.queue) + + def make_a012(self, b_f, b_a, b_b, addr, I, fic): + # reference shape (= GPU global dims) + sh = I.shape + + # stopper + maxz = I.shape[0] + + A0 = self.gpu.Imodel + A1 = self.gpu.LLerr + A2 = self.gpu.LLden + + z = np.int32(sh[0]) + maxz = np.int32(maxz) + y = np.int32(self.nmodes) + x = np.int32(sh[1]*sh[2]) + bx = 1024 + self.make_a012_cuda(b_f, b_a, b_b, I, fic, + A0, A1, A2, z, y, x, maxz, + block=(bx, 1, 1), + grid=(int((x + bx - 1) // bx), 1, int(z)), + stream=self.queue) + + def fill_b(self, addr, Brenorm, w, B): + # stopper + maxz = w.shape[0] + + A0 = self.gpu.Imodel + A1 = self.gpu.LLerr + A2 = self.gpu.LLden + + sz = np.int32(np.prod(w.shape)) + blks = int((sz + 1023) // 1024) + # print('blocks={}, Btmp={}, fshape={}, wshape={}, modes={}'.format(blks, self.gpu.Btmp.shape, self.fshape, w.shape, self.nmodes)) + assert self.gpu.Btmp.shape[1] >= blks + # 2-stage reduction - even if 1 block, as we have a += in second kernel + self.fill_b_cuda(A0, A1, A2, w, + np.float32(Brenorm) if self.ftype == np.float32 else np.float64( + Brenorm), + sz, self.gpu.Btmp, + block=(1024, 1, 1), + grid=(blks, 1, 1), + stream=self.queue) + self.fill_b_reduce_cuda(self.gpu.Btmp, B, np.int32(blks), + block=(1024, 1, 1), + grid=(1, 1, 1), + stream=self.queue) + + def error_reduce(self, addr, err_sum): + # reference shape (= GPU global dims) + sh = err_sum.shape + + # stopper + maxz = err_sum.shape[0] + + # batch buffers + ferr = self.gpu.LLerr + + # print('maxz={}, ferr={}'.format(maxz, ferr.shape)) + assert(maxz <= np.prod(ferr.shape[:-2])) + + # Reduces the LL error along the last 2 dimensions.fd + self.error_reduce_cuda(ferr, err_sum, + np.int32(ferr.shape[-2]), + np.int32(ferr.shape[-1]), + block=(32, 32, 1), + grid=(int(maxz), 1, 1), + stream=self.queue) + + def floating_intensity(self, addr, w, I, fic): + + # reference shape (= GPU global dims) + sh = I.shape + + # stopper + maxz = I.shape[0] + + # internal buffers + num = self.gpu.LLerr + den = self.gpu.LLden + Imodel = self.gpu.Imodel + fic_tmp = self.gpu.fic_tmp + + ## math ## + xall = np.int32(maxz * sh[1] * sh[2]) + bx = 1024 + + self.floating_intensity_cuda_step1(Imodel, I, w, num, den, + xall, + block=(bx, 1, 1), + grid=(int((xall + bx - 1) // bx), 1, 1), + stream=self.queue) + + self.error_reduce_cuda(num, fic, + np.int32(num.shape[-2]), + np.int32(num.shape[-1]), + block=(32, 32, 1), + grid=(int(maxz), 1, 1), + stream=self.queue) + + self.error_reduce_cuda(den, fic_tmp, + np.int32(den.shape[-2]), + np.int32(den.shape[-1]), + block=(32, 32, 1), + grid=(int(maxz), 1, 1), + stream=self.queue) + + self.floating_intensity_cuda_step2(fic_tmp, fic, Imodel, + np.int32(Imodel.shape[-2]), + np.int32(Imodel.shape[-1]), + block=(32, 32, 1), + grid=(1, 1, int(maxz)), + stream=self.queue) + + + def main(self, b_aux, addr, w, I): + nmodes = self.nmodes + # stopper + maxz = I.shape[0] + + # batch buffers + err = self.gpu.LLerr + Imodel = self.gpu.Imodel + aux = b_aux + + # write-to shape (= GPU global dims) + ish = aux.shape + + x = np.int32(ish[1] * ish[2]) + y = np.int32(nmodes) + z = np.int32(maxz) + bx = 1024 + + #print(Imodel.dtype, I.dtype, w.dtype, err.dtype, aux.dtype, z, y, x) + self.main_cuda(Imodel, I, w, err, aux, + z, y, x, + block=(bx, 1, 1), + grid=(int((x + bx - 1) // bx), 1, int(z)), + stream=self.queue) + + +class PoUpdateKernel(ab.PoUpdateKernel): + + def __init__(self, queue_thread=None, + math_type='float', accumulator_type='float'): + super(PoUpdateKernel, self).__init__() + # and now initialise the cuda + if math_type not in ['double', 'float']: + raise ValueError('only float and double are supported for math_type') + if accumulator_type not in ['double', 'float']: + raise ValueError('only float and double are supported for accumulator_type') + self.math_type = math_type + self.accumulator_type = accumulator_type + self.queue = queue_thread + self.norm = None + self.MAK = MaxAbs2Kernel(self.queue) + self.ob_update_cuda = load_kernel("ob_update", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.ob_update2_cuda = None # load_kernel("ob_update2") + self.pr_update_cuda = load_kernel("pr_update", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.pr_update2_cuda = None + self.ob_update_ML_cuda = load_kernel("ob_update_ML", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.ob_update2_ML_cuda = None + self.pr_update_ML_cuda = load_kernel("pr_update_ML", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.pr_update2_ML_cuda = None + self.ob_update_local_cuda = load_kernel("ob_update_local", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + self.pr_update_local_cuda = load_kernel("pr_update_local", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + self.ob_norm_local_cuda = load_kernel("ob_norm_local", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + self.pr_norm_local_cuda = load_kernel("pr_norm_local", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + + + def ob_update(self, addr, ob, obn, pr, ex, atomics=True): + obsh = [np.int32(ax) for ax in ob.shape] + prsh = [np.int32(ax) for ax in pr.shape] + if obn.dtype != np.float32: + raise ValueError("Denominator must be float32 in current implementation") + + if atomics: + if addr.shape[3] != 3 or addr.shape[2] != 5: + raise ValueError('Address not in required shape for atomics ob_update') + num_pods = np.int32(addr.shape[0] * addr.shape[1]) + self.ob_update_cuda(ex, num_pods, prsh[1], prsh[2], + pr, prsh[0], prsh[1], prsh[2], + ob, obsh[0], obsh[1], obsh[2], + addr, + obn, + block=(32, 32, 1), grid=(int(num_pods), 1, 1), stream=self.queue) + else: + if addr.shape[0] != 5 or addr.shape[1] != 3: + raise ValueError('Address not in required shape for tiled ob_update') + num_pods = np.int32(addr.shape[2] * addr.shape[3]) + if not self.ob_update2_cuda: + self.ob_update2_cuda = load_kernel("ob_update2", { + "NUM_MODES": obsh[0], + "BDIM_X": 16, + "BDIM_Y": 16, + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + + grid = [int((x+15)//16) for x in ob.shape[-2:]] + grid = (grid[1], grid[0], int(1)) + self.ob_update2_cuda(prsh[-1], obsh[0], num_pods, obsh[-2], obsh[-1], + prsh[0], + np.int32(ex.shape[0]), + np.int32(ex.shape[1]), + np.int32(ex.shape[2]), + ob, obn, pr, ex, addr, + block=(16, 16, 1), grid=grid, stream=self.queue) + + def pr_update(self, addr, pr, prn, ob, ex, atomics=True): + obsh = [np.int32(ax) for ax in ob.shape] + prsh = [np.int32(ax) for ax in pr.shape] + if prn.dtype != np.float32: + raise ValueError("Denominator must be float32 in current implementation") + if atomics: + if addr.shape[3] != 3 or addr.shape[2] != 5: + raise ValueError('Address not in required shape for atomics pr_update') + + num_pods = np.int32(addr.shape[0] * addr.shape[1]) + self.pr_update_cuda(ex, num_pods, prsh[1], prsh[2], + pr, prsh[0], prsh[1], prsh[2], + ob, obsh[0], obsh[1], obsh[2], + addr, + prn, + block=(32, 32, 1), grid=(int(num_pods), 1, 1), stream=self.queue) + else: + if addr.shape[0] != 5 or addr.shape[1] != 3: + raise ValueError('Address not in required shape for tiled pr_update') + + num_pods = np.int32(addr.shape[2] * addr.shape[3]) + if not self.pr_update2_cuda: + self.pr_update2_cuda = load_kernel("pr_update2", { + "NUM_MODES": prsh[0], + "BDIM_X": 16, + "BDIM_Y": 16, + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + + grid = [int((x+15)//16) for x in pr.shape[-2:]] + grid = (grid[0], grid[1], int(1)) + self.pr_update2_cuda(prsh[-1], obsh[-2], obsh[-1], + prsh[0], obsh[0], num_pods, + pr, prn, ob, ex, addr, + block=(16, 16, 1), grid=grid, stream=self.queue) + + def ob_update_ML(self, addr, ob, pr, ex, fac=2.0, atomics=True): + obsh = [np.int32(ax) for ax in ob.shape] + prsh = [np.int32(ax) for ax in pr.shape] + exsh = [np.int32(ax) for ax in ex.shape] + + if atomics: + if addr.shape[3] != 3 or addr.shape[2] != 5: + raise ValueError('Address not in required shape for tiled ob_update') + + num_pods = np.int32(addr.shape[0] * addr.shape[1]) + self.ob_update_ML_cuda(ex, num_pods, exsh[1], exsh[2], + pr, prsh[0], prsh[1], prsh[2], + ob, obsh[0], obsh[1], obsh[2], + addr, + np.float32(fac) if ex.dtype == np.complex64 else np.float64(fac), + block=(32, 32, 1), grid=(int(num_pods), 1, 1), stream=self.queue) + else: + if addr.shape[0] != 5 or addr.shape[1] != 3: + raise ValueError('Address not in required shape for tiled ob_update') + + num_pods = np.int32(addr.shape[2] * addr.shape[3]) + if not self.ob_update2_ML_cuda: + self.ob_update2_ML_cuda = load_kernel("ob_update2_ML", { + "NUM_MODES": obsh[0], + "BDIM_X": 16, + "BDIM_Y": 16, + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + grid = [int((x+15)//16) for x in ob.shape[-2:]] + grid = (grid[1], grid[0], int(1)) + self.ob_update2_ML_cuda(prsh[-1], obsh[0], num_pods, obsh[-2], obsh[-1], + prsh[0], + np.int32(ex.shape[0]), + np.int32(ex.shape[1]), + np.int32(ex.shape[2]), + ob, pr, ex, addr, + np.float32(fac) if ex.dtype == np.complex64 else np.float64(fac), + block=(16, 16, 1), grid=grid, stream=self.queue) + + def pr_update_ML(self, addr, pr, ob, ex, fac=2.0, atomics=False): + obsh = [np.int32(ax) for ax in ob.shape] + prsh = [np.int32(ax) for ax in pr.shape] + if atomics: + if addr.shape[3] != 3 or addr.shape[2] != 5: + raise ValueError('Address not in required shape for tiled pr_update') + num_pods = np.int32(addr.shape[0] * addr.shape[1]) + self.pr_update_ML_cuda(ex, num_pods, prsh[1], prsh[2], + pr, prsh[0], prsh[1], prsh[2], + ob, obsh[0], obsh[1], obsh[2], + addr, + np.float32(fac) if ex.dtype == np.complex64 else np.float64(fac), + block=(32, 32, 1), grid=(int(num_pods), 1, 1), stream=self.queue) + else: + if addr.shape[0] != 5 or addr.shape[1] != 3: + raise ValueError('Address not in required shape for tiled pr_update') + num_pods = np.int32(addr.shape[2] * addr.shape[3]) + if not self.pr_update2_ML_cuda: + self.pr_update2_ML_cuda = load_kernel("pr_update2_ML", { + "NUM_MODES": prsh[0], + "BDIM_X": 16, + "BDIM_Y": 16, + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type, + 'ACC_TYPE': self.accumulator_type + }) + + grid = [int((x+15)//16) for x in pr.shape[-2:]] + grid = (grid[0], grid[1], int(1)) + self.pr_update2_ML_cuda(prsh[-1], obsh[-2], obsh[-1], + prsh[0], obsh[0], num_pods, + pr, ob, ex, addr, + np.float32(fac) if ex.dtype == np.complex64 else np.float64(fac), + block=(16, 16, 1), grid=grid, stream=self.queue) + + + def ob_update_local(self, addr, ob, pr, ex, aux, prn, a=0., b=1.): + prn_max = gpuarray.max(prn, stream=self.queue) + obsh = [np.int32(ax) for ax in ob.shape] + prsh = [np.int32(ax) for ax in pr.shape] + exsh = [np.int32(ax) for ax in ex.shape] + # atomics version only + if addr.shape[3] != 3 or addr.shape[2] != 5: + raise ValueError('Address not in required shape for tiled ob_update') + num_pods = np.int32(addr.shape[0] * addr.shape[1]) + bx = 64 + by = 1 + self.ob_update_local_cuda(ex, aux, + exsh[0], exsh[1], exsh[2], + pr, + prsh[0], prsh[1], prsh[2], + prn, + ob, + obsh[0], obsh[1], obsh[2], + addr, + prn_max, + np.float32(a), + np.float32(b), + block=(bx, by, 1), + grid=(1, int((exsh[1] + by - 1)//by), int(num_pods)), + stream=self.queue) + + def pr_update_local(self, addr, pr, ob, ex, aux, obn, obn_max, a=0., b=1.): + obsh = [np.int32(ax) for ax in ob.shape] + prsh = [np.int32(ax) for ax in pr.shape] + exsh = [np.int32(ax) for ax in ex.shape] + # atomics version only + if addr.shape[3] != 3 or addr.shape[2] != 5: + raise ValueError('Address not in required shape for tiled pr_update') + num_pods = np.int32(addr.shape[0] * addr.shape[1]) + bx = 64 + by = 1 + self.pr_update_local_cuda(ex, aux, + exsh[0], exsh[1], exsh[2], + pr, + prsh[0], prsh[1], prsh[2], + obn, + ob, + obsh[0], obsh[1], obsh[2], + addr, + obn_max, + np.float32(a), + np.float32(b), + block=(bx, by, 1), + grid=(1, int((exsh[1] + by - 1) // by), int(num_pods)), + stream=self.queue) + + def ob_norm_local(self, addr, ob, obn): + obsh = [np.int32(ax) for ax in ob.shape] + obnsh = [np.int32(ax) for ax in obn.shape] + bx = 64 + by = 1 + self.ob_norm_local_cuda(obn, + obnsh[0], obnsh[1], obnsh[2], + ob, + obsh[0], obsh[1], obsh[2], + addr, + block=(bx, by, 1), + grid=(1, int((obnsh[1] + by - 1)//by), int(obnsh[0])), + stream=self.queue) + + def pr_norm_local(self, addr, pr, prn): + prsh = [np.int32(ax) for ax in pr.shape] + prnsh = [np.int32(ax) for ax in prn.shape] + bx = 64 + by = 1 + self.pr_norm_local_cuda(prn, + prnsh[0], prnsh[1], prnsh[2], + pr, + prsh[0], prsh[1], prsh[2], + addr, + block=(bx, by, 1), + grid=(1, int((prnsh[1] + by - 1)//by), int(prnsh[0])), + stream=self.queue) + + +class PositionCorrectionKernel(ab.PositionCorrectionKernel): + from ptypy.accelerate.cuda_pycuda import address_manglers + + # these are used by the self.setup method - replacing them with the GPU implementation + MANGLERS = { + 'Annealing': address_manglers.RandomIntMangler, + 'GridSearch': address_manglers.GridSearchMangler + } + + def __init__(self, *args, queue_thread=None, math_type='float', accumulate_type='float', **kwargs): + super(PositionCorrectionKernel, self).__init__(*args, **kwargs) + # make sure we set the right stream in the mangler + self.mangler.queue = queue_thread + if math_type not in ['float', 'double']: + raise ValueError('Only float or double math is supported') + if accumulate_type not in ['float', 'double']: + raise ValueError('Only float or double math is supported') + + # add kernels + self.math_type = math_type + self.accumulate_type = accumulate_type + self.queue = queue_thread + self._ob_shape = None + self._ob_id = None + self.fourier_error_cuda = load_kernel("fourier_error",{ + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.error_reduce_cuda = load_kernel("error_reduce", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'BDIM_X': 32, + 'BDIM_Y': 32, + 'ACC_TYPE': self.accumulate_type + }) + self.log_likelihood_cuda, self.log_likelihood_ml_cuda = load_kernel( + ("log_likelihood", "log_likelihood_ml"), { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }, "log_likelihood.cu") + self.build_aux_pc_cuda = load_kernel("build_aux_position_correction", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float', + 'MATH_TYPE': self.math_type + }) + self.update_addr_and_error_state_cuda = load_kernel("update_addr_error_state", { + 'IN_TYPE': 'float', + 'OUT_TYPE': 'float' + }) + + self.gpu = Adict() + self.gpu.fdev = None + self.gpu.ferr = None + + def allocate(self): + self.gpu.fdev = gpuarray.zeros(self.fshape, dtype=np.float32) + self.gpu.ferr = gpuarray.zeros(self.fshape, dtype=np.float32) + + def build_aux(self, b_aux, addr, ob, pr): + obr, obc = self._cache_object_shape(ob) + sh = addr.shape + nmodes = sh[1] + maxz = sh[0] + self.build_aux_pc_cuda(b_aux, + pr, + np.int32(pr.shape[1]), np.int32(pr.shape[2]), + ob, + obr, obc, + addr, + block=(32, 32, 1), grid=(int(maxz * nmodes), 1, 1), stream=self.queue) + + def fourier_error(self, f, addr, fmag, fmask, mask_sum): + fdev = self.gpu.fdev + ferr = self.gpu.ferr + self.fourier_error_cuda(np.int32(self.nmodes), + f, + fmask, + fmag, + fdev, + ferr, + mask_sum, + addr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(fmag.shape[0]), 1, 1), + stream=self.queue) + + def error_reduce(self, addr, err_fmag): + #import sys + # float_size = sys.getsizeof(np.float32(4)) + # shared_memory_size =int(2 * 32 * 32 *float_size) # this doesn't work even though its the same... + # shared_memory_size = int(49152) + self.error_reduce_cuda(self.gpu.ferr, + err_fmag, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(err_fmag.shape[0]), 1, 1), + stream=self.queue) + + def log_likelihood(self, b_aux, addr, mag, mask, err_phot): + ferr = self.gpu.ferr + self.log_likelihood_cuda(np.int32(self.nmodes), + b_aux, + mask, + mag, + addr, + ferr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(mag.shape[0]), 1, 1), + stream=self.queue) + # TODO: we might want to move this call outside of here + self.error_reduce(addr, err_phot) + + def log_likelihood_ml(self, b_aux, addr, I, weights, err_phot): + ferr = self.gpu.ferr + self.log_likelihood_ml_cuda(np.int32(self.nmodes), + b_aux, + weights, + I, + addr, + ferr, + np.int32(self.fshape[1]), + np.int32(self.fshape[2]), + block=(32, 32, 1), + grid=(int(I.shape[0]), 1, 1), + stream=self.queue) + # TODO: we might want to move this call outside of here + self.error_reduce(addr, err_phot) + + def update_addr_and_error_state(self, addr, error_state, mangled_addr, err_sum): + # assume all data is on GPU! + self.update_addr_and_error_state_cuda(addr, mangled_addr, error_state, err_sum, + np.int32(addr.shape[1]), + block=(32, 2, 1), + grid=(1, int((err_sum.shape[0] + 1) // 2), 1), + stream=self.queue) + + def _cache_object_shape(self, ob): + oid = id(ob) + + if not oid == self._ob_id: + self._ob_id = oid + self._ob_shape = (np.int32(ob.shape[-2]), np.int32(ob.shape[-1])) + + return self._ob_shape + diff --git a/ptypy/accelerate/cuda_pycuda/mem_utils.py b/ptypy/accelerate/cuda_pycuda/mem_utils.py new file mode 100644 index 000000000..88c993fa6 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/mem_utils.py @@ -0,0 +1,426 @@ +import numpy as np +from pycuda import gpuarray +import pycuda.driver as cuda +from pycuda.tools import DeviceMemoryPool +from collections import deque + +def make_pagelocked_paired_arrays(ar, flags=0): + mem = cuda.pagelocked_empty(ar.shape, ar.dtype, order="C", mem_flags=flags) + mem[:] = ar + return gpuarray.to_gpu(mem), mem + + +class GpuData: + """ + Manages one block of GPU data with corresponding CPU data. + Keeps track of which cpu array is currently on GPU by its id, + and transfers if it's not already there. + + To be used for the exit wave, ma, and mag arrays. + Note: Allocator should be pooled for best performance + """ + + def __init__(self, nbytes, syncback=False): + """ + New instance of GpuData. Allocates the GPU-side array. + + :param nbytes: Number of bytes held by this instance. + :param syncback: Should the data be synced back to CPU any time it's swapped out + """ + + self.gpu = None + self.gpuraw = cuda.mem_alloc(nbytes) + self.nbytes = nbytes + self.nbytes_buffer = nbytes + self.gpuId = None + self.cpu = None + self.syncback = syncback + self.ev_done = None + + def _allocator(self, nbytes): + if nbytes > self.nbytes: + raise Exception('requested more bytes than maximum given before: {} vs {}'.format(nbytes, self.nbytes)) + return self.gpuraw + + def record_done(self, stream): + self.ev_done = cuda.Event() + self.ev_done.record(stream) + + def to_gpu(self, cpu, id, stream): + """ + Transfer cpu array to GPU on stream (async), keeping track of its id + """ + if self.gpuId != id: + if self.syncback: + self.from_gpu(stream) + self.gpuId = id + self.cpu = cpu + if self.ev_done is not None: + self.ev_done.synchronize() + self.gpu = gpuarray.to_gpu_async(cpu, allocator=self._allocator, stream=stream) + return self.gpu + + def from_gpu(self, stream): + """ + Transfer data back to CPU, into same data handle it was copied from + before. + """ + if self.cpu is not None and self.gpuId is not None and self.gpu is not None: + if self.ev_done is not None: + stream.wait_for_event(self.ev_done) + self.gpu.get_async(stream, self.cpu) + self.ev_done = cuda.Event() + self.ev_done.record(stream) + + def resize(self, nbytes): + """ + Resize the size of the underlying buffer, to allow re-use in different contexts. + Note that memory will only be freed/reallocated if the new number of bytes are + either larger than before, or if they are less than 90% of the original size - + otherwise it reuses the existing buffer + """ + if nbytes > self.nbytes_buffer or nbytes < self.nbytes_buffer * .9: + self.nbytes_buffer = nbytes + self.gpuraw.free() + self.gpuraw = cuda.mem_alloc(nbytes) + self.nbytes = nbytes + self.reset() + + def reset(self): + """ + Resets handles of cpu references and ids, so that all data will be transfered + again even if IDs match. + """ + self.gpuId = None + self.cpu = None + self.ev_done = None + + def free(self): + """ + Free the underlying buffer on GPU - this object should not be used afterwards + """ + self.gpuraw.free() + self.gpuraw = None + + +class GpuData2(GpuData): + """ + Manages one block of GPU data with corresponding CPU data. + Keeps track of which cpu array is currently on GPU by its id, + and transfers if it's not already there. + + To be used for the exit wave, ma, and mag arrays. + Note: Allocator should be pooled for best performance + """ + + def __init__(self, nbytes, syncback=False): + """ + New instance of GpuData. Allocates the GPU-side array. + + :param nbytes: Number of bytes held by this instance. + :param syncback: Should the data be synced back to CPU any time it's swapped out + """ + self.done_what = None + super().__init__(nbytes, syncback) + + def record_done(self, stream, what): + assert what in ['dtoh','htod','compute'] + self.ev_done = cuda.Event() + self.ev_done.record(stream) + self.done_what = what + + def to_gpu(self, cpu, ident, stream): + """ + Transfer cpu array to GPU on stream (async), keeping track of its id + """ + ident = id(cpu) if ident is None else ident + if self.gpuId != ident: + if self.ev_done is not None: + stream.wait_for_event(self.ev_done) + # Safety measure. This is asynchronous, but it should still work + # Essentially we want to copy the data held in gpu array back to its CPU + # handle before the buffer can be reused. + if self.done_what != 'dtoh' and self.syncback: + # uploads on the download stream, easy to spot in nsight-sys + self.from_gpu(stream) + self.gpuId = ident + self.cpu = cpu + self.gpu = gpuarray.to_gpu_async(cpu, allocator=self._allocator, stream=stream) + self.record_done(stream, 'htod') + return self.ev_done, self.gpu + + def from_gpu(self, stream): + """ + Transfer data back to CPU, into same data handle it was copied from + before. + """ + if self.cpu is not None and self.gpuId is not None and self.gpu is not None: + # Wait for any action recorded with this array + if self.ev_done is not None: + stream.wait_for_event(self.ev_done) + self.gpu.get_async(stream, self.cpu) + self.record_done(stream, 'dtoh') + # Mark for reuse + self.gpuId = None + return self.ev_done + else: + return None + +class GpuDataManager: + """ + Manages a set of GpuData instances, to keep several blocks on device. + + Currently all blocks must be the same size. + + Note that the syncback property is used so that during fourier updates, + the exit wave array is synced bck to cpu (it is updated), + while during probe update, it's not. + """ + + def __init__(self, nbytes, num, max=None, syncback=False): + """ + Create an instance of GpuDataManager. + Parameters are the same as for GpuData, and num is the number of + GpuData instances to create (blocks on device). + """ + self._syncback = syncback + self._nbytes = nbytes + self.data = [] + self.max = max + for i in range(num): + self.add_data_block() + + def add_data_block(self, nbytes=None): + """ + Add a GpuData block. + + Parameters + ---------- + nbytes - Size of block + + Returns + ------- + """ + if self.max is None or len(self) self.mem_avail + + def to_gpu(self, ar, ev=None): + """ + Issues asynchronous copy to device. Waits for optional event ev + Emits event for other streams to synchronize with + """ + stream = self.queue_in + id_cpu = id(ar) + gpu_ar = self.on_device.get(id_cpu) + + if gpu_ar is None: + if ev is not None: + stream.wait_for_event(ev) + if self.device_is_full(ar.nbytes): + self.wait_for_freeing_events(ar.nbytes) + + # TOD0: try /except with garbage collection to make sure there is space + gpu_ar = gpuarray.to_gpu_async(ar, allocator=self.dmp.allocate, stream=stream) + + # keeps gpuarray alive + self.on_device[id_cpu] = gpu_ar + + # for deleting later + self.on_device_inv[id(gpu_ar)] = ar + + self.bytes += gpu_ar.mem_size * gpu_ar.dtype.itemsize + + + ev = cuda.Event() + ev.record(stream) + return ev, gpu_ar + + + def wait_for_freeing_events(self, nbytes): + """ + Wait until at least nbytes have been copied back to the host. Or marked for deletion + """ + freed = 0 + if not self.out_events: + #print('Waiting for memory to be released on device failed as no release event was scheduled') + self.queue_out.synchronize() + while self.out_events and freed < nbytes: + ev, id_cpu, id_gpu = self.out_events.popleft() + gpu_ar = self.on_device.pop(id_cpu) + cpu_ar = self.on_device_inv.pop(id_gpu) + ev.synchronize() + freed += cpu_ar.nbytes + self.bytes -= gpu_ar.mem_size * gpu_ar.dtype.itemsize + + def mark_release_from_gpu(self, gpu_ar, to_cpu=False, ev=None): + stream = self.queue_out + if ev is not None: + stream.wait_for_event(ev) + if to_cpu: + cpu_ar = self.on_device_inv[id(gpu_ar)] + gpu_ar.get_asynch(stream, cpu_ar) + + ev_out = cuda.Event() + ev_out.record(stream) + self.out_events.append((ev_out, id(cpu_ar), id(gpu_ar))) + return ev_out \ No newline at end of file diff --git a/ptypy/accelerate/cuda_pycuda/multi_gpu.py b/ptypy/accelerate/cuda_pycuda/multi_gpu.py new file mode 100644 index 000000000..33113c273 --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/multi_gpu.py @@ -0,0 +1,179 @@ +""" +Multi-GPU AllReduce Wrapper, that uses NCCL via cupy if it's available, +and otherwise falls back to CUDA-aware MPI, +and if that doesn't work, uses host/device copies with regular MPI. + +Findings: + +1) NCCL works with unit tests, but not in the engines. It seems to +add something to the existing pycuda Context or create a new one, +as a later event recording on an exit wave transfer fails with +'ivalid resource handle' Cuda Error. This error typically happens if for example +a CUDA event is created in a different context than what it is used in, +or on a different device. PyCuda uses the driver API, NCCL uses the runtime. +Even though those are interoperable, there seems to be an issue. +Note that this is before any allreduce call - straight after initialising. + +2) NCCL requires cupy - the Python wrapper is in there + +3) OpenMPI with CUDA support needs to be available, and: + - mpi4py needs to be compiled from master (3.1.0a - latest stable release 3.0.x doesn't have it) + - pycuda needs to be compile from master (for __cuda_array_interface__ - 2020.1 version doesn't have it) + - OpenMPI in a conda install needs to have the environment variable + --> if cuda support isn't enabled, the application simply crashes with a seg fault + +4) For NCCL peer-to-peer transfers, the EXCLUSIVE compute mode cannot be used. + It should be in DEFAULT mode. + +""" + +from pkg_resources import parse_version +import numpy as np +from pycuda import gpuarray +import pycuda.driver as cuda +from ptypy.utils import parallel +from ptypy.utils.verbose import logger, log +import os + +try: + from cupy.cuda import nccl + import cupy as cp +except ImportError: + nccl = None + +try: + import mpi4py +except ImportError: + mpi4py = None + +# properties to check which versions are available + +# use NCCL is it is available, and the user didn't override the +# default selection with environment variables +have_nccl = (nccl is not None) and \ + (not 'PTYPY_USE_CUDAMPI' in os.environ) and \ + (not 'PTYPY_USE_MPI' in os.environ) and \ + ('PTYPY_USE_NCCL' in os.environ) + +# At the moment, we require: +# the OpenMPI env var OMPI_MCA_opal_cuda_support to be set to true, +# mpi4py >= 3.1.0 +# pycuda with __cuda_array_interface__ +# and not setting the PTYPY_USE_MPI environment variable +# +# -> we ideally want to allow enabling support from a parameter in ptypy +have_cuda_mpi = (mpi4py is not None) and \ + "OMPI_MCA_opal_cuda_support" in os.environ and \ + os.environ["OMPI_MCA_opal_cuda_support"] == "true" and \ + parse_version(parse_version(mpi4py.__version__).base_version) >= parse_version("3.1.0") and \ + hasattr(gpuarray.GPUArray, '__cuda_array_interface__') and \ + not ('PTYPY_USE_MPI' in os.environ) + + +class MultiGpuCommunicatorBase: + """Base class for multi-GPU communicator options, to aggregate common bits""" + + def __init__(self): + self.rank = parallel.rank + self.ndev = parallel.size + + def allReduceSum(self, arr): + """Call MPI.all_reduce in-place, with array on GPU""" + # base class only checks properties of arrays + assert isinstance(arr, gpuarray.GPUArray), "Input must be a GPUArray" + + +class MultiGpuCommunicatorMpi(MultiGpuCommunicatorBase): + """Communicator for AllReduce that uses MPI on the CPU, i.e. D2H, allreduce, H2D""" + + def allReduceSum(self, arr): + """Call MPI.all_reduce in-place, with array on GPU""" + super().allReduceSum(arr) + + if parallel.MPIenabled: + # note: this creates a temporary CPU array + data = arr.get() + parallel.allreduce(data) + arr.set(data) + +class MultiGpuCommunicatorCudaMpi(MultiGpuCommunicatorBase): + + def allReduceSum(self, arr): + """Call MPI.all_reduce in-place, with array on GPU""" + + # Check if cuda array interface is available + if not hasattr(arr, '__cuda_array_interface__'): + raise RuntimeError("input array should have a cuda array interface") + + if parallel.MPIenabled: + comm = parallel.comm + comm.Allreduce(parallel.MPI.IN_PLACE, arr) + + +class MultiGpuCommunicatorNccl(MultiGpuCommunicatorBase): + + def __init__(self): + super().__init__() + + # Check if GPUs are in default mode + if cuda.Context.get_device().get_attributes()[cuda.device_attribute.COMPUTE_MODE] != cuda.compute_mode.DEFAULT: + raise RuntimeError("Compute mode must be default in order to use NCCL") + + # get a unique identifier for the NCCL communicator and + # broadcast it to all MPI processes (assuming one device per process) + if self.rank == 0: + self.id = nccl.get_unique_id() + else: + self.id = None + + self.id = parallel.bcast(self.id) + + self.com = nccl.NcclCommunicator(self.ndev, self.id, self.rank) + + def allReduceSum(self, arr): + """Call MPI.all_reduce in-place, with array on GPU""" + + buf = int(arr.gpudata) + count, datatype = self.__get_NCCL_count_dtype(arr) + + # no stream support here for now - it fails in NCCL when + # pycuda.Stream.handle is used for some unexplained reason + stream = cp.cuda.Stream.null.ptr + + self.com.allReduce(buf, buf, count, datatype, nccl.NCCL_SUM, stream) + + def __get_NCCL_count_dtype(self, arr): + if arr.dtype == np.complex64: + return arr.size*2, nccl.NCCL_FLOAT32 + elif arr.dtype == np.complex128: + return arr.size*2, nccl.NCCL_FLOAT64 + elif arr.dtype == np.float32: + return arr.size, nccl.NCCL_FLOAT32 + elif arr.dtype == np.float64: + return arr.size, nccl.NCCL_FLOAT64 + else: + raise ValueError("This dtype is not supported by NCCL.") + + +# pick the appropriate communicator depending on installed packages +def get_multi_gpu_communicator(use_nccl=True, use_cuda_mpi=True): + if have_nccl and use_nccl: + try: + comm = MultiGpuCommunicatorNccl() + log(4, "Using NCCL communicator") + return comm + except RuntimeError: + pass + except AttributeError: + # see issue #323 + pass + if have_cuda_mpi and use_cuda_mpi: + try: + comm = MultiGpuCommunicatorCudaMpi() + log(4, "Using CUDA-aware MPI communicator") + return comm + except RuntimeError: + pass + comm = MultiGpuCommunicatorMpi() + log(4, "Using MPI communicator") + return comm diff --git a/ptypy/accelerate/cuda_pycuda/optimisation_log.md b/ptypy/accelerate/cuda_pycuda/optimisation_log.md new file mode 100644 index 000000000..99726af5b --- /dev/null +++ b/ptypy/accelerate/cuda_pycuda/optimisation_log.md @@ -0,0 +1,417 @@ +# PyCuda Optimisation Log + +This log summarises the optimisations performed on the PyCuda engine, +including the attempted ones that did not lead to improvements, +during the December-February 2019/2020 project. +It should serve as a future reference to help understand the optimisations, +and to avoid re-attempting optimisations that were not beneficial. + +**Contents** + +- [DM Engine Kernel Optimisations](#dm-engine-kernel-optimisations) + - [General Notes](#general-notes) + - [Object and Probe Update](#object-and-probe-update) + - [Tiled Version Optimisations](#tiled-version-optimisations) + - [Atomic Version Optimisations](#atomic-version-optimisations) + - [Build Exit Wave](#build-exit-wave) + - [Error Reduce](#error-reduce) + - [Fourier Error](#fourier-error) + - [FFT](#fft) + - [Analysis](#analysis) + - [Implementation](#implementation) + - [Kernel Fusion](#kernel-fusion) +- [ML Engine Kernels](#ml-engine-kernels) + - [Finite Differences (Forward / Backward)](#finite-differences-forward--backward) + - [Fill_b](#fill_b) + - [Make_a012](#make_a012) + - [Make_model](#make_model) + - [Probe/Object Update](#probeobject-update) + - [Dot Product](#dot-product) +- [MPI](#mpi) +- [Position Refinement](#position-refinement) +- [Streaming Engine](#streaming-engine) + - [Data and Streaming Management Classes](#data-and-streaming-management-classes) + - [GpuData](#gpudata) + - [GpuDataManager](#gpudatamanager) + - [Stream Management](#stream-management) + - [Synchronisation](#synchronisation) + - [Page-Locked Memory](#page-locked-memory) + +## DM Engine Kernel Optimisations + +### General Notes + +- All kernels have been written / modified to make no assumptions about the + input data sizes (for example being powers of two), or maximum number of modes, + etc. +- This has been proven by successfully running the insanity test case using the + PyCuda streaming engine +- The following optimisations were found to be most important (in order): + - Coalescing global memory access + - Reducing redundant global memory access by making use of shared memory. + In once instance this required transposing the data on the CPU. + - Tuning thread block sizes through experiments + - Maximising occupancy, for example by giving hints to the compiler using `__launch_bounds__` + so that it can optimise register usage to fit more blocks + - Using texture caches + - Math optimisations + - loop unrolling +- Kernels not explicitly mentioned in the list below either follow the same optimisations, + or were already found to be high performance + +### Object and Probe Update + +- The kernels for both are similar so the same optimisations apply for both +- 2 versions of the kernel exist: + 1. A version using a thread block per line in the address array, + with atomic adds to avoid race conditions in the output array -> [ob_update.cu](cuda/ob_update.cu). + This version was developed in the original CUDA Python module in 2018. + 2. Another version using a thread block per tile of the output array, + iterating over all lines of the address array and only updating + the part relevant to the current tile -> [ob_update2.cu](cuda/ob_update2.cu). + This version is based on the original OpenCL version with PyOpenCL. +- Performance trade-offs are: + - *Atomic Adds Version:* + - No redudant loads of the address array (one line per thread block) + - No checks if the update is relevant in the current tile + (unconditional application of the update) + - BUT the overhead of atomics in global memory (requires a global load, add, and store atomically every time) + - *Tiled Version:* + - No atomics - all updates are local + - BUT conditional if the update is affecting the current thread-block's tile. + - Becomes more efficient if the probe array is larger so that the conditional becomes true for most cases and there are less misses + - Initial tests showed that both versions are valid, depending on the size of the probe array -> the right version can be chosed based on the data sizes + - These tests need to be repeated after all optimisations have been applied + +#### Tiled Version Optimisations + +1. Starting Point: + - Every thread loads the full address array and iterates over it + - Reads all the modes for the local thread into a local array (stored in registers) + - Then updates this local array by iterating over all addresses + - Writes back to global memory at the end +2. Shared Memory: + - Avoids every thread loading the address array redudantly by + collaborating within a thread block for these loads + - Lets every thread in a threadblock load a line of the address array + into shared memory, then sync + - Then the iteration can be done in shared memory, avoiding global memory access + - Reduces redundant loads +3. Coalesced Address Array Loads: + - Transposes the address array before transfering to GPU, + so that global memory accesses to the address lines are coalesced between threads + - Reduces the global loads again as coalesced loads reduce global memory + access +4. Compile-time Constants: + - As PyCuda compiles kernels on the fly, we can set the number of modes + and array sizes as constants before compilation, allowing better compiler optimisation +5. Real-part Updates: + - Only the real part of the denominator needs updating + - The imaginary part is always zero, so it didn't need to be computed / added +6. Texture Caches: + - The constant kernel parameters were put into texture caches using the + `const X* __restrict__` modifiers + - This accelerates the repeated loads and frees the L2/L1 caches for other data +7. Loop unrolling: + - Through experimentation it was found that the update loop could be unrolled by factor 4 for best performance +8. Real denominator: + - Change obn/prn to real data type globally in ptypy, which benefits this kernel's performance (less data to load) + +#### Atomic Version Optimisations + +1. Starting Point: + - Version based on 2018 CUDA effort [extract_array_from_exit_wave.cu](../../../archive/cuda_extension/cuda/func/extract_array_from_exit_wave.cu) +2. Coalesced Access: + - Swap loop order to iterate of the `threadIdx.x` dimension in the inner loop (this is the fast-running index between threads) + - This makes sure that the global memory loads and stores are coalesced +3. Texture Caches: + - The constant kernel parameters were put into texture caches using the + `const X* __restrict__` modifiers + - This accelerates the repeated loads and frees the L2/L1 caches for other data +4. Loop Unrolling: + - Experiments where made with different loop unrolling factors, but none of them made a difference +5. Real-part Updates: + - Only the real part of the denominator needs updating + - The imaginary part is always zero, so it didn't need to be computed / added +6. Real denominator: + - Change obn/prn to real data type globally in ptypy, which benefits this kernel's performance (less data to load) + +### Build Exit Wave + +1. Starting Point + - Version with atomic adds to update the exit wave array +2. Coalesced Access: + - Swap loop order to iterate of the `threadIdx.x` dimension in the inner loop (this is the fast-running index between threads) + - This makes sure that the global memory loads and stores are coalesced +3. Remove Atomics + - The exit wave output is actually not overlapping, so atomics are not needed +4. Loop Unrolling + - Unrolling innner loop by factor for gives a slight performance advantage + +### Error Reduce + +1. Coalesced Access: + - Swap loop order to iterate of the `threadIdx.x` dimension in the inner loop (this is the fast-running index between threads) + - This makes sure that the global memory loads and stores are coalesced +2. Texture Cache + - Not beneficial on any of the constant inputs +3. Boolean Mask + - Using a boolean expression with `m < 0.5 ? X : Y` is slightly slower than the floating point version (48.8ms -> 49.2ms), so we keep using it as a number +4. Loop Unrolling + - Make no difference + +### Fourier Error + +1. Coalesced Access: + - Swap loop order to iterate of the `threadIdx.x` dimension in the inner loop (this is the fast-running index between threads) + - This makes sure that the global memory loads and stores are coalesced +2. Texture Cache + - Not beneficial on any of the constant inputs +3. Store fdev in register + - original code writes back to global memory and reading it back immediately + - seems that compiler already does this optimisation -> no difference +4. Avoid absolute value calculation + - The fdev value is squared and as it's real-valued, so there's to calc absolute value first + - --> makes no noticable difference +5. Use Mask as Boolean + - Changing expression with the boolean ? operator to avoid unnecessary loads when mask is 0 + - --> didn't affect performance +6. Occupancy + - Kernel only achieved 50% occupancy, due to too many registers per block + - Specifying `__launch_bounds__` on the kernel made compiler generate less registers --> 100% occupancy + - Speedup: 35.8ms -> 31.5ms +7. Using one thread per mode + shared memory: + - original lets every thread go in a loop over all modes to su + - tried different version which uses a thread per mode + shared memory + reduction instead + - implemented in [fourier_error2.cu](cuda/fourier_error2.cu) + - Test results on P100, minimal prep and run template for DM, 20 iterations: + - original : 35.80ms total (40 calls) + - shared mem: 80.12ms + - --> it's far worse, so not using this +8. Future Optimisations (not done) + - kernel calculates `real(abs(f)^2)`, which is mathematically the same as `real(f)*real(f) + imag(f)*imag(f)` + - The second version is faster to compute + - Code has a comment that with the second version, results differ from numpy + - However, OpenCL uses the second version + - This should be reconsidered and changed to the faster version + +### FFT + +#### Analysis + +- The Reikna version is used with pre-FFT and post-IFFT arrays built-in for scaling and shifting +- Comparisons showed large speedups with cuFFT if built-in load/store callback mechanism is used + for the scaling and shifting +- cuFFT separate kernels for pre- and post-filtering it's worse than Reikna, as shown below: + +| Version | 128x128x2000 | 256x256x2000 | +|------------------------------------|--------------|--------------| +| Reikna with or without filters | 389ms | 1,470ms | +| cuFFT without filters | 194ms | 793ms | +| cuFFT with separate filter kernels | 388ms | 1,564ms | +| cuFFT with callbacks | 223ms | 916ms | +| **=> cuFFT/callback vs Reikna** | **1.74x** | **1.60x** | + +#### Implementation + +- cuFFT with callbacks only works from C++/CUDA, as callbacks are device functions + which need to be compiled with device-side linking and then pointers to them need + to be obtained and passed to the cuFFT calls. +- This mechanism is not supported in SciKit-CUDA or PyCUDA, hence we need a native + compiled Python module for the task +- We don't want a compilation step at package-install time, prefer PyCUDA's runtime + compilation approach +- Hence we used the [cppimport](https://github.com/tbenthompson/cppimport) to build a + Python module with the [pybind11](https://pybind11.readthedocs.io/en/stable/) C++ library (header-only) for the Python bindings +- The headers can be install with pip conveniently +- CppImport compiles the code when it's imported on-the-fly (and caches it) +- We also used compile-time constant for the FFT sizes, so that the compiler is more efficient + in the callbacks (see comments in the code) +- The [import_fft.py](import_fft.py) file mangaes the cppimport itself, setting the compiler + to nvcc and setting up the compilation flags +- The [cufft.py](cufft.py) wraps the import and calls, providing the same interface as the + [Reikna FFT class](fft.py) + +### Kernel Fusion + +- Of all kernels called in a sequence, the [fourier_error](cuda/fourier_error.cu), + [error_reduce](cuda/error_reduce.cu) and [fmag_all_update](cuda/fmag_all_update.cu) kernels are joinable in principle +- In the former, all modes are calculated by 1 thread, while the latter flattens the data over + the modes +- An attempt was made in [fourier_update.cu](cuda/fourier_update.cu): + - Using shared memory reduction rather than one thread to add the modes, which unifies the thread pattern between both kernels + - Then joining the fmag_all_update kernel + - This was found to be more than twice slower than individual calls + - Also there is a potential race condition, as the err_fmag array is accessed at a different + address (using address book) than when the kernel writes to it. This should be fixable though, by working with the address book all along the fused kernel +- *Future optimisation* + - Another attempt could be done by not using shared memory, but changing fmag_all_update + to iterate over the modes in one thread as well + - the race condition would also need to be fixed, using the address book (if possible) + - This could potentially improve the performance of these kernel combined by up to 2x, + but in the overall timeline, that's only in the single-digit percentages + +## ML Engine Kernels + +Several new kernels have been implemented for the ML engine. The ones with non-trivial +optimisations are given below. +Note that all these kernels have been written to support both float and double +inputs, using compile-time replacements. +It is recommended to modify the DM kernels in a similar fashion for flexibility. + +### Finite Differences (Forward / Backward) + +- These kernels are calculating the difference of 3D arrays with a shifted version of itself: [delx_mid.cu](cuda/delx_mid.cu) and [delx_last.cu](cuda/delx_last.cu) +- They use shared memory to avoid loading the current and next pixel in every thread (reduces global loads by 2x) +- The implementation uses one thread block to iterate along the difference axis, + while the other dimensions are tiled using as many thread blocks as necessary +- Array dimenions are folded, i.e. any dimenions > 3 can be expressed as an array + with 3 dimensions where the axes dimensions before and after the difference axis are multiplied together +- For < 3D, the other dimensions can be considered of size 1 +- For coalescing the load operations, there are 2 kernels: one for the last axis + and one for all previous axes. The difference is that the loads into shared + memory are transposed. +- the code is commented in detail + +### Fill_b + +- the fill_b kernel is essentially some computations and then a reduction along the final 2 dimensions +- The math and first-stage of the reduction (within thread blocks) is in [fill_b](cuda/fill_b.cu) +- The final reduction stage across blocks is in [fill_b_reduce](cuda/fill_b_reduce.cu) +- We're reducing all 3 arrays in a single kernel, using shared memory, to avoid + the call overheads +- the block sizes have been optimised via experiments + +### Make_a012 + +- The kernel [make_a012](cuda/make_a012.cu) implements this functionality +- The summation across modes is done in a single thread, as this was found to be more effecient than shared memory (given only a small number of modes is typically used) + +### Make_model + +- The [make_model](cuda/make_model.cu) is similar, though simpler, to make_a012, + and uses the same pattern (summation across modes in one thread) + +### Probe/Object Update + +- These kernels are the same as for DM, except that there is no denominator +- The same optimisations apply + +### Dot Product + +- The real and complex vector dot product has been implemented in [dot.cu](cuda/dot.cu), including blockwise reduction. +- This is followed by a full reduction over all blocks in [full_reduce.cu](cuda/full_reduce.cu) +- Note that for complex values, this is not mathematically computing the dot product - it calculates `real(a)*real(b) + imag(a)*imag(b)` + +## MPI + +- The MPI all-reduce calls are a major bottleneck, after all the GPU optimisations +- Measurements were taken on DLS cluster, using the [mpi_allreduce_bench.sh](../../../benchmark/mpi_allreduce_bench.sh) script, for single-node and multi-node MPI +- Results are in [this sheet](https://docs.google.com/spreadsheets/d/1OyIWTkFit-0EXKzdODkcw7WfqQC2ldjLXzdz05jTQHY/edit#gid=188734367) +- Key findings: + - synchronisation time is quasi-linear with the number of nodes in DLS + - OpenMPI is far faster than MPICH2 in DLS + +## Position Refinement + +- An optional position refinement algorithm was added to the PyCUDA engines +- So far, it mangles and shifts the addresses on the CPU, and uses the GPU + to evaluate the errors +- It's working, but there is singificant data transfers between host and device +- Transposing the addresses for tiled version of ob/pr update is done on GPU though +- Suggested future optimisations: + - Implement a GPU kernel to pick the indexes from the mangled and original arrays, i.e. for the `update_addr_and_error_state` method + - this is straightforward, just reading a line for one of the two input arrays + depending on a flag + - This avoids a GPU->CPU-GPU turnaround + - Implement a kernel for the `mangle_address` method, taking the deltas as + an input for added flexibility. + - This is mostly indexing operations, adding + delta to the correct elements. + - This avoids another GPU-CPU-GPU turaround, eliminating most of them + - Perform deltas computation on GPU as well + - for example, placing the random numbers on GPU for everything at the start + - or generating them on the fly on the GPU - a simple integer XOR-shift based + random generator should suffice, as statistical properties are less + important here + +## Streaming Engine + +- implemented in [DM_pycuda_stream.py](../../engines/DM_pycuda_stream.py) +- Purpose: allow processing more blocks than fit in GPU memory, by transferring blocks as needed between GPU and CPU, in a chunked fashion +- Implemented using multiple CUDA streams, to overlap transfers with compute, + and using page-locked CPU memory to make transfers asynchronous and fast +- Using separate class for these purposes enables to use more GPU memory blocks + for the exit waves than for the ma/mag arrays, as the latter are faster to + transfer. + +### Data and Streaming Management Classes + +- Data that needs to be cycled in/out of GPU is managed by 2 classes: GpuData and GpuDataManager +- GpuStreamData manages the streams and links it to the data classes +- All these have been documented in detail with Python doc strings + +#### GpuData + +- GpuData handles one block of memory for one array (e.g. exit_wave) +- It allocates a raw GPU buffer to hold any instance of the exit_wave, + and uses a custom allocate function to return the same buffer every time for + a new array +- It keeps track of which CPU array was transferred by means of an ID +- If the same object is used to transfer a new cpu array (different ID), + it can copy the existing GPU data back to the held cpu reference before + replacing it (if syncback=True) +- Some arrays aren't modified on GPU, so the syncback parameter may be false +- It also supports resizing, in case a second engine_prepare is called with different blocks +- Explicit free methods are also given, as we can't wait for the garbage collector + to free GPU memory in case we need to reallocate immediately, to make sure + that memory is available + +#### GpuDataManager + +- manages an array of GpuData objects for multiple blocks of the same array (e.g. exit_wave) +- The GpuData objects can be seen as a list of GPU memory blocks that are fixed + size +- All interactions with GpuData objects are through this manager +- Supports looking up if a requested transfer to GPU is already present with + the same ID, in which case it just returns that without transfer +- Otherwise, takes the oldest instance and replaces the memory with the new data +- Supports resizing, relaying calls to all internal GpuData instances + +### Stream Management + +- GpuDataStream manages one CUDA stream, the related computations and transfers +- It has references to the exit wave, ma, and mag GpuDataManagers +- Decoupling this from the data managment allows to re-use the same data on + different streams, or have more streams than data blocks +- It provides events to synchronise data re-use for ptypy engines + +### Synchronisation + +- Even though we use streams to give an ordering of the computation, + some synchronisation is still needed: + - exit wave, ma, and mag memory blocks might still be in use when a new one + is about to be transferred + - the FFT plan scratch memory and aux blocks are re-used in all streams, + so there can't be compute kernels of other streams +- for the data (exit wave etc.), this is managed in the GpuData class internally. + An event is used in this class to mark when transfers are finished, + and it is synchronised before the same memory block is used again for a new + transfer to GPU. That means in the engine, we need to mark when we are done + with the data, using `record_done`. +- for the compute overlaps (aux and FFT), an `end_compute` method is called + when the compute is done, returning an event that was recorded. + Then `start_compute` is called before starting to compute the next block, + waiting for this event + +### Page-Locked Memory + +- For transfers to be truly asynchronous, the CPU-side memory needs to be + page-locked (pinned). +- That is CPU memory that cannot be paged out to disk +- PyCUDA provides methods to reserve such memory and use it with numpy arrays +- The memory flag 0 (default) means it's regular pinned memory that is fast to + read and write from CPU (used for the arrays that need to be MPI synced) +- The memory flag 4 means write-combined memory, which is fast to write on CPU + and very fast to transfer from CPU to GPU, but its extremely slow to read on CPU. + It is therefore used only for arrays that are not read/modified on CPU. diff --git a/ptypy/accelerate/ocl_pyopencl/__init__.py b/ptypy/accelerate/ocl_pyopencl/__init__.py new file mode 100644 index 000000000..648b9de7f --- /dev/null +++ b/ptypy/accelerate/ocl_pyopencl/__init__.py @@ -0,0 +1,27 @@ + + +ocl_context = None +ocl_queue = None + +def get_ocl_queue(new_queue=False): + + from ptypy.utils import parallel + import pyopencl as cl + devices = cl.get_platforms()[0].get_devices(cl.device_type.GPU) + + global ocl_context + global ocl_queue + + if ocl_context is None and parallel.rank_local < len(devices): + #ocl_context = cl.Context([devices[parallel.rank_local]]) + ocl_context = cl.Context([devices[-1]]) + print("parallel.rank:%s, parallel.rank_local:%s" % (str(parallel.rank), + str(parallel.rank_local))) + + if ocl_context is not None: + if new_queue or ocl_queue is None: + ocl_queue = cl.CommandQueue(ocl_context) + return ocl_queue + else: + return None + diff --git a/ptypy/accelerate/ocl_pyopencl/engines/DM_ocl.py b/ptypy/accelerate/ocl_pyopencl/engines/DM_ocl.py new file mode 100644 index 000000000..2a3604c2a --- /dev/null +++ b/ptypy/accelerate/ocl_pyopencl/engines/DM_ocl.py @@ -0,0 +1,411 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import numpy as np +import time +import pyopencl as cl + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines import BaseEngine, register +from ptypy.accelerate.base.engines import projectional_serial + +from pyopencl import array as cla +from ptypy.accelerate.ocl_pyopencl.ocl_kernels import FourierUpdateKernel, AuxiliaryWaveKernel, PoUpdateKernel +from ptypy.accelerate import ocl_pyopencl as gpu + +### TODOS +# +# - Get it running faster with MPI (partial sync) +# - implement "batching" when processing frames to lower the pressure on memory +# - Be smarter about the engine.prepare() part +# - Propagator needs to be reconfigurable for a certain batch size, gpyfft hates that. +# - Fourier_update_kernel needs to allow batched execution + +## for debugging +#from matplotlib import pyplot as plt + +__all__ = ['DM_ocl'] + +parallel = u.parallel + +serialize_array_access = projectional_serial.serialize_array_access +gaussian_kernel = projectional_serial.gaussian_kernel + + +@register() +class DM_ocl(projectional_serial.DM_serial): + + def __init__(self, ptycho_parent, pars=None): + """ + Difference map reconstruction engine. + """ + + super(DM_ocl, self).__init__(ptycho_parent, pars) + + self.queue = gpu.get_ocl_queue() + + # allocator for READ only buffers + # self.const_allocator = cl.tools.ImmediateAllocator(queue, cl.mem_flags.READ_ONLY) + ## gaussian filter + # dummy kernel + if not self.p.obj_smooth_std: + gauss_kernel = gaussian_kernel(1, 1).astype(np.float32) + else: + gauss_kernel = gaussian_kernel(self.p.obj_smooth_std, self.p.obj_smooth_std).astype(np.float32) + + self.gauss_kernel_gpu = cla.to_device(self.queue, gauss_kernel) + + def engine_initialize(self): + """ + Prepare for reconstruction. + """ + super(DM_ocl, self).engine_initialize() + + self.error = [] + + def constbuffer(nbytes): + return cl.Buffer(self.queue.context, cl.mem_flags.READ_ONLY, size=nbytes) + + self.ob_cfact_gpu = {} + self.pr_cfact_gpu = {} + + def _setup_kernels(self): + """ + Setup kernels, one for each scan. Derive scans from ptycho class + """ + # get the scans + for label, scan in self.ptycho.model.scans.items(): + + kern = u.Param() + self.kernels[label] = kern + + # TODO: needs to be adapted for broad bandwidth + geo = scan.geometries[0] + + # Get info to shape buffer arrays + fpc = scan.max_frames_per_block + + # TODO : make this more foolproof + try: + nmodes = scan.p.coherence.num_probe_modes * \ + scan.p.coherence.num_object_modes + except: + nmodes = 1 + + # create buffer arrays + ash = (fpc * nmodes,) + tuple(geo.shape) + aux = np.zeros(ash, dtype=np.complex64) + kern.aux = cla.to_device(self.queue, aux) + + # setup kernels, one for each SCAN. + kern.FUK = FourierUpdateKernel(aux, nmodes) + kern.FUK.allocate() + + kern.POK = PoUpdateKernel() + kern.POK.allocate() + + kern.AWK = AuxiliaryWaveKernel() + kern.AWK.allocate() + + from ptypy.accelerate.ocl_pyopencl.ocl_fft import FFT_2D_ocl_reikna as FFT + kern.FW = FFT(self.queue, aux, + pre_fft=geo.propagator.pre_fft, + post_fft=geo.propagator.post_fft, + inplace=True, + symmetric=True).ft + kern.BW = FFT(self.queue, aux, + pre_fft=geo.propagator.pre_ifft, + post_fft=geo.propagator.post_ifft, + inplace=True, + symmetric=True).ift + self.queue.finish() + + def engine_prepare(self): + + super(DM_ocl, self).engine_prepare() + + ## The following should be restricted to new data + + # For Streaming / Queuing: Limit to data that stays on GPU like pr & ob + # recursive copy to gpu + for name, c in self.ptycho.containers.items(): + for name, s in c.S.items(): + ## convert data here + if s.data.dtype.name == 'bool': + data = s.data.astype(np.float32) + else: + data = s.data + s.gpu = cla.to_device(self.queue, data) + + # For streaming, part of this needs to be moved to engine_iterate + # this contains stuff that aligns with the data + # also only new data should be considered here. + for prep in self.diff_info.values(): + prep.addr = cla.to_device(self.queue, prep.addr) + prep.mag = cla.to_device(self.queue, prep.mag) + prep.ma_sum = cla.to_device(self.queue, prep.ma_sum) + prep.err_fourier = cla.to_device(self.queue, prep.err_fourier) + ## potentially + #prep.ex = ... + #prep.ma = ... + + # finish init queue + self.queue.finish() + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + + for it in range(num): + + error_dct = {} + + for dID in self.di.S.keys(): + t1 = time.time() + + prep = self.diff_info[dID] + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # references for kernels + kern = self.kernels[prep.label] + FUK = kern.FUK + AWK = kern.AWK + + pbound = self.pbound_scan[prep.label] + aux = kern.aux + FW = kern.FW + BW = kern.BW + + # get addresses + addr = prep.addr + mag = prep.mag + ma_sum = prep.ma_sum + err_fourier = prep.err_fourier + + # local references + ma = self.ma.S[dID].gpu + ob = self.ob.S[oID].gpu + pr = self.pr.S[pID].gpu + ex = self.ex.S[eID].gpu + + queue = self.queue + + t1 = time.time() + AWK.build_aux(aux, addr, ob, pr, ex, alpha=self.p.alpha) + queue.finish() + + self.benchmark.A_Build_aux += time.time() - t1 + + ## FFT + t1 = time.time() + FW(aux, aux) + queue.finish() + self.benchmark.B_Prop += time.time() - t1 + + ## Deviation from measured data + t1 = time.time() + FUK.fourier_error(aux, addr, mag, ma, ma_sum) + FUK.error_reduce(addr, err_fourier) + FUK.fmag_all_update(aux, addr, mag, ma, err_fourier, pbound) + queue.finish() + self.benchmark.C_Fourier_update += time.time() - t1 + + ## iFFT + t1 = time.time() + BW(aux, aux) + queue.finish() + + self.benchmark.D_iProp += time.time() - t1 + + ## apply changes #2 + t1 = time.time() + AWK.build_exit(aux, addr, ob, pr, ex) + queue.finish() + self.benchmark.E_Build_exit += time.time() - t1 + + err_phot = np.zeros_like(err_fourier) + err_exit = np.zeros_like(err_fourier) + errs = np.array(list(zip(err_fourier.get(self.queue), err_phot, err_exit))) + error = dict(zip(prep.view_IDs, errs)) + + self.benchmark.calls_fourier += 1 + + parallel.barrier() + + sync = (self.curiter % 1 == 0) + self.overlap_update(MPI=True) + + parallel.barrier() + self.curiter += 1 + queue.finish() + + for name, s in self.ob.S.items(): + s.data[:] = s.gpu.get(queue=self.queue) + for name, s in self.pr.S.items(): + s.data[:] = s.gpu.get(queue=self.queue) + + # costly but needed to sync back with + for name, s in self.ex.S.items(): + s.data[:] = s.gpu.get(queue=self.queue) + + self.queue.finish() + + self.error = error + return error + + ## object update + def object_update(self, MPI=False): + t1 = time.time() + queue = self.queue + queue.finish() + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + """ + if self.p.obj_smooth_std is not None: + logger.info('Smoothing object, cfact is %.2f' % cfact) + t2 = time.time() + self.prg.gaussian_filter(queue, (info[3],info[4]), None, obj_gpu.data, self.gauss_kernel_gpu.data) + queue.finish() + obj_gpu *= cfact + print 'gauss: ' + str(time.time()-t2) + else: + obj_gpu *= cfact + """ + cfact = self.ob_cfact[oID] + ob.gpu *= cfact + # obn.gpu[:] = cfact + obn.gpu.fill(cfact) + queue.finish() + + # storage for-loop + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + POK = self.kernels[prep.label].POK + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for loop + ev = POK.ob_update(prep.addr, + self.ob.S[oID].gpu, + self.ob_nrm.S[oID].gpu, + self.pr.S[pID].gpu, + self.ex.S[eID].gpu) + queue.finish() + + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + # MPI test + if MPI: + ob.data[:] = ob.gpu.get(queue=queue) + obn.data[:] = obn.gpu.get(queue=queue) + queue.finish() + parallel.allreduce(ob.data) + parallel.allreduce(obn.data) + ob.data /= obn.data + + # Clip object (This call takes like one ms. Not time critical) + if self.p.clip_object is not None: + clip_min, clip_max = self.p.clip_object + ampl_obj = np.abs(ob.data) + phase_obj = np.exp(1j * np.angle(ob.data)) + too_high = (ampl_obj > clip_max) + too_low = (ampl_obj < clip_min) + ob.data[too_high] = clip_max * phase_obj[too_high] + ob.data[too_low] = clip_min * phase_obj[too_low] + ob.gpu.set(ob.data) + else: + ob.gpu /= obn.gpu + + queue.finish() + + # print 'object update: ' + str(time.time()-t1) + self.benchmark.object_update += time.time() - t1 + self.benchmark.calls_object += 1 + + ## probe update + def probe_update(self, MPI=False): + t1 = time.time() + queue = self.queue + + # storage for-loop + change = 0 + cfact = self.p.probe_inertia + for pID, pr in self.pr.storages.items(): + prn = self.pr_nrm.S[pID] + cfact = self.pr_cfact[pID] + pr.gpu *= cfact + prn.gpu.fill(cfact) + + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + POK = self.kernels[prep.label].POK + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for-loop + ev = POK.pr_update(prep.addr, + self.pr.S[pID].gpu, + self.pr_nrm.S[pID].gpu, + self.ob.S[oID].gpu, + self.ex.S[eID].gpu) + queue.finish() + + for pID, pr in self.pr.storages.items(): + + buf = self.pr_buf.S[pID] + prn = self.pr_nrm.S[pID] + + # MPI test + if MPI: + # if False: + pr.data[:] = pr.gpu.get(queue=queue) + prn.data[:] = prn.gpu.get(queue=queue) + queue.finish() + parallel.allreduce(pr.data) + parallel.allreduce(prn.data) + pr.data /= prn.data + + self.support_constraint(pr) + + pr.gpu.set(pr.data) + else: + pr.gpu /= prn.gpu + # ca. 0.3 ms + # self.pr.S[pID].gpu = probe_gpu + pr.data[:] = pr.gpu.get(queue=queue) + + ## this should be done on GPU + + queue.finish() + change += u.norm2(pr.data - buf.data) / u.norm2(pr.data) + buf.data[:] = pr.data + if MPI: + change = parallel.allreduce(change) / parallel.size + + # print 'probe update: ' + str(time.time()-t1) + self.benchmark.probe_update += time.time() - t1 + self.benchmark.calls_probe += 1 + + return np.sqrt(change) + + def engine_finalize(self): + """ + try deleting ever helper contianer + """ + super(DM_ocl, self).engine_finalize() + self.queue.finish() + + # delete local references to container buffer copies diff --git a/ptypy/accelerate/ocl_pyopencl/engines/DM_ocl_npy.py b/ptypy/accelerate/ocl_pyopencl/engines/DM_ocl_npy.py new file mode 100644 index 000000000..3539f7c15 --- /dev/null +++ b/ptypy/accelerate/ocl_pyopencl/engines/DM_ocl_npy.py @@ -0,0 +1,572 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import os.path +import numpy as np +import time +import pyopencl as cl + +from ptypy import utils as u +from ptypy.utils.verbose import logger, log +from ptypy.utils import parallel +from ptypy.engines import BaseEngine, register, projectional +from ptypy.accelerate import ocl_pyopencl as gpu + +from pyopencl import array as cla + +### TODOS +# +# - Get it running faster with MPI (partial sync) +# - implement "batching" when processing frames to lower the pressure on memory +# - Be smarter about the engine.prepare() part +# - Propagator needs to be reconfigurable for a certain batch size, gpyfft hates that. +# - Fourier_update_kernel needs to allow batched execution + +## for debugging +#from matplotlib import pyplot as plt + +__all__ = ['DM_ocl_npy'] + +parallel = u.parallel + + +def gaussian_kernel(sigma, size=None, sigma_y=None, size_y=None): + size = int(size) + sigma = np.float(sigma) + if not size_y: + size_y = size + if not sigma_y: + sigma_y = sigma + + x, y = np.mgrid[-size:size + 1, -size_y:size_y + 1] + + g = np.exp(-(x ** 2 / (2 * sigma ** 2) + y ** 2 / (2 * sigma_y ** 2))) + return g / g.sum() + + +def serialize_array_access(diff_storage): + # Sort views according to layer in diffraction stack + views = diff_storage.views + dlayers = [view.dlayer for view in views] + views = [views[i] for i in np.argsort(dlayers)] + view_IDs = [view.ID for view in views] + + # Master pod + mpod = views[0].pod + + # Determine linked storages for probe, object and exit waves + pr = mpod.pr_view.storage + ob = mpod.ob_view.storage + ex = mpod.ex_view.storage + + poe_ID = (pr.ID, ob.ID, ex.ID) + + addr = [] + for view in views: + address = [] + + for pname, pod in view.pods.items(): + ## store them for each pod + # create addresses + a = np.array( + [(pod.pr_view.dlayer, pod.pr_view.dlow[0], pod.pr_view.dlow[1]), + (pod.ob_view.dlayer, pod.ob_view.dlow[0], pod.ob_view.dlow[1]), + (pod.ex_view.dlayer, pod.ex_view.dlow[0], pod.ex_view.dlow[1]), + (pod.di_view.dlayer, pod.di_view.dlow[0], pod.di_view.dlow[1]), + (pod.ma_view.dlayer, pod.ma_view.dlow[0], pod.ma_view.dlow[1])]) + + address.append(a) + + if pod.pr_view.storage.ID != pr.ID: + log(1, "Splitting probes for one diffraction stack is not supported in " + __name__) + if pod.ob_view.storage.ID != ob.ID: + log(1, "Splitting objects for one diffraction stack is not supported in " + __name__) + if pod.ex_view.storage.ID != ex.ID: + log(1, "Splitting exit stacks for one diffraction stack is not supported in " + __name__) + + ## store data for each view + # adresses + addr.append(address) + + # store them for each storage + return view_IDs, poe_ID, np.array(addr).astype(np.int32) + + +@register() +class DM_ocl_npy(projectional.DM): + + def __init__(self, ptycho_parent, pars=None): + """ + Difference map reconstruction engine. + """ + + super(DM_ocl_npy, self).__init__(ptycho_parent, pars) + + self.queue = gpu.get_ocl_queue() + + # allocator for READ only buffers + # self.const_allocator = cl.tools.ImmediateAllocator(queue, cl.mem_flags.READ_ONLY) + ## gaussian filter + # dummy kernel + if not self.p.obj_smooth_std: + gauss_kernel = gaussian_kernel(1, 1).astype(np.float32) + else: + gauss_kernel = gaussian_kernel(self.p.obj_smooth_std, self.p.obj_smooth_std).astype(np.float32) + kernel_pars = {'kernel_sh_x': gauss_kernel.shape[0], 'kernel_sh_y': gauss_kernel.shape[1]} + + self.gauss_kernel_gpu = cla.to_device(self.queue, gauss_kernel) + + def engine_initialize(self): + """ + Prepare for reconstruction. + """ + super(DM_ocl_npy, self).engine_initialize() + + self.benchmark = u.Param() + self.benchmark.A_Build_aux = 0. + self.benchmark.B_Prop = 0. + self.benchmark.C_Fourier_update = 0. + self.benchmark.D_iProp = 0. + self.benchmark.E_Build_exit = 0. + self.benchmark.probe_update = 0. + self.benchmark.object_update = 0. + self.benchmark.calls_fourier = 0 + self.benchmark.calls_object = 0 + self.benchmark.calls_probe = 0 + self.dattype = np.complex64 + + self.error = [] + + self.diff_info = {} + self.ob_cfact = {} + self.pr_cfact = {} + + def constbuffer(nbytes): + return cl.Buffer(self.queue.context, cl.mem_flags.READ_ONLY, size=nbytes) + + self.ob_cfact_gpu = {} + self.pr_cfact_gpu = {} + + def engine_prepare(self): + + super(DM_ocl_npy, self).engine_prepare() + + # object padding on high side (due to 16x16 wg size) + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + obv = self.ob_viewcover.S[oID] + misfit = np.asarray(ob.shape[-2:]) % 32 + if (misfit != 0).any(): + pad = 32 - np.asarray(ob.shape[-2:]) % 32 + ob.data = u.crop_pad(ob.data, [[0, pad[0]], [0, pad[1]]], axes=[-2, -1], filltype='project') + obv.data = u.crop_pad(obv.data, [[0, pad[0]], [0, pad[1]]], axes=[-2, -1], filltype='project') + obn.data = u.crop_pad(obn.data, [[0, pad[0]], [0, pad[1]]], axes=[-2, -1], filltype='project') + ob.shape = ob.data.shape + obv.shape = obv.data.shape + obn.shape = obn.data.shape + ## calculating cfacts. This should actually belong to the parent class + #cfact = self.p.object_inertia * self.mean_power * \ + # (obv.data + 1.) + #cfact /= u.parallel.size + #self.ob_cfact[oID] = cfact + #self.ob_cfact_gpu[oID] = cla.to_device(self.queue, cfact) + self.ob_cfact[oID] = self.p.object_inertia * self.mean_power / u.parallel.size + + for pID, pr in self.pr.storages.items(): + cfact = self.p.probe_inertia * len(pr.views) / pr.data.shape[0] + self.pr_cfact[pID] = cfact / u.parallel.size + + ## The following should be restricted to new data + + # recursive copy to gpu + for name, c in self.ptycho.containers.items(): + for name, s in c.S.items(): + ## convert data here + if s.data.dtype.name == 'bool': + data = s.data.astype(np.float32) + else: + data = s.data + s.gpu = cla.to_device(self.queue, data) + + for dID, diffs in self.di.S.items(): + prep = u.Param() + self.diff_info[dID] = prep + + prep.view_IDs, prep.poe_IDs, addr = serialize_array_access(diffs) + + all_modes = addr.shape[1] + # master pod + mpod = self.di.V[prep.view_IDs[0]].pod + pr = mpod.pr_view.storage + ob = mpod.ob_view.storage + ex = mpod.ex_view.storage + + prep.addr_gpu = cla.to_device(self.queue, addr) + prep.addr = addr + + ## auxiliary wave buffer + aux = np.zeros_like(ex.data) + prep.aux_gpu = cla.to_device(self.queue, aux) + prep.aux = aux + self.queue.finish() + + ## setup kernels + from ptypy.accelerate.ocl_pyopencl.ocl_kernels import Fourier_update_kernel as FUK + prep.fourier_kernel = FUK(self.queue, nmodes=all_modes, pbound=self.pbound[dID]) + mask = self.ma.S[dID].data.astype(np.float32) + prep.fourier_kernel.configure(diffs.data, mask, aux) + + from ptypy.accelerate.ocl_pyopencl.ocl_kernels import Auxiliary_wave_kernel as AWK + prep.aux_ex_kernel = AWK(self.queue) + prep.aux_ex_kernel.configure(ob.data, addr, self.p.alpha) + + from ptypy.accelerate.ocl_pyopencl.ocl_kernels import PO_update_kernel as PUK + prep.po_kernel = PUK(self.queue) + prep.po_kernel.configure(ob.data, pr.data, addr) + + geo = mpod.geometry + # you cannot use gpyfft multiple times due to + if not hasattr(geo, 'transform'): + from ptypy.accelerate.ocl_pyopencl.ocl_fft import FFT_2D_ocl_reikna as FFT + + geo.transform = FFT(self.queue, aux, + pre_fft=geo.propagator.pre_fft, + post_fft=geo.propagator.post_fft, + inplace=True, + symmetric=True) + geo.itransform = FFT(self.queue, aux, + pre_fft=geo.propagator.pre_ifft, + post_fft=geo.propagator.post_ifft, + inplace=True, + symmetric=True) + self.queue.finish() + prep.geo = geo + + # finish init queue + self.queue.finish() + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + + for it in range(num): + + error_dct = {} + + for dID in self.di.S.keys(): + t1 = time.time() + + prep = self.diff_info[dID] + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # get addresses + addr = prep.addr + + # local references + ma = self.ma.S[dID].data + ob = self.ob.S[oID].data + pr = self.pr.S[pID].data + ex = self.ex.S[eID].data + + aux = prep.aux + + geo = prep.geo + queue = self.queue + + t1 = time.time() + ev = prep.aux_ex_kernel.npy_build_aux(aux, ob, pr, ex, addr) + queue.finish() + + self.benchmark.A_Build_aux += time.time() - t1 + + ## FFT + t1 = time.time() + #geo.transform.ft(aux, aux) + aux[:] = geo.propagator.fw(aux) + queue.finish() + self.benchmark.B_Prop += time.time() - t1 + + ## Deviation from measured data + t1 = time.time() + prep.fourier_kernel.npy.f = aux + err_fourier = prep.fourier_kernel.execute_npy() + queue.finish() + self.benchmark.C_Fourier_update += time.time() - t1 + + ## iFFT + t1 = time.time() + #geo.itransform.ift(aux, aux) + aux[:] = geo.propagator.bw(aux) + queue.finish() + + self.benchmark.D_iProp += time.time() - t1 + + ## apply changes #2 + t1 = time.time() + ev = prep.aux_ex_kernel.npy_build_exit(aux, ob, pr, ex, addr) + queue.finish() + + # self.prg.reduce_one_step(queue, (shape_merged[0],64), (1,64), info_gpu.data, err_temp.data, err_exit.data) + # queue.finish() + + self.benchmark.E_Build_exit += time.time() - t1 + + err_phot = np.zeros_like(err_fourier) + err_exit = np.zeros_like(err_fourier) + errs = np.array(list(zip(err_fourier, err_phot, err_exit))) + error = dict(zip(prep.view_IDs, errs)) + + self.benchmark.calls_fourier += 1 + + parallel.barrier() + + sync = (self.curiter % 1 == 0) + self.overlap_update(MPI=True) + + parallel.barrier() + self.curiter += 1 + queue.finish() + """ + for name, s in self.ob.S.items(): + s.data[:] = s.gpu.get(queue=self.queue) + for name, s in self.pr.S.items(): + s.data[:] = s.gpu.get(queue=self.queue) + + # costly but needed to sync back with + for name, s in self.ex.S.items(): + s.data[:] = s.gpu.get(queue=self.queue) + """ + self.queue.finish() + + self.error = error + return error + + def overlap_update(self, MPI=True): + """ + DM overlap constraint update. + """ + change = 1. + # Condition to update probe + do_update_probe = (self.p.probe_update_start <= self.curiter) + + for inner in range(self.p.overlap_max_iterations): + prestr = '%d Iteration (Overlap) #%02d: ' % (parallel.rank, inner) + # Update object first + if self.p.update_object_first or (inner > 0): + # Update object + log(4, prestr + '----- object update -----', True) + self.object_update(MPI=(parallel.size > 1 and MPI)) + + # Exit if probe should not yet be updated + if not do_update_probe: break + + # Update probe + log(4, prestr + '----- probe update -----', True) + change = self.probe_update(MPI=(parallel.size > 1 and MPI)) + # change = self.probe_update(MPI=(parallel.size>1 and MPI)) + + log(4, prestr + 'change in probe is %.3f' % change, True) + + # stop iteration if probe change is small + if change < self.p.overlap_converge_factor: break + + ## object update + def object_update(self, MPI=False): + t1 = time.time() + queue = self.queue + queue.finish() + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + """ + if self.p.obj_smooth_std is not None: + logger.info('Smoothing object, cfact is %.2f' % cfact) + t2 = time.time() + self.prg.gaussian_filter(queue, (info[3],info[4]), None, obj_gpu.data, self.gauss_kernel_gpu.data) + queue.finish() + obj_gpu *= cfact + print 'gauss: ' + str(time.time()-t2) + else: + obj_gpu *= cfact + """ + cfact = self.ob_cfact[oID] + ob.data *= cfact + #obn.gpu[:] = cfact + obn.data.fill(cfact) + queue.finish() + + # storage for-loop + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for loop + ev = prep.po_kernel.npy_ob_update(self.ob.S[oID].data, + self.ob_nrm.S[oID].data, + self.pr.S[pID].data, + self.ex.S[eID].data, + prep.addr) + + queue.finish() + + for oID, ob in self.ob.storages.items(): + obn = self.ob_nrm.S[oID] + # MPI test + if MPI: + #ob.data[:] = ob.gpu.get(queue=queue) + #obn.data[:] = obn.gpu.get(queue=queue) + queue.finish() + parallel.allreduce(ob.data) + parallel.allreduce(obn.data) + ob.data /= obn.data + + # Clip object (This call takes like one ms. Not time critical) + if self.p.clip_object is not None: + clip_min, clip_max = self.p.clip_object + ampl_obj = np.abs(ob.data) + phase_obj = np.exp(1j * np.angle(ob.data)) + too_high = (ampl_obj > clip_max) + too_low = (ampl_obj < clip_min) + ob.data[too_high] = clip_max * phase_obj[too_high] + ob.data[too_low] = clip_min * phase_obj[too_low] + #ob.gpu.set(ob.data) + else: + ob.data /= obn.data + + queue.finish() + + # print 'object update: ' + str(time.time()-t1) + self.benchmark.object_update += time.time() - t1 + self.benchmark.calls_object += 1 + + ## probe update + def probe_update(self, MPI=False): + t1 = time.time() + queue = self.queue + + # storage for-loop + change = 0 + cfact = self.p.probe_inertia + for pID, pr in self.pr.storages.items(): + prn = self.pr_nrm.S[pID] + cfact = self.pr_cfact[pID] + pr.data *= cfact + prn.data.fill(cfact) + + for dID in self.di.S.keys(): + prep = self.diff_info[dID] + + # find probe, object in exit ID in dependence of dID + pID, oID, eID = prep.poe_IDs + + # scan for-loop + ev = prep.po_kernel.npy_pr_update(self.pr.S[pID].data, + self.pr_nrm.S[pID].data, + self.ob.S[oID].data, + self.ex.S[eID].data, + prep.addr) + + queue.finish() + + for pID, pr in self.pr.storages.items(): + + buf = self.pr_buf.S[pID] + prn = self.pr_nrm.S[pID] + + # MPI test + if MPI: + # if False: + #pr.data[:] = pr.gpu.get(queue=queue) + #prn.data[:] = prn.gpu.get(queue=queue) + queue.finish() + parallel.allreduce(pr.data) + parallel.allreduce(prn.data) + pr.data /= prn.data + + self.support_constraint(pr) + # Apply probe support if requested + #support = self.probe_support.get(pID) + #if support is not None: + # pr.data *= support + + # Apply probe support in Fourier space (This could be better done on GPU) + #support = self.probe_fourier_support.get(pID) + #if support is not None: + # pr.data[:] = np.fft.ifft2(support * np.fft.fft2(pr.data)) + + #pr.gpu.set(pr.data) + else: + pr.data /= prn.data + # ca. 0.3 ms + # self.pr.S[pID].gpu = probe_gpu + #pr.data[:] = pr.gpu.get(queue=queue) + + ## this should be done on GPU + + queue.finish() + # change += u.norm2(pr[i]-buf_pr[i]) / u.norm2(pr[i]) + change += u.norm2(pr.data - buf.data) / u.norm2(pr.data) + buf.data[:] = pr.data + if MPI: + change = parallel.allreduce(change) / parallel.size + + # print 'probe update: ' + str(time.time()-t1) + self.benchmark.probe_update += time.time() - t1 + self.benchmark.calls_probe += 1 + + return np.sqrt(change) + + def engine_finalize(self): + """ + try deleting ever helper contianer + """ + self.queue.finish() + if parallel.master: + print("----- BENCHMARKS ----") + acc = 0. + for name in sorted(self.benchmark.keys()): + t = self.benchmark[name] + if name[0] in 'ABCDEFGHI': + print('%20s : %1.3f ms per iteration' % (name, t / self.benchmark.calls_fourier * 1000)) + acc += t + elif str(name) == 'probe_update': + # pass + print('%20s : %1.3f ms per call. %d calls' % ( + name, t / self.benchmark.calls_probe * 1000, self.benchmark.calls_probe)) + elif str(name) == 'object_update': + print('%20s : %1.3f ms per call. %d calls' % ( + name, t / self.benchmark.calls_object * 1000, self.benchmark.calls_object)) + + print('%20s : %1.3f ms per iteration. %d calls' % ( + 'Fourier_total', acc / self.benchmark.calls_fourier * 1000, self.benchmark.calls_fourier)) + + """ + for name, s in self.ob.S.items(): + plt.figure('obj') + d = s.gpu.get() + #print np.abs(d[0][300:-300,300:-300]).mean() + plt.imshow(u.imsave(d[0][400:-400,400:-400])) + for name, s in self.pr.S.items(): + d = s.gpu.get() + for l in d: + plt.figure() + plt.imshow(u.imsave(l)) + #print u.norm2(d) + + plt.show() + """ + + for original in [self.pr, self.ob, self.ex, self.di, self.ma]: + original.delete_copy() + + # delete local references to container buffer copies diff --git a/full_dependencies.yml b/ptypy/accelerate/ocl_pyopencl/full_dependencies.yml similarity index 83% rename from full_dependencies.yml rename to ptypy/accelerate/ocl_pyopencl/full_dependencies.yml index d6bd742a7..bd08f317c 100644 --- a/full_dependencies.yml +++ b/ptypy/accelerate/ocl_pyopencl/full_dependencies.yml @@ -1,4 +1,4 @@ -name: full_dependencies +name: ptypy_pyopencl channels: - conda-forge dependencies: @@ -17,3 +17,4 @@ dependencies: - pytest-cov - coveralls - fabio + - pyopencl==2020.1 \ No newline at end of file diff --git a/ptypy/accelerate/ocl_pyopencl/kernel_heap.txt b/ptypy/accelerate/ocl_pyopencl/kernel_heap.txt new file mode 100644 index 000000000..9275c2270 --- /dev/null +++ b/ptypy/accelerate/ocl_pyopencl/kernel_heap.txt @@ -0,0 +1,550 @@ +#include + +// filter shape +#define KERNEL_SHAPE_X %(kernel_sh_x)d +#define KERNEL_SHAPE_Y %(kernel_sh_y)d + +// Define usable names for buffer access + +#define obj_dlayer(k) addr[k*15 + 3] +#define pr_dlayer(k) addr[k*15] +#define ex_dlayer(k) addr[k*15 + 6] + +#define obj_roi_row(k) addr[k*15 + 4] +#define obj_roi_column(k) addr[k*15 + 5] + +#define obj_sh_row info[3] +#define obj_sh_column info[4] + +#define pr_sh info[5] + +#define N_pods info[0]*info[1] + +#define nmodes info[1] + +__kernel void calc_fm(float pbound, + __global float *fm, + __global float *fmask, + __global float *fmag, + __global float *fdev, + __global float *ferr) +{ + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z_merged = get_global_id(0); + size_t lx = get_local_id(2); + size_t idx = z_merged*dx*dx + y*dx + x; + + __private float a[3]; + + float error = ferr[z_merged]; + float renorm = sqrt(pbound/error); + const float eps = 1e-10; + + if (renorm < 1.){ + a[0] = fmask[idx]; + a[1] = 1. - a[0]; + a[2] = fdev[idx] * renorm + fmag[idx]; + a[2] /= fdev[idx] + fmag[idx] + eps; + fm[idx] = a[0] * a[2] + a[1]; + } + else { + fm[idx] = 1.0; + } +} +__kernel void fmag_update(int nmodes, + __global cfloat_t *f, + __global float *fm + //__global cfloat_t *pre_ifft_g, + ) +{ + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z = get_global_id(0); + size_t z_merged = z/nmodes; + + float fac = fm[z_merged*dx*dx + y*dx + x]; + //cfloat_t ft = cfloat_mul(f[z*dx*dx + y*dx + x], pre_ifft_g[y*dx + x]); + f[z*dx*dx + y*dx + x] = cfloat_mulr(f[z*dx*dx + y*dx + x] , fac); + +} + +__kernel void build_aux(__global int *info, + __global float *DM_info, + __global cfloat_t *ob_g, + __global cfloat_t *pr_g, + __global cfloat_t *ex_g, + __global cfloat_t *f_g, // calculate: (1+alpha)*pod.probe*pod.object - alpha* pod.exit + //__global float *af2, + __global cfloat_t *pre_fft_g, + __global int *addr) +{ + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z = get_global_id(0); + //__private cfloat_t loc_sub [4]; + //__private cfloat_t loc_res [1]; + __private float alpha = DM_info[0]; + + //loc_sub[0] = cfloat_fromreal(DM_info[0]); + cfloat_t ex0 = cfloat_rmul(alpha,ex_g[ex_dlayer(z)*dx*dx + y*dx + x]); + cfloat_t ex1 = cfloat_mul(ob_g[obj_dlayer(z)*obj_sh_row*obj_sh_column + (y+obj_roi_row(z))*obj_sh_column + obj_roi_column(z)+x],pr_g[pr_dlayer(z)*dx*dx + y*dx+x]); + //loc_sub[3] = cfloat_fromreal(1. + loc_sub[0].real); + + cfloat_t ex2 = cfloat_sub(cfloat_rmul(1.+alpha,ex1),ex0); + f_g[z*dx*dx + y*dx + x] = cfloat_mul(ex2,pre_fft_g[y*dx+x]); + //af2[(z/nmodes)*dx*dx + y*dx + x] = 0; //maybe better with if z < af2_size + +} + + +__kernel void post_fft(__global int *info, + __global cfloat_t *f_g, + __global float *af2, + __global cfloat_t *post_fft_g) +{ + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z = get_global_id(0); + size_t z_z = z*nmodes; + __private float loc_f[2]; + loc_f[1] = 0; + + + for(int i=0; i=0)&&(v1=0)&&(v2=0)&&(v1=0)&&(v2 0; + offset = offset / 2) + { + + if (ly < offset) { + scratch[ly] += scratch[ly + offset]; + } + + barrier(CLK_LOCAL_MEM_FENCE); + } + + if (ly == 0) { + result[z] = scratch[0]; + } +} + +__kernel void sum2(__global int *info, __global float *buffer, __global float *result) +{ + size_t z = get_global_id(0); + size_t dz = get_global_size(0); + size_t lz = get_local_id(0); + + for (int y=0; y0; stride/=2){ + barrier(CLK_LOCAL_MEM_FENCE); + if(lidx < stride){ + loc_sum[lidx] += loc_sum[lidx + stride]; + } + } + barrier(CLK_LOCAL_MEM_FENCE); + if (lidx==0){ + result[i*dz*dz + gid] = loc_sum[0]; + } + } + + + +} + +__kernel void gaussian_filter(__global cfloat_t *buffer, __global float *gauss_kernel){ + + size_t x = get_global_id(0); + size_t y = get_global_id(1); + size_t dx = get_global_size(0); + size_t dy = get_global_size(1); + + int ksx = KERNEL_SHAPE_X; + int ksy = KERNEL_SHAPE_Y; + __private cfloat_t sum; + sum = cfloat_fromreal(0.0); + cfloat_t img = buffer[x*dx + y]; + for(int kidx=0; kidx=0 && y-kidy+(ksy-1)/2>=0 && x-kidx+(ksx-1)/2= g_err_sum + # fm = 1.0 (as renorm = 1, i.e. renorm[~ind]) + # pbound < g_err_sum : + # fm = (1 - g_mask) + g_mask * (g_mag + fdev * renorm) / (af + 1e-10) + # (as renorm in [0,1]) + # pbound == 0.0 + # fm = (1 - g_mask) + g_mask * g_mag / (af + 1e-10) (as renorm=0) + + ind = err_sum > pbound + renorm[ind] = np.sqrt(pbound / err_sum[ind]) + renorm = renorm.reshape((renorm.shape[0], 1, 1)) + + af = fdev + mag + fm[:] = (1 - mask) + mask * (mag + fdev * renorm) / (af + 1e-7) + + #fm[:] = mag / (af + 1e-6) + # upcasting + aux[:] = (aux.reshape(ish[0] // nmodes, nmodes, ish[1], ish[2]) * fm[:, np.newaxis, :, :]).reshape(ish) + + def build_aux(self, alpha, ob, pr, ex, addr, offset=0): + + sh = addr.shape + nmodes = sh[1] + + # stopper + maxz = min(sh[0] - offset, self.fshape[0]) + + # batch buffers + addr = addr[:maxz * nmodes] + aux = self.npy.aux[:maxz * nmodes] + + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + tmp = ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], :, :] * \ + (1. + alpha) - \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] * \ + alpha + aux[ind, :, :] = tmp + + def build_exit(self, ob, pr, ex, addr, offset=0): + + sh = addr.shape + nmodes = sh[1] + + # stopper + maxz = min(sh[0] - offset, self.fshape[0]) + + # batch buffers + addr = addr[:maxz * nmodes] + aux = self.npy.aux[:maxz * nmodes] + + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + dex = aux[ind, :, :] - \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] += dex + aux[ind, :, :] = dex + + +class PO_update_kernel(BaseKernel): + + def __init__(self): + + super(PO_update_kernel, self).__init__() + + def allocate(self): + pass + + def ob_update(self, ob, obn, pr, ex, addr): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + obn[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + return + + def pr_update(self, pr, prn, ob, ex, addr): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + prn[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] + return diff --git a/ptypy/accelerate/ocl_pyopencl/npy_kernels_for_block.py b/ptypy/accelerate/ocl_pyopencl/npy_kernels_for_block.py new file mode 100644 index 000000000..85c01d4be --- /dev/null +++ b/ptypy/accelerate/ocl_pyopencl/npy_kernels_for_block.py @@ -0,0 +1,235 @@ +import numpy as np +from collections import OrderedDict + + +class Adict(object): + + def __init__(self): + pass + + +class BaseKernel(object): + + def __init__(self): + self.verbose = False + self.npy = Adict() + self.benchmark = OrderedDict() + + def log(self, x): + if self.verbose: + print(x) + + +class FourierUpdateKernel(BaseKernel): + + def __init__(self, aux, nmodes=1): + + super(FourierUpdateKernel, self).__init__() + self.denom = 1e-7 + self.nmodes = np.int32(nmodes) + ash = aux.shape + self.fshape = (ash[0] // nmodes, ash[1], ash[2]) + + # temporary buffer arrays + self.npy.fdev = None + self.npy.ferr = None + + self.kernels = [ + 'fourier_error', + 'error_reduce', + 'fmag_all_update' + ] + + def allocate(self): + """ + Allocate memory according to the number of modes and + shape of the diffraction stack. + """ + # temporary buffer arrays + self.npy.fdev = np.zeros(self.fshape, dtype=np.float32) + self.npy.ferr = np.zeros(self.fshape, dtype=np.float32) + + def fourier_error(self, b_aux, addr, mag, mask, mask_sum): + # reference shape (write-to shape) + sh = self.fshape + # stopper + maxz = mag.shape[0] + + # batch buffers + fdev = self.npy.fdev[:maxz] + ferr = self.npy.ferr[:maxz] + aux = b_aux[:maxz * self.nmodes] + + ## Actual math ## + + # build model from complex fourier magnitudes, summing up + # all modes incoherently + tf = aux.reshape(maxz, self.nmodes, sh[1], sh[2]) + af = np.sqrt((np.abs(tf) ** 2).sum(1)) + + # calculate difference to real data (g_mag) + fdev[:] = af - mag + + # Calculate error on fourier magnitudes on a per-pixel basis + ferr[:] = mask * np.abs(fdev) ** 2 / mask_sum.reshape((maxz, 1, 1)) + return + + def error_reduce(self, addr, err_sum): + # reference shape (write-to shape) + sh = self.fshape + + # stopper + maxz = err_sum.shape[0] + + # batch buffers + ferr = self.npy.ferr[:maxz] + + ## Actual math ## + + # Reduceses the Fourier error along the last 2 dimensions.fd + #err_sum[:] = ferr.astype(np.double).sum(-1).sum(-1).astype(np.float) + err_sum[:] = ferr.sum(-1).sum(-1) + return + + def fmag_all_update(self, b_aux, addr, mag, mask, err_sum, pbound=0.0): + + sh = self.fshape + nmodes = self.nmodes + + # stopper + maxz = mag.shape[0] + + # batch buffers + fdev = self.npy.fdev[:maxz] + aux = b_aux[:maxz * nmodes] + + # write-to shape + ish = aux.shape + + ## Actual math ## + + # local values + fm = np.ones((maxz, sh[1], sh[2]), np.float32) + renorm = np.ones((maxz,), np.float32) + + ## As opposed to DM we use renorm to differentiate the cases. + + # pbound >= g_err_sum + # fm = 1.0 (as renorm = 1, i.e. renorm[~ind]) + # pbound < g_err_sum : + # fm = (1 - g_mask) + g_mask * (g_mag + fdev * renorm) / (af + 1e-10) + # (as renorm in [0,1]) + # pbound == 0.0 + # fm = (1 - g_mask) + g_mask * g_mag / (af + 1e-10) (as renorm=0) + + ind = err_sum > pbound + renorm[ind] = np.sqrt(pbound / err_sum[ind]) + renorm = renorm.reshape((renorm.shape[0], 1, 1)) + + af = fdev + mag + fm[:] = (1 - mask) + mask * (mag + fdev * renorm) / (af + self.denom) + + #fm[:] = mag / (af + 1e-6) + # upcasting + aux[:] = (aux.reshape(ish[0] // nmodes, nmodes, ish[1], ish[2]) * fm[:, np.newaxis, :, :]).reshape(ish) + return + +class AuxiliaryWaveKernel(BaseKernel): + + def __init__(self): + super(AuxiliaryWaveKernel, self).__init__() + self.kernels = [ + 'build_aux', + 'build_exit', + ] + + def allocate(self): + pass + + def build_aux(self, b_aux, addr, ob, pr, ex, alpha=1.0): + + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + tmp = ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], :, :] * \ + (1. + alpha) - \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] * \ + alpha + aux[ind, :, :] = tmp + return + + def build_exit(self, b_aux, addr, ob, pr, ex): + + sh = addr.shape + + nmodes = sh[1] + + # stopper + maxz = sh[0] + + # batch buffers + aux = b_aux[:maxz * nmodes] + + flat_addr = addr.reshape(maxz * nmodes, sh[2], sh[3]) + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + dex = aux[ind, :, :] - \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] += dex + aux[ind, :, :] = dex + return + +class PoUpdateKernel(BaseKernel): + + def __init__(self): + + super(PoUpdateKernel, self).__init__() + self.kernels = [ + 'pr_update', + 'ob_update', + ] + + def allocate(self): + pass + + def ob_update(self, addr, ob, obn, pr, ex): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + obn[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] += \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols].conj() * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + return + + def pr_update(self, addr, pr, prn, ob, ex): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] + prn[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] += \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols].conj() * \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] + return diff --git a/ptypy/accelerate/ocl_pyopencl/ocl_fft.py b/ptypy/accelerate/ocl_pyopencl/ocl_fft.py new file mode 100644 index 000000000..26e08c298 --- /dev/null +++ b/ptypy/accelerate/ocl_pyopencl/ocl_fft.py @@ -0,0 +1,233 @@ +import pyopencl as cl +from pyopencl import array as cla +import numpy as np +import time + + +class FFT_2D_ocl_gpyfft(object): + + def __init__(self, queue, array, + pre_fft=None, + post_fft=None, + inplace=False, + symmetric=False): + + import gpyfft + GFFT = gpyfft.GpyFFT(debug=False) + import gpyfft.gpyfftlib as gfft + + self.queue = queue + + a = np.empty_like(array) + + dims = array.ndim + + if dims == 2: + a = a.reshape((1,) + a.shape) + elif dims == 3: + pass + else: + raise AssertionError('Input array must be 2 or 3-dimensional') + + shape = array.shape[-2:] + distance = a.strides[0] / a.itemsize + strides = (a.strides[1] / a.itemsize, a.strides[2] / a.itemsize) + + batchsize = a.shape[0] + + if a.dtype.type is np.complex64: + precision = gfft.CLFFT_SINGLE + elif a.dtype.type is np.complex128: + precision = gfft.CLFFT_DOUBLE + else: + raise AssertionError( + 'Input array data type must be either complex64 or complex128 but is %s' % str(a.dtype.name)) + + layout = gfft.CLFFT_COMPLEX_INTERLEAVED + + plan = GFFT.create_plan(self.queue.context, shape) + plan.inplace = inplace + plan.strides_in = strides + plan.strides_out = strides + plan.distances = (distance, distance) + plan.batch_size = batchsize + plan.precision = precision + plan.layouts = (layout, layout) + if symmetric: + plan.scale_forward /= np.sqrt(np.prod(shape)) + plan.scale_backward *= np.sqrt(np.prod(shape)) + self.plan = plan + # self.print_plan_info() + # allocate buffers + CA = cl.tools.ImmediateAllocator(self.queue, cl.mem_flags.READ_ONLY) + + if pre_fft is not None: + if np.isscalar(pre_fft): + pre_fft = np.ones(plan.shape, a.dtype) * pre_fft + + self.pre_fft = cla.to_device(queue, pre_fft) # allocator = CA) + queue.finish() + + precallbackstr = "#define PLANE %d\n" % np.prod(plan.shape) + precallbackstr += """float2 prefft(__global void* input, \n + uint inoffset, \n + __global void* userdata) \n + { \n + float2 fac = *((__global float2*)userdata + inoffset % PLANE); \n + float2 in = *((__global float2*)input + inoffset); \n + float2 ret; \n + ret.x = in.x * fac.x - in.y * fac.y ; \n + ret.y = in.x * fac.y + in.y * fac.x ; \n + return ret; \n + }\n""" + if precision is gfft.CLFFT_DOUBLE: + precallbackstr = precallbackstr.replace('float2', 'double2') + plan.set_callback("prefft", precallbackstr, 'pre', user_data=self.pre_fft.data) + + if post_fft is not None: + if np.isscalar(post_fft): + post_fft = np.ones(plan.shape, a.dtype) * post_fft + + # A = np.ones(self.t_shape, out_array.dtype) + self.post_fft = cla.to_device(queue, post_fft) # allocator = CA) + queue.finish() + postcallbackstr = "#define PLANE %d\n" % np.prod(plan.shape) + postcallbackstr += """void postfft(__global void* output, \n + uint outoffset, \n + __global void* userdata, \n + float2 fftoutput) \n + { \n + float2 fac = *((__global float2*)userdata + outoffset % PLANE); \n + float2 res; \n + res.x = fftoutput.x * fac.x - fftoutput.y * fac.y;\n + res.y = fftoutput.x * fac.y + fftoutput.y * fac.x;\n + *((__global float2*)output + outoffset) = res;\n + }\n""" + if precision is gfft.CLFFT_DOUBLE: + postcallbackstr = postcallbackstr.replace('float2', 'double2') + plan.set_callback("postfft", postcallbackstr, 'post', user_data=self.post_fft.data) + + plan.bake(self.queue) + temp_size = plan.temp_array_size + if temp_size: + self.temp_buffer = cl.Buffer(self.queue.context, cl.mem_flags.READ_WRITE, size=temp_size) + else: + self.temp_buffer = None + + self.plan = plan + + def _ft(self, inarray, outarray=None, forward=True): + if not self.plan.inplace and outarray is None: + raise RuntimeError('Specify an opencl array to store the results') + + elif self.plan.inplace: + events = self.plan.enqueue_transform((self.queue,), (inarray.data,), + direction_forward=forward, temp_buffer=self.temp_buffer) + else: + events = self.plan.enqueue_transform((self.queue,), (inarray.data,), (outarray.data,), + direction_forward=forward, temp_buffer=self.temp_buffer) + + return events + + def ft(self, inarray, outarray=None): + + return self._ft(inarray, outarray, True) + + def ift(self, inarray, outarray=None): + + return self._ft(inarray, outarray, False) + + # def print_plan_info(self): + # plan = self.plan + # print('in_array.shape: ', plan.shape) + # print('in_array.strides/itemsize', tuple(s // in_array.dtype.itemsize for s in in_array.strides)) + # print('shape transform ', t_shape) + # print('t_strides ', t_strides_in) + # print('distance_in ', t_distance_in) + # print('batchsize ', t_batchsize_in) + # print('t_stride_out ', t_strides_out) + # print('inplace ', t_inplace) + + +class FFT_2D_ocl_reikna(object): + + def __init__(self, queue, array, + pre_fft=None, + post_fft=None, + inplace=False, + symmetric=True): + + self.queue = queue + ## reikna + from reikna import cluda + api = cluda.ocl_api() + thr = api.Thread(queue) + + dims = array.ndim + if dims < 2: + raise AssertionError('Input array must be at least 2-dimensional') + axes = (array.ndim - 2, array.ndim - 1) + + # build the fft + from reikna.fft import fft + ftreikna = fft.FFT(array, axes) + + # attach scaling + from reikna.transformations import mul_param + sc = mul_param(array, np.float) + ftreikna.parameter.output.connect(sc, sc.input, out=sc.output, scale=sc.param) + iscale = np.sqrt(np.prod(array.shape[-2:])) if symmetric else 1.0 + scale = 1.0 / iscale + + # attach arbitrary multiplication + from reikna import core as rc + from reikna.cluda import functions + # get the IO type + T_io = ftreikna.parameter[0] + T_2d = rc.Type(T_io.dtype, T_io.shape[-2:]) + tr = rc.Transformation( + [ + rc.Parameter('output', rc.Annotation(T_io, 'o')), + rc.Parameter('fac', rc.Annotation(T_2d, 'i')), + rc.Parameter('input', rc.Annotation(T_io, 'i')), + ], + """ + const VSIZE_T x = ${idxs[%d]}; + const VSIZE_T y = ${idxs[%d]}; + + ${output.store_same}(${mul}(${input.load_same}, ${fac.load_idx}(x,y))); + """ % axes, + render_kwds={'mul': functions.mul(T_io.dtype, T_io.dtype)}, + connectors=['input', 'output'] + ) + + if pre_fft is None and post_fft is None: + self._ftreikna = ftreikna.compile(thr) + self.ft = lambda x, y: self._ftreikna(y, scale, x, 0) + self.ift = lambda x, y: self._ftreikna(y, iscale, x, 1) + + elif pre_fft is not None and post_fft is None: + self.pre_fft = cla.to_device(queue, pre_fft) + ftreikna.parameter.input.connect(tr, tr.output, pre_fft=tr.fac, data=tr.input) + self._ftreikna = ftreikna.compile(thr) + self.ft = lambda x, y: self._ftreikna(y, scale, self.pre_fft, x, 0) + self.ift = lambda x, y: self._ftreikna(y, iscale, self.pre_fft, x, 1) + + elif pre_fft is None and post_fft is not None: + self.post_fft = cla.to_device(queue, post_fft) + ftreikna.parameter.out.connect(tr, tr.input, post_fft=tr.fac, result=tr.output) + self._ftreikna = ftreikna.compile(thr) + self.ft = lambda x, y: self._ftreikna(y, self.post_fft, scale, x, 0) + self.ift = lambda x, y: self._ftreikna(y, self.post_fft, iscale, x, 1) + + else: + self.pre_fft = cla.to_device(queue, pre_fft) + self.post_fft = cla.to_device(queue, post_fft) + ftreikna.parameter.input.connect(tr, tr.output, pre_fft=tr.fac, data=tr.input) + ftreikna.parameter.out.connect(tr, tr.input, post_fft=tr.fac, result=tr.output) + # print self._ftreikna.signature.parameters.keys() + self._ftreikna = ftreikna.compile(thr) + self.ft = lambda x, y: self._ftreikna(y, self.post_fft, scale, self.pre_fft, x, 0) + self.ift = lambda x, y: self._ftreikna(y, self.post_fft, iscale, self.pre_fft, x, 1) + + queue.finish() diff --git a/ptypy/accelerate/ocl_pyopencl/ocl_kernels.py b/ptypy/accelerate/ocl_pyopencl/ocl_kernels.py new file mode 100644 index 000000000..7ecf7214e --- /dev/null +++ b/ptypy/accelerate/ocl_pyopencl/ocl_kernels.py @@ -0,0 +1,421 @@ +import pyopencl as cl +from pyopencl import array as cla +import numpy as np +import time + +from . import get_ocl_queue +from .npy_kernels_for_block import AuxiliaryWaveKernel as AWK_NPY +from .npy_kernels_for_block import PoUpdateKernel as POK_NPY +from .npy_kernels_for_block import FourierUpdateKernel as FUK_NPY + + +class Adict(object): + + def __init__(self): + pass + + +class OclBase(object): + + def __init__(self, queue_thread=None): + + self.queue = queue_thread if queue_thread is not None else get_ocl_queue() + self._check_profiling() + self.benchmark = dict() + self.ocl_wg_size = (1, 16, 16) + + def _check_profiling(self): + if self.queue.properties == cl.command_queue_properties.PROFILING_ENABLE: + self.profile = True + else: + self.profile = False + + +class FourierUpdateKernel(FUK_NPY, OclBase): + + def __init__(self, aux, nmodes=1, queue_thread=None): + FUK_NPY.__init__(self, aux, nmodes) + OclBase.__init__(self, queue_thread) + + self.framesize = np.int32(np.prod(aux.shape[-2:])) + + assert self.queue is not None + self.prg = cl.Program(self.queue.context, """ + #include + __kernel void fourier_error(int nmodes, + __global cfloat_t *exit, + __global float *fmag, + __global float *fdev, // fdev = af - fmag + __global float *ferr, // fmask*fdev**2 + __global float *fmask, + __global float *mask_sum + //__global cfloat_t *post_fft_g + ) + { + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z_merged = get_global_id(0); + size_t z_z = z_merged*nmodes; + + __private float loc_f [3]; + __private float loc_af2a = 0.; + __private float loc_af2b = 0.; + + // saves model intensity + //loc_af2[1] = 0; + + #pragma unroll + + for(int i=0; i 0; + offset = offset / 2) + { + + if (ly < offset) { + scratch[ly] += scratch[ly + offset]; + } + + barrier(CLK_LOCAL_MEM_FENCE); + } + + if (ly == 0) { + result[z] = scratch[0]; + } + } + """).build() + + def allocate(self): + self.npy.fdev = cla.zeros(self.queue, self.fshape, dtype=np.float32) + self.npy.ferr = cla.zeros(self.queue, self.fshape, dtype=np.float32) + + def fourier_error(self, b_aux, addr, mag, mask, mask_sum): + fdev = self.npy.fdev + ferr = self.npy.ferr + + self.prg.fourier_error(self.queue, mag.shape, self.ocl_wg_size, + self.nmodes, + b_aux.data, mag.data, fdev.data, ferr.data, + mask.data, mask_sum.data) + self.queue.finish() + + def error_reduce(self, addr, err_sum): + # batch buffers + ferr = self.npy.ferr + + self.prg.reduce_one_step(self.queue, (err_sum.shape[0], 64), (1, 64), + self.framesize, + ferr.data, err_sum.data) + self.queue.finish() + + def fmag_all_update(self, b_aux, addr, mag, mask, err_sum, pbound=0.0): + # maybe cache this? + pbound = np.float32(pbound) + + sh = mag.shape + shape = (sh[0] * self.nmodes, sh[1], sh[2]) # could have also used `addr` for this + fdev = self.npy.fdev + + self.prg.fmag_all_update(self.queue, shape, self.ocl_wg_size, + self.nmodes, pbound, + b_aux.data, mask.data, mag.data, fdev.data, + err_sum.data) + self.queue.finish() + + +class AuxiliaryWaveKernel(AWK_NPY, OclBase): + + def __init__(self, queue_thread=None): + AWK_NPY.__init__(self) + OclBase.__init__(self, queue_thread) + + self._ob_shape = None + self._ob_id = None + # self.ocl_wg_size = (1, 16, 16) + + self.prg = cl.Program(self.queue.context, """ + #include + + // Define usable names for buffer access + + + #define pr_dlayer(k) addr[k*15] + #define ex_dlayer(k) addr[k*15 + 6] + + #define obj_dlayer(k) addr[k*15 + 3] + #define obj_roi_row(k) addr[k*15 + 4] + #define obj_roi_column(k) addr[k*15 + 5] + + // calculates: + // aux = (1+alpha)*pod.probe*pod.object - alpha* pod.exit + __kernel void build_aux(float alpha, + int ob_sh_row, + int ob_sh_col, + __global cfloat_t *aux, + __global cfloat_t *ob, + __global cfloat_t *pr, + __global cfloat_t *ex, + __global int *addr) + { + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z = get_global_id(0); + size_t zb = get_global_id(0); + + //size_t obj_idx = obj_dlayer(z)*ob_sh_row*ob_sh_col + (y+obj_roi_row(z))*ob_sh_col + obj_roi_column(z)+x; + + cfloat_t ex0 = cfloat_rmul(alpha,ex[ex_dlayer(z)*dx*dx + y*dx + x]); + cfloat_t ex1 = cfloat_mul(ob[obj_dlayer(z)*ob_sh_row*ob_sh_col + (y+obj_roi_row(z))*ob_sh_col + obj_roi_column(z)+x],pr[pr_dlayer(z)*dx*dx + y*dx+x]); + //loc_sub[3] = cfloat_fromreal(1. + loc_sub[0].real); + + //cfloat_t ex2 = cfloat_sub(cfloat_rmul(1.+alpha,ex1),ex0); + aux[zb*dx*dx + y*dx + x] = cfloat_sub(cfloat_rmul(1.+alpha,ex1),ex0); + } + + __kernel void build_exit(int ob_sh_row, + int ob_sh_col, + __global cfloat_t *f, + __global cfloat_t *ob, + __global cfloat_t *pr, + __global cfloat_t *ex, + __global int *addr) + { + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z = get_global_id(0); + size_t zb = get_global_id(0); + + size_t obj_idx = obj_dlayer(z)*ob_sh_row*ob_sh_col + (y+obj_roi_row(z))*ob_sh_col + obj_roi_column(z)+x; + + cfloat_t ex1 = cfloat_mul(ob[obj_idx],pr[pr_dlayer(z)*dx*dx + y*dx+x]); + cfloat_t df = cfloat_sub(f[zb*dx*dx + y*dx + x] , ex1); + f[zb*dx*dx + y*dx + x] = df ; // t.b. removed later + ex[ex_dlayer(z)*dx*dx + y*dx + x] = cfloat_add(ex[ex_dlayer(z)*dx*dx + y*dx + x] , df); + } + + """).build() + + def build_aux(self, b_aux, addr, ob, pr, ex, alpha=1.0): + obr, obc = self._cache_object_shape(ob) + ev = self.prg.build_aux(self.queue, ex.shape, self.ocl_wg_size, + np.float32(alpha), obr, obc, + b_aux.data, ob.data, pr.data, ex.data, addr.data) + return ev + + def build_exit(self, b_aux, addr, ob, pr, ex): + obr, obc = self._cache_object_shape(ob) + ev = self.prg.build_exit(self.queue, ex.shape, self.ocl_wg_size, + obr, obc, + b_aux.data, ob.data, pr.data, ex.data, addr.data) + return ev + + def _cache_object_shape(self, ob): + oid = id(ob) + + if not oid == self._ob_id: + self._ob_id = oid + self._ob_shape = (np.int32(ob.shape[-2]), np.int32(ob.shape[-1])) + + return self._ob_shape + + +class PoUpdateKernel(POK_NPY, OclBase): + + def __init__(self, queue_thread=None): + POK_NPY.__init__(self) + OclBase.__init__(self, queue_thread) + self.ocl_wg_size = (16, 16) + self.prg = cl.Program(self.queue.context, """ + #include + + // Define usable names for buffer access + + #define pr_dlayer(k) addr[k*15] + #define ex_dlayer(k) addr[k*15 + 6] + + #define obj_dlayer(k) addr[k*15 + 3] + #define obj_roi_row(k) addr[k*15 + 4] + #define obj_roi_column(k) addr[k*15 + 5] + + __kernel void ob_update(int pr_sh, + int ob_modes, + int num_pods, + __global cfloat_t *ob_g, + __global float *obn_g, + __global cfloat_t *pr_g, + __global cfloat_t *ex_g, + __global int *addr) + { + size_t z = get_global_id(1); + size_t dz = get_global_size(1); + size_t y = get_global_id(0); + size_t dy = get_global_size(0); + __private cfloat_t ob[8]; + __private float obn[8]; + + int v1 = 0; + int v2 = 0; + size_t x = y*dz + z; + cfloat_t pr = pr_g[0]; + + for (int i=0;i=0)&&(v1=0)&&(v2=0)&&(v1=0)&&(v2 + __kernel void fourier_error(int nmodes, + __global cfloat_t *exit, + __global float *fmag, + __global float *fdev, // fdev = af - fmag + __global float *ferr, // fmask*fdev**2 + __global float *fmask, + __global float *mask_sum + //__global cfloat_t *post_fft_g + ) + { + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z_merged = get_global_id(0); + size_t z_z = z_merged*nmodes; + + __private float loc_f [3]; + __private float loc_af2a = 0.; + __private float loc_af2b = 0.; + + // saves model intensity + //loc_af2[1] = 0; + + #pragma unroll + + for(int i=0; i 0; + offset = offset / 2) + { + + if (ly < offset) { + scratch[ly] += scratch[ly + offset]; + } + + barrier(CLK_LOCAL_MEM_FENCE); + } + + if (ly == 0) { + result[z] = scratch[0]; + } + } + """).build() + + def execute_ocl(self, kernel_name=None, compare=False, sync=False): + + if kernel_name is None: + for kernel in self.kernels: + self.execute_ocl(kernel, compare, sync) + else: + self.log("KERNEL " + kernel_name) + m_ocl = getattr(self, 'ocl_' + kernel_name) + m_npy = getattr(self, 'npy_' + kernel_name) + ocl_kernel_args = getfullargspec(m_ocl).args[1:] + npy_kernel_args = getfullargspec(m_npy).args[1:] + assert ocl_kernel_args == npy_kernel_args + # OCL + if sync: + self.sync_ocl() + args = [getattr(self.ocl, a).data for a in ocl_kernel_args] + + self.benchmark[kernel_name] = -time.time() + m_ocl(*args) + self.benchmark[kernel_name] += time.time() + + if compare: + args = [getattr(self.npy, a) for a in npy_kernel_args] + m_npy(*args) + self.verify_ocl() + + return self.ocl.err_fmag.get() + + def execute_npy(self, kernel_name=None): + + if kernel_name is None: + for kernel in self.kernels: + self.execute_npy(kernel) + else: + self.log("KERNEL " + kernel_name) + m_npy = getattr(self, 'npy_' + kernel_name) + npy_kernel_args = getfullargspec(m_npy).args[1:] + args = [getattr(self.npy, a) for a in npy_kernel_args] + m_npy(*args) + + return self.npy.err_fmag + + def npy_fourier_error(self, f, fmag, fdev, ferr, fmask, mask_sum): + sh = f.shape + tf = f.reshape(sh[0] // self.nmodes, self.nmodes, sh[1], sh[2]) + + af = np.sqrt((np.abs(tf) ** 2).sum(1)) + + fdev[:] = af - fmag + ferr[:] = fmask * np.abs(fdev) ** 2 / mask_sum.reshape((mask_sum.shape[0], 1, 1)) + + def ocl_fourier_error(self, f, fmag, fdev, ferr, fmask, mask_sum): + self.prg.fourier_error(self.queue, self.fshape, self.ocl_wg_size, self.nmodes, + f, fmag, fdev, ferr, fmask, mask_sum) + self.queue.finish() + + def npy_error_reduce(self, ferr, err_fmag): + err_fmag[:] = ferr.astype(np.double).sum(-1).sum(-1).astype(np.float) + + def ocl_error_reduce(self, ferr, err_fmag): + shape = (self.fshape[0], 64), + self.prg.reduce_one_step(self.queue, (self.fshape[0], 64), (1, 64), self.framesize, + ferr, err_fmag) + self.queue.finish() + + def _npy_calc_fm(self, fm, fmask, fmag, fdev, err_fmag): + + renorm = np.ones_like(err_fmag) + ind = err_fmag > self.pbound + renorm[ind] = np.sqrt(self.pbound / err_fmag[ind]) + renorm = renorm.reshape((renorm.shape[0], 1, 1)) + af = fdev + fmag + fm[:] = (1 - fmask) + fmask * (fmag + fdev * renorm) / (af + 1e-7) + """ + # C Amplitude correction + if err_fmag > self.pbound: + # Power bound is applied + renorm = np.sqrt(pbound / err_fmag) + fm = (1 - fmask) + fmask * (fmag + fdev * renorm) / (af + 1e-10) + else: + fm = 1.0 + """ + + def _npy_fmag_update(self, f, fm): + sh = f.shape + tf = f.reshape(sh[0] // self.nmodes, self.nmodes, sh[1], sh[2]) + sh = fm.shape + tf *= fm.reshape(sh[0], 1, sh[1], sh[2]) + + def npy_fmag_all_update(self, f, fmask, fmag, fdev, err_fmag): + fm = np.ones_like(fmask) + self._npy_calc_fm(fm, fmask, fmag, fdev, err_fmag) + self._npy_fmag_update(f, fm) + + def ocl_fmag_all_update(self, f, fmask, fmag, fdev, err_fmag): + self.prg.fmag_all_update(self.queue, self.shape, self.ocl_wg_size, + self.nmodes, self.pbound, f, fmask, fmag, fdev, err_fmag) + self.queue.finish() + + def verify_ocl(self, precision=2 ** (-23)): + + for name, val in self.npy.__dict__.items(): + val2 = self.ocl.__dict__[name].get() + val = val + if np.allclose(val, val2, atol=precision): + continue + else: + dev = np.std(val - val2) + print("Key %s : %.2e std, %.2e mean" % (name, dev.real, np.mean(val).real)) + + @classmethod + def test(cls, shape=(739, 256, 256), nmodes=1, pbound=0.05): + + L, M, N = shape + fshape = shape + shape = (nmodes * L, M, N) + + f = np.random.rand(*shape).astype(np.complex64) * 200 + I = np.random.rand(*fshape).astype(np.float32) * 200 ** 2 * nmodes + mask = (I > 10).astype(np.float32) + + devices = cl.get_platforms()[0].get_devices(cl.device_type.GPU) + queue = cl.CommandQueue(cl.Context([devices[0]])) + + inst = cls(queue_thread=queue, nmodes=nmodes, pbound=pbound) + inst.configure(I, mask, f.copy()) + inst.verbose = True + inst.configure_ocl() + + inst.execute_ocl(compare=True, sync=True) + #inst.execute_ocl() + g = inst.ocl.f.get() + inst.execute_npy() + f = inst.npy.f + """ + g = f.copy() + inst.configure(I, mask, g) + err = inst.execute_npy(g) + inst.verify_ocl() + """ + print('Pipeline Error : %.2e' % np.std(f - g)) + for key, val in inst.benchmark.items(): + print('Kernel %s : %.2f ms' % (key, val * 1000)) + + +class Auxiliary_wave_kernel(BaseKernel): + + def __init__(self, queue_thread=None): + + super(Auxiliary_wave_kernel, self).__init__(queue_thread) + + self.prg = cl.Program(self.queue.context, """ + #include + + // Define usable names for buffer access + + + #define pr_dlayer(k) addr[k*15] + #define ex_dlayer(k) addr[k*15 + 6] + + #define obj_dlayer(k) addr[k*15 + 3] + #define obj_roi_row(k) addr[k*15 + 4] + #define obj_roi_column(k) addr[k*15 + 5] + + // calculates: + // aux = (1+alpha)*pod.probe*pod.object - alpha* pod.exit + __kernel void build_aux(float alpha, + int ob_sh_row, + int ob_sh_col, + int batch_offset, + __global cfloat_t *aux, + __global cfloat_t *ob, + __global cfloat_t *pr, + __global cfloat_t *ex, + __global int *addr) + { + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z = get_global_id(0) + batch_offset; + size_t zb = get_global_id(0); + + size_t obj_idx = obj_dlayer(z)*ob_sh_row*ob_sh_col + (y+obj_roi_row(z))*ob_sh_col + obj_roi_column(z)+x; + + cfloat_t ex0 = cfloat_rmul(alpha,ex[ex_dlayer(z)*dx*dx + y*dx + x]); + cfloat_t ex1 = cfloat_mul(ob[obj_dlayer(z)*ob_sh_row*ob_sh_col + (y+obj_roi_row(z))*ob_sh_col + obj_roi_column(z)+x],pr[pr_dlayer(z)*dx*dx + y*dx+x]); + //loc_sub[3] = cfloat_fromreal(1. + loc_sub[0].real); + + //cfloat_t ex2 = cfloat_sub(cfloat_rmul(1.+alpha,ex1),ex0); + aux[zb*dx*dx + y*dx + x] = cfloat_sub(cfloat_rmul(1.+alpha,ex1),ex0); + } + + __kernel void build_exit(float alpha, + int ob_sh_row, + int ob_sh_col, + int batch_offset, + __global cfloat_t *f, + __global cfloat_t *ob, + __global cfloat_t *pr, + __global cfloat_t *ex, + __global int *addr) + { + size_t x = get_global_id(2); + size_t dx = get_global_size(2); + size_t y = get_global_id(1); + size_t z = get_global_id(0) + batch_offset; + size_t zb = get_global_id(0); + + size_t obj_idx = obj_dlayer(z)*ob_sh_row*ob_sh_col + (y+obj_roi_row(z))*ob_sh_col + obj_roi_column(z)+x; + + cfloat_t ex1 = cfloat_mul(ob[obj_idx],pr[pr_dlayer(z)*dx*dx + y*dx+x]); + cfloat_t df = cfloat_sub(f[zb*dx*dx + y*dx + x] , ex1); + f[zb*dx*dx + y*dx + x] = df ; // t.b. removed later + ex[ex_dlayer(z)*dx*dx + y*dx + x] = cfloat_add(ex[ex_dlayer(z)*dx*dx + y*dx + x] , df); + } + + """).build() + + self.kernels = [ + 'build_aux', + 'build_exit', + ] + + def configure(self, ob, addr, alpha=1.0): + + self.batch_offset = 0 + self.alpha = np.float32(alpha) + self.ob_shape = (np.int32(ob.shape[-2]), np.int32(ob.shape[-1])) + + self.nviews, self.nmodes, self.ncoords, self.naxes = addr.shape + self.ocl_wg_size = (1, 1, 32) + + @property + def batch_offset(self): + return self._offset + + @batch_offset.setter + def batch_offset(self, x): + self._offset = np.int32(x) + + def load(self, aux, ob, pr, ex, addr): + + assert pr.dtype == np.complex64 + assert ex.dtype == np.complex64 + assert aux.dtype == np.complex64 + assert ob.dtype == np.complex64 + assert addr.dtype == np.int32 + + self.npy.aux = aux + self.npy.pr = pr + self.npy.ob = ob + self.npy.ex = ex + self.npy.addr = addr + + for key, array in self.npy.__dict__.items(): + self.ocl.__dict__[key] = cla.to_device(self.queue, array) + + def sync_ocl(self): + for key, array in self.npy.__dict__.items(): + self.ocl.__dict__[key].set(array) + + def execute_ocl(self, kernel_name=None, compare=False, sync=False): + + if kernel_name is None: + for kernel in self.kernels: + self.execute_ocl(kernel, compare, sync) + else: + self.log("KERNEL " + kernel_name) + m_ocl = getattr(self, 'ocl_' + kernel_name) + m_npy = getattr(self, 'npy_' + kernel_name) + ocl_kernel_args = getfullargspec(m_ocl).args[1:] + npy_kernel_args = getfullargspec(m_npy).args[1:] + assert ocl_kernel_args == npy_kernel_args + # OCL + if sync: + self.sync_ocl() + args = [getattr(self.ocl, a) for a in ocl_kernel_args] + + self.benchmark[kernel_name] = -time.time() + m_ocl(*args) + self.benchmark[kernel_name] += time.time() + + if compare: + args = [getattr(self.npy, a) for a in npy_kernel_args] + m_npy(*args) + self.verify_ocl() + + return + + def execute_npy(self, kernel_name=None): + + if kernel_name is None: + for kernel in self.kernels: + self.execute_npy(kernel) + else: + self.log("KERNEL " + kernel_name) + m_npy = getattr(self, '_npy_' + kernel_name) + npy_kernel_args = getfullargspec(m_npy).args[1:] + args = [getattr(self.npy, a) for a in npy_kernel_args] + m_npy(*args) + + return + + def ocl_build_aux(self, aux, ob, pr, ex, addr): + obsh = self.ob_shape + ev = self.prg.build_aux(self.queue, aux.shape, self.ocl_wg_size, + self.alpha, obsh[0], obsh[1], self._offset, + aux.data, ob.data, pr.data, ex.data, addr.data) + return ev + + def npy_build_aux(self, aux, ob, pr, ex, addr): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + off = self.batch_offset + flat_addr = flat_addr[off:off + aux.shape[0]] + rows, cols = ex.shape[-2:] + + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + tmp = ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], :, :] * \ + (1. + self.alpha) - \ + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] * \ + self.alpha + aux[ind, :, :] = tmp + + def ocl_build_exit(self, aux, ob, pr, ex, addr): + obsh = self.ob_shape + ev = self.prg.build_exit(self.queue, aux.shape, self.ocl_wg_size, + self.alpha, obsh[0], obsh[1], self._offset, + aux.data, ob.data, pr.data, ex.data, addr.data) + + return ev + + def npy_build_exit(self, aux, ob, pr, ex, addr): + + sh = addr.shape + flat_addr = addr.reshape(sh[0] * sh[1], sh[2], sh[3]) + off = self.batch_offset + flat_addr = flat_addr[off:off + aux.shape[0]] + rows, cols = ex.shape[-2:] + for ind, (prc, obc, exc, mac, dic) in enumerate(flat_addr): + dex = aux[ind, :, :] - \ + ob[obc[0], obc[1]:obc[1] + rows, obc[2]:obc[2] + cols] * \ + pr[prc[0], prc[1]:prc[1] + rows, prc[2]:prc[2] + cols] + + ex[exc[0], exc[1]:exc[1] + rows, exc[2]:exc[2] + cols] += dex + aux[ind, :, :] = dex + + def verify_ocl(self, precision=2 ** (-23)): + + for name, val in self.npy.__dict__.items(): + val2 = self.ocl.__dict__[name].get() + val = val + if np.allclose(val, val2, atol=precision): + continue + else: + dev = np.std(val - val2) + mn = np.mean(np.abs(val)) + self.log("Key %s : %.2e std, %.2e mean" % (name, dev, mn)) + + @classmethod + def test(cls, ob_shape=(10, 300, 300), pr_shape=(1, 256, 256)): + + nviews, rows, cols = ob_shape + ex_shape = (nviews,) + pr_shape[-2:] + addr = np.zeros((nviews, 1, 5, 3), dtype=np.int32) + for i in range(nviews): + obc = (0, 2 * i, i) + prc = (0, 0, 0) + exc = (i, 0, 0) + mac = (i, 0, 0) # unimportant + dic = (i, 0, 0) # same here + addr[i, 0, :, :] = np.array([prc, obc, exc, mac, dic], dtype=np.int32) + + ob = np.random.rand(*ob_shape).astype(np.complex64) + pr = np.random.rand(*pr_shape).astype(np.complex64) + ex = np.random.rand(*ex_shape).astype(np.complex64) + + devices = cl.get_platforms()[0].get_devices(cl.device_type.GPU) + queue = cl.CommandQueue(cl.Context([devices[0]]), properties=cl.command_queue_properties.PROFILING_ENABLE) + + inst = cls(queue_thread=queue) + inst.verbose = True + bsize = nviews // 2 + batch = np.zeros((bsize,) + pr_shape[-2:], dtype=np.complex64) + args = (batch, ob, pr, ex, addr) + ocl_args = tuple([cla.to_device(queue, arg) for arg in args]) + inst.configure(ob, addr) + # ns = inst._ocl_build_exit(*ocl_args, batch_offset = 0) + # inst._npy_build_exit(*args, batch_offset = 0) + + # print ns + inst.load(*args) + inst.bath_offset = 3 + inst.execute_ocl(compare=True, sync=False) + + """ + inst.execute_ocl() + g = inst.ocl.f.get() + inst.execute_npy() + f = inst.npy.f + + g = f.copy() + inst.configure(I, mask, g) + err = inst.execute_npy(g) + inst.verify_ocl() + + print('Error : %.2e' % np.std(f-g)) + for key, val in inst.benchmark.items(): + print('Kernel %s : %.2f ms' % (key,val*1000)) + """ + + +class PO_update_kernel(BaseKernel): + + def __init__(self, queue_thread=None): + + super(PO_update_kernel, self).__init__(queue_thread) + + self.prg = cl.Program(self.queue.context, """ + #include + + // Define usable names for buffer access + + #define pr_dlayer(k) addr[k*15] + #define ex_dlayer(k) addr[k*15 + 6] + + #define obj_dlayer(k) addr[k*15 + 3] + #define obj_roi_row(k) addr[k*15 + 4] + #define obj_roi_column(k) addr[k*15 + 5] + + __kernel void ob_update(int pr_sh, + int ob_modes, + int num_pods, + __global cfloat_t *ob_g, + __global cfloat_t *obn_g, + __global cfloat_t *pr_g, + __global cfloat_t *ex_g, + __global int *addr) + { + size_t z = get_global_id(1); + size_t dz = get_global_size(1); + size_t y = get_global_id(0); + size_t dy = get_global_size(0); + __private cfloat_t ob[8]; + __private cfloat_t obn[8]; + + int v1 = 0; + int v2 = 0; + size_t x = y*dz + z; + cfloat_t pr = pr_g[0]; + + for (int i=0;i=0)&&(v1=0)&&(v2=0)&&(v1=0)&&(v2 10).astype(np.float32) + + + devices = cl.get_platforms()[0].get_devices(cl.device_type.GPU) + queue = cl.CommandQueue(cl.Context([devices[0]])) + + inst = Fourier_update_kernel(queue_thread=queue, nmodes = nmodes, pbound = 0.0) + inst.configure(I, mask, f.copy()) + inst.configure_ocl() + + inst2 = Fourier_update_kernel(queue_thread=queue, nmodes = nmodes, pbound = 0.0) + inst2.configure(I, mask, f.copy()) + inst2.configure_ocl() + #err = inst.execute_npy(f) + #gf = cla.to_device(queue,f) + #err_ocl = inst.execute_ocl(gf) + + inst.execute_ocl_auto(True,False) + g = inst.ocl.f.get() + f = inst.npy.f + print np.std(f-g) + """ diff --git a/ptypy/core/classes.py b/ptypy/core/classes.py index f1d912490..aee749811 100644 --- a/ptypy/core/classes.py +++ b/ptypy/core/classes.py @@ -453,7 +453,7 @@ def __init__(self, container, ID=None, data=None, shape=DEFAULT_SHAPE, self.model_initialized = False # MPI flag: is the storage distributed across nodes or are all nodes holding the same copy? - self._update_distributed(self.layermap,[0],[0]) + self._is_scattered = container._is_scattered # Instance attributes # self._psize = None @@ -551,7 +551,10 @@ def update(self): """ # Update the access information for the views # (i.e. pcoord, dlow, dhigh and sp) - self.update_views() + # do this only for the original container + # to avoid iterating through all the views when creating copies + if self.owner.original is self.owner: + self.update_views() def update_views(self, v=None): """ @@ -653,13 +656,16 @@ def reformat(self, newID=None, update=True): if v.layer not in layers: layers.append(v.layer) - # Check if storage is distributed - # A storage is "distributed" if and only if layer maps are different across nodes. + # Check if storage is scattered + # A storage is "scattered" if and only if layer maps are different across nodes. new_layermap = sorted(layers) - self._update_distributed(new_layermap, dlow_fov, dhigh_fov) + # Update boundaries + if not self._is_scattered and u.parallel.MPIenabled: + dlow_fov[:] = u.parallel.comm.allreduce(dlow_fov, u.parallel.MPI.MIN) + dhigh_fov[:] = u.parallel.comm.allreduce(dhigh_fov, u.parallel.MPI.MAX) - # Return if no views, it is important that this only happens after self.distributed is updated + # Return if no views, it is important that this only happens after self._is_scattered is updated if not views: return self @@ -749,21 +755,6 @@ def reformat(self, newID=None, update=True): self.data = new_data self.shape = new_shape self.center = new_center - - def _update_distributed(self, layermap, mn, mx): - self.distributed = False - if u.parallel.MPIenabled: - all_layers = u.parallel.comm.gather(layermap, root=0) - if u.parallel.master: - for other_layers in all_layers[1:]: - self.distributed |= (other_layers != layermap) - self.distributed = u.parallel.comm.bcast(self.distributed, root=0) - # synchronize if not distributed, this ensures the data is of the - # same shape across the nodes - # We could always consider to synchronize - if not self.distributed: - mn[:] = u.parallel.comm.allreduce(mn, u.parallel.MPI.MIN) - mx[:] = u.parallel.comm.allreduce(mx, u.parallel.MPI.MAX) def _to_pix(self, coord): """ @@ -860,7 +851,7 @@ def allreduce(self, op=None): ptypy.utils.parallel.allreduce Container.allreduce """ - if not self.distributed: + if not self._is_scattered: u.parallel.allreduce(self.data, op=op) def zoom_to_psize(self, new_psize, **kwargs): @@ -947,7 +938,7 @@ def report(self): def formatted_report(self, table_format=None, offset=8, align='right', separator=" : ", include_header=True): - """ + r""" Returns formatted string and a dict with the respective information Parameters @@ -1213,7 +1204,7 @@ def __init__(self, container, ID=None, accessrule=None, **kwargs): # Prepare a dictionary for PODs (volatile!) self._pods = None - """ Potential volatile dictionary for all :any:`POD`\ s that + r""" Potential volatile dictionary for all :any:`POD`\ s that connect to this view. Set by :any:`POD` """ # A single pod lookup (weak reference), set by POD instance. @@ -1365,7 +1356,7 @@ def pod(self): @property def pods(self): - """ + r""" Returns all :any:`POD`\ s still connected to this view as a dict. """ if self._pods is not None: @@ -1558,7 +1549,7 @@ class Container(Base): """ _PREFIX = CONTAINER_PREFIX - def __init__(self, owner=None, ID=None, data_type='complex', data_dims=2): + def __init__(self, owner=None, ID=None, data_type='complex', data_dims=2, distribution="cloned"): """ Parameters ---------- @@ -1572,6 +1563,12 @@ def __init__(self, owner=None, ID=None, data_type='complex', data_dims=2): data type - either a numpy.dtype object or 'complex' or 'real' (precision is taken from ptycho.FType or ptycho.CType) + data_dims : int + dimension of data, can be 2 or 3 + + distribution : str + Indicates if the data is "cloned" in all MPI processes or "scattered" + """ super(Container, self).__init__(owner, ID) @@ -1587,6 +1584,9 @@ def __init__(self, owner=None, ID=None, data_type='complex', data_dims=2): # self.original = original if original is not None else self self.original = self + # boolean parameter for distributed containers + self._is_scattered = (distribution == "scattered") + @property def copies(self): """ @@ -1838,7 +1838,7 @@ def report(self): def formatted_report(self, table_format=None, offset=8, align='right', separator=" : ", include_header=True): - """ + r""" Returns formatted string and a dict with the respective information Parameters diff --git a/ptypy/core/data.py b/ptypy/core/data.py index 025bad35f..89b725b80 100644 --- a/ptypy/core/data.py +++ b/ptypy/core/data.py @@ -984,7 +984,7 @@ def _mpi_pipeline_with_dictionaries(self, indices): # (re)distribute position information - every node should now be # aware of all positions - parallel.bcast_dict(pos) + pos = parallel.bcast_dict(pos) # Prepare data across nodes data, weights = self.correct(raw, weights, self.common) @@ -1419,7 +1419,7 @@ def load(self, indices): parallel.barrier() self._ch_frame_ind = parallel.bcast(self._ch_frame_ind) parallel.barrier() - parallel.bcast_dict(self._checked) + self._checked = parallel.bcast_dict(self._checked) # Get the coordinates in the chunks coords = self._ch_frame_ind[indices] @@ -1524,10 +1524,10 @@ def __init__(self, pars=None, **kwargs): geo = geometry.Geo(pars=self.meta) # Derive scan pattern - if p.model is 'raster': + if p.model == 'raster': pos = u.Param() pos.spacing = geo.resolution * geo.shape * p.density - pos.steps = np.int(np.round(np.sqrt(self.num_frames))) + 1 + pos.steps = int(np.round(np.sqrt(self.num_frames))) + 1 pos.extent = pos.steps * pos.spacing pos.model = p.model self.num_frames = pos.steps**2 @@ -1536,7 +1536,7 @@ def __init__(self, pars=None, **kwargs): else: pos = u.Param() pos.spacing = geo.resolution * geo.shape * p.density - pos.steps = np.int(np.round(np.sqrt(self.num_frames) + 1)) + pos.steps = int(np.round(np.sqrt(self.num_frames) + 1)) pos.extent = pos.steps * pos.spacing pos.model = p.model pos.count = self.num_frames @@ -1664,7 +1664,7 @@ def __init__(self, pars=None, **kwargs): # Derive scan pattern pos = u.Param() pos.spacing = geo.resolution * geo.shape * p.density - pos.steps = np.int(np.round(np.sqrt(self.num_frames))) + 1 + pos.steps = int(np.round(np.sqrt(self.num_frames))) + 1 pos.extent = pos.steps * pos.spacing pos.model = 'round' pos.count = self.num_frames diff --git a/ptypy/core/geometry_bragg.py b/ptypy/core/geometry_bragg.py index 262c04dea..5ecc27a79 100644 --- a/ptypy/core/geometry_bragg.py +++ b/ptypy/core/geometry_bragg.py @@ -10,7 +10,7 @@ import numpy as np from scipy.ndimage.interpolation import map_coordinates -__all__ = ['DEFAULT', 'Geo_Bragg'] +__all__ = ['Geo_Bragg'] local_tree = EvalDescriptor('') diff --git a/ptypy/core/illumination.py b/ptypy/core/illumination.py index ce4430400..cb0c7fb15 100644 --- a/ptypy/core/illumination.py +++ b/ptypy/core/illumination.py @@ -253,7 +253,7 @@ def aperture(A, grids=None, pars=None, **kwargs): if p.form is not None: off = u.expect2(p.offset) cgrid = grids[0].astype(complex) + 1j*grids[1] - cgrid -= np.complex(off[0], off[1]) + cgrid -= complex(off[0], off[1]) cgrid *= np.exp(1j * p.rotate) grids[0] = cgrid.real / psize[0] grids[1] = cgrid.imag / psize[1] @@ -594,42 +594,42 @@ def _propagation(prop_pars, shape=None, resolution=None, energy=None, ) -if __name__ == '__main__': - energy = 6. - shape = 512 - resolution = 8e-8 - p = u.Param() - p.aperture = u.Param() - p.aperture.form = 'circ' - p.aperture.diffuser = (10.0, 5, 0.1, 20.0) - p.aperture.size = 100e-6 - # (int) Edge width of aperture in pixel to suppress aliasing - p.aperture.edge = 2 - p.aperture.central_stop = 0.3 - p.aperture.offset = 0. - # (float) rotate aperture by this value - p.aperture.rotate = 0. - # Parameters for propagation after aperture plane - p.propagation = u.Param() - # (float) Parallel propagation distance - p.propagation.parallel = 0.015 - # (float) Propagation distance from aperture to focus - p.propagation.focussed = 0.1 - # (float) Focal spot diameter - p.propagation.spot_size = None - # (float) antialiasing factor - p.propagation.antialiasing = None - # (str) User-defined probe (if type is None) - p.probe = None - # (int, float, None) Number of photons in the incident illumination - p.photons = None - # (float) Noise added on top add the end of initialisation - p.noise = None - - probe = from_pars_no_storage(pars=p, - energy=energy, - shape=shape, - resolution=resolution) - - from matplotlib import pyplot as plt - plt.imshow(u.imsave(abs(probe[0]))) +# if __name__ == '__main__': +# energy = 6. +# shape = 512 +# resolution = 8e-8 +# p = u.Param() +# p.aperture = u.Param() +# p.aperture.form = 'circ' +# p.aperture.diffuser = (10.0, 5, 0.1, 20.0) +# p.aperture.size = 100e-6 +# # (int) Edge width of aperture in pixel to suppress aliasing +# p.aperture.edge = 2 +# p.aperture.central_stop = 0.3 +# p.aperture.offset = 0. +# # (float) rotate aperture by this value +# p.aperture.rotate = 0. +# # Parameters for propagation after aperture plane +# p.propagation = u.Param() +# # (float) Parallel propagation distance +# p.propagation.parallel = 0.015 +# # (float) Propagation distance from aperture to focus +# p.propagation.focussed = 0.1 +# # (float) Focal spot diameter +# p.propagation.spot_size = None +# # (float) antialiasing factor +# p.propagation.antialiasing = None +# # (str) User-defined probe (if type is None) +# p.probe = None +# # (int, float, None) Number of photons in the incident illumination +# p.photons = None +# # (float) Noise added on top add the end of initialisation +# p.noise = None + +# probe = from_pars_no_storage(pars=p, +# energy=energy, +# shape=shape, +# resolution=resolution) + +# from matplotlib import pyplot as plt +# plt.imshow(u.imsave(abs(probe[0]))) diff --git a/ptypy/core/manager.py b/ptypy/core/manager.py index 96189d17b..8acbc7b86 100644 --- a/ptypy/core/manager.py +++ b/ptypy/core/manager.py @@ -23,6 +23,7 @@ from .. import utils as u from ..utils.verbose import logger, headerline, log +from ..utils.verbose import ilog_message, ilog_streamer, ilog_newline from .classes import * from .classes import DEFAULT_ACCESSRULE from .classes import MODEL_PREFIX @@ -43,7 +44,7 @@ def __init__(self): self._t = time.time() def __call__(self, msg=None): - logger.warning('Duration %.2f for ' % (time.time() - self._t) + str(msg)) + logger.info('Duration %.2f for ' % (time.time() - self._t) + str(msg)) self._t = time.time() @@ -151,6 +152,14 @@ def __init__(self, ptycho=None, pars=None, label=None): self.CType = CType self.FType = FType + # Keep track of the maximum frames in a block + # For the ScanModel this will be equivalent to the total nr. of frames in the scan + # For the BlockScanModel this is defined by the user (frames_per_block) and the MPI settings + self.max_frames_per_block = 0 + + # By default we create a new exit buffer for each view + self._single_exit_buffer_for_all_views = False + @classmethod def makePtyScan(cls, pars): """ @@ -325,6 +334,9 @@ def new_data(self, max_frames): self.diff.data[self.diff.layermap.index(idx)][:] = diff_data self.mask.data[self.mask.layermap.index(idx)][:] = dct.get('mask', np.ones_like(diff_data)) + # Update maximum nr. of frames in a block + self.max_frames_per_block = self.diff.nlayers + self.diff.nlayers = parallel.MPImax(self.diff.layermap) + 1 self.mask.nlayers = parallel.MPImax(self.mask.layermap) + 1 @@ -537,6 +549,7 @@ def new_data(self, max_frames): fill=0.0, layermap=indices_node) mask = self.Cmask.new_storage(shape=sh, psize=self.psize, padonly=True, fill=1.0, layermap=indices_node) + # Prepare for View generation AR_diff = DEFAULT_ACCESSRULE.copy() AR_diff.shape = self.diff_shape # this is None due to init @@ -589,6 +602,9 @@ def new_data(self, max_frames): ## warning message for empty postions? + # Update maximum nr. of frames in a block + self.max_frames_per_block = max(diff.nlayers, self.max_frames_per_block) + # this is not absolutely necessary # diff.update_views() # mask.update_views() @@ -930,7 +946,11 @@ def _create_pods(self): for i in range(len(self.new_diff_views)): dv, mv = self.new_diff_views.pop(0), self.new_mask_views.pop(0) - index = dv.layer + # For stochastic engines (e.g. ePIE) we only need one exit buffer + if self._single_exit_buffer_for_all_views: + index = 0 + else: + index = dv.layer # Object and probe position pos_pr = u.expect2(0.0) @@ -1228,6 +1248,18 @@ class OPRModel(_OPRModel, Full): class BlockOPRModel(_OPRModel, BlockFull): pass +@defaults_tree.parse_doc('scan.GradFull') +class GradFull(Full): + def __init__(self, ptycho=None, pars=None, label=None): + super(GradFull, self).__init__(ptycho, pars, label) + self._single_exit_buffer_for_all_views = True + +@defaults_tree.parse_doc('scan.BlockGradFull') +class BlockGradFull(BlockFull): + def __init__(self, ptycho=None, pars=None, label=None): + super(BlockGradFull, self).__init__(ptycho, pars, label) + self._single_exit_buffer_for_all_views = True + # Append illumination and sample defaults defaults_tree['scan.Full'].add_child(illumination.illumination_desc) defaults_tree['scan.BlockFull'].add_child(illumination.illumination_desc) @@ -1618,14 +1650,16 @@ def new_data(self): logger.info('Processing new data.') - # Attempt to get new data - _nframes = self.ptycho.frames_per_block + # making sure frames_per_block is defined per rank + _nframes = self.ptycho.frames_per_block * parallel.size + # Attempt to get new data new_data = [] for label, scan in self.scans.items(): if not scan.data_available: continue else: + ilog_streamer('%s: loading data for scan %s' %(type(scan).__name__,label)) prb_ids, obj_ids, pod_ids = dict(), dict(), set() nd = scan.new_data(_nframes) while nd: @@ -1633,14 +1667,19 @@ def new_data(self): prb_ids.update(nd[1]) obj_ids.update(nd[2]) pod_ids = pod_ids.union(nd[3]) + ilog_streamer('%s: loading data for scan %s (%d diffraction frames, %d PODs, %d probe(s) and %d object(s))' + %(type(scan).__name__,label, sum([d.shape[0] if l==label else 0 for l,d in new_data]), len(pod_ids), len(prb_ids), len(obj_ids))) nd = scan.new_data(_nframes) + ilog_newline() # Reformatting + ilog_message('%s: loading data for scan %s (reformatting probe/obj/exit)' %(type(scan).__name__,label)) self.ptycho.probe.reformat(True) self.ptycho.obj.reformat(True) self.ptycho.exit.reformat(True) # Initialize probe/object/exit + ilog_message('%s: loading data for scan %s (initializing probe/obj/exit)' %(type(scan).__name__,label)) scan._initialize_probe(prb_ids) scan._initialize_object(obj_ids) scan._initialize_exit(list(pod_ids)) diff --git a/ptypy/core/ptycho.py b/ptypy/core/ptycho.py index a7c54594d..0cd49405c 100644 --- a/ptypy/core/ptycho.py +++ b/ptypy/core/ptycho.py @@ -9,11 +9,13 @@ """ import numpy as np import time +import json from . import paths from collections import OrderedDict from .. import utils as u -from ..utils.verbose import logger, _, report, headerline, log +from ..utils.verbose import logger, _, report, headerline, log, LogTime +from ..utils.verbose import ilog_message, ilog_streamer, ilog_newline from ..utils import parallel from .. import engines from .classes import Base, Container, Storage, PTYCHO_PREFIX @@ -70,19 +72,17 @@ class Ptycho(Base): Defaults: [verbose_level] - default = 1 + default = 'ERROR' help = Verbosity level doc = Verbosity level for information logging. - - ``0``: Only critical errors - - ``1``: All errors - - ``2``: Warning - - ``3``: Process Information - - ``4``: Object Information - - ``5``: Debug - type = int + - ``CRITICAL``: Only critical errors + - ``ERROR``: All errors + - ``WARNING``: Warning + - ``INFO``: Process Information + - ``INSPECT``: Object Information + - ``DEBUG``: Debug + type = str, int userlevel = 0 - lowlim = 0 - uplim = 5 [data_type] default = 'single' @@ -142,6 +142,15 @@ class Ptycho(Base): help = Reconstruction file name (or format string) doc = Reconstruction file name or format string (constructed against runtime dictionary) + [io.rformat] + default = "minimal" + type = str + help = Reconstruction file format + doc = Choose a reconstruction file format for after engine completion. + - ``'minimal'``: Bare minimum of information + - ``'dls'``: Custom format for Diamond Light Source + choices = 'minimal','dls' + [io.interaction] default = None type = Param @@ -248,6 +257,14 @@ class Ptycho(Base): doc = Switch to request the production of a movie from the dumped plots at the end of the reconstruction. + [io.benchmark] + default = None + type = str + help = Produce timings for benchmarking the performance of data loaders and engines + doc = Switch to get timings and save results to a json file in p.io.home + Choose ``'all'`` for timing data loading, engine_init, engine_prepare, engine_iterate and engine_finalize + userlevel = 2 + [scans] default = None type = Param @@ -332,6 +349,7 @@ def __init__(self, pars=None, level=2, **kwargs): # Communication self.interactor = None self.plotter = None + self.record_positions = False # Early boot strapping self._configure() @@ -389,9 +407,9 @@ def _configure(self): self.data_type = p.data_type assert p.data_type in ['single', 'double'] self.FType = np.dtype( - 'f' + str(np.dtype(np.typeDict[p.data_type]).itemsize)).type + 'f' + str(np.dtype(np.sctypeDict[p.data_type]).itemsize)).type self.CType = np.dtype( - 'c' + str(2 * np.dtype(np.typeDict[p.data_type]).itemsize)).type + 'c' + str(2 * np.dtype(np.sctypeDict[p.data_type]).itemsize)).type logger.info(_('Data type', self.data_type)) # Check if there is already a runtime container if not hasattr(self, 'runtime'): @@ -414,6 +432,15 @@ def _configure(self): # Find run name self.runtime.run = self.paths.run(p.run) + # Benchmark + if self.p.io.benchmark == 'all': + self.benchmark = u.Param() + self.benchmark.data_load = 0 + self.benchmark.engine_init = 0 + self.benchmark.engine_prepare = 0 + self.benchmark.engine_iterate = 0 + self.benchmark.engine_finalize = 0 + def init_communication(self): """ Called on __init__ if ``level >= 3``. @@ -479,9 +506,9 @@ def init_structures(self): """ self.probe = Container(self, ID='Cprobe', data_type='complex') self.obj = Container(self, ID='Cobj', data_type='complex') - self.exit = Container(self, ID='Cexit', data_type='complex') - self.diff = Container(self, ID='Cdiff', data_type='real') - self.mask = Container(self, ID='Cmask', data_type='bool') + self.exit = Container(self, ID='Cexit', data_type='complex', distribution="scattered") + self.diff = Container(self, ID='Cdiff', data_type='real', distribution="scattered") + self.mask = Container(self, ID='Cmask', data_type='bool', distribution="scattered") # Initialize the model manager. This also initializes the # containers. self.model = ModelManager(self, self.p.scans) @@ -495,7 +522,9 @@ def init_data(self, print_stats=True): """ # Load the data. This call creates automatically the scan managers, # which create the views and the PODs. Sets self.new_data - self.new_data = self.model.new_data() + with LogTime(self.p.io.benchmark == 'all') as t: + self.new_data = self.model.new_data() + if (self.p.io.benchmark == 'all') and parallel.master: self.benchmark.data_load += t.duration # Print stats parallel.barrier() @@ -614,13 +643,20 @@ def run(self, label=None, epars=None, engine=None): self.runtime.last_plot = 0 # Prepare the engine - engine.initialize() + ilog_message('%s: initializing engine' %engine.p.name) + with LogTime(self.p.io.benchmark == 'all') as t: + engine.initialize() + if (self.p.io.benchmark == 'all') and parallel.master: self.benchmark.engine_init += t.duration # One .prepare() is always executed, as Ptycho may hold data + ilog_message('%s: preparing engine' %engine.p.name) self.new_data = [(d.label, d) for d in self.diff.S.values()] - engine.prepare() + with LogTime(self.p.io.benchmark == 'all') as t: + engine.prepare() + if (self.p.io.benchmark == 'all') and parallel.master: self.benchmark.engine_prepare += t.duration # Start the iteration loop + ilog_streamer('%s: starting engine' %engine.p.name) while not engine.finished: # Check for client requests if parallel.master and self.interactor is not None: @@ -629,12 +665,16 @@ def run(self, label=None, epars=None, engine=None): parallel.barrier() # Check for new data - self.new_data = self.model.new_data() + with LogTime(self.p.io.benchmark == 'all') as t: + self.new_data = self.model.new_data() + if (self.p.io.benchmark == 'all') and parallel.master: self.benchmark.data_load += t.duration # Last minute preparation before a contiguous block of # iterations if self.new_data: - engine.prepare() + with LogTime(self.p.io.benchmark == 'all') as t: + engine.prepare() + if (self.p.io.benchmark == 'all') and parallel.master: self.benchmark.engine_prepare += t.duration auto_save = self.p.io.autosave if auto_save.active and auto_save.interval > 0: @@ -646,7 +686,9 @@ def run(self, label=None, epars=None, engine=None): logger.info(headerline()) # One iteration - engine.iterate() + with LogTime(self.p.io.benchmark == 'all') as t: + engine.iterate() + if (self.p.io.benchmark == 'all') and parallel.master: self.benchmark.engine_iterate += t.duration # Display runtime information and do saving if parallel.master: @@ -658,20 +700,35 @@ def run(self, label=None, epars=None, engine=None): 'Time %(duration).3f' % info) logger.info('Errors :: Fourier %.2e, Photons %.2e, ' 'Exit %.2e' % tuple(err)) + ilog_streamer('%(engine)s: Iteration # %(iteration)d/%(numiter)d :: ' %info + + 'Fourier %.2e, Photons %.2e, Exit %.2e' %tuple(err)) parallel.barrier() + ilog_newline() + # Done. Let the engine finish up - engine.finalize() + with LogTime(self.p.io.benchmark == 'all') as t: + engine.finalize() + if (self.p.io.benchmark == 'all') and parallel.master: self.benchmark.engine_finalize += t.duration # Save if self.p.io.rfile: - self.save_run() + self.save_run(kind=self.p.io.rformat) else: pass # Time the initialization self.runtime.stop = time.asctime() + # Save benchmarks to json file + if (self.p.io.benchmark == 'all') and parallel.master: + try: + with open(self.paths.home + "/benchmark.json", "w") as json_file: + json.dump(self.benchmark, json_file) + logger.info("Benchmarks have been written to %s" %self.paths.home + "/benchmark.json") + except Exception as e: + logger.warning("Failed to write benchmarks to file: %s" %e) + elif epars is not None: # A fresh set of engine parameters arrived. label = self.init_engine(epars=epars) @@ -719,7 +776,7 @@ def finalize(self): citation_info = '\n'.join([headerline('This reconstruction relied on the following work', 'l', '='), str(self.citations), headerline('', 'l', '=')]) - logger.warning(citation_info) + log("CITATION", citation_info) @classmethod def _from_dict(cls, dct): @@ -759,7 +816,7 @@ def load_run(cls, runfile, load_data=True): logger.info('Creating Ptycho instance from %s' % runfile) header = u.Param(io.h5read(runfile, 'header')['header']) - if header['kind'] == 'minimal': + if header['kind'] == 'minimal' or header['kind'] == 'dls': logger.info('Found minimal ptypy dump') content = io.h5read(runfile, 'content')['content'] @@ -904,7 +961,7 @@ def save_run(self, alt_file=None, kind='minimal', force_overwrite=True): content = dump - elif kind == 'minimal': + elif kind == 'minimal' or kind == 'dls': # if self.interactor is not None: # self.interactor.stop() logger.info('Generating shallow copies of probe, object and ' @@ -912,17 +969,9 @@ def save_run(self, alt_file=None, kind='minimal', force_overwrite=True): minimal = u.Param() minimal.probe = {ID: S._to_dict() for ID, S in self.probe.storages.items()} - for ID, S in self.probe.storages.items(): - minimal.probe[ID]['grids'] = S.grids() minimal.obj = {ID: S._to_dict() for ID, S in self.obj.storages.items()} - - minimal.positions = {} - for ID, S in self.obj.storages.items(): - minimal.obj[ID]['grids'] = S.grids() - minimal.positions[ID] = np.array([v.coord for v in S.views if v.pod.pr_view.layer==0]) - try: defaults_tree['ptycho'].validate(self.p) # check the parameters are actually able to be read back in except RuntimeError: @@ -931,6 +980,20 @@ def save_run(self, alt_file=None, kind='minimal', force_overwrite=True): minimal.runtime = self.runtime.copy() content = minimal + else: + raise RuntimeError("Save file format '" + str(kind) + "' is not supported") + + if kind == 'dls': + for ID, S in self.probe.storages.items(): + content.probe[ID]['grids'] = S.grids() + + for ID, S in self.obj.storages.items(): + content.obj[ID]['grids'] = S.grids() + + if kind in ['minimal', 'dls'] and self.record_positions: + content.positions = {} + for ID, S in self.obj.storages.items(): + content.positions[ID] = np.array([v.coord for v in S.views if v.pod.pr_view.layer==0]) h5opt = io.h5options['UNSUPPORTED'] io.h5options['UNSUPPORTED'] = 'ignore' diff --git a/ptypy/core/sample.py b/ptypy/core/sample.py index d58228c45..a2577bfd9 100644 --- a/ptypy/core/sample.py +++ b/ptypy/core/sample.py @@ -238,7 +238,7 @@ def init_storage(storage, sample_pars=None, energy=None): elif type(p.model) is np.ndarray: model = p.model elif p.model in resources.objects: - model = resources.objects[p.model](A.shape) + model = resources.objects[p.model](s.shape) elif str(p.model) == 'recon': # Loading from a reconstruction file layer = p.recon.get('layer') @@ -438,104 +438,3 @@ def simulate(A, pars, energy, fill=1.0, prefix="", **kwargs): # more modes requested than weight given mode_weights=[1., 0.1] ) - - -def from_pars_old(shape, lam, pars=None, dtype=np.complex): - """ - *DEPRECATED* - """ - p = u.Param(DEFAULT) - if pars is not None and (isinstance(pars, dict) - or isinstance(pars, u.Param)): - p.update(pars) - if p.obj is not None: - # Abort here if object is set - return p - else: - if isinstance(p.source, np.ndarray): - logger.info('Found nd-array') - obj = p.source - else: - logger.info('Fill with ones!') - obj = np.ones(shape) - - obj = obj.astype(dtype) - - off = u.expect2(p.offset) - - if p.zoom is not None: - obj = u.zoom(obj, p.zoom) - - if p.smoothing_mfs is not None: - obj = u.gf(obj, p.smoothing_mfs / 2.35) - - k = 2 * np.pi / lam - ri = p.ref_index - if p.formula is not None or ri is not None: - # use only magnitude of obj and scale to [0 1] - if ri is None: - en = u.keV2m(1e-3)/lam - if u.parallel.master: - logger.info( - 'Queuing cxro database for refractive index in object ' - 'creation with parameters:\n' - 'Formula=%s Energy=%d Density=%.2f' - % (p.formula, en, p.density)) - result = np.array(iofr(p.formula, en, density=p.density)) - else: - result = None - result = u.parallel.bcast(result) - energy, delta, beta = result - ri = - delta + 1j*beta - else: - logger.info("using given refractive index in object creation") - - ob = np.abs(obj).astype(np.float) - ob -= ob.min() - if p.thickness is not None: - ob /= ob.max() / p.thickness - - obj = np.exp(1.j * ob * k * ri) - - shape = u.expect2(shape) - crops = list(-np.array(obj.shape) + shape + 2*np.abs(off)) - obj = u.crop_pad(obj, crops, fillpar=p.fill) - - if p.noise_rms is not None: - n = u.expect2(p.noise_rms) - noise = np.random.normal(1.0, n[0] + 1e-10, obj.shape) * np.exp( - 2j * np.pi * np.random.normal(0.0, n[1] + 1e-10, obj.shape)) - if p.noise_mfs is not None: - noise = u.gf(noise, p.noise_mfs / 2.35) - obj *= noise - - off += np.abs(off) - p.obj = obj[off[0]:off[0]+shape[0], off[1]:off[1]+shape[1]] - - return p - - -def _create_modes(layers, pars): - """ - **DEPRECATED** - """ - p = u.Param(pars) - pr = p.obj - sh_old = pr.shape - if pr.ndim == 2: - ppr = np.zeros((1,) + pr.shape).astype(pr.dtype) - ppr[0] = pr - pr = ppr - elif pr.ndim == 4: - pr = pr[0] - w = p.mode_weights - # press w into 1d flattened array: - w = np.atleast_1d(w).flatten() - w = u.crop_pad(w, [[0, layers-w.shape[0]]], filltype='project') - w /= w.sum() - # make it an array now: flattens - pr = u.crop_pad(pr, [[0, layers-pr.shape[0]]], axes=[0], filltype='project') - # if p.mode_diversity =='noise' - p.mode_weights = w - p.obj = pr * w.reshape((layers, 1, 1)) - return p diff --git a/ptypy/core/xy.py b/ptypy/core/xy.py index 98b531f43..9661b2c0c 100644 --- a/ptypy/core/xy.py +++ b/ptypy/core/xy.py @@ -86,11 +86,11 @@ def from_pars(xypars=None): return None elif str(xypars) == xypars: if xypars in TEMPLATES.keys(): - return from_pars(TEMPLATES[sam]) + return from_pars(TEMPLATES[xypars]) else: raise RuntimeError( 'Template string `%s` for pattern creation is not understood' - % sam) + % xypars) elif type(xypars) in [np.ndarray, list]: return np.array(xypars) else: diff --git a/ptypy/engines/DMOPR.py b/ptypy/custom/DMOPR.py similarity index 94% rename from ptypy/engines/DMOPR.py rename to ptypy/custom/DMOPR.py index 0467cbaef..706198fb4 100644 --- a/ptypy/engines/DMOPR.py +++ b/ptypy/custom/DMOPR.py @@ -9,13 +9,12 @@ :license: GPLv2, see LICENSE for details. """ import numpy as np -from .. import utils as u -from ..utils.verbose import logger -from ..utils import parallel -from .utils import reduce_dimension -from . import register -from .DM import DM -from ..core.manager import OPRModel +from ptypy import utils as u +from ptypy.utils import parallel +from ptypy.engines.utils import reduce_dimension +from ptypy.engines import register +from ptypy.engines.projectional import DM +from ptypy.core.manager import OPRModel __all__ = ['DMOPR'] diff --git a/ptypy/custom/DM_object_regul.py b/ptypy/custom/DM_object_regul.py new file mode 100644 index 000000000..5895a5e9f --- /dev/null +++ b/ptypy/custom/DM_object_regul.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" +An extension plugin of the Difference Map engine +with object regularisation for air/vacuum regions. + +authors: Benedikt J. Daurer +""" +from ptypy.engines import projectional +from ptypy.engines import register +import numpy as np + +@register() +class DM_object_regul(projectional.DM): + """ + An extension of DM with the following additional parameters + + Defaults: + + [object_regul_mask] + default = None + type = ndarray + help = A mask used for regularisation of the object + doc = Numpy.ndarray with same shape as the object that will be casted to a boolean mask + + [object_regul_fill] + default = 0.0 + 0.0j + type = float, complex + help = Fill value for regularisation of the object + doc = Providing a complex number, e.g. 1.0 + 0.1j will replace both real and imaginary parts\ + Providing a floating number, e.g. 0.5 will replace only the phase + + [object_regul_start] + default = None + type = int + help = Number of iterations until object regularisation starts + doc = If None, object regularisation starts at first iteration + + [object_regul_stop] + default = None + type = int + help = Number of iterations after which object regularisation stops + doc = If None, object regularisation stops after last iteration + + """ + + def __init__(self, ptycho_parent, pars=None): + super(DM_object_regul, self).__init__(ptycho_parent, pars) + + def object_update(self): + """ + Replace values inside mask with given fill value. + """ + super().object_update() + do_regul = True + if (self.p.object_regul_start is not None): + do_regul &= (self.curiter >= self.p.object_regul_start) + if (self.p.object_regul_stop is not None): + do_regul &= (self.curiter < self.p.object_regul_stop) + + if (self.p.object_regul_mask is not None) and do_regul: + for name, s in self.ob.storages.items(): + assert s.shape == self.p.object_regul_mask.shape, "Object regulariser mask needs to have same shape as object = {}".format(s.shape) + if isinstance(self.p.object_regul_fill, complex): + s.data[self.p.object_regul_mask.astype(bool)] = self.p.object_regul_fill + elif isinstance(self.p.object_regul_fill, float): + s.data[self.p.object_regul_mask.astype(bool)] = np.abs(s.data[self.p.object_regul_mask.astype(bool)]) * np.exp(1j*self.p.object_regul_fill) diff --git a/ptypy/custom/DM_pycuda_object_regul.py b/ptypy/custom/DM_pycuda_object_regul.py new file mode 100644 index 000000000..124231629 --- /dev/null +++ b/ptypy/custom/DM_pycuda_object_regul.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" +An extension plugin of the accelerated (pycuda) Difference Map engine +with object regularisation for air/vacuum regions. + +authors: Benedikt J. Daurer +""" +from ptypy.accelerate.cuda_pycuda.engines import projectional_pycuda +from ptypy.engines import register +from pycuda import gpuarray +import numpy as np + +@register() +class DM_pycuda_object_regul(projectional_pycuda.DM_pycuda): + """ + An extension of DM_pycuda with the following additional parameters + + Defaults: + + [object_regul_mask] + default = None + type = ndarray + help = A mask used for regularisation of the object + doc = Numpy.ndarray with same shape as the object that will be casted to a complex-valued mask + + [object_regul_fill] + default = 0.0 + 0.0j + type = float, complex + help = Fill value for regularisation of the object + doc = Providing a complex number, e.g. 1.0 + 0.1j will replace both real and imaginary parts\ + Providing a floating number, e.g. 0.5 will replace only the phase + + [object_regul_start] + default = None + type = int + help = Number of iterations until object regularisation starts + doc = If None, object regularisation starts at first iteration + + [object_regul_stop] + default = None + type = int + help = Number of iterations after which object regularisation stops + doc = If None, object regularisation stops after last iteration + + """ + + def __init__(self, ptycho_parent, pars=None): + super(DM_pycuda_object_regul, self).__init__(ptycho_parent, pars) + + def engine_prepare(self): + super().engine_prepare() + if self.p.object_regul_mask is not None: + self.object_mask_gpu = gpuarray.to_gpu(self.p.object_regul_mask.astype(np.complex64)) + + def _setup_kernels(self): + super()._setup_kernels() + from pycuda.elementwise import ElementwiseKernel + self.obj_regul_complex = ElementwiseKernel( + "pycuda::complex *in, pycuda::complex *mask, pycuda::complex fill", + "in[i] = fill*mask[i] + in[i]*(pycuda::complex(1) - mask[i])", + "obj_regulariser_complex") + self.obj_regul_phase = ElementwiseKernel( + "pycuda::complex *in, pycuda::complex *mask, float fill", + "in[i] = pycuda::abs(in[i])*mask[i]*pycuda::exp(fill*pycuda::complex(1)) + in[i]*(pycuda::complex(1) - mask[i])", + "obj_regulariser_phase") + + def object_update(self,*args, **kwargs): + """ + Replace values inside mask with given fill value. + """ + super().object_update(*args,**kwargs) + do_regul = True + if (self.p.object_regul_start is not None): + do_regul &= (self.curiter >= self.p.object_regul_start) + if (self.p.object_regul_stop is not None): + do_regul &= (self.curiter < self.p.object_regul_stop) + if (self.p.object_regul_mask is not None) and do_regul: + for oID, ob in self.ob.storages.items(): + assert ob.shape == self.object_mask_gpu.shape, "Object regulariser mask needs to have same shape as object = {}".format(ob.shape) + if isinstance(self.p.object_regul_fill, complex): + self.obj_regul_complex(ob.gpu, self.object_mask_gpu, self.p.object_regul_fill) + elif isinstance(self.p.object_regul_fill, float): + self.obj_regul_phase(ob.gpu, self.object_mask_gpu, self.p.object_regul_fill) diff --git a/ptypy/engines/MLOPR.py b/ptypy/custom/MLOPR.py similarity index 92% rename from ptypy/engines/MLOPR.py rename to ptypy/custom/MLOPR.py index 15f479fb4..000bb73bf 100644 --- a/ptypy/engines/MLOPR.py +++ b/ptypy/custom/MLOPR.py @@ -12,13 +12,13 @@ """ import numpy as np import time -from .. import utils as u -from ..utils.verbose import logger -from ..utils import parallel -from .utils import reduce_dimension -from . import register -from .ML import ML -from ..core.manager import OPRModel +from ptypy import utils as u +from ptypy.utils.verbose import logger +from ptypy.utils import parallel +from ptypy.engines.utils import reduce_dimension +from ptypy.engines import register +from ptypy.engines.ML import ML +from ptypy.core.manager import OPRModel __all__ = ['MLOPR'] diff --git a/ptypy/custom/__init__.py b/ptypy/custom/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ptypy/engines/ePIE.py b/ptypy/custom/ePIE_parallel.py similarity index 93% rename from ptypy/engines/ePIE.py rename to ptypy/custom/ePIE_parallel.py index 5a34e9e59..1fb3754d5 100644 --- a/ptypy/engines/ePIE.py +++ b/ptypy/custom/ePIE_parallel.py @@ -26,26 +26,25 @@ import time import random -from .. import utils as u -from ..utils.verbose import logger -from ..utils import parallel -from . import BaseEngine, register -from .. import defaults_tree -from ..core.manager import Full, Vanilla +from ptypy import utils as u +from ptypy.utils.verbose import logger +from ptypy.utils import parallel +from ptypy.engines import BaseEngine, register +from ptypy.core.manager import Full, Vanilla -__all__ = ['EPIE'] +__all__ = ['EPIEParallel'] -@register(name = 'ePIE') -class EPIE(BaseEngine): +@register(name = 'ePIEparallel') +class EPIEParallel(BaseEngine): """ - ePIE reconstruction engine. + Parallel ePIE reconstruction engine. Defaults: [name] - default = ePIE + default = ePIEparallel type = str help = doc = @@ -128,7 +127,7 @@ def __init__(self, ptycho_parent, pars=None): """ ePIE reconstruction engine. """ - super(EPIE, self).__init__(ptycho_parent, pars) + super().__init__(ptycho_parent, pars) p = self.DEFAULT.copy() if pars is not None: @@ -144,7 +143,7 @@ def __init__(self, ptycho_parent, pars=None): # Instance attributes self.ob_nodecover = None self.mean_power = None - + self.ptycho.citations.add_article( title='An improved ptychographical phase retrieval algorithm for diffractive imaging', author='Maiden A. and Rodenburg J.', @@ -191,7 +190,7 @@ def engine_prepare(self): mean_power += s.mean_power self.mean_power = mean_power / len(self.di.storages) - + # DEBUGGING: show the actual domain decomposition # if self.curiter == 0: # import matplotlib.pyplot as plt @@ -264,7 +263,7 @@ def engine_iterate(self, num=1): # scale probe such that its mean power equals the mean power of the diffraction data # This stabilizes the ePIE algorithm and prevents the probe from growing too large. pod.probe *= np.sqrt(self.mean_power / u.abs2(pod.probe).mean()) - + # Object update: logger.debug(pre_str + '----- ePIE object update -----') pod.object += (self.p.alpha @@ -342,8 +341,7 @@ def engine_iterate(self, num=1): # and that Ptycho expects. In DM, that dict is overwritten on # every iteration, so we only gather the dicts corresponding to # the last iteration of each contiguous block. - error = parallel.gather_dict(error_dct) - return error + return error_dct def engine_finalize(self): """ @@ -468,14 +466,25 @@ def center_probe(self): Stolen in its entirety from the DM engine. """ if self.p.probe_center_tol is not None: - for name, s in self.pr.S.items(): - c1 = u.mass_center(u.abs2(s.data).sum(0)) + for name, pr_s in self.pr.storages.items(): + c1 = u.mass_center(u.abs2(pr_s.data).sum(0)) + c2 = np.asarray(pr_s.shape[-2:]) // 2 # fft convention should however use geometry instead - c2 = np.asarray(s.shape[-2:]) // 2 if u.norm(c1 - c2) < self.p.probe_center_tol: break # SC: possible BUG here, wrong input parameter - s.data[:] = u.shift_zoom( - s.data, (1.,) * 3, (0, c1[0], c1[1]), (0, c2[0], c2[1])) + pr_s.data[:] = u.shift_zoom(pr_s.data, (1.,)*3, + (0, c1[0], c1[1]), (0, c2[0], c2[1])) + + # shift the object + ob_s = pr_s.views[0].pod.ob_view.storage + ob_s.data[:] = u.shift_zoom(ob_s.data, (1.,)*3, + (0, c1[0], c1[1]), (0, c2[0], c2[1])) + + # shift the exit waves, loop through different exit wave views + for pv in pr_s.views: + pv.pod.exit = u.shift_zoom(pv.pod.exit, (1.,)*2, + (c1[0], c1[1]), (c2[0], c2[1])) + logger.info('Probe recentered from %s to %s' % (str(tuple(c1)), str(tuple(c2)))) diff --git a/ptypy/engines/Bragg3d_engines.py b/ptypy/engines/Bragg3d_engines.py index 2314eccab..87ecfe519 100644 --- a/ptypy/engines/Bragg3d_engines.py +++ b/ptypy/engines/Bragg3d_engines.py @@ -7,7 +7,7 @@ :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. :license: GPLv2, see LICENSE for details. """ -from .DM import DM +from .projectional import DM from . import register from ..core.manager import Bragg3dModel from ..utils import parallel diff --git a/ptypy/engines/ML.py b/ptypy/engines/ML.py index 54f4e9e64..cf38c12a1 100644 --- a/ptypy/engines/ML.py +++ b/ptypy/engines/ML.py @@ -19,9 +19,9 @@ from ..utils import parallel from .utils import Cnorm2, Cdot from . import register -from .base import PositionCorrectionEngine -from .. import defaults_tree -from ..core.manager import Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull +from .base import BaseEngine, PositionCorrectionEngine +from ..core.manager import Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull, GradFull, BlockGradFull + __all__ = ['ML'] @@ -99,10 +99,10 @@ class ML(PositionCorrectionEngine): type = int lowlim = 0 help = Number of iterations before probe update starts - + """ - SUPPORTED_MODELS = [Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull] + SUPPORTED_MODELS = [Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull, GradFull, BlockGradFull] def __init__(self, ptycho_parent, pars=None): """ @@ -153,9 +153,9 @@ def __init__(self, ptycho_parent, pars=None): def engine_initialize(self): """ Prepare for ML reconstruction. - """ + """ super(ML, self).engine_initialize() - + # Object gradient and minimization direction self.ob_grad = self.ob.copy(self.ob.ID + '_grad', fill=0.) self.ob_grad_new = self.ob.copy(self.ob.ID + '_grad_new', fill=0.) @@ -233,9 +233,10 @@ def engine_iterate(self, num=1): # probe/object rescaling if self.p.scale_precond: cn2_new_pr_grad = Cnorm2(new_pr_grad) + cn2_new_ob_grad = Cnorm2(new_ob_grad) if cn2_new_pr_grad > 1e-5: - scale_p_o = (self.p.scale_probe_object * Cnorm2(new_ob_grad) - / Cnorm2(new_pr_grad)) + scale_p_o = (self.p.scale_probe_object * cn2_new_ob_grad + / cn2_new_pr_grad) else: scale_p_o = self.p.scale_probe_object if self.scale_p_o is None: @@ -263,11 +264,12 @@ def engine_iterate(self, num=1): bt = max(0, bt_num/bt_denom) - # verbose(3,'Polak-Ribiere coefficient: %f ' % bt) + # logger.info('Polak-Ribiere coefficient: %f ' % bt) self.ob_grad << new_ob_grad self.pr_grad << new_pr_grad + dt = self.ptycho.FType # 3. Next conjugate self.ob_h *= bt / self.tmin @@ -277,6 +279,7 @@ def engine_iterate(self, num=1): s.data[:] -= self.smooth_gradient(self.ob_grad.storages[name].data) else: self.ob_h -= self.ob_grad + self.pr_h *= bt / self.tmin self.pr_grad *= self.scale_p_o self.pr_h -= self.pr_grad @@ -286,20 +289,23 @@ def engine_iterate(self, num=1): t2 = time.time() B = self.ML_model.poly_line_coeffs(self.ob_h, self.pr_h) tc += time.time() - t2 - #print(B, Cnorm2(self.ob_h), Cnorm2(self.ob_grad), Cnorm2(self.pr_h), Cnorm2(self.pr_grad)) + if np.isinf(B).any() or np.isnan(B).any(): logger.warning( 'Warning! inf or nan found! Trying to continue...') B[np.isinf(B)] = 0. B[np.isnan(B)] = 0. - self.tmin = -.5 * B[1] / B[2] + self.tmin = dt(-.5 * B[1] / B[2]) self.ob_h *= self.tmin self.pr_h *= self.tmin self.ob += self.ob_h self.pr += self.pr_h # Newton-Raphson loop would end here + # Position correction + self.position_update() + # Allow for customized modifications at the end of each iteration self._post_iterate_update() @@ -320,7 +326,6 @@ def engine_finalize(self): """ Delete temporary containers. """ - super(ML, self).engine_finalize() del self.ptycho.containers[self.ob_grad.ID] del self.ob_grad del self.ptycho.containers[self.ob_grad_new.ID] @@ -490,7 +495,7 @@ def new_grad(self): / (w * Imodel**2).sum()) Imodel *= self.float_intens_coeff[dname] - DI = Imodel - I + DI = np.double(Imodel) - I # Second pod loop: gradients computation LLL = np.sum((w * DI**2).astype(np.float64)) @@ -516,7 +521,6 @@ def new_grad(self): self.ob_grad.storages[name].data += self.regularizer.grad( s.data) LL += self.regularizer.LL - self.LL = LL / self.tot_measpts return error_dct @@ -566,9 +570,10 @@ def poly_line_coeffs(self, ob_h, pr_h): A1 *= self.float_intens_coeff[dname] A2 *= self.float_intens_coeff[dname] - A0 -= pod.upsample(I) + A0 = np.double(A0) - pod.upsample(I) + #A0 -= pod.upsample(I) w = pod.upsample(w) - + B[0] += np.dot(w.flat, (A0**2).flat) * Brenorm B[1] += np.dot(w.flat, (2 * A0 * A1).flat) * Brenorm B[2] += np.dot(w.flat, (A1**2 + 2*A0*A2).flat) * Brenorm diff --git a/ptypy/engines/__init__.py b/ptypy/engines/__init__.py index 9704d283e..96fe8146d 100644 --- a/ptypy/engines/__init__.py +++ b/ptypy/engines/__init__.py @@ -10,10 +10,11 @@ :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. :license: GPLv2, see LICENSE for details. """ -from .. import utils as u -from .. import defaults_tree + from .utils import * + + ENGINES = dict() @@ -38,16 +39,11 @@ def by_name(name): from .base import BaseEngine, DEFAULT_iter_info # These imports should be executable separately -from . import DM -from . import DM_simple -from . import DMOPR +from . import projectional +from . import stochastic from . import ML -from . import MLOPR -from . import dummy -from . import ePIE from . import Bragg3d_engines - # dynamic load, maybe discarded in future -dynamic_load('./', ['BaseEngine', 'PositionCorrectionEngine'] + list(ENGINES.keys()), True) -dynamic_load('~/.ptypy/', ['BaseEngine', 'PositionCorrectionEngine'] + list(ENGINES.keys()), True) +#dynamic_load('./', ['BaseEngine', 'PositionCorrectionEngine'] + list(ENGINES.keys()), True) +#dynamic_load('~/.ptypy/', ['BaseEngine', 'PositionCorrectionEngine'] + list(ENGINES.keys()), True) diff --git a/ptypy/engines/base.py b/ptypy/engines/base.py index 7eed876a9..bf6d32e3f 100644 --- a/ptypy/engines/base.py +++ b/ptypy/engines/base.py @@ -13,15 +13,14 @@ from .. import utils as u from ..utils import parallel from ..utils.verbose import logger, headerline, log -from ..utils.descriptor import EvalDescriptor -from .posref import AnnealingRefine -import gc +from .posref import AnnealingRefine, GridSearchRefine __all__ = ['BaseEngine', 'Base3dBraggEngine', 'DEFAULT_iter_info', 'PositionCorrectionEngine'] DEFAULT_iter_info = u.Param( iteration=0, iterations=0, + numiter=0, engine='None', duration=0., error=np.zeros((3,)) @@ -65,6 +64,12 @@ class BaseEngine(object): help = Valid probe area in frequency domain as fraction of the probe frame doc = Defines a circular area centered on the probe frame (in frequency domain), in which the probe is allowed to be nonzero. + [record_local_error] + default = False + type = bool + help = If True, save the local map of errors into the runtime dictionary. + userlevel = 2 + """ # Define with which models this engine can work. @@ -111,7 +116,7 @@ def initialize(self): """ logger.info('\n' + headerline('Starting %s-algorithm.' - % str(type(self).__name__), 'l', '=') + '\n') + % (self.p.name), 'l', '=') + '\n') logger.info('Parameter set:') logger.info(u.verbose.report(self.p, noheader=True).strip()) logger.info(headerline('', 'l', '=')) @@ -134,6 +139,17 @@ def initialize(self): self._probe_fourier_support = {} # Call engine specific initialization # TODO: Maybe child classes should be calling this? + + # # Make sure all the pods are supported + # for label_, pod_ in self.pods.items(): + # if not pod_.model.__class__ in self.SUPPORTED_MODELS: + # raise Exception('Model %s not supported by engine' % pod_.model.__class__) + + # Make sure all scan models are supported + for model in self.ptycho.model.scans.values(): + if not model.__class__ in self.SUPPORTED_MODELS: + raise Exception('Model %s not supported by engine %s' % (model.__class__,self.p.name)) + self.engine_initialize() def prepare(self): @@ -142,11 +158,6 @@ def prepare(self): """ self.finished = False - # Make sure all the pods are supported - for label_, pod_ in self.pods.items(): - if not pod_.model.__class__ in self.SUPPORTED_MODELS: - raise Exception('Model %s not supported by engine' % pod_.model.__class__) - # Calculate probe support # an individual support for each storage is calculated in saved # in the dict self.probe_support @@ -177,16 +188,16 @@ def support_constraint(self, storage=None): for s in self.pr.storages.values(): self.support_contraint(s) - # Real space - support = self._probe_support.get(storage.ID) - if support is not None: - storage.data *= support - # Fourier space support = self._probe_fourier_support.get(storage.ID) if support is not None: storage.data[:] = np.fft.ifft2(support * np.fft.fft2(storage.data)) + # Real space + support = self._probe_support.get(storage.ID) + if support is not None: + storage.data *= support + def iterate(self, num=None): """ Compute one or several iterations. @@ -225,7 +236,7 @@ def iterate(self, num=None): logger.warning("""Engine %s did not increase iteration counter `self.curiter` internally. Accessing this attribute in that - engine is inaccurate""" % self.__class__.__name__) + engine is inaccurate""" % self.p.name) self.curiter += niter_contiguous @@ -233,7 +244,7 @@ def iterate(self, num=None): logger.error("""Engine %s increased iteration counter `self.curiter` by %d instead of %d. This may lead to - unexpected behaviour""" % (self.__class__.__name__, + unexpected behaviour""" % (self.p.name, self.curiter-it, niter_contiguous)) else: @@ -258,13 +269,15 @@ def _fill_runtime(self): info = dict( iteration=self.curiter, iterations=self.alliter, - engine=type(self).__name__, + numiter=self.numiter, + engine=self.p.name, duration=time.time() - self.t, error=error ) self.ptycho.runtime.iter_info.append(info) - self.ptycho.runtime.error_local = local_error + if self.p.record_local_error: + self.ptycho.runtime.error_local = local_error def finalize(self): """ @@ -316,6 +329,11 @@ class PositionCorrectionEngine(BaseEngine): type = Param, bool help = If True refine scan positions + [position_refinement.method] + default = Annealing + type = str + help = Annealing or GridSearch + [position_refinement.start] default = None type = int @@ -343,6 +361,11 @@ class PositionCorrectionEngine(BaseEngine): type = float help = Distance from original position per random shift [m] + [position_refinement.amplitude_decay] + default = True + type = bool + help = After each interation, multiply amplitude by factor (stop - iteration) / (stop - start) + [position_refinement.max_shift] default = 0.000002 type = float @@ -359,6 +382,11 @@ class PositionCorrectionEngine(BaseEngine): help = record movement of positions """ + POSREF_ENGINES = { + "Annealing": AnnealingRefine, + "GridSearch": GridSearchRefine + } + def __init__(self, ptycho_parent, pars): """ Position Correction engine. @@ -387,18 +415,23 @@ def engine_initialize(self): if (self.p.position_refinement.start is None) and (self.p.position_refinement.stop is None): self.do_position_refinement = False else: + for label, scan in self.ptycho.model.scans.items(): + if self.p.position_refinement.amplitude < scan.geometries[0].resolution[0]: + self.do_position_refinement = False + log(3,"Failed to initialise position refinement, search amplitude is smaller than the resolution") + return self.do_position_refinement = True - log(3, "Initialising position refinement") + log(3, "Initialising position refinement (%s)" %self.p.position_refinement.method) # Enlarge object arrays, # This can be skipped though if the boundary is less important for name, s in self.ob.storages.items(): - s.padding = int(self.p.position_refinement.max_shift // np.max(s.psize)) - s.reformat() + s.padding = int(self.p.position_refinement.max_shift // np.max(s.psize)) + s.reformat() - # this could be some kind of dictionary lookup if we want to add more - self.position_refinement = AnnealingRefine(self.p.position_refinement, self.ob, metric=self.p.position_refinement.metric) - log(3, "Position refinement initialised") + # Choose position refinement engine from dictionary + PosrefEngine = self.POSREF_ENGINES[self.p.position_refinement.method] + self.position_refinement = PosrefEngine(self.p.position_refinement, self.ob, metric=self.p.position_refinement.metric) self.ptycho.citations.add_article(**self.position_refinement.citation_dictionary) if self.p.position_refinement.stop is None: self.p.position_refinement.stop = self.p.numiter @@ -409,7 +442,7 @@ def position_update(self): """ Position refinement update. """ - if self.do_position_refinement is False: + if not self.do_position_refinement: return do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 @@ -438,6 +471,8 @@ def engine_finalize(self): """ if self.do_position_refinement is False: return + if self.p.position_refinement.record is False: + return # Gather all new positions from each node coords = {} @@ -453,6 +488,8 @@ def engine_finalize(self): if v.pod.pr_view.layer == 0: v.coord = coords[v.ID] + self.ptycho.record_positions = True + class Base3dBraggEngine(BaseEngine): """ diff --git a/ptypy/engines/posref.py b/ptypy/engines/posref.py index c0f12a857..8528a3cc0 100644 --- a/ptypy/engines/posref.py +++ b/ptypy/engines/posref.py @@ -41,14 +41,63 @@ def update_constraints(self, iteration): iteration : int The current iteration of the engine. ''' + start, end = self.p.start, self.p.stop + # Compute the maximum shift allowed at this iteration + self.max_shift_dist = self.p.amplitude + if self.p.amplitude_decay: + self.max_shift_dist *= (end - iteration) / (end - start) - raise NotImplementedError('This method needs to be overridden in order to position correct') + def estimate_fourier_metric(self, di_view, obj): + ''' + Calculates error based on DM fourier error estimate. + + Parameters + ---------- + di_view : ptypy.core.classes.View + A diffraction view for which we wish to calculate the error. + + obj : numpy.ndarray + The current calculated object for which we wish to evaluate the error against. + Returns + ------- + np.float + The calculated fourier error + ''' + af2 = np.zeros_like(di_view.data) + for name, pod in di_view.pods.items(): + af2 += pod.downsample(u.abs2(pod.fw(pod.probe*obj))) + return np.sum(di_view.pod.mask * (np.sqrt(af2) - np.sqrt(np.abs(di_view.data)))**2) / di_view.pod.mask.sum() + + def estimate_photon_metric(self, di_view, obj): + ''' + Calculates error based on reduced likelihood estimate. + + Parameters + ---------- + di_view : ptypy.core.classes.View + A diffraction view for which we wish to calculate the error. + + obj : numpy.ndarray + The current calculated object for which we wish to evaluate the error against. + Returns + ------- + np.float + The calculated fourier error + ''' + af2 = np.zeros_like(di_view.data) + for name, pod in di_view.pods.items(): + af2 += pod.downsample(u.abs2(pod.fw(pod.probe*obj))) + return (np.sum(di_view.pod.mask * (af2 - di_view.data)**2 / (di_view.data + 1.)) / np.prod(af2.shape)) def cleanup(self): ''' Cleans up every iteration ''' + @property + def citation_dictionary(self): + return {} + class AnnealingRefine(PositionRefine): @@ -85,48 +134,6 @@ def __init__(self, position_refinement_parameters, Cobj, metric="fourier"): else: raise NotImplementedError("Metric %s is currently not implemented" %metric) - def estimate_fourier_metric(self, di_view, obj): - ''' - Calculates error based on DM fourier error estimate. - - Parameters - ---------- - di_view : ptypy.core.classes.View - A diffraction view for which we wish to calculate the error. - - obj : numpy.ndarray - The current calculated object for which we wish to evaluate the error against. - Returns - ------- - np.float - The calculated fourier error - ''' - af2 = np.zeros_like(di_view.data) - for name, pod in di_view.pods.items(): - af2 += pod.downsample(u.abs2(pod.fw(pod.probe*obj))) - return np.sum(di_view.pod.mask * (np.sqrt(af2) - np.sqrt(np.abs(di_view.data)))**2) - - def estimate_photon_metric(self, di_view, obj): - ''' - Calculates error based on reduced likelihood estimate. - - Parameters - ---------- - di_view : ptypy.core.classes.View - A diffraction view for which we wish to calculate the error. - - obj : numpy.ndarray - The current calculated object for which we wish to evaluate the error against. - Returns - ------- - np.float - The calculated fourier error - ''' - af2 = np.zeros_like(di_view.data) - for name, pod in di_view.pods.items(): - af2 += pod.downsample(u.abs2(pod.fw(pod.probe*obj))) - return (np.sum(di_view.pod.mask * (af2 - di_view.data)**2 / (di_view.data + 1.)) / np.prod(af2.shape)) - def update_view_position(self, di_view): ''' Refines the positions by the following algorithm: @@ -183,30 +190,124 @@ def update_view_position(self, di_view): continue new_error = self.fourier_error(di_view, data) - + if new_error < error: # keep error = new_error coord = new_coord log(4, "Position correction: %s, coord: %s, delta: %s" % (di_view.ID, coord, delta)) - + ob_view.coord = coord ob_view.storage.update_views(ob_view) return coord - initial_coord - def update_constraints(self, iteration): + @property + def citation_dictionary(self): + return { + "title" : 'An annealing algorithm to correct positioning errors in ptychography', + "author" : 'Maiden et al.', + "journal" : 'Ultramicroscopy', + "volume" : 120, + "year" : 2012, + "page" : 64, + "doi" : '10.1016/j.ultramic.2012.06.001', + "comment" : 'Position Refinement using annealing algorithm'} + +class GridSearchRefine(PositionRefine): + + def __init__(self, position_refinement_parameters, Cobj, metric="fourier"): ''' + Grid Search Position Refinement. - Parameters ---------- - iteration : int - The current iteration of the engine. + position_refinement_parameters : ptypy.utils.parameters.Param + The parameter tree for the refinement + + Cobj : ptypy.core.classes.Container + The current pbject container object + metric : str + "fourier" or "photon" ''' + super(GridSearchRefine, self).__init__(position_refinement_parameters) - start, end = self.p.start, self.p.stop + self.Cobj = Cobj # take a reference here. It would be cool if we could make this read-only or something - # Compute the maximum shift allowed at this iteration - self.max_shift_dist = self.p.amplitude * (end - iteration) / (end - start) + # Updated before each iteration by self.update_constraints + self.max_shift_dist = None + + # Choose metric for fourier error + if metric == "fourier": + self.fourier_error = self.estimate_fourier_metric + elif metric == "photon": + self.fourier_error = self.estimate_photon_metric + else: + raise NotImplementedError("Metric %s is currently not implemented" %metric) + + def update_view_position(self, di_view): + ''' + Refines the positions by the following algorithm: + + Calculates all shifts in a given radius around the original position and calculates the fourier error. + If the fourier error decreased the calculated postion will be used as new position. + + Parameters + ---------- + di_view : ptypy.core.classes.View + A diffraction view that we wish to refine. + + Returns + ------- + numpy.ndarray + A length 2 numpy array with the position increments for x and y co-ordinates respectively + ''' + # there might be more than one object view + ob_view = di_view.pod.ob_view + + initial_coord = ob_view.coord.copy() + coord = initial_coord + psize = ob_view.psize.copy() + + # if you cannot move far, do nothing + if np.max(psize) >= self.max_shift_dist: + return np.zeros((2,)) + + # This can be optimized by saving existing iteration fourier error... + error = self.fourier_error(di_view, ob_view.data) + + max_shift_pix = np.ceil(self.max_shift_dist / np.min(psize)) + max_bound_pix = np.ceil(self.p.max_shift / np.min(psize)) + + # Create the search grid + deltas = np.mgrid[-max_shift_pix:max_shift_pix+1:1, + -max_shift_pix:max_shift_pix+1:1] + within_bound = (deltas[0]**2 + deltas[1]**2) < (max_bound_pix**2) + deltas = (deltas[:,within_bound] * np.min(psize)).T + + for i in range(deltas.shape[0]): + # Current shift + delta = deltas[i] + + # Move view to new position + new_coord = initial_coord + delta + ob_view.coord = new_coord + ob_view.storage.update_views(ob_view) + data = ob_view.data + + # catch bad slicing + if not np.allclose(data.shape, ob_view.shape): + continue + + new_error = self.fourier_error(di_view, data) + + if new_error < error: + # keep + error = new_error + coord = new_coord + log(4, "Position correction: %s, coord: %s, delta: %s" % (di_view.ID, coord, delta)) + + ob_view.coord = coord + ob_view.storage.update_views(ob_view) + return coord - initial_coord @property def citation_dictionary(self): @@ -218,4 +319,4 @@ def citation_dictionary(self): "year" : 2012, "page" : 64, "doi" : '10.1016/j.ultramic.2012.06.001', - "comment" : 'Position Refinement using annealing algorithm'} + "comment" : 'Position Refinement using annealing algorithm'} \ No newline at end of file diff --git a/ptypy/engines/projectional.py b/ptypy/engines/projectional.py new file mode 100644 index 000000000..89dddd675 --- /dev/null +++ b/ptypy/engines/projectional.py @@ -0,0 +1,553 @@ +# -*- coding: utf-8 -*- +""" +Difference Map reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import numpy as np +import time +from .. import utils as u +from ..utils.verbose import logger, log +from ..utils import parallel +from .utils import projection_update_generalized, log_likelihood +from . import register +from .base import PositionCorrectionEngine +from ..core.manager import Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull + +__all__ = ['DM', 'RAAR'] + +class _ProjectionEngine(PositionCorrectionEngine): + """ + Defaults: + + [probe_update_start] + default = 2 + type = int + lowlim = 0 + help = Number of iterations before probe update starts + + [subpix_start] + default = 0 + type = int + lowlim = 0 + help = Number of iterations before starting subpixel interpolation + + [subpix] + default = 'linear' + type = str + help = Subpixel interpolation; 'fourier','linear' or None for no interpolation + + [update_object_first] + default = True + type = bool + help = If True update object before probe + + [overlap_converge_factor] + default = 0.05 + type = float + lowlim = 0.0 + help = Threshold for interruption of the inner overlap loop + doc = The inner overlap loop refines the probe and the object simultaneously. This loop is escaped as soon as the overall change in probe, relative to the first iteration, is less than this value. + + [overlap_max_iterations] + default = 10 + type = int + lowlim = 1 + help = Maximum of iterations for the overlap constraint inner loop + + [probe_inertia] + default = 1e-9 + type = float + lowlim = 0.0 + help = Weight of the current probe estimate in the update + + [object_inertia] + default = 1e-4 + type = float + lowlim = 0.0 + help = Weight of the current object in the update + + [fourier_power_bound] + default = None + type = float + help = If rms error of model vs diffraction data is smaller than this value, Fourier constraint is met + doc = For Poisson-sampled data, the theoretical value for this parameter is 1/4. Set this value higher for noisy data. By default, power bound is calculated using fourier_relax_factor + + [fourier_relax_factor] + default = 0.05 + type = float + lowlim = 0.0 + help = A factor used to calculate the Fourier power bound as 0.25 * fourier_relax_factor**2 * maximum power in diffraction data + doc = Set this value higher for noisy data. + + [obj_smooth_std] + default = None + type = float + lowlim = 0 + help = Gaussian smoothing (pixel) of the current object prior to update + doc = If None, smoothing is deactivated. This smoothing can be used to reduce the amplitude of spurious pixels in the outer, least constrained areas of the object. + + [clip_object] + default = None + type = tuple + help = Clip object amplitude into this interval + + [probe_center_tol] + default = None + type = float + lowlim = 0.0 + help = Pixel radius around optical axes that the probe mass center must reside in + + [compute_log_likelihood] + default = True + type = bool + help = A switch for computing the log-likelihood error (this can impact the performance of the engine) + + """ + + SUPPORTED_MODELS = [Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull] + + def __init__(self, ptycho_parent, pars=None): + """ + Difference map reconstruction engine. + """ + super().__init__(ptycho_parent, pars) + + self._a = 0. + self._b = 1. + self._c = 1. + + self.error = None + + self.ob_buf = None + self.ob_nrm = None + self.ob_viewcover = None + + self.pr_buf = None + self.pr_nrm = None + + self.pbound = None + + # Required to get proper normalization of object inertia + # The actual value is computed in engine_prepare + # Another possibility would be to use the maximum value of all probe storages. + self.mean_power = None + + + def engine_initialize(self): + """ + Prepare for reconstruction. + """ + super().engine_initialize() + + self.error = [] + + # Generate container copies + self.ob_buf = self.ob.copy(self.ob.ID + '_alt', fill=0.) + self.ob_nrm = self.ob.copy(self.ob.ID + '_nrm', fill=0., dtype='real') + self.ob_viewcover = self.ob.copy(self.ob.ID + '_vcover', fill=0.) + + self.pr_buf = self.pr.copy(self.pr.ID + '_alt', fill=0.) + self.pr_nrm = self.pr.copy(self.pr.ID + '_nrm', fill=0., dtype='real') + + def engine_prepare(self): + + """ + Last minute initialization. + + Everything that needs to be recalculated when new data arrives. + """ + if self.ptycho.new_data: + + # recalculate everything + mean_power = 0. + self.pbound_scan = {} + for s in self.di.storages.values(): + if self.p.fourier_power_bound is None: + pb = .25 * self.p.fourier_relax_factor**2 * s.pbound_stub + else: + pb = self.p.fourier_power_bound + log(4, "power bound for scan %s = %f" %(s.label, pb)) + if not self.pbound_scan.get(s.label): + self.pbound_scan[s.label] = pb + else: + self.pbound_scan[s.label] = max(pb, self.pbound_scan[s.label]) + mean_power += s.mean_power + self.mean_power = mean_power / len(self.di.storages) + + # Fill object with coverage of views + for name, s in self.ob_viewcover.storages.items(): + s.fill(s.get_view_coverage()) + + def engine_iterate(self, num=1): + """ + Compute `num` iterations. + """ + to = 0. + tf = 0. + tp = 0. + for it in range(num): + t1 = time.time() + + # Fourier update + error_dct = self.fourier_update() + + t2 = time.time() + tf += t2 - t1 + + # Overlap update + self.overlap_update() + + # Recenter the probe + self.center_probe() + + t3 = time.time() + to += t3 - t2 + + # Position update + self.position_update() + + t4 = time.time() + tp += t4 - t3 + + # count up + self.curiter +=1 + + logger.info('Time spent in Fourier update: %.2f' % tf) + logger.info('Time spent in Overlap update: %.2f' % to) + logger.info('Time spent in Position update: %.2f' % tp) + return error_dct + + def engine_finalize(self): + """ + Try deleting ever helper container. + """ + super().engine_finalize() + + containers = [ + self.ob_buf, + self.ob_nrm, + self.ob_viewcover, + self.pr_buf, + self.pr_nrm] + + for c in containers: + logger.debug('Attempt to remove container %s' % c.ID) + del self.ptycho.containers[c.ID] + # IDM.used.remove(c.ID) + + del self.ob_buf + del self.ob_nrm + del self.ob_viewcover + del self.pr_buf + del self.pr_nrm + + del containers + + def fourier_update(self): + """ + DM Fourier constraint update (including DM step). + """ + error_dct = {} + for name, di_view in self.di.views.items(): + if not di_view.active: + continue + #pbound = self.pbound[di_view.storage.ID] + pbound = self.pbound_scan[di_view.storage.label] + """ + error_dct[name] = basic_fourier_update(di_view, + pbound=pbound, + alpha=self.p.alpha, + LL_error=self.p.compute_log_likelihood) + """ + err_fmag, err_exit = projection_update_generalized(di_view, self._a, self._b, self._c, pbound) + if self.p.compute_log_likelihood: + err_phot = log_likelihood(di_view) + else: + err_phot = 0. + error_dct[name] = np.array([err_fmag, err_phot, err_exit]) + + return error_dct + + def clip_object(self, ob): + # Clip object (This call takes like one ms. Not time critical) + if self.p.clip_object is not None: + clip_min, clip_max = self.p.clip_object + ampl_obj = np.abs(ob.data) + phase_obj = np.exp(1j * np.angle(ob.data)) + too_high = (ampl_obj > clip_max) + too_low = (ampl_obj < clip_min) + ob.data[too_high] = clip_max * phase_obj[too_high] + ob.data[too_low] = clip_min * phase_obj[too_low] + + def overlap_update(self): + """ + DM overlap constraint update. + """ + # Condition to update probe + do_update_probe = (self.p.probe_update_start <= self.curiter) + + for inner in range(self.p.overlap_max_iterations): + pre_str = 'Iteration (Overlap) #%02d: ' % inner + + # Update object first + if self.p.update_object_first or (inner > 0) or not do_update_probe: + # Update object + log(4, pre_str + '----- object update -----') + self.object_update() + + # Exit if probe should not be updated yet + if not do_update_probe: + break + + # Update probe + log(4, pre_str + '----- probe update -----') + change = self.probe_update() + log(4, pre_str + 'change in probe is %.3f' % change) + + # Stop iteration if probe change is small + if change < self.p.overlap_converge_factor: + break + + def center_probe(self): + if self.p.probe_center_tol is not None: + for name, pr_s in self.pr.storages.items(): + c1 = u.mass_center(u.abs2(pr_s.data).sum(0)) + c2 = np.asarray(pr_s.shape[-2:]) // 2 + # fft convention should however use geometry instead + if u.norm(c1 - c2) < self.p.probe_center_tol: + break + # SC: possible BUG here, wrong input parameter + pr_s.data[:] = u.shift_zoom(pr_s.data, (1.,)*3, + (0, c1[0], c1[1]), (0, c2[0], c2[1])) + + # shift the object + ob_s = pr_s.views[0].pod.ob_view.storage + ob_s.data[:] = u.shift_zoom(ob_s.data, (1.,)*3, + (0, c1[0], c1[1]), (0, c2[0], c2[1])) + + # shift the exit waves, loop through different exit wave views + for pv in pr_s.views: + pv.pod.exit = u.shift_zoom(pv.pod.exit, (1.,)*2, + (c1[0], c1[1]), (c2[0], c2[1])) + + log(4,'Probe recentered from %s to %s' + % (str(tuple(c1)), str(tuple(c2)))) + + def object_update(self): + """ + DM object update. + """ + ob = self.ob + ob_nrm = self.ob_nrm + + # Fill container + if not parallel.master: + ob.fill(0.0) + ob_nrm.fill(0.) + else: + for name, s in self.ob.storages.items(): + # The amplitude of the regularization term has to be scaled with the + # power of the probe (which is estimated from the power in diffraction patterns). + # This estimate assumes that the probe power is uniformly distributed through the + # array and therefore underestimate the strength of the probe terms. + cfact = self.p.object_inertia * self.mean_power + if self.p.obj_smooth_std is not None: + log(4, 'Smoothing object, average cfact is %.2f' + % np.mean(cfact).real) + smooth_mfs = [0, + self.p.obj_smooth_std, + self.p.obj_smooth_std] + s.data[:] = cfact * u.c_gf(s.data, smooth_mfs) + else: + s.data[:] = s.data * cfact + + ob_nrm.storages[name].fill(cfact) + + # DM update per node + for name, pod in self.pods.items(): + if not pod.active: + continue + pod.object += pod.probe.conj() * pod.exit * pod.object_weight + ob_nrm[pod.ob_view] += u.abs2(pod.probe) * pod.object_weight + + # Distribute result with MPI + for name, s in self.ob.storages.items(): + # Get the np arrays + nrm = ob_nrm.storages[name].data + parallel.allreduce(s.data) + parallel.allreduce(nrm) + s.data /= nrm + + # A possible (but costly) sanity check would be as follows: + # if all((np.abs(nrm)-np.abs(cfact))/np.abs(cfact) < 1.): + # logger.warning('object_inertia seem too high!') + self.clip_object(s) + + def probe_update(self): + """ + DM probe update. + """ + pr = self.pr + pr_nrm = self.pr_nrm + pr_buf = self.pr_buf + + # Fill container + # "cfact" fill + # BE: was this asymmetric in original code + # only because of the number of MPI nodes ? + if parallel.master: + for name, s in pr.storages.items(): + # Instead of Npts_scan, the number of views should be considered + # Please note that a call to s.views may be + # slow for many views in the probe. + cfact = self.p.probe_inertia * len(s.views) / s.data.shape[0] + s.data[:] = cfact * s.data + pr_nrm.storages[name].fill(cfact) + else: + pr.fill(0.0) + pr_nrm.fill(0.0) + + # DM update per node + for name, pod in self.pods.items(): + if not pod.active: + continue + pod.probe += pod.object.conj() * pod.exit * pod.probe_weight + pr_nrm[pod.pr_view] += u.abs2(pod.object) * pod.probe_weight + + change = 0. + + # Distribute result with MPI + for name, s in pr.storages.items(): + # MPI reduction of results + nrm = pr_nrm.storages[name].data + parallel.allreduce(s.data) + parallel.allreduce(nrm) + s.data /= nrm + + # Apply probe support if requested + self.support_constraint(s) + + # Compute relative change in probe + buf = pr_buf.storages[name].data + change += u.norm2(s.data - buf) / u.norm2(s.data) + + # Fill buffer with new probe + buf[:] = s.data + + return np.sqrt(change / len(pr.storages)) + + +class DMMixin: + + """ + Defaults: + + [alpha] + default = 1. + type = float + lowlim = 0.0 + help = Mix parameter between Difference Map (alpha=1.) and Alternating Projections (alpha=0.) + """ + + def __init__(self, alpha): + self._alpha = 1. + self._a = -alpha + self._b = 1 + self._c = 1.+alpha + self.alpha = alpha + self.article = dict( + title='Probe retrieval in ptychographic coherent diffractive imaging', + author='Thibault et al.', + journal='Ultramicroscopy', + volume=109, + year=2009, + page=338, + doi='10.1016/j.ultramic.2008.12.011', + comment='The difference map reconstruction algorithm', + ) + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, alpha): + self._alpha = alpha + self._a = -alpha + self._b = 1 + self._c = 1.+alpha + +class RAARMixin: + """ + Defaults: + + [beta] + default = 0.75 + type = float + lowlim = 0.0 + help = Beta parameter for RAAR algorithm + """ + + def __init__(self, beta): + self._beta = 1. + self._a = 1. - 2. * beta + self._b = beta + self._c = 2 + self.beta = beta + + @property + def beta(self): + return self._beta + + @beta.setter + def beta(self, beta): + self._beta = beta + self._a = 1. - 2. * beta + self._b = beta + self._c = 2 + + +@register() +class DM(_ProjectionEngine, DMMixin): + """ + A full-fledged Difference Map engine. + + Defaults: + + [name] + default = DM + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + _ProjectionEngine.__init__(self, ptycho_parent, pars) + DMMixin.__init__(self, self.p.alpha) + ptycho_parent.citations.add_article(**self.article) + + +@register() +class RAAR(_ProjectionEngine, RAARMixin): + """ + A RAAR engine. + + Defaults: + + [name] + default = RAAR + type = str + help = + doc = + + """ + + def __init__(self, ptycho_parent, pars=None): + + _ProjectionEngine.__init__(self, ptycho_parent, pars) + RAARMixin.__init__(self, self.p.beta) diff --git a/ptypy/engines/stochastic.py b/ptypy/engines/stochastic.py new file mode 100644 index 000000000..c1c64c378 --- /dev/null +++ b/ptypy/engines/stochastic.py @@ -0,0 +1,476 @@ +# -*- coding: utf-8 -*- +""" +Stochastic reconstruction engine. + +This file is part of the PTYPY package. + + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import numpy as np +import time +from .. import utils as u +from ..utils.verbose import logger, log +from ..utils import parallel +from .utils import projection_update_generalized, log_likelihood +from .base import PositionCorrectionEngine +from . import register +from ..core.manager import Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull, GradFull, BlockGradFull + +__all__ = ['EPIE', 'SDR'] + +class _StochasticEngine(PositionCorrectionEngine): + """ + The base implementation of a stochastic algorithm for ptychography + + Defaults: + + [probe_update_start] + default = 0 + type = int + lowlim = 0 + help = Number of iterations before probe update starts + + [probe_center_tol] + default = None + type = float + lowlim = 0.0 + help = Pixel radius around optical axes that the probe mass center must reside in + + [compute_log_likelihood] + default = True + type = bool + help = A switch for computing the log-likelihood error (this can impact the performance of the engine) + + """ + + def __init__(self, ptycho_parent, pars=None): + """ + Stochastic Douglas-Rachford reconstruction engine. + """ + super().__init__(ptycho_parent, pars) + if parallel.MPIenabled: + raise NotImplementedError("The stochastic engines are not compatible with MPI") + + # Adjustment parameters for fourier update + self._a = 0 + self._b = 1 + self._c = 1 + + # Adjustment parameters for probe update + self._pr_a = 0.0 + self._pr_b = 1.0 + + # Adjustment parameters for object update + self._ob_a = 0.0 + self._ob_b = 1.0 + + # By default the object norm is based on the local object + self._object_norm_is_global = False + + def engine_prepare(self): + """ + Last minute initialization. + Everything that needs to be recalculated when new data arrives. + """ + pass + + def engine_iterate(self, num=1): + """ + Compute one iteration. + """ + vieworder = list(self.di.views.keys()) + vieworder.sort() + rng = np.random.default_rng() + + for it in range(num): + + error_dct = {} + rng.shuffle(vieworder) + + for name in vieworder: + view = self.di.views[name] + if not view.active: + continue + + # Position update + self.position_update_local(view) + + # Fourier update + error_dct[name] = self.fourier_update(view) + + # A copy of the old exit wave + exit_wave = {} + for name, pod in view.pods.items(): + exit_wave[name] = pod.object * pod.probe + + # Object update + self.object_update(view, exit_wave) + + # Probe update + self.probe_update(view, exit_wave) + + # Recenter the probe + self.center_probe() + + self.curiter += 1 + + return error_dct + + def position_update_local(self, view): + """ + Position refinement update for current view. + """ + if not self.do_position_refinement: + return + do_update_pos = (self.p.position_refinement.stop > self.curiter >= self.p.position_refinement.start) + do_update_pos &= (self.curiter % self.p.position_refinement.interval) == 0 + + # Update positions + if do_update_pos: + """ + refines position of current view by a given algorithm. + """ + self.position_refinement.update_constraints(self.curiter) # this stays here + + # Check for new coordinates + if view.active: + self.position_refinement.update_view_position(view) + + def fourier_update(self, view): + """ + General implementation of Fourier update + + Parameters + ---------- + view : View + View to diffraction data + """ + #return basic_fourier_update(view, alpha=self._alpha, tau=self._tau, + # LL_error=self.p.compute_log_likelihood) + + err_fmag, err_exit = projection_update_generalized(view, self._a, self._b, self._c) + if self.p.compute_log_likelihood: + err_phot = log_likelihood(view) + else: + err_phot = 0. + return np.array([err_fmag, err_phot, err_exit]) + + def object_update(self, view, exit_wave): + """ + Engine-specific implementation of object update + + Parameters + ---------- + view : View + View to diffraction data + + exit_wave: dict + Collection of exit waves associated with the current view + """ + self._generic_object_update(view, exit_wave, a=self._ob_a, b=self._ob_b) + + def probe_update(self, view, exit_wave): + """ + Engine-specific implementation of probe update + + Parameters + ---------- + view : View + View to diffraction data + + exit_wave: dict + Collection of exit waves associated with the current view + """ + if self.p.probe_update_start > self.curiter: + return + self._generic_probe_update(view, exit_wave, a=self._pr_a, b=self._pr_b) + + def center_probe(self): + if self.p.probe_center_tol is not None: + for name, pr_s in self.pr.storages.items(): + c1 = u.mass_center(u.abs2(pr_s.data).sum(0)) + c2 = np.asarray(pr_s.shape[-2:]) // 2 + # fft convention should however use geometry instead + if u.norm(c1 - c2) < self.p.probe_center_tol: + break + # SC: possible BUG here, wrong input parameter + pr_s.data[:] = u.shift_zoom(pr_s.data, (1.,)*3, + (0, c1[0], c1[1]), (0, c2[0], c2[1])) + + # shift the object + ob_s = pr_s.views[0].pod.ob_view.storage + ob_s.data[:] = u.shift_zoom(ob_s.data, (1.,)*3, + (0, c1[0], c1[1]), (0, c2[0], c2[1])) + + # shift the exit waves, loop through different exit wave views + for pv in pr_s.views: + pv.pod.exit = u.shift_zoom(pv.pod.exit, (1.,)*2, + (c1[0], c1[1]), (c2[0], c2[1])) + + log(4,'Probe recentered from %s to %s' + % (str(tuple(c1)), str(tuple(c2)))) + + def _generic_object_update(self, view, exit_wave, a=0., b=1.): + """ + A generic object update for stochastic algorithms. + + Parameters + ---------- + view : View + View to diffraction data + + exit_wave: dict + Collection of exit waves associated with the current view + + a : float + Generic parameter for adjusting step size of object update + + b : float + Generic parameter for adjusting step size of object update + + a = 0, b = \\alpha is the ePIE update with parameter \\alpha. + a = \\beta_O, b = 0 is the SDR update with parameter \\beta_O. + + .. math:: + O^{j+1} += (a + b) * \\bar{P^{j}} * (\\Psi^{\\prime} - \\Psi^{j}) / P_{norm} + P_{norm} = (1 - a) * ||P^{j}||_{max}^2 + a * |P^{j}|^2 + + """ + probe_power = 0 + for name, pod in view.pods.items(): + probe_power += u.abs2(pod.probe) + probe_norm = (1 - a) * np.max(probe_power) + a * probe_power + for name, pod in view.pods.items(): + pod.object += (a + b) * np.conj(pod.probe) * (pod.exit - exit_wave[name]) / probe_norm + + def _generic_probe_update(self, view, exit_wave, a=0., b=1.): + """ + A generic probe update for stochastic algorithms. + + Parameters + ---------- + view : View + View to diffraction data + + exit_wave: dict + Collection of exit waves associated with the current view + + a : float + Generic parameter for adjusting step size of probe update + + b : float + Generic parameter for adjusting step size of probe update + + a = 0, b = \\beta is the ePIE update with parameter \\beta. + a = \\beta_P, b = 0 is the SDR update with parameter \\beta_P. + + .. math:: + P^{j+1} += (a + b) * \\bar{O^{j}} * (\\Psi^{\\prime} - \\Psi^{j}) / O_{norm} + O_{norm} = (1 - a) * ||O^{j}||_{max}^2 + a * |O^{j}|^2 + + """ + # Calculate the object norm based on the global object + # This only works if a = 0. + if self._object_norm_is_global and a == 0: + object_norm = np.max(u.abs2(view.pod.ob_view.storage.data).sum(axis=0)) + # Calculate the object norm based on the local object + else: + object_power = 0 + for name, pod in view.pods.items(): + object_power += u.abs2(pod.object) + object_norm = (1 - a) * np.max(object_power) + a * object_power + for name, pod in view.pods.items(): + pod.probe += (a + b) * np.conj(pod.object) * (pod.exit - exit_wave[name]) / object_norm + +class EPIEMixin: + """ + Defaults: + + [alpha] + default = 1.0 + type = float + lowlim = 0.0 + help = Parameter for adjusting the step size of the object update + + [beta] + default = 1.0 + type = float + lowlim = 0.0 + help = Parameter for adjusting the step size of the probe update + + [object_norm_is_global] + default = False + type = bool + help = Calculate the object norm based on the global object instead of the local object + + """ + SUPPORTED_MODELS = [Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull, GradFull, BlockGradFull] + + def __init__(self, alpha, beta): + # EPIE adjustment parameters + self._a = 0 + self._b = 1 + self._c = 1 + self._pr_a = 0.0 + self._ob_a = 0.0 + self._pr_b = alpha + self._ob_b = beta + self._object_norm_is_global = self.p.object_norm_is_global + self.article = dict( + title='An improved ptychographical phase retrieval algorithm for diffractive imaging', + author='Maiden A. and Rodenburg J.', + journal='Ultramicroscopy', + volume=10, + year=2009, + page=1256, + doi='10.1016/j.ultramic.2009.05.012', + comment='The ePIE reconstruction algorithm', + ) + + @property + def alpha(self): + return self._pr_a + + @alpha.setter + def alpha(self, alpha): + self._pr_b = alpha + + @property + def beta(self): + return self._ob_b + + @beta.setter + def beta(self, beta): + self._ob_b = beta + + +class SDRMixin: + """ + Defaults: + + [sigma] + default = 1 + type = float + lowlim = 0.0 + help = Relaxed Fourier reflection parameter. + + [tau] + default = 1 + type = float + lowlim = 0.0 + help = Relaxed modulus constraint parameter. + + [beta_probe] + default = 0.1 + type = float + lowlim = 0.0 + help = Parameter for adjusting the step size of the probe update + + [beta_object] + default = 0.9 + type = float + lowlim = 0.0 + help = Parameter for adjusting the step size of the object update + + """ + SUPPORTED_MODELS = [Full, Vanilla, Bragg3dModel, BlockVanilla, BlockFull] + + def __init__(self, sigma, tau, beta_probe, beta_object): + # SDR Adjustment parameters + self._sigma = sigma + self._tau = tau + self._update_abc() + self._pr_a = beta_probe + self._ob_a = beta_object + self._pr_b = 0.0 + self._ob_b = 0.0 + + self.article = dict( + title='Semi-implicit relaxed Douglas-Rachford algorithm (sDR) for ptychography', + author='Pham et al.', + journal='Opt. Express', + volume=27, + year=2019, + page=31246, + doi='10.1364/OE.27.031246', + comment='The semi-implicit relaxed Douglas-Rachford reconstruction algorithm', + ) + + def _update_abc(self): + self._a = 1 - self._tau * (1 + self._sigma) + self._b = self._tau + self._c = 1 + self._sigma + + @property + def sigma(self): + return self._sigma + + @sigma.setter + def sigma(self, sigma): + self._sigma = sigma + self._update_abc() + + @property + def tau(self): + return self._tau + + @tau.setter + def tau(self, tau): + self._tau = tau + self._update_abc() + + @property + def beta_probe(self): + return self._pr_a + + @beta_probe.setter + def beta_probe(self, beta): + self._pr_a = beta + + @property + def beta_object(self): + return self._ob_a + + @beta_object.setter + def beta_object(self, beta): + self._ob_a = beta + +@register() +class EPIE(_StochasticEngine, EPIEMixin): + """ + The ePIE algorithm. + + Defaults: + + [name] + default = EPIE + type = str + help = + doc = + + """ + def __init__(self, ptycho_parent, pars=None): + _StochasticEngine.__init__(self, ptycho_parent, pars) + EPIEMixin.__init__(self, self.p.alpha, self.p.beta) + ptycho_parent.citations.add_article(**self.article) + + +@register() +class SDR(_StochasticEngine, SDRMixin): + """ + The stochastic Douglas-Rachford algorithm. + + Defaults: + + [name] + default = SDR + type = str + help = + doc = + + """ + def __init__(self, ptycho_parent, pars=None): + _StochasticEngine.__init__(self, ptycho_parent, pars) + SDRMixin.__init__(self, self.p.sigma, self.p.tau, self.p.beta_probe, self.p.beta_object) + ptycho_parent.citations.add_article(**self.article) diff --git a/ptypy/engines/utils.py b/ptypy/engines/utils.py index fadb012c9..6c47e123e 100644 --- a/ptypy/engines/utils.py +++ b/ptypy/engines/utils.py @@ -50,7 +50,7 @@ def dynamic_load(path, baselist, fail_silently = True): # Find classes res = re.findall( - '^class (.*)\((.*)\)', file(filename, 'r').read(), re.M) + r'^class (.*)\((.*)\)', open(filename, 'r').read(), re.M) for classname, basename in res: if (basename in baselist) and classname not in baselist: @@ -76,8 +76,230 @@ def dynamic_load(path, baselist, fail_silently = True): u.logger.warning(traceback.format_exc(tb)) +def log_likelihood(diff_view): + """ + Calculates the log-likelihood for a diffraction view. + + Parameters + ---------- + diff_view : View + View to diffraction data + + Returns + ------- + ll_error : float + Log-likelihood error + """ + I = diff_view.data + LL = np.zeros_like(I) + for name, pod in diff_view.pods.items(): + LL += pod.downsample(u.abs2(pod.fw(pod.probe * pod.object))) + return np.sum(diff_view.pod.mask * (LL - I)**2 / (I + 1.)) / np.prod(LL.shape) + + +def projection_update_generalized(diff_view, a, b, c, pbound=None): + """ + Generalized projection update of a single view using its associated pods. + Updates on all pods' exit waves. We assume here that the current state + is held in pod.exit, while the product of pod.probe & pod.object hold + the state after overlap constraint has been applied. With O() denoting + the overlap constraint and F() denoting the Data/Fourier constraint, + the general projection update can be expressed with four coefficients + + .. math:: + \\psi^{j+1} = [x 1 + a O + b F (c O + y 1)](\\psi^{j}) + + However, the coefficients aren't all independent as the sum of + x+a+b and d+y must be 1, thus we choose + + .. math:: + x = 1 - a - b + + and + + .. math:: + y = 1 - c + + The choice of a,b,c should enable a wide range of projection based + algorithms. + + For memory efficiency, this projection update includes the Fourier update + which is why the power bound mechanism is included but deactivated by + default. + + Parameters + ---------- + diff_view : View + View to diffraction data + + a,b,c : float + Coefficients for Overlap, Fourier and Fourier * Overlap constraints, + respectively + + pbound : float, optional + Power bound. Fourier update is bypassed if the quadratic deviation + between diffraction data and `diff_view` is below this value. + If ``None``, fourier update always happens. + + Returns + ------- + err_fmag, err_exit : float + + - `err_fmag`, Fourier magnitude error; quadratic deviation from + root of experimental data + - `err_exit`, quadratic deviation between exit waves before and after + projection + """ + # Prepare dict for storing propagated waves + f = {} + + # Buffer for accumulated photons + af2 = np.zeros_like(diff_view.data) + # Get measured data + I = diff_view.data + + # Get the mask + fmask = diff_view.pod.mask + + # Propagate the exit waves + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + f[name] = pod.fw((1-c) * pod.exit + c * pod.probe * pod.object) + af2 += pod.downsample(u.abs2(f[name])) + + fmag = np.sqrt(np.abs(I)) + af = np.sqrt(af2) + + # Fourier magnitudes deviations + fdev = af - fmag + err_fmag = np.sum(fmask * fdev**2) / fmask.sum() + err_exit = 0. + + """ + if pbound is None: + # No power bound + fm = (1 - fmask) + fmask * fmag / (af + 1e-10) + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + df = pod.bw(pod.upsample(fm) * f[name]) + \ + a * pod.probe * pod.object - (a + b + c) * pod.exit + pod.exit += df + err_exit += np.mean(u.abs2(df)) + elif err_fmag > pbound: + # Power bound is applied + renorm = np.sqrt(pbound / err_fmag) + fm = (1 - fmask) + fmask * (fmag + fdev * renorm) / (af + 1e-10) + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + df = pod.bw(pod.upsample(fm) * f[name]) + \ + a * pod.probe * pod.object - (a + b + c) * pod.exit + pod.exit += df + err_exit += np.mean(u.abs2(df)) + else: + # Within power bound so no constraint applied. + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + df = (a + c) * (pod.probe * pod.object - pod.exit) + pod.exit += df + err_exit += np.mean(u.abs2(df)) + """ + # Essentially, the following is all the same formula + # fm = (1 - fmask) + fmask * (fmag + fdev * renorm) + # with + # renorm = 1.0 for pbound >= err_fmag + # renorm = np.sqrt(pbound / err_fmag) for pbound < err_fmag + # renorm = 0.0 for pbound == None (off-switch) + # und we use that for GPU and the serial/batched engines. + # See the basic_fourier_update_LEGACY function for the original + # implementation. We'll save a few FFTs this way but that only + # makes a difference if all ranks get similar numbers of diffraction + # frames with err_fmag inside the pbound. + if pbound is None: + fm = (1 - fmask) + fmask * fmag / (af + 1e-10) + elif err_fmag > pbound: + renorm = np.sqrt(pbound / err_fmag) + fm = (1 - fmask) + fmask * (fmag + fdev * renorm) / (af + 1e-10) + else: + fm = None + + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + + if fm is not None: + df = b * pod.bw(pod.upsample(fm) * f[name]) + \ + a * pod.probe * pod.object - (a + b) * pod.exit + else: + df = (a + b*c) * (pod.probe * pod.object - pod.exit) + + pod.exit += df + err_exit += np.mean(u.abs2(df)) + + return err_fmag, err_exit + + +def projection_update_DM_AP(diff_view, alpha=1.0, pbound=None): + """ + Linear interpolation between Difference Map algorithm (a,b,c = -1,1,2) + and Alternating Projections algorithm (a,b,c = 0,1,1) with coefficients + a = -alpha, b = 1, c = 1 + alpha. Alpha = 1.0 corresponds to DM and + alpha = 0.0 to AP. + + Parameters + ---------- + diff_view : View + View to diffraction data + + alpha : float, optional + Blend between AP (alpha=0.0 and DM (alpha=1.0) . Valid interval ``[0, 1]`` + + pbound : float, optional + Power bound. Fourier update is bypassed if the quadratic deviation + between diffraction data and `diff_view` is below this value. + If ``None``, fourier update always happens. + + Returns + ------- + err_fmag, err_exit : float + + - `err_fmag`, Fourier magnitude error; quadratic deviation from + root of experimental data + - `err_exit`, quadratic deviation between exit waves before and after + projection + """ + a = -alpha + b = 1 + c = 1.+alpha + return projection_update_generalized(diff_view, a, b, c, pbound=pbound) + + def basic_fourier_update(diff_view, pbound=None, alpha=1., LL_error=True): - """\ + """ + *** DEPRECATED *** + Backwards compatible function, for reference only. Contains LL error. + Please replace with log_likelihood and projection_update_DM_AP + + See also + -------- + basic_fourier_update_LEGACY + """ + if LL_error: + err_phot = log_likelihood(diff_view) + else: + err_phot = 0.0 + + err_fmag, err_exit = projection_update_DM_AP(diff_view, alpha=alpha, pbound=pbound) + + return np.array([err_fmag, err_phot, err_exit]) + + +def basic_fourier_update_LEGACY(diff_view, pbound=None, alpha=1., LL_error=True): + """ + *** DEPRECATED *** Fourier update a single view using its associated pods. Updates on all pods' exit waves. @@ -134,8 +356,8 @@ def basic_fourier_update(diff_view, pbound=None, alpha=1., LL_error=True): for name, pod in diff_view.pods.items(): if not pod.active: continue - f[name] = pod.fw((1 + alpha) * pod.probe * pod.object - - alpha * pod.exit) + f[name] = pod.fw(-alpha * pod.exit+ + (1 + alpha) * pod.probe * pod.object) af2 += pod.downsample(u.abs2(f[name])) fmag = np.sqrt(np.abs(I)) @@ -152,7 +374,7 @@ def basic_fourier_update(diff_view, pbound=None, alpha=1., LL_error=True): for name, pod in diff_view.pods.items(): if not pod.active: continue - df = pod.bw(pod.upsample(fm) * f[name]) - pod.probe * pod.object + df = pod.bw(pod.upsample(fm) * f[name]) - alpha * pod.probe * pod.object + (alpha - 1) * pod.exit pod.exit += df err_exit += np.mean(u.abs2(df)) elif err_fmag > pbound: @@ -162,7 +384,7 @@ def basic_fourier_update(diff_view, pbound=None, alpha=1., LL_error=True): for name, pod in diff_view.pods.items(): if not pod.active: continue - df = pod.bw(pod.upsample(fm) * f[name]) - pod.probe * pod.object + df = pod.bw(pod.upsample(fm) * f[name]) - alpha * pod.probe * pod.object + (alpha - 1) * pod.exit pod.exit += df err_exit += np.mean(u.abs2(df)) else: @@ -170,7 +392,7 @@ def basic_fourier_update(diff_view, pbound=None, alpha=1., LL_error=True): for name, pod in diff_view.pods.items(): if not pod.active: continue - df = alpha * (pod.probe * pod.object - pod.exit) + df = (pod.probe * pod.object - pod.exit) pod.exit += df err_exit += np.mean(u.abs2(df)) @@ -181,24 +403,26 @@ def reduce_dimension(a, dim, local_indices=None): """ Apply a low-rank approximation on a. - :param a: - 3D numpy array. + Parameters + ---------- + a : ndarray + 3D numpy array - :param dim: - The number of dimensions to retain. The case dim=0 (which would - just reduce all layers to a mean) is not implemented. + dim : int + The number of dimensions to retain. The case dim=0 (which would + just reduce all layers to a mean) is not implemented. - :param local_indices: - Used for Containers distributed across nodes. Local indices of - the current node. + local_indices : + Used for Containers distributed across nodes. Local indices of + the current node. - :return: [reduced array, modes, coefficients] - Where: - - reduced array is the result of dimensionality reduction - (same shape as a) - - modes: 3D array of length dim containing eigenmodes - (aka singular vectors) - - coefficients: 2D matrix representing the decomposition of a. + Returns + ------- + reduced array, modes, coefficients : + where: + - reduced array is the result of dimensionality reduction (same shape as a) + - modes: 3D array of length dim containing eigenmodes (aka singular vectors) + - coefficients: 2D matrix representing the decomposition of a. """ if local_indices is None: # No MPI - generate a list of indices Nl = len(a) @@ -285,7 +509,7 @@ def reduce_dimension(a, dim, local_indices=None): def Cnorm2(c): - """\ + """ Computes a norm2 on whole container `c`. :param Container c: Input @@ -302,7 +526,7 @@ def Cnorm2(c): def Cdot(c1, c2): - """\ + """ Compute the dot product on two containers `c1` and `c2`. No check is made to ensure they are of the same kind. diff --git a/ptypy/experiment/__init__.py b/ptypy/experiment/__init__.py index f587d86f0..4838e9186 100644 --- a/ptypy/experiment/__init__.py +++ b/ptypy/experiment/__init__.py @@ -47,32 +47,3 @@ def _register_PtyScan_class(cls, name=None): globals()[name] = cls __all__.append(name) return cls - - -ptyscan_modules = [('.hdf5_loader', 'Hdf5Loader'), - ('.cSAXS', 'cSAXSScan'), - ('.savu', 'Savu'), - ('.plugin', 'makeScanPlugin'), - ('.ID16Anfp', 'ID16AScan'), - ('.AMO_LCLS', 'AMOScan'), - ('.DiProI_FERMI', 'DiProIFERMIScan'), - ('.optiklabor', 'FliSpecScanMultexp'), - ('.UCL', 'UCLLaserScan'), - ('.nanomax', 'NanomaxStepscanNov2018'), - ('.nanomax', 'NanomaxFlyscanMay2019'), - ('.nanomax', 'NanomaxStepscanSep2019'), - ('.nanomax', 'NanomaxContrast'), - ('.nanomax_streaming', 'NanomaxZmqScan'), - ('.ALS_5321', 'ALS5321Scan'), - ('.Bragg3dSim', 'Bragg3dSimScan')] - -for module, obj in ptyscan_modules: - try: - lib = import_module(module, 'ptypy.experiment') - except ImportError as exception: - log(2, 'Could not import experiment %s from %s, Reason: %s' % (obj, module, exception)) - pass - else: - globals()[obj] = lib.__dict__[obj] - - diff --git a/ptypy/experiment/diamond_nexus.py b/ptypy/experiment/diamond_nexus.py index 1cb72c000..f00cac086 100644 --- a/ptypy/experiment/diamond_nexus.py +++ b/ptypy/experiment/diamond_nexus.py @@ -214,7 +214,7 @@ def __init__(self, pars=None, **kwargs): if None not in [INPUT_FILE, ENERGY_KEY]: - self.p.energy = np.float(h5.File(INPUT_FILE, 'r')[ENERGY_KEY][()] * self.ENERGY_MULTIPLIER) + self.p.energy = float(h5.File(INPUT_FILE, 'r')[ENERGY_KEY][()] * self.ENERGY_MULTIPLIER) self.meta.energy = self.p.energy log(3, "loading energy={} from file".format(self.p.energy)) @@ -257,16 +257,16 @@ def __init__(self, pars=None, **kwargs): log(3, "The loader will not do any cropping.") # it's much better to have this logic here than in load! - if (self._ismapped and (self._scantype is 'arb')): + if (self._ismapped and (self._scantype == 'arb')): # easy peasy log(3, "This scan looks to be a mapped arbitrary trajectory scan.") self.load = self.load_mapped_and_arbitrary_scan - if (self._ismapped and (self._scantype is 'raster')): + if (self._ismapped and (self._scantype == 'raster')): log(3, "This scan looks to be a mapped raster scan.") self.load = self.loaded_mapped_and_raster_scan - if (self._scantype is 'raster') and not self._ismapped: + if (self._scantype == 'raster') and not self._ismapped: log(3, "This scan looks to be an unmapped raster scan.") self.load = self.load_unmapped_raster_scan @@ -349,7 +349,7 @@ def get_corrected_intensities(self, index): else: mask = self.mask[self.frame_slices].squeeze() else: - mask = np.ones_like(intensity, dtype=np.int) + mask = np.ones_like(intensity, dtype=int) return mask, intensity diff --git a/ptypy/experiment/hdf5_loader.py b/ptypy/experiment/hdf5_loader.py index 95779f015..6a47b7857 100644 --- a/ptypy/experiment/hdf5_loader.py +++ b/ptypy/experiment/hdf5_loader.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """\ -Scan loading recipe for the I13 beamline, Diamond. +Scan loading recipe for the Diamond beamlines. This file is part of the PTYPY package. @@ -14,9 +14,12 @@ from ptypy import utils as u from ptypy.core.data import PtyScan from ptypy.experiment import register +from ptypy.utils import parallel from ptypy.utils.verbose import log from ptypy.utils.array_utils import _translate_to_pix +import os +from multiprocessing import Pool, RawArray @register() class Hdf5Loader(PtyScan): @@ -317,6 +320,12 @@ class Hdf5Loader(PtyScan): type = int default = None help = Index for outer dimension (e.g. tomography, spectro scans), default is None. + + [padding] + type = int, tuple, list + default = None + help = Option to pad the detector frames on all sides + doc = A tuple of list with padding given as ( top, bottom, left, right) """ @@ -372,9 +381,10 @@ def __init__(self, pars=None, **kwargs): log(3, "This is appears to be a spectro scan, selecting index = {}".format(self.p.outer_index)) self.intensities = h5.File(self.p.intensities.file, 'r')[self.p.intensities.key] - if self._is_spectro_scan and self.p.outer_index is not None: - self.intensities = self.intensities[self.p.outer_index] + self.intensities_dtype = self.intensities.dtype data_shape = self.intensities.shape + if self._is_spectro_scan and self.p.outer_index is not None: + data_shape = tuple(np.array(data_shape)[1:]) fast_axis = h5.File(self.p.positions.file, 'r')[self.p.positions.fast_key][...] if self._is_spectro_scan and self.p.outer_index is not None: @@ -397,7 +407,9 @@ def __init__(self, pars=None, **kwargs): log(3, "Skipping every {:d} positions".format(self.p.positions.skip)) if None not in [self.p.framefilter.file, self.p.framefilter.key]: - self.framefilter = h5.File(self.p.framefilter.file, 'r')[self.p.framefilter.key] + self.framefilter = h5.File(self.p.framefilter.file, 'r')[self.p.framefilter.key][()].squeeze() > 0 # turn into boolean + if self._is_spectro_scan and self.p.outer_index is not None: + self.framefilter = self.framefilter[self.p.outer_index] if (self.framefilter.shape == self.fast_axis.shape == self.slow_axis.shape): log(3, "The frame filter has the same dimensionality as the axis information.") elif self.framefilter.shape[:2] == self.fast_axis.shape == self.slow_axis.shape: @@ -439,6 +451,7 @@ def __init__(self, pars=None, **kwargs): if None not in [self.p.mask.file, self.p.mask.key]: self.mask = h5.File(self.p.mask.file, 'r')[self.p.mask.key] + self.mask_dtype = self.mask.dtype log(3, "The mask has shape: {}".format(self.mask.shape)) if self.mask.shape == data_shape: log(3, "The mask is laid out like the data.") @@ -449,6 +462,7 @@ def __init__(self, pars=None, **kwargs): else: raise RuntimeError("I have no idea what to do with this shape of mask data.") else: + self.mask_dtype = np.int64 log(3, "No mask will be applied.") @@ -469,31 +483,39 @@ def __init__(self, pars=None, **kwargs): if None not in [self.p.recorded_energy.file, self.p.recorded_energy.key]: if self._is_spectro_scan and self.p.outer_index is not None: - self.p.energy = np.float(h5.File(self.p.recorded_energy.file, 'r')[self.p.recorded_energy.key][self.p.outer_index]) + self.p.energy = float(h5.File(self.p.recorded_energy.file, 'r')[self.p.recorded_energy.key][self.p.outer_index]) else: - self.p.energy = np.float(h5.File(self.p.recorded_energy.file, 'r')[self.p.recorded_energy.key][()]) + self.p.energy = float(h5.File(self.p.recorded_energy.file, 'r')[self.p.recorded_energy.key][()]) self.p.energy = self.p.energy * self.p.recorded_energy.multiplier + self.p.recorded_energy.offset self.meta.energy = self.p.energy log(3, "loading energy={} from file".format(self.p.energy)) if None not in [self.p.recorded_distance.file, self.p.recorded_distance.key]: - self.p.distance = np.float(h5.File(self.p.recorded_distance.file, 'r')[self.p.recorded_distance.key][()] * self.p.recorded_distance.multiplier) + self.p.distance = float(h5.File(self.p.recorded_distance.file, 'r')[self.p.recorded_distance.key][()] * self.p.recorded_distance.multiplier) self.meta.distance = self.p.distance log(3, "loading distance={} from file".format(self.p.distance)) if None not in [self.p.recorded_psize.file, self.p.recorded_psize.key]: - self.p.psize = np.float(h5.File(self.p.recorded_psize.file, 'r')[self.p.recorded_psize.key][()] * self.p.recorded_psize.multiplier) + self.p.psize = float(h5.File(self.p.recorded_psize.file, 'r')[self.p.recorded_psize.key][()] * self.p.recorded_psize.multiplier) self.meta.psize = self.p.psize log(3, "loading psize={} from file".format(self.p.psize)) + if self.p.padding is None: + self.pad = np.array([0,0,0,0]) + log(3, "No padding will be applied.") + else: + self.pad = np.array(self.p.padding, dtype=int) + assert self.pad.size == 4, "self.p.padding needs to of size 4" + log(3, "Padding the detector frames by {}".format(self.p.padding)) # now lets figure out the cropping and centering roughly so we don't load the full data in. - frame_shape = np.array(data_shape[-2:]) + frame_shape = np.array(data_shape[-2:]) + self.pad.reshape(2,2).sum(1) center = frame_shape // 2 if self.p.center is None else u.expect2(self.p.center) center = np.array([_translate_to_pix(frame_shape[ix], center[ix]) for ix in range(len(frame_shape))]) if self.p.shape is None: self.frame_slices = (slice(None, None, 1), slice(None, None, 1)) + self.frame_shape = data_shape[-2:] self.p.shape = frame_shape log(3, "Loading full shape frame.") elif self.p.shape is not None and not self.p.auto_center: @@ -501,30 +523,31 @@ def __init__(self, pars=None, **kwargs): low_pix = center - pshape // 2 high_pix = low_pix + pshape self.frame_slices = (slice(int(low_pix[0]), int(high_pix[0]), 1), slice(int(low_pix[1]), int(high_pix[1]), 1)) + self.frame_shape = self.p.shape self.p.center = pshape // 2 #the new center going forward self.info.center = self.p.center self.p.shape = pshape log(3, "Loading in frame based on a center in:%i, %i" % tuple(center)) else: self.frame_slices = (slice(None, None, 1), slice(None, None, 1)) + self.frame_shape = data_shape[-2:] self.info.center = None self.info.auto_center = self.p.auto_center log(3, "center is %s, auto_center: %s" % (self.info.center, self.info.auto_center)) - log(3, "The loader will not do any cropping.") # it's much better to have this logic here than in load! - if (self._ismapped and (self._scantype is 'arb')): + if (self._ismapped and (self._scantype == 'arb')): # easy peasy log(3, "This scan looks to be a mapped arbitrary trajectory scan.") self.load = self.load_mapped_and_arbitrary_scan - if (self._ismapped and (self._scantype is 'raster')): + if (self._ismapped and (self._scantype == 'raster')): log(3, "This scan looks to be a mapped raster scan.") - self.load = self.loaded_mapped_and_raster_scan + self.load = self.load_mapped_and_raster_scan - if (self._scantype is 'raster') and not self._ismapped: + if (self._scantype == 'raster') and not self._ismapped: log(3, "This scan looks to be an unmapped raster scan.") self.load = self.load_unmapped_raster_scan @@ -542,7 +565,7 @@ def load_unmapped_raster_scan(self, indices): log(3, 'Data loaded successfully.') return intensities, positions, weights - def loaded_mapped_and_raster_scan(self, indices): + def load_mapped_and_raster_scan(self, indices): intensities = {} positions = {} weights = {} @@ -572,12 +595,14 @@ def get_corrected_intensities(self, index): ''' Corrects the intensities for darkfield, flatfield and normalisations if they exist. There is a lot of logic here, I wonder if there is a better way to get rid of it. - Limited a bit by the MPI, adn thinking about extension to large data size. + Limited a bit by the MPI, and thinking about extension to large data size. ''' if not hasattr(index, '__iter__'): index = (index,) indexed_frame_slices = tuple([slice(ix, ix+1, 1) for ix in index]) indexed_frame_slices += self.frame_slices + if self._is_spectro_scan and self.p.outer_index is not None: + indexed_frame_slices = (self.p.outer_index,) + indexed_frame_slices intensity = self.intensities[indexed_frame_slices].squeeze() # TODO: Remove these logic blocks into something a bit more sensible. @@ -607,7 +632,12 @@ def get_corrected_intensities(self, index): else: mask = self.mask[self.frame_slices].squeeze() else: - mask = np.ones_like(intensity, dtype=np.int) + mask = np.ones_like(intensity, dtype=int) + + if self.p.padding: + intensity = np.pad(intensity, tuple(self.pad.reshape(2,2)), mode='constant') + mask = np.pad(mask, tuple(self.pad.reshape(2,2)), mode='constant') + return mask, intensity @@ -765,3 +795,207 @@ def compute_scan_mapping_and_trajectory(self, data_shape, positions_fast_shape, else: raise IOError("I don't know what to do with these positions/data shapes") + +@register() +class Hdf5LoaderFast(Hdf5Loader): + def __init__(self, pars=None, **kwargs): + super().__init__(pars=pars, **kwargs) + self.cpu_count_per_rank = max(os.cpu_count() // parallel.size,1) + print("Rank %d has access to %d processes" %(parallel.rank, self.cpu_count_per_rank)) + self.intensities_array = None + self.weights_array = None + + @staticmethod + def _init_worker(intensities_raw_array, weights_raw_array, + intensities_handle, + weights_handle, + darkfield_handle, + flatfield_handle, + intensities_dtype, weights_dtype, + array_shape, + mask_laid_out_like_data, + darkfield_laid_out_like_data, + flatfield_laid_out_like_data): + Hdf5LoaderFast.worker_intensities_handle = intensities_handle + Hdf5LoaderFast.worker_intensities_array = np.frombuffer(intensities_raw_array, intensities_dtype, -1).reshape(array_shape) + Hdf5LoaderFast.worker_weights_handle = weights_handle + Hdf5LoaderFast.worker_weights_array = np.frombuffer(weights_raw_array, weights_dtype, -1).reshape(array_shape) if weights_raw_array else None + Hdf5LoaderFast.worker_mask_laid_out_like_data = mask_laid_out_like_data + Hdf5LoaderFast.worker_darkfield_handle = darkfield_handle + Hdf5LoaderFast.worker_darkfield_laid_out_like_data = darkfield_laid_out_like_data + Hdf5LoaderFast.worker_flatfield_handle = flatfield_handle + Hdf5LoaderFast.worker_flatfield_laid_out_like_data = flatfield_laid_out_like_data + + @staticmethod + def _read_intensities_and_weights(slices): + ''' + Copy intensities/weights into memory and correct for + darkfield/flatfield if they exist + ''' + indexed_frame_slices, dest_slices = slices + frame_slices = tuple(np.array(indexed_frame_slices)[-2:]) + + # Handle / target array for intensities + src_intensities = Hdf5LoaderFast.worker_intensities_handle + dest_intensities = Hdf5LoaderFast.worker_intensities_array + + # Handle / target array for mask/weights + src_weights = Hdf5LoaderFast.worker_weights_handle + dest_weights = Hdf5LoaderFast.worker_weights_array + mask_laid_out_like_data = Hdf5LoaderFast.worker_mask_laid_out_like_data + + # Handle for darkfield + src_darkfield = Hdf5LoaderFast.worker_darkfield_handle + darkfield_laid_out_like_data = Hdf5LoaderFast.worker_darkfield_laid_out_like_data + + # Handle for flatfield + src_flatfield = Hdf5LoaderFast.worker_flatfield_handle + flatfield_laid_out_like_data = Hdf5LoaderFast.worker_flatfield_laid_out_like_data + + # Copy intensities and weights + src_intensities.read_direct(dest_intensities, indexed_frame_slices, dest_slices) + if src_weights is not None: + if mask_laid_out_like_data: + src_weights.read_direct(dest_weights, indexed_frame_slices, dest_slices) + else: + src_weights.read_direct(dest_weights, frame_slices, dest_slices) + + # Correct darkfield + if src_darkfield is not None: + if darkfield_laid_out_like_data: + dest_intensities[dest_slices] -= src_darkfield[indexed_frame_slices].squeeze() + else: + dest_intensities[dest_slices] -= src_darkfield[frame_slices].squeeze() + + # Correct flatfield + if src_flatfield is not None: + if flatfield_laid_out_like_data: + dest_intensities[dest_slices] /= src_flatfield[indexed_frame_slices].squeeze() + else: + dest_intensities[dest_slices] /= src_flatfield[frame_slices].squeeze() + + def _setup_raw_intensity_buffer(self, dtype, sh): + npixels = int(np.prod(sh)) + if (self.intensities_array is not None) and (self.intensities_array.size == npixels): + return + self._intensities_raw_array = RawArray(np.ctypeslib.as_ctypes_type(dtype), npixels) + self.intensities_array = np.frombuffer(self._intensities_raw_array, self.intensities_dtype, -1).reshape(sh) + + def _setup_raw_weights_buffer(self, dtype, sh): + npixels = int(np.prod(sh)) + if (self.weights_array is not None) and (self.weights_array.size == npixels): + return + if self.mask is not None: + self._weights_raw_array = RawArray(np.ctypeslib.as_ctypes_type(dtype), npixels) + self.weights_array = np.frombuffer(self._weights_raw_array, dtype, -1).reshape(sh) + else: + self._weights_raw_array = None + self.weights_array = np.ones(sh, dtype=int) + + def load_multiprocessing(self, src_slices): + sh = (len(src_slices),) + self.frame_shape + self._setup_raw_intensity_buffer(self.intensities_dtype, sh) + self._setup_raw_weights_buffer(self.mask_dtype, sh) + dest_slices = [np.s_[i:i+1] for i in range(len(src_slices))] + + with Pool(self.cpu_count_per_rank, + initializer=Hdf5LoaderFast._init_worker, + initargs=(self._intensities_raw_array, self._weights_raw_array, + self.intensities, self.mask, self.darkfield, self.flatfield, + self.intensities_dtype, self.mask_dtype, + sh, self.mask_laid_out_like_data, + self.darkfield_laid_out_like_data, + self.flatfield_field_laid_out_like_data)) as p: + p.map(self._read_intensities_and_weights, zip(src_slices, dest_slices)) + + def load_unmapped_raster_scan(self, indices): + + slices = [] + for ii in indices: + slow_idx, fast_idx = self.preview_indices[:, ii] + jj = slow_idx * self.slow_axis.shape[1] + fast_idx + indexed_frame_slices = (jj,) + indexed_frame_slices += self.frame_slices + if self._is_spectro_scan and self.p.outer_index is not None: + indexed_frame_slices = (self.p.outer_index,) + indexed_frame_slices + slices.append(indexed_frame_slices) + + self.load_multiprocessing(slices) + + intensities = {} + positions = {} + weights = {} + for k,ii in enumerate(indices): + slow_idx, fast_idx = self.preview_indices[:,ii] + weights[ii], intensities[ii] = self.get_corrected_intensities(self.weights_array[k], self.intensities_array[k], ii, slices[k]) + positions[ii] = np.array([self.slow_axis[slow_idx, fast_idx] * self.p.positions.slow_multiplier, + self.fast_axis[slow_idx, fast_idx] * self.p.positions.fast_multiplier]) + log(3, 'Data loaded successfully.') + return intensities, positions, weights + + def load_mapped_and_raster_scan(self, indices): + + slices = [] + for ii in indices: + index = self.preview_indices[:, ii] + indexed_frame_slices = tuple(index) + indexed_frame_slices += self.frame_slices + if self._is_spectro_scan and self.p.outer_index is not None: + indexed_frame_slices = (self.p.outer_index,) + indexed_frame_slices + slices.append(indexed_frame_slices) + + self.load_multiprocessing(slices) + + intensities = {} + positions = {} + weights = {} + for k,ii in enumerate(indices): + slow_idx, fast_idx = self.preview_indices[:, ii] + weights[ii], intensities[ii] = self.get_corrected_intensities(self.weights_array[k], self.intensities_array[k], ii, slices[k]) + positions[ii] = np.array([self.slow_axis[slow_idx, fast_idx] * self.p.positions.slow_multiplier, + self.fast_axis[slow_idx, fast_idx] * self.p.positions.fast_multiplier]) + log(3, 'Data loaded successfully.') + return intensities, positions, weights + + def load_mapped_and_arbitrary_scan(self, indices): + + slices = [] + for ii in indices: + jj = self.preview_indices[ii] + indexed_frame_slices = (jj,) + indexed_frame_slices += self.frame_slices + if self._is_spectro_scan and self.p.outer_index is not None: + indexed_frame_slices = (self.p.outer_index,) + indexed_frame_slices + slices.append(indexed_frame_slices) + + self.load_multiprocessing(slices) + + intensities = {} + positions = {} + weights = {} + for k,ii in enumerate(indices): + jj = self.preview_indices[ii] + weights[ii], intensities[ii] = self.get_corrected_intensities(self.weights_array[k], self.intensities_array[k], ii, slices[k]) + positions[ii] = np.array([self.slow_axis[jj] * self.p.positions.slow_multiplier, + self.fast_axis[jj] * self.p.positions.fast_multiplier]) + log(3, 'Data loaded successfully.') + return intensities, positions, weights + + def get_corrected_intensities(self, weights, intensities, index, indexed_frame_slice): + ''' + Corrects the intensities for normalisation and padding + ''' + + if self.normalisation is not None: + if self.normalisation_laid_out_like_positions: + scale = self.normalisation[index] + else: + scale = np.squeeze(self.normalisation[indexed_frame_slice]) + if np.abs(scale - self.normalisation_mean) < (self.p.normalisation.sigma * self.normalisation_std): + intensities *= 1 / (scale * self.normalisation_mean) + + if self.p.padding: + intensities = np.pad(intensities, tuple(self.pad.reshape(2,2)), mode='constant') + weights = np.pad(weights, tuple(self.pad.reshape(2,2)), mode='constant') + + return weights, intensities \ No newline at end of file diff --git a/ptypy/experiment/nanomax.py b/ptypy/experiment/nanomax.py index 395a09c18..f3fef13d2 100644 --- a/ptypy/experiment/nanomax.py +++ b/ptypy/experiment/nanomax.py @@ -616,7 +616,7 @@ def load_positions(self): if self.info.I0 is not None: with h5py.File(fullfilename, 'r') as hf: normdata = np.array(hf['%s/measurement/%s' % (entry, self.info.I0)], dtype=float) - normdata = normdata[self.firstLine:self.lastLine+1, :Nsteps].flatten() + normdata = normdata[self.firstLine:self.lastLine+1, :Nsteps].flatten() # noqa: F821 self.normdata = normdata / np.mean(normdata) logger.info('*** going to normalize by channel %s - loaded %d values' % (self.info.I0, len(self.normdata))) diff --git a/ptypy/experiment/optiklabor.py b/ptypy/experiment/optiklabor.py index 79f69242d..96e757067 100644 --- a/ptypy/experiment/optiklabor.py +++ b/ptypy/experiment/optiklabor.py @@ -186,20 +186,20 @@ def correct(self, raw, weights, common): return data, weights -if __name__ == '__main__': - u.verbose.set_level(3) - RS = RawScan(p,num_frames=50,roi=512 ) - RS.initialize() - RS.report() - print('loading data') - msg = True - for i in range(200): - if msg is False: - break - time.sleep(1) - msg = RS.auto(10) - logger.info(u.verbose.report(msg), extra={'allprocesses': True}) - u.parallel.barrier() +# if __name__ == '__main__': +# u.verbose.set_level(3) +# RS = RawScan(p,num_frames=50,roi=512 ) +# RS.initialize() +# RS.report() +# print('loading data') +# msg = True +# for i in range(200): +# if msg is False: +# break +# time.sleep(1) +# msg = RS.auto(10) +# logger.info(u.verbose.report(msg), extra={'allprocesses': True}) +# u.parallel.barrier() #RS.report() #%RS.load_raw([0,1,2]) diff --git a/ptypy/experiment/spec.py b/ptypy/experiment/spec.py index c1ef67d80..142d3a3da 100644 --- a/ptypy/experiment/spec.py +++ b/ptypy/experiment/spec.py @@ -28,7 +28,7 @@ class SpecScan(object): class SpecInfo(object): def __init__(self, spec_filename): self.spec_filename = spec_filename - self.spec_file = file(spec_filename,'r') + self.spec_file = open(spec_filename,'r') self.scans = {} self.parse() global lastSpecInfo @@ -166,7 +166,7 @@ def parse(self, rehash=False): scans[scannr] = scan self.scans = scans -""" +r""" assert scannr > 0 assert burst > 0 assert multexp > 0 diff --git a/ptypy/io/edfIO.py b/ptypy/io/edfIO.py index f16a04c1b..29b2e123e 100644 --- a/ptypy/io/edfIO.py +++ b/ptypy/io/edfIO.py @@ -125,7 +125,7 @@ def readData(filenameprefix,imgstart=0,imgnumber = 1,xi = 0, xf = 0, bin_fact = meta = [] if multiple == 1: headerlength=2048 - if (rowTo < rowFrom and rowTo is not 0): + if (rowTo < rowFrom and rowTo != 0): raise ValueError('The last row has to be equal or larger than the first row.\n') for imgnum in range(imgnumber): filename = filenameprefix + '_' + utils.num2str(imgstart,'%04d') + '_' + utils.num2str(imgnum,'%04d') + '.edf' diff --git a/ptypy/io/h5rw.py b/ptypy/io/h5rw.py index 61a8e0f74..c786c2d88 100644 --- a/ptypy/io/h5rw.py +++ b/ptypy/io/h5rw.py @@ -169,23 +169,23 @@ def _store_dict(group, d, name): pop_id(id(d)) return dset - # @sdebug - def _store_ordered_dict(group, d, name): - check_id(id(d)) - if any([type(k) not in [str,] for k in d.keys()]): - raise RuntimeError('Only dictionaries with string keys are supported.') - dset = group.create_group(name) - dset.attrs['type'] = 'ordered_dict' - for k, v in d.items(): - if k.find('/') > -1: - k = k.replace('/', h5options['SLASH_ESCAPE']) - ndset = _store(dset, v, k) - if ndset is not None: - ndset.attrs['escaped'] = '1' - else: - _store(dset, v, k) - pop_id(id(d)) - return dset + # # @sdebug + # def _store_ordered_dict(group, d, name): + # check_id(id(d)) + # if any([type(k) not in [str,] for k in d.keys()]): + # raise RuntimeError('Only dictionaries with string keys are supported.') + # dset = group.create_group(name) + # dset.attrs['type'] = 'ordered_dict' + # for k, v in d.items(): + # if k.find('/') > -1: + # k = k.replace('/', h5options['SLASH_ESCAPE']) + # ndset = _store(dset, v, k) + # if ndset is not None: + # ndset.attrs['escaped'] = '1' + # else: + # _store(dset, v, k) + # pop_id(id(d)) + # return dset # @sdebug def _store_param(group, d, name): @@ -231,7 +231,7 @@ def _store(group, a, name): elif type(a) is dict: dset = _store_dict(group, a, name) elif type(a) is OrderedDict: - dset = _store_ordered_dict(group, a, name) + dset = _store_dict(group, a, name) elif type(a) is Param: dset = _store_param(group, a, name) elif type(a) is list: @@ -432,14 +432,14 @@ def _load_scalar(dset): except: return dset[...] - def _load_ordered_dict(dset, depth): - d = OrderedDict() - if depth > 0: - for k, v in dset.items(): - if v.attrs.get('escaped', None) is not None: - k = k.replace(h5options['SLASH_ESCAPE'], '/') - d[k] = _load(v, depth - 1) - return d + # def _load_ordered_dict(dset, depth): + # d = OrderedDict() + # if depth > 0: + # for k, v in dset.items(): + # if v.attrs.get('escaped', None) is not None: + # k = k.replace(h5options['SLASH_ESCAPE'], '/') + # d[k] = _load(v, depth - 1) + # return d def _load_str(dset): if h5py.version.version_tuple[0]>2: @@ -480,7 +480,7 @@ def _load(dset, depth, sl=None): if sl is not None: val = val[sl] elif dset_type == 'ordered_dict': - val = _load_ordered_dict(dset, depth) + val = _load_dict(dset, depth) elif dset_type == 'array': val = _load_numpy(dset, sl) elif dset_type == 'arraylist': @@ -609,7 +609,7 @@ def _format_list(d, key, dset): stringout += _format(d - 1, (key[0] + indent, ''), dset[k]) return stringout - def _format_tuple(key, dset): + def _format_tuple(d, key, dset): stringout = ' ' * key[0] + ' * %s [tuple]:\n' % key[1] if d > 0: keys = list(dset.keys()) @@ -620,15 +620,12 @@ def _format_tuple(key, dset): def _format_arraytuple(key, dset): a = dset[...] - if len(a) < 5: + if len(a) < 5 and a.ndim==1: stringout = ' ' * key[0] + ' * ' + key[1] + ' [tuple = ' + str(tuple(a.ravel())) + ']\n' else: - try: - float(a.ravel()[0]) - stringout = ' ' * key[0] + ' * ' + key[1] + ' [tuple = (' + ( - ('%f, ' * 4) % tuple(a.ravel()[:4])) + ' ...)]\n' - except ValueError: - stringout = ' ' * key[0] + ' * ' + key[1] + ' [tuple = (%d x %s objects)]\n' % (a.size, str(a.dtype)) + stringout = ' ' * key[0] + ' * ' + key[1] + \ + ' [tuple = ' + str(len(a)) + 'x[' + (('%dx' * (a[0].ndim - 1) + '%d') % a[0].shape) + \ + ' ' + str(a.dtype) + ' array]]\n' return stringout def _format_arraylist(key, dset): @@ -686,7 +683,7 @@ def _format(d, key, dset): if (dset_type is None) and (type(dset) is h5py.Group): dset_type = 'dict' - if dset_type == 'dict': + if dset_type == 'dict' or dset_type == 'ordered_dict': stringout = _format_dict(d, key, dset, False) elif dset_type == 'param': stringout = _format_dict(d, key, dset, True) diff --git a/ptypy/io/interaction.py b/ptypy/io/interaction.py index 884619588..07c3da537 100644 --- a/ptypy/io/interaction.py +++ b/ptypy/io/interaction.py @@ -83,7 +83,7 @@ def default(self, obj): NE = NumpyEncoder() # This is the string to match against when decoding -NPYARRAYmatch = re.compile("NPYARRAY\[([0-9]{3})\]") +NPYARRAYmatch = re.compile(r"NPYARRAY\[([0-9]{3})\]") def numpy_replace(obj, arraylist): diff --git a/ptypy/io/rawIO.py b/ptypy/io/rawIO.py index 8cdca3891..df9dabe95 100644 --- a/ptypy/io/rawIO.py +++ b/ptypy/io/rawIO.py @@ -73,7 +73,7 @@ def rawread(filename, doglob=None, roi=None): return dat,meta def _read(filename,dtype): - f=file(filename) + f=open(filename) header = [] header.append(f.readline()) diff --git a/ptypy/resources/__init__.py b/ptypy/resources/__init__.py index f0872bb1c..50bab0b5e 100644 --- a/ptypy/resources/__init__.py +++ b/ptypy/resources/__init__.py @@ -1,4 +1,5 @@ import pkg_resources +import numpy as np flowerfile = pkg_resources.resource_filename(__name__,'flowers.png') moonfile = pkg_resources.resource_filename(__name__,'moon.png') diff --git a/ptypy/resources/default_parameters_configparser.txt b/ptypy/resources/default_parameters_configparser.txt index 2199ceb9b..8dded064f 100644 --- a/ptypy/resources/default_parameters_configparser.txt +++ b/ptypy/resources/default_parameters_configparser.txt @@ -150,6 +150,21 @@ doc = userlevel = 2 type = float +[engine.DM.probe_update_cuda_atomics] +help = For GPU, use the atomics version for probe updates kernel +default = False +type = bool +userlevel = 2 +doc = + +[engine.DM.object_update_cuda_atomics] +help = For GPU, use the atomics version for object updates kernel +default = True +type = bool +userlevel = 2 +doc = + + [engine.common] help = Parameters common to all engines default = None diff --git a/ptypy/resources/parameter_descriptions.configparser b/ptypy/resources/parameter_descriptions.configparser index 40980781e..8a54e0966 100644 --- a/ptypy/resources/parameter_descriptions.configparser +++ b/ptypy/resources/parameter_descriptions.configparser @@ -1088,6 +1088,21 @@ type = int userlevel = 2 lowlim = 0 +[engine.DM.object_update_cuda_atomics] +help = For GPU, use the atomics version for object update kernel +default = True +type = bool +userlevel = 2 +doc = + +[engine.DM.probe_update_cuda_atomics] +help = For GPU, use the atomics version for probe update kernel +default = False +type = bool +userlevel = 2 +doc = + + [engine.ML] default = help = Maximum Likelihood parameters diff --git a/ptypy/simulations/detector.py b/ptypy/simulations/detector.py index 05742785b..4228b1579 100644 --- a/ptypy/simulations/detector.py +++ b/ptypy/simulations/detector.py @@ -272,4 +272,4 @@ def smooth_step(x,mfs): plt.imshow(A) plt.figure() - plt.imshow(log10(D.expose(I[1])[0]+1)) + plt.imshow(np.log10(D.expose(I[1])[0]+1)) diff --git a/ptypy/simulations/ptysim_utils.py b/ptypy/simulations/ptysim_utils.py index 4f1179b99..1fbc5d0d4 100644 --- a/ptypy/simulations/ptysim_utils.py +++ b/ptypy/simulations/ptysim_utils.py @@ -37,7 +37,7 @@ def exp_positions(positions, drift=0.0,scale= 0.0,noise=0.0): return pos -def make_sim_datasource(model_inst,drift=0.0,scale= 0.0,noise=0.0): +""" def make_sim_datasource(model_inst,drift=0.0,scale= 0.0,noise=0.0): labels=[] sources =[] @@ -71,7 +71,7 @@ def framepositions(pos_pixel,probe_shape,frame_overhead=(10,10)): pos_pixel += np.round(w.expect2(frame_overhead) /2) positions = [(p[0],p[0]+probe_shape[0],p[1],p[1]+probe_shape[1]) for p in pos_pixel] - return shape,positions + return shape,positions """ def augment_to_coordlist(a,Npos): diff --git a/ptypy/simulations/simscan.py b/ptypy/simulations/simscan.py index 9e7ca414e..796cefed4 100644 --- a/ptypy/simulations/simscan.py +++ b/ptypy/simulations/simscan.py @@ -249,32 +249,32 @@ def manipulate_ptycho(self, ptycho): return ptycho -if __name__ == "__main__": - from ptypy import resources +# if __name__ == "__main__": +# from ptypy import resources - s = scan_DEFAULT.copy() - s.xy.model = "round" - s.xy.spacing = 1e-6 - s.xy.steps = 8 - s.xy.extent = 5e-6 - shape = 256 - s.geometry.energy = 6.2 - s.geometry.lam = None - s.geometry.distance = 7 - s.geometry.psize = 172e-6 - s.geometry.shape = shape - s.geometry.propagation = "farfield" - s.illumination = resources.moon_pr((shape,shape))*1e2 - s.sample = resources.flower_obj((shape*2,shape*2)) - - - u.verbose.set_level(3) - MS = SimScan(None,s) - #MS.P.plot_overview() - u.verbose.set_level(3) - u.pause(10) - MS.initialize() - for i in range(20): - msg = MS.auto(10) - u.verbose.logger.info(u.verbose.report(msg), extra={'allprocesses': True}) - u.parallel.barrier() +# s = scan_DEFAULT.copy() +# s.xy.model = "round" +# s.xy.spacing = 1e-6 +# s.xy.steps = 8 +# s.xy.extent = 5e-6 +# shape = 256 +# s.geometry.energy = 6.2 +# s.geometry.lam = None +# s.geometry.distance = 7 +# s.geometry.psize = 172e-6 +# s.geometry.shape = shape +# s.geometry.propagation = "farfield" +# s.illumination = resources.moon_pr((shape,shape))*1e2 +# s.sample = resources.flower_obj((shape*2,shape*2)) + + +# u.verbose.set_level(3) +# MS = SimScan(None,s) +# #MS.P.plot_overview() +# u.verbose.set_level(3) +# u.pause(10) +# MS.initialize() +# for i in range(20): +# msg = MS.auto(10) +# u.verbose.logger.info(u.verbose.report(msg), extra={'allprocesses': True}) +# u.parallel.barrier() diff --git a/ptypy/utils/array_utils.py b/ptypy/utils/array_utils.py index dbd7a2366..a6dc3ede9 100644 --- a/ptypy/utils/array_utils.py +++ b/ptypy/utils/array_utils.py @@ -54,9 +54,9 @@ def switch_orientation(A, orientation, center=None): o = 0 if orientation is None else orientation if np.isscalar(o): - o = [i=='1' for i in '%03d' % int(np.base_repr(o))] + o = [i == '1' for i in '%03d' % int(np.base_repr(o))] - assert len(o)==3 + assert len(o) == 3 # switch orientation if o[0]: axes = list(range(A.ndim - 2)) + [-1, -2] @@ -101,10 +101,11 @@ def rebin_2d(A, rebin=1): sh = np.asarray(A.shape[-2:]) newdim = sh // rebin if not (sh % rebin == 0).all(): - raise ValueError('Last two axes %s of input array `A` cannot be binned by %s' % (str(tuple(sh)),str(rebin))) + raise ValueError('Last two axes %s of input array `A` cannot be binned by %s' % (str(tuple(sh)), str(rebin))) else: return A.reshape(-1, newdim[0], rebin, newdim[1], rebin).mean(-1).mean(-2) + def crop_pad_symmetric_2d(A, newshape, center=None): """ Crops or pads Array `A` symmetrically along the last two axes `(-2,-1)` @@ -148,7 +149,8 @@ def crop_pad_symmetric_2d(A, newshape, center=None): return A, c + low -def rebin(a, *args,**kwargs): + +def rebin(a, *args, **kwargs): """ Rebin ndarray data into a smaller ndarray of the same rank whose dimensions are factors of the original dimensions. @@ -184,46 +186,52 @@ def rebin(a, *args,**kwargs): """ shape = a.shape lenShape = a.ndim - factor = np.asarray(shape)//np.asarray(args) + factor = np.asarray(shape) // np.asarray(args) evList = ['a.reshape('] + \ - ['args[%d],factor[%d],'%(i,i) for i in range(lenShape)] + \ - [')'] + ['.sum(%d)'%(i+1) for i in range(lenShape)] + \ - ['*( 1.'] + ['/factor[%d]'%i for i in range(lenShape)] + [')'] - if kwargs.get('verbose',False): + ['args[%d],factor[%d],' % (i, i) for i in range(lenShape)] + \ + [')'] + ['.sum(%d)' % (i + 1) for i in range(lenShape)] + \ + ['*( 1.'] + ['/factor[%d]' % i for i in range(lenShape)] + [')'] + if kwargs.get('verbose', False): print(''.join(evList)) return eval(''.join(evList)) + def _confine(A): """\ Doc TODO. """ - sh=np.asarray(A.shape)[1:] - A=A.astype(float) - m=np.reshape(sh,(len(sh),) + len(sh)*(1,)) - return (A+m//2.0) % m - m//2.0 + sh = np.asarray(A.shape)[1:] + A = A.astype(float) + m = np.reshape(sh, (len(sh),) + len(sh) * (1,)) + return (A + m // 2.0) % m - m // 2.0 -def _translate_to_pix(sh,center): + +def _translate_to_pix(sh, center): """\ Take arbitrary input and translate it to a pixel position with respect to sh. """ - sh=np.array(sh) + sh = np.array(sh) if not isstr(center): cen = np.asarray(center) % sh - elif center=='fftshift': - cen=sh//2.0 - elif center=='geometric': - cen=sh/2.0-0.5 - elif center=='fft': - cen=sh*0.0 + elif center == 'fftshift': + cen = sh // 2.0 + elif center == 'geometric': + cen = sh / 2.0 - 0.5 + elif center == 'fft': + cen = sh * 0.0 else: raise TypeError('Input %s not understood for center' % str(center)) return cen + + """ def center_2d(sh,center): return translate_to_pix(sh[-2:],expect2(center)) """ -def grids(sh,psize=None,center='geometric',FFTlike=True): + + +def grids(sh, psize=None, center='geometric', FFTlike=True): """\ ``q0,q1,... = grids(sh)`` returns centered coordinates for a N-dimensional array of shape sh (pixel units) @@ -258,14 +266,14 @@ def grids(sh,psize=None,center='geometric',FFTlike=True): ndarray The coordinate grids """ - sh=np.asarray(sh) + sh = np.asarray(sh) - cen = _translate_to_pix(sh,center) + cen = _translate_to_pix(sh, center) - grid=np.indices(sh).astype(float) - np.reshape(cen,(len(sh),) + len(sh)*(1,)) + grid = np.indices(sh).astype(float) - np.reshape(cen, (len(sh),) + len(sh) * (1,)) if FFTlike: - grid=_confine(grid) + grid = _confine(grid) if psize is None: return grid @@ -273,16 +281,17 @@ def grids(sh,psize=None,center='geometric',FFTlike=True): psize = np.asarray(psize) if psize.size == 1: psize = psize * np.ones((len(sh),)) - psize = np.asarray(psize).reshape( (len(sh),) + len(sh)*(1,)) + psize = np.asarray(psize).reshape((len(sh),) + len(sh) * (1,)) return grid * psize + def rectangle(grids, dims=None, ew=2): if dims is None: dims = (grids.shape[-2] / 2., grids.shape[-1] / 2.) v, h = dims V, H = grids - return (smooth_step(-np.abs(V) + v/2, ew) - * smooth_step(-np.abs(H) + h/2, ew)) + return (smooth_step(-np.abs(V) + v / 2, ew) + * smooth_step(-np.abs(H) + h / 2, ew)) def ellipsis(grids, dims=None, ew=2): @@ -291,9 +300,10 @@ def ellipsis(grids, dims=None, ew=2): v, h = dims V, H = grids return smooth_step( - 0.5 - np.sqrt(V**2/v**2 + H**2/h**2), ew/np.sqrt(v * h)) + 0.5 - np.sqrt(V ** 2 / v ** 2 + H ** 2 / h ** 2), ew / np.sqrt(v * h)) + -def zoom(c,*arg,**kwargs): +def zoom(c, *arg, **kwargs): """ Wrapper `scipy.ndimage.zoom `_ function and shares @@ -311,25 +321,27 @@ def zoom(c,*arg,**kwargs): numpy.ndarray Zoomed array """ - #if np.all(arg[0] == 1): + # if np.all(arg[0] == 1): # return c # from scipy.ndimage import zoom as _zoom if np.iscomplexobj(c): - return complex_overload(_zoom)(c,*arg,**kwargs) + return complex_overload(_zoom)(c, *arg, **kwargs) else: - return _zoom(c,*arg,**kwargs) + return _zoom(c, *arg, **kwargs) + c_zoom = zoom -c_zoom.__doc__='*Deprecated*, kept for backward compatibility only.\n\n' + zoom.__doc__ +c_zoom.__doc__ = '*Deprecated*, kept for backward compatibility only.\n\n' + zoom.__doc__ """ c_affine_transform=complex_overload(ndi.affine_transform) c_affine_transform.__doc__='*complex input*\n\n'+c_affine_transform.__doc__ """ -def shift_zoom(c,zoom,cen_old,cen_new,**kwargs): + +def shift_zoom(c, zoom, cen_old, cen_new, **kwargs): """ Move array from center `cen_old` to `cen_new` and perform a zoom `zoom`. @@ -359,39 +371,40 @@ def shift_zoom(c,zoom,cen_old,cen_new,**kwargs): numpy.ndarray Shifted and zoomed array """ - + from scipy.ndimage import affine_transform as at zoom = np.diag(zoom) - offset=np.asarray(cen_old)-np.asarray(cen_new).dot(zoom) + offset = np.asarray(cen_old) - np.asarray(cen_new).dot(zoom) if np.iscomplexobj(c): - return complex_overload(at)(c,zoom,offset,**kwargs) + return complex_overload(at)(c, zoom, offset, **kwargs) else: - return at(c,zoom,offset,**kwargs) + return at(c, zoom, offset, **kwargs) -def fill3D(A,B,offset=[0,0,0]): +def fill3D(A, B, offset=[0, 0, 0]): """ Fill 3-dimensional array A with B. """ - if A.ndim != 3 or B.ndim!=3: + if A.ndim != 3 or B.ndim != 3: raise ValueError('3D a numpy arrays expected') - Alim=np.array(A.shape) - Blim=np.array(B.shape) - off=np.array(offset) + Alim = np.array(A.shape) + Blim = np.array(B.shape) + off = np.array(offset) Ao = off.copy() - Ao[Ao<0]=0 + Ao[Ao < 0] = 0 Bo = -off.copy() - Bo[Bo<0]=0 - print(Ao,Bo) + Bo[Bo < 0] = 0 if (Bo > Blim).any() or (Ao > Alim).any(): print("misfit") pass else: - A[Ao[0]:min(off[0]+Blim[0],Alim[0]),Ao[1]:min(off[1]+Blim[1],Alim[1]),Ao[2]:min(off[2]+Blim[2],Alim[2])] \ - =B[Bo[0]:min(Alim[0]-off[0],Blim[0]),Bo[1]:min(Alim[1]-off[1],Blim[1]),Bo[2]:min(Alim[2]-off[2],Blim[2])] + A[Ao[0]:min(off[0] + Blim[0], Alim[0]), Ao[1]:min(off[1] + Blim[1], Alim[1]), + Ao[2]:min(off[2] + Blim[2], Alim[2])] \ + = B[Bo[0]:min(Alim[0] - off[0], Blim[0]), Bo[1]:min(Alim[1] - off[1], Blim[1]), + Bo[2]:min(Alim[2] - off[2], Blim[2])] -def mirror(A,axis=-1): +def mirror(A, axis=-1): """ Mirrors array `A` along one axis `axis` @@ -409,9 +422,10 @@ def mirror(A,axis=-1): A view to the mirrored array. """ - return np.flipud(np.asarray(A).swapaxes(axis,0)).swapaxes(0,axis) + return np.flipud(np.asarray(A).swapaxes(axis, 0)).swapaxes(0, axis) + -def pad_lr(A,axis,l,r,fillpar=0.0, filltype='scalar'): +def pad_lr(A, axis, l, r, fillpar=0.0, filltype='scalar'): """ Pads ndarray `A` orthogonal to `axis` with `l` layers (pixels,lines,planes,...) on low side an `r` layers on high side. @@ -445,62 +459,61 @@ def pad_lr(A,axis,l,r,fillpar=0.0, filltype='scalar'): crop_pad crop_pad_symmetric_2d """ - fsh=np.array(A.shape) - if l>fsh[axis]: #rare case - l-=fsh[axis] - A=pad_lr(A,axis,fsh[axis],0,fillpar, filltype) - return pad_lr(A,axis,l,r,fillpar, filltype) - elif r>fsh[axis]: - r-=fsh[axis] - A=pad_lr(A,axis,0,fsh[axis],fillpar, filltype) - return pad_lr(A,axis,l,r,fillpar, filltype) - elif filltype=='mirror': - left=mirror(np.split(A,[l],axis)[0],axis) - right=mirror(np.split(A,[A.shape[axis]-r],axis)[1],axis) - elif filltype=='periodic': - right=np.split(A,[r],axis)[0] - left=np.split(A,[A.shape[axis]-l],axis)[1] - elif filltype=='project': - fsh[axis]=l - left=np.ones(fsh,A.dtype)*np.split(A,[1],axis)[0] - fsh[axis]=r - right=np.ones(fsh,A.dtype)*np.split(A,[A.shape[axis]-1],axis)[1] - if filltype=='scalar' or l==0: - fsh[axis]=l - left=np.ones(fsh,A.dtype)*fillpar - if filltype=='scalar' or r==0: - fsh[axis]=r - right=np.ones(fsh,A.dtype)*fillpar - if filltype=='custom': - left=fillpar[0].astype(A.dtype) - right=fillpar[1].astype(A.dtype) - return np.concatenate((left,A,right),axis=axis) - - -def _roll_from_pixcenter(sh,center): + fsh = np.array(A.shape) + if l > fsh[axis]: # rare case + l -= fsh[axis] + A = pad_lr(A, axis, fsh[axis], 0, fillpar, filltype) + return pad_lr(A, axis, l, r, fillpar, filltype) + elif r > fsh[axis]: + r -= fsh[axis] + A = pad_lr(A, axis, 0, fsh[axis], fillpar, filltype) + return pad_lr(A, axis, l, r, fillpar, filltype) + elif filltype == 'mirror': + left = mirror(np.split(A, [l], axis)[0], axis) + right = mirror(np.split(A, [A.shape[axis] - r], axis)[1], axis) + elif filltype == 'periodic': + right = np.split(A, [r], axis)[0] + left = np.split(A, [A.shape[axis] - l], axis)[1] + elif filltype == 'project': + fsh[axis] = l + left = np.ones(fsh, A.dtype) * np.split(A, [1], axis)[0] + fsh[axis] = r + right = np.ones(fsh, A.dtype) * np.split(A, [A.shape[axis] - 1], axis)[1] + if filltype == 'scalar' or l == 0: + fsh[axis] = l + left = np.ones(fsh, A.dtype) * fillpar + if filltype == 'scalar' or r == 0: + fsh[axis] = r + right = np.ones(fsh, A.dtype) * fillpar + if filltype == 'custom': + left = fillpar[0].astype(A.dtype) + right = fillpar[1].astype(A.dtype) + return np.concatenate((left, A, right), axis=axis) + + +def _roll_from_pixcenter(sh, center): """\ returns array of ints as input for np.roll use np.roll(A,-roll_from_pixcenter(sh,cen)[ax],ax) to put 'cen' in geometric center of array A """ - sh=np.array(sh) + sh = np.array(sh) if center != None: - if center=='fftshift': - cen=sh//2.0 - elif center=='geometric': - cen=sh/2.0-0.5 - elif center=='fft': - cen=sh*0.0 + if center == 'fftshift': + cen = sh // 2.0 + elif center == 'geometric': + cen = sh / 2.0 - 0.5 + elif center == 'fft': + cen = sh * 0.0 elif center is not None: - cen=sh*np.asarray(center) % sh - 0.5 + cen = sh * np.asarray(center) % sh - 0.5 - roll=np.ceil(cen - sh/2.0) % sh + roll = np.ceil(cen - sh / 2.0) % sh else: - roll=np.zeros_like(sh) + roll = np.zeros_like(sh) return roll.astype(int) - -def crop_pad_axis(A,hplanes,axis=-1,roll=0,fillpar=0.0, filltype='scalar'): +def crop_pad_axis(A, hplanes, axis=-1, roll=0, fillpar=0.0, filltype='scalar'): """ Crops or pads a volume array `A` at beginning and end of axis `axis` with a number of hyperplanes specified by `hplanes` @@ -573,37 +586,36 @@ def crop_pad_axis(A,hplanes,axis=-1,roll=0,fillpar=0.0, filltype='scalar'): >>> B=crop_pad_axis(V,(3,-2),1,filltype='mirror') """ if np.isscalar(hplanes): - hplanes=int(hplanes) - r=np.abs(hplanes) // 2 * np.sign(hplanes) - l=hplanes - r - elif len(hplanes)==2: - l=int(hplanes[0]) - r=int(hplanes[1]) + hplanes = int(hplanes) + r = np.abs(hplanes) // 2 * np.sign(hplanes) + l = hplanes - r + elif len(hplanes) == 2: + l = int(hplanes[0]) + r = int(hplanes[1]) else: raise RuntimeError('unsupoorted input for \'hplanes\'') - if roll!=0: - A=np.roll(A,-roll,axis=axis) - - if l<=0 and r<=0: - A=np.split(A,[-l,A.shape[axis]+r],axis)[1] - elif l>0 and r>0: - A=pad_lr(A,axis,l,r,fillpar,filltype) - elif l>0 and r<=0: - A=pad_lr(A,axis,l,0,fillpar,filltype) - A=np.split(A,[0,A.shape[axis]+r],axis)[1] - elif l<=0 and r>0: - A=pad_lr(A,axis,0,r,fillpar,filltype) - A=np.split(A,[-l,A.shape[axis]],axis)[1] - - - if roll!=0: - return np.roll(A,roll+r,axis=axis) + if roll != 0: + A = np.roll(A, -roll, axis=axis) + + if l <= 0 and r <= 0: + A = np.split(A, [-l, A.shape[axis] + r], axis)[1] + elif l > 0 and r > 0: + A = pad_lr(A, axis, l, r, fillpar, filltype) + elif l > 0 and r <= 0: + A = pad_lr(A, axis, l, 0, fillpar, filltype) + A = np.split(A, [0, A.shape[axis] + r], axis)[1] + elif l <= 0 and r > 0: + A = pad_lr(A, axis, 0, r, fillpar, filltype) + A = np.split(A, [-l, A.shape[axis]], axis)[1] + + if roll != 0: + return np.roll(A, roll + r, axis=axis) else: return A -def crop_pad(A,hplane_list,axes=None,cen=None,fillpar=0.0,filltype='scalar'): +def crop_pad(A, hplane_list, axes=None, cen=None, fillpar=0.0, filltype='scalar'): """\ Crops or pads a volume array `A` with a number of hyperplanes according to parameters in `hplanes` Wrapper for crop_pad_axis. @@ -660,14 +672,13 @@ def crop_pad(A,hplane_list,axes=None,cen=None,fillpar=0.0,filltype='scalar'): """ if axes is None: - axes=np.arange(len(hplane_list))-len(hplane_list) - elif not(len(axes)==len(hplane_list)): + axes = np.arange(len(hplane_list)) - len(hplane_list) + elif not (len(axes) == len(hplane_list)): raise RuntimeError('if axes is specified, hplane_list has to be same length as axes') - sh=np.array(A.shape) - roll = _roll_from_pixcenter(sh,cen) + sh = np.array(A.shape) + roll = _roll_from_pixcenter(sh, cen) - for ax,cut in zip(axes,hplane_list): - A=crop_pad_axis(A,cut,ax,roll[ax],fillpar,filltype) + for ax, cut in zip(axes, hplane_list): + A = crop_pad_axis(A, cut, ax, roll[ax], fillpar, filltype) return A - diff --git a/ptypy/utils/misc.py b/ptypy/utils/misc.py index 5ec395316..eae31e89f 100644 --- a/ptypy/utils/misc.py +++ b/ptypy/utils/misc.py @@ -139,7 +139,7 @@ def isstr(s): if sys.version_info[0] == 3: string_types = str, else: - string_types = basestring, + string_types = basestring, # noqa: F821 return isinstance(s, string_types) @@ -290,7 +290,7 @@ def expectN(a, N): raise ValueError('N must be 2 or 3') def complex_overload(func): - """\ + r"""\ Overloads function specified only for floats in the following manner .. math:: @@ -329,4 +329,3 @@ def clean_path(filename): if not os.path.exists(base): os.makedirs(base) return filename - diff --git a/ptypy/utils/parallel.py b/ptypy/utils/parallel.py index c78f715b2..933cca306 100644 --- a/ptypy/utils/parallel.py +++ b/ptypy/utils/parallel.py @@ -27,7 +27,8 @@ __all__ = ['MPIenabled', 'comm', 'MPI', 'master','barrier', 'LoadManager', 'loadmanager','allreduce','send','receive','bcast', - 'bcast_dict', 'gather_dict', 'gather_list', 'MPIrand_normal', 'MPIrand_uniform','MPInoise2d'] + 'bcast_dict', 'gather_dict', 'gather_list', + 'MPIrand_normal', 'MPIrand_uniform','MPInoise2d'] def useMPI(do=None): @@ -456,11 +457,6 @@ def bcast(data, source=0): def bcast_dict(dct, keys='all', source=0): """ Broadcasts or scatters a dict `dct` from ``rank==source``. - If value is a numpy ndarray, `comm.Bcast` is used instead `comm.bcast`, - such that transfer is accelerated. - - Fills dict `dct` in place for receiving nodes, although this is a - bit inconsistent compared to :any:`gather_dict` Parameters ---------- @@ -497,35 +493,37 @@ def bcast_dict(dct, keys='all', source=0): out = dict(dct) return out - # communicate the dict length + # Broadcast all keys (the full dict) + if str(keys) == 'all': + out = comm.bcast(dct) + return out + + # Broadcast only given keys of dict if rank == source: out = {} length = comm.bcast(len(dct), source) for k, v in dct.items(): comm.bcast(k,source) bcast(v,source) - if str(keys) == 'all' or k in keys: + if k in keys: out[k] = v - return out else: - if dct is None: - dct = {} + out = {} length = comm.bcast(None, source) for k in range(length): k = comm.bcast(None,source) v = bcast(None,source) - if str(keys) == 'all' or k in keys: - dct[k] = v - - return dct + if k in keys: + out[k] = v + return out def allgather_dict(dct): """ Allgather dict in place. """ gdict = gather_dict(dct) - bcast_dict(gdict) + gdict = bcast_dict(gdict) dct.update(gdict) def gather_dict(dct, target=0): @@ -558,35 +556,39 @@ def gather_dict(dct, target=0): if not MPIenabled: out.update(dct) return out - - for r in range(size): - if r == target: - if rank == target: - #print rank,dct - out.update(dct) - continue - - if rank == target: - l = comm.recv(source=r,tag=9999) - for i in range(l): - #k = receive(r) - k = comm.recv(source=r,tag=9999) - v = receive(r) - #print rank,str(k),v - out[k] = v - elif r == rank: - # your turn to send - l = len(dct) - comm.send(l, dest=target,tag=9999) - for k,v in dct.items(): - #print rank,str(k),v - #send(k, dest=target) - comm.send(k, dest=target,tag=9999) - send(v, dest=target) - barrier() - + ret = comm.gather(dct, root=target) + if rank == target: + for d in ret: + out.update(d) return out + # for r in range(size): + # if r == target: + # if rank == target: + # #print rank,dct + # out.update(dct) + # continue + + # if rank == target: + # l = comm.recv(source=r,tag=9999) + # for i in range(l): + # #k = receive(r) + # k = comm.recv(source=r,tag=9999) + # v = receive(r) + # #print rank,str(k),v + # out[k] = v + # elif r == rank: + # # your turn to send + # l = len(dct) + # comm.send(l, dest=target,tag=9999) + # for k,v in dct.items(): + # #print rank,str(k),v + # #send(k, dest=target) + # comm.send(k, dest=target,tag=9999) + # send(v, dest=target) + # barrier() + # return out + def _send(data, dest=0, tag=0): """ Wrapper for comm.Send @@ -750,7 +752,7 @@ def MPIrand_uniform(low=0.0, high=1.0, size=(1)): else: hosts_ranks[v].append(k) - bcast_dict(hosts_ranks) + hosts_ranks = bcast_dict(hosts_ranks) rank_local = hosts_ranks[host].index(rank) del rank_host else: diff --git a/ptypy/utils/parameters.py b/ptypy/utils/parameters.py index adc6a4dcc..96630355e 100644 --- a/ptypy/utils/parameters.py +++ b/ptypy/utils/parameters.py @@ -261,7 +261,7 @@ def validate_standard_param(sp, p=None, prefix=None): return good else: - raise NotimplementedError('Checking if a param fits with a standard is not yet implemented') + raise NotImplementedError('Checking if a param fits with a standard is not yet implemented') def format_standard_param(p): diff --git a/ptypy/utils/plot_client.py b/ptypy/utils/plot_client.py index b5d2d7cf0..e6297a3af 100644 --- a/ptypy/utils/plot_client.py +++ b/ptypy/utils/plot_client.py @@ -344,7 +344,7 @@ def _set_autolayout(self,pars): try: pars = TEMPLATES[pars] except KeyError: - log(self.log_level,'Plotting template "\%s" not found, using default settings' % str(pars)) + log(self.log_level,'Plotting template "\\%s" not found, using default settings' % str(pars)) if hasattr(pars,'items'): self.p.update(pars,in_place_depth=4) @@ -581,7 +581,7 @@ def plot_storage(self, storage, plot_pars, title="", typ='obj'): #ptya._update_colorbar() if channel == 'c': if typ == 'obj': - mm = np.mean(np.abs(data[layer]*plot_mask)**2) + mm = np.mean(np.abs(data[layer]*mask)**2) info = 'T=%.2f' % mm else: mm = np.sum(np.abs(data[layer])**2) diff --git a/ptypy/utils/plot_utils.py b/ptypy/utils/plot_utils.py index e96b25e36..882a2ed20 100644 --- a/ptypy/utils/plot_utils.py +++ b/ptypy/utils/plot_utils.py @@ -128,12 +128,15 @@ def pause(timeout=-1, message=None): print(message) time.sleep(timeout) +# BD: With version 9.1.0 of PIL, _MODE_CONV has been removed, +# see here: https://github.com/python-pillow/Pillow/pull/6057 +# can't see a reason why this is still needed, therefore commenting it out # FIXME: Is this still needed? # Fix tif import problem -Image._MODE_CONV['I;16'] = (Image._ENDIAN + 'u2', None) +#Image._MODE_CONV['I;16'] = (Image._ENDIAN + 'u2', None) # Grayscale + alpha should also work -Image._MODE_CONV['LA'] = (Image._ENDIAN + 'u1', 2) +#Image._MODE_CONV['LA'] = (Image._ENDIAN + 'u1', 2) def complex2hsv(cin, vmin=0., vmax=None): @@ -285,7 +288,7 @@ def rgb2complex(rgb): def imsave(a, filename=None, vmin=None, vmax=None, cmap=None): - """ + r""" Take array `a` and transform to `PIL.Image` object that may be used by `pyplot.imshow` for example. Also save image buffer directly without the sometimes unnecessary Gui-frame and overhead. @@ -357,9 +360,9 @@ def imsave(a, filename=None, vmin=None, vmax=None, cmap=None): vmin, vmax = 0.9 * vmin, 1.1 * vmax im = Image.fromarray((255*(a.clip(vmin,vmax)-vmin)/(vmax-vmin)).astype('uint8')) if cmap is not None: - r = im.point(lambda x: cmap(x/255.0)[0] * 255) - g = im.point(lambda x: cmap(x/255.0)[1] * 255) - b = im.point(lambda x: cmap(x/255.0)[2] * 255) + r = im.point(lambda x: int(cmap(x/255.0)[0] * 255)) + g = im.point(lambda x: int(cmap(x/255.0)[1] * 255)) + b = im.point(lambda x: int(cmap(x/255.0)[2] * 255)) im = Image.merge("RGB", (r, g, b)) if filename is not None: @@ -460,7 +463,7 @@ def rmphaseramp(a, weight=None, return_phaseramp=False): useweight = True if weight is None: useweight = False - elif weight == 'abs': + elif isinstance(weight,str) and weight == 'abs': weight = np.abs(a) ph = np.exp(1j*np.angle(a)) @@ -817,7 +820,7 @@ def _update_colorbar(self, mn=None, mx=None): if self.channel == 'c': self.cax.xaxis.set_major_locator(mpl.ticker.FixedLocator([0,np.pi, 2*np.pi])) - self.cax.xaxis.set_major_formatter(mpl.ticker.FixedFormatter(['0', '$\pi$', '2$\pi$'])) + self.cax.xaxis.set_major_formatter(mpl.ticker.FixedFormatter(['0', r'$\pi$', r'2$\pi$'])) self.cax.set_xlabel('phase [rad]', fontsize=self.fontsize+2) self.cax.xaxis.set_label_position("top") diff --git a/ptypy/utils/scripts.py b/ptypy/utils/scripts.py index 72fa62c9b..268894434 100644 --- a/ptypy/utils/scripts.py +++ b/ptypy/utils/scripts.py @@ -144,9 +144,9 @@ def hdr_image(img_list, exp_list, thresholds=[3000,50000], dark_list=[], elif len(dark_list) == 1: dark_list = dark_list * len(img_list) # Convert to floats except for mask_list - img_list = [img.astype(np.float) for img in img_list] - dark_list = [dark.astype(np.float) for dark in dark_list] - exp_list = [np.float(exp) for exp in exp_list] + img_list = [img.astype(float) for img in img_list] + dark_list = [dark.astype(float) for dark in dark_list] + exp_list = [float(exp) for exp in exp_list] mask_list = [mask.astype(np.int) for mask in mask_list] for img, dark, exp,mask in zip(img_list, dark_list,exp_list,mask_list): @@ -190,7 +190,7 @@ def hdr_image(img_list, exp_list, thresholds=[3000,50000], dark_list=[], def png2mpg(listoffiles, framefile='frames.txt', fps=5, bitrate=2000, codec='wmv2', Encode=True, RemoveImages=False): - """ + r""" Makes a movie (\*.mpg) from a collection of \*.png or \*.jpeg frames. *Requires* binary of **mencoder** installed on system @@ -303,7 +303,7 @@ def png2mpg(listoffiles, framefile='frames.txt', fps=5, bitrate=2000, # Trying to find similar images. body, imagetype = os.path.splitext(tail) # Replace possible numbers by a wildcard. - newbody = re.sub('\d+', '*', body) + newbody = re.sub(r'\d+', '*', body) wcard = head + os.sep + newbody + imagetype #print wcard imagfiles = glob.glob(wcard) @@ -531,9 +531,9 @@ def mass_center(A, axes=None, mask=None): axes = tuple(np.array(axes) + 1) if mask is None: - return np.sum(A * np.indices(A.shape), axis=axes, dtype=np.float) / np.sum(A, dtype=np.float) + return np.sum(A * np.indices(A.shape), axis=axes, dtype=float) / np.sum(A, dtype=float) else: - return np.sum(A * mask * np.indices(A.shape), axis=axes, dtype=np.float) / np.sum(A * mask, dtype=np.float) + return np.sum(A * mask * np.indices(A.shape), axis=axes, dtype=float) / np.sum(A * mask, dtype=float) def radial_distribution(A, radii=None): diff --git a/ptypy/utils/verbose.py b/ptypy/utils/verbose.py index 6e6ae300c..d069444c7 100644 --- a/ptypy/utils/verbose.py +++ b/ptypy/utils/verbose.py @@ -21,25 +21,32 @@ import sys import inspect import logging +from time import perf_counter from . import parallel __all__ = ['logger', 'set_level', 'report', 'log'] -# custom logging level to diplay python objects (not as detailed as debug but also not that important for info) +# custom logging levels INSPECT = 15 +INTERACTIVE = 35 +CITATION = 45 -CONSOLE_FORMAT = {logging.ERROR : 'ERROR %(name)s - %(message)s', +CONSOLE_FORMAT = {CITATION: '%(message)s', + logging.ERROR : 'ERROR %(name)s - %(message)s', + INTERACTIVE : '%(message)s', logging.WARNING : 'WARNING %(name)s - %(message)s', logging.INFO : '%(message)s', INSPECT : 'INSPECT %(message)s', logging.DEBUG : 'DEBUG %(pathname)s [%(lineno)d] - %(message)s'} -FILE_FORMAT = {logging.ERROR : '%(asctime)s ERROR %(name)s - %(message)s', - logging.WARNING : '%(asctime)s WARNING %(name)s - %(message)s', - logging.INFO : '%(asctime)s %(message)s', - INSPECT : '%(asctime)s INSPECT %(message)s', - logging.DEBUG : '%(asctime)s DEBUG %(pathname)s [%(lineno)d] - %(message)s'} +FILE_FORMAT = {CITATION: '%(message)s', + logging.ERROR : '%(asctime)s ERROR %(name)s - %(message)s', + INTERACTIVE : '%(asctime)s %(message)s', + logging.WARNING : '%(asctime)s WARNING %(name)s - %(message)s', + logging.INFO : '%(asctime)s %(message)s', + INSPECT : '%(asctime)s INSPECT %(message)s', + logging.DEBUG : '%(asctime)s DEBUG %(pathname)s [%(lineno)d] - %(message)s'} # How many characters per line in console LINEMAX = 80 @@ -82,8 +89,7 @@ class CustomFormatter(logging.Formatter): """ Flexible formatting, depending on the logging level. - Adapted from http://stackoverflow.com/questions/1343227 - Will have to be updated for python > 3.2. + Adapted from https://stackoverflow.com/questions/14844970 """ DEFAULT = '%(levelname)s: %(message)s' @@ -92,11 +98,11 @@ def __init__(self, FORMATS=None): self.FORMATS = {} if FORMATS is None else FORMATS def format(self, record): - self._fmt = self.FORMATS.get(record.levelno, self.DEFAULT) + self._style._fmt = self.FORMATS.get(record.levelno, self.DEFAULT) return logging.Formatter.format(self, record) # Create logger -logger = logging.getLogger() +logger = logging.getLogger("ptypy") # Default level - should be changed as soon as possible logger.setLevel(logging.WARNING) @@ -113,26 +119,71 @@ def format(self, record): consolefilter = MPIFilter() logger.addFilter(consolefilter) +# Capture warnings and log them +logging.captureWarnings(True) + level_from_verbosity = {0:logging.CRITICAL, 1:logging.ERROR, 2:logging.WARN, 3:logging.INFO, 4: INSPECT, 5:logging.DEBUG} -level_from_string = {'CRITICAL':logging.CRITICAL, 'ERROR':logging.ERROR, 'WARN':logging.WARN, 'WARNING':logging.WARN, 'INFO':logging.INFO, 'INSPECT': INSPECT, 'DEBUG':logging.DEBUG} +level_from_string = {'CITATION':CITATION, 'CRITICAL':logging.CRITICAL, 'ERROR':logging.ERROR, 'WARN':logging.WARN, 'WARNING':logging.WARN, + 'INTERACTIVE':INTERACTIVE, 'INFO':logging.INFO, 'INSPECT': INSPECT, 'DEBUG':logging.DEBUG} vlevel_from_logging = dict([(v,k) for k,v in level_from_verbosity.items()]) slevel_from_logging = dict([(v,k) for k,v in level_from_string.items()]) +def ilog_message(msg): + """ + Interactive logging for jupyter notebooks, prints a normal message. + """ + if not slevel_from_logging[logger.level] == "INTERACTIVE": + return + logger.log(level_from_string["INTERACTIVE"], msg) + +def ilog_streamer(msg): + """ + Interactive logging for jupyter notebooks, + streams a message by overwriting the same line. + """ + if not slevel_from_logging[logger.level] == "INTERACTIVE": + return + consolehandler.terminator = "" + logger.log(level_from_string["INTERACTIVE"], "\r"+msg) + consolehandler.terminator = "\n" + +def ilog_newline(): + """ + Interactive logging for jupyter notebooks, + moves cursor to next line. Call this after + ilog_streamer() to escape the streaming. + """ + if not slevel_from_logging[logger.level] == "INTERACTIVE": + return + consolehandler.terminator = "" + logger.log(level_from_string["INTERACTIVE"], "\n") + consolehandler.terminator = "\n" + def log(level,msg,parallel=False): + if isinstance(level, int): + _level = level_from_verbosity[level] + elif isinstance(level, str): + _level = level_from_string[level.upper()] + else: + raise TypeError("Verbosity level should be an integer or a string") if not parallel: - logger.log(level_from_verbosity[level], msg) + logger.log(_level, msg) else: - logger.log(level_from_verbosity[level], msg,extra={'allprocesses':True}) + logger.log(_level, msg,extra={'allprocesses':True}) def set_level(level): """ Set verbosity level. Kept here for backward compatibility """ logger.info('Verbosity set to %s' % str(level)) - if str(level) == level: + if isinstance(level, str): + if level.upper() not in level_from_string: + raise KeyError("Verbosity level %s does not exist" %level) logger.setLevel(level_from_string[level.upper()]) - else: + elif isinstance(level, int): logger.setLevel(level_from_verbosity[level]) + else: + raise TypeError("Verbosity level should be an integer or a string") logger.info('Verbosity set to %s' % str(level)) def get_level(num_or_string='num'): @@ -260,3 +311,19 @@ def _format(key,level, obj): report.maxchar = LINEMAX report.headernewline='\n' report.asterisk='*' + + +class LogTime: + def __init__(self, active=False): + self.active = active + self.duration = 0 + def __enter__(self): + if not self.active: + return + self.time = perf_counter() + return self + def __exit__(self, type, value, traceback): + if self.active: + self.duration = perf_counter() - self.time + self.duration = parallel.MPImax([self.duration]) + self.readout = f'{self.duration:.3f} seconds' \ No newline at end of file diff --git a/ptypy_core_dependencies.yml b/ptypy_core_dependencies.yml deleted file mode 100644 index 2900fa651..000000000 --- a/ptypy_core_dependencies.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: ptypy_core_dependencies -channels: - - conda-forge -dependencies: - - python=3.7 - - numpy - - scipy - - h5py diff --git a/release_notes.md b/release_notes.md index a028abaf1..8ee1bc645 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,7 +1,130 @@ +# PtyPy 0.5 release notes + +We're excited to bring you a new release, with new engines, GPU accelerations and +many smaller improvements. + +## Engine Updates + +### New abstraction layer for most engines, new engines. + + * generalised projectional engine with derived engines DM, RAAR + * generalised stochastic engine with derived engines EPIE, SDR + +Engines that are based on global projections now all derive from a generalized +base engine that is able to express most common projection algorithms with 4 scalar parameters. +DM and RAAR are two such derived classes. Similarly, algorithms based on a stochastic +sequence of local projections (SDR, EPIE) now inherit from a common base engine. + +### GPU acceleration + + * GPU-acceleration for all major engines DM, ML, EPIE, SDR, RAAR + * accelerated engines needs to be imported **explicitly** with + ```python + import ptypy + ptypy.load_gpu_engines('cuda') + ``` + +We accelerated three engines (projectional, stochastic and ML) using +the [`PyCUDA`](https://documen.tician.de/pycuda/) and +[`Reikna`](http://reikna.publicfields.net/en/latest/) library and a whole +collection of custom kernels. + +All GPU engines leverage a "streaming" model which means that the +primary locations of all objects are on the host (CPU) memory. +Diffraction data arrays and all other arrys that scale linearly with +the number of shifts/positons are segmented into blocks (of frames). +The idea is that these blocks are moved on and off the device (GPU) during +engine iteration if the GPU does not have enough memory to store all +blocks. The number of frames per block can +be adjusted with the new top-level +[`frames_per_block`](https://ptycho.github.io/ptypy/rst/parameters.html#ptycho.frames_per_block) +parameter. This parameter has little influence for smaller problem size, +but needs to be adjusted if your GPU has too little memory to fit even +a single block. + +Each engine iteration will cycle through all blocks, DM needs to even cycle +once for each projection. We therefore recommend to make the block size small +enough such that at least a couple of blocks fit on the GPU to hide the latency of +data transfers. For best +performance, we employ a mirror scheme such that each cycle reverses the +block order and reduces the host to device copies (and vice versa) to the +absolute minimum. + +GPU engines work in parallel when each MPI rank takes one GPU. For sending +data between ranks, PtyPy will perform a host copy first in most cases or +use whatever the underlying MPI implementations does for CUDA-aware MPI +(only tested for OpenMPI). Unfortunately, this mapping of one rank per +GPU will leave CPU cores idle if there are more cores on the system than GPUs. + +Within a node, PtyPy can use nccl (requires a CuPy install +and setting `PTYPY_USE_NCCL=1`) for passing data between ranks/GPUs. + + +## Breaking changes + +### Ptyscan classes (experiment) need to be imported explicitly + +Most derived PtyScan classes (all those in the `/experiment` folder) now need +to be imported explicitly. We took this step to separate the user space +more clearly from the base package and to avoid dependency creep from +user-introduced code. At the beginning of your script, you now +need to import your module explicitly or use one of the helper +functions. + +```python +import ptypy +ptypy.load_ptyscan_module(module='') +ptypy.load_all_ptyscan_modules() +``` + +Any PtyScan derived class in these modules that is decorated +with the `ptypy.experiment.register()` function will now be included +in the parameter tree and selectable by name. + +If you prefer the old way of importing ptypy "fully loaded", just use +```python +import ptypy +ptypy.load_all() +``` +which attempts to load all optional PtyScan classes and all engines. + + +## Other updates + + 1. Code for `utils.parallel.bcast_dict` and `gather_dict` has been simplified and + should be backwards compatible. + 2. The `fourier_power_bound` that was previously calculated internally from + the `fourier_relax_factor` can now be set explicitly and we recommend that from + now on. The recommended value for the`fourier_power_bound` is 0.25 for Poisson statistics + (see supplementary of [`this paper`](https://www.pnas.org/doi/10.1073/pnas.0905846107#supplementary-materials)) + 3. Position correction now supports an alternate search scheme, i.e. along a fixed grid. + This scheme is more accurate than a stochastic search and the overhead incurred + for this brute force search is acceptable for GPU engines. + 4. We switched to a pip install within a conda environment as the main supported way of installation + +## Roadmap + + * Automatic adjustment of the block sizes. + * Improve scaling behavior across multiple nodes and high frame counts. + * Better support for live processing (on a continuous detector data stream). + * More tests. + * Branch cleaning. + +## Contributors + +Thanks to the efforts at the Diamond Light Source that made this +update possible. + + * Aaron Parsons + * Bjoern Enders + * Benedikt Daurer + * Joerg Lotze + + # PtyPy 0.4 release notes After quite some work we announce ptypy 0.4. Apart from including all the fixes and improvements from 0.3.0 to 0.3.1, it includes two bigger changes - 1. Ptypy has now been converted to python 3 and will be **python 3 only** in future. The python 2 version will not be actively maintained anymore, we keep a branch for it for a while but we don't expect to put in many fixes and certainly not anny new features. Team work by Julio, Alex, Bjoern and Aaron. + 1. Ptypy has now been converted to python 3 and will be **python 3 only** in future. The python 2 version will not be actively maintained anymore, we keep a branch for it for a while but we don't expect to put in many fixes and certainly not any new features. Team work by Julio, Alex, Bjoern and Aaron. *Please note: all branches that haven’t been converted to python 3 by the end of 2019 will most likely be removed during 2020.* Please rebase your effort on version 0.4. If you need help rebasing your efforts, please let us know soon. 2. Position correction is now supported in most engines. It has been implemented by Wilhelm Eschen following the annealing approach introduced by A.M. Maiden et al. (Ultramicroscopy, Volume 120, 2012, Pages 64-72). Bjoern, Benedikt and Aaron helped refine and test it. diff --git a/resources/__init__.py b/resources/__init__.py deleted file mode 100644 index 666bf4ba9..000000000 --- a/resources/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ - -def flowers(shape=None): - from ptypy import utils as u - im = u.HSV_to_P1A(u.RGB_to_HSV(u.imload(flowers.jpg))) - if shape is not None: - sh = u.expect2(shape) - ish = np.array(im.shape[:2]) - d = ish-sh - im = u.crop_pad(im,d,axes=[0,1],cen=None,fillpar=0.0,filltype='mirror') - - return im diff --git a/resources/ptypy_logo_1M.png b/resources/ptypy_logo_1M.png deleted file mode 100644 index 9c5a02e7f..000000000 Binary files a/resources/ptypy_logo_1M.png and /dev/null differ diff --git a/resources/tree.bmp b/resources/tree.bmp deleted file mode 100644 index 9168fe4af..000000000 Binary files a/resources/tree.bmp and /dev/null differ diff --git a/setup.cfg b/setup.cfg index 3a85bffa1..ee4a5cac1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,3 +5,6 @@ all_files = 1 [upload_sphinx] upload-dir = doc/build/html + +[tool:pytest] +testpaths = test/core_tests test/engine_tests test/io_tests test/ptyscan_tests test/template_tests test/util_tests diff --git a/setup.py b/setup.py index 241ebc647..98b469622 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ #!/usr/bin/env python -from distutils.core import setup, Extension +# we should aim to remove the distutils dependency +import setuptools +from distutils.core import setup +import sys CLASSIFIERS = """\ Development Status :: 3 - Alpha @@ -12,15 +15,18 @@ Operating System :: Unix """ + MAJOR = 0 -MINOR = 4 -MICRO = 1 +MINOR = 5 +MICRO = 0 ISRELEASED = False VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO) -#import os -#if os.path.exists('MANIFEST'): os.remove('MANIFEST') +# import os +# if os.path.exists('MANIFEST'): os.remove('MANIFEST') + +DEBUG = False def write_version_py(filename='ptypy/version.py'): cnt = """ @@ -48,46 +54,32 @@ def write_version_py(filename='ptypy/version.py'): finally: a.close() + if __name__ == '__main__': write_version_py() write_version_py('doc/version.py') try: - execfile('ptypy/version.py') - vers = version + exec(open('ptypy/version.py').read()) + vers = version # noqa: F821 except: vers = VERSION +exclude_packages = ["test.*", "test"] +package_list = setuptools.find_packages(exclude=exclude_packages) setup( name='Python Ptychography toolbox', version=VERSION, author='Pierre Thibault, Bjoern Enders, Martin Dierolf and others', description='Ptychographic reconstruction toolbox', long_description=open('README.rst', 'r').read(), - #install_requires = ['numpy>=1.8',\ - #'h5py>=2.2',\ - #'matplotlib>=1.3',\ - #'pyzmq>=14.0',\ - #'scipy>=0.13',\ - #'mpi4py>=1.3'], package_dir={'ptypy': 'ptypy'}, - packages=['ptypy', - 'ptypy.core', - 'ptypy.debug', - 'ptypy.utils', - 'ptypy.simulations', - 'ptypy.engines', - 'ptypy.io', - 'ptypy.resources', - 'ptypy.experiment', - 'ptypy.experiment.legacy'], - package_data={'ptypy': ['resources/*', ]}, - #include_package_data=True - scripts=[ - 'scripts/ptypy.plot', - 'scripts/ptypy.inspect', - 'scripts/ptypy.plotclient', - 'scripts/ptypy.new', - 'scripts/ptypy.csv2cp', - 'scripts/ptypy.run' - ], - ) + packages=package_list, + package_data={'ptypy': ['resources/*',], + 'ptypy.accelerate.cuda_pycuda.cuda': ['*.cu']}, + scripts=['scripts/ptypy.plot', + 'scripts/ptypy.inspect', + 'scripts/ptypy.plotclient', + 'scripts/ptypy.new', + 'scripts/ptypy.csv2cp', + 'scripts/ptypy.run'], +) diff --git a/templates/accelerate/ptypy_i13_AuStar_farfield_pycuda.py b/templates/accelerate/ptypy_i13_AuStar_farfield_pycuda.py new file mode 100644 index 000000000..1e7f14645 --- /dev/null +++ b/templates/accelerate/ptypy_i13_AuStar_farfield_pycuda.py @@ -0,0 +1,115 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental farfield conditions in the hard X-ray regime. +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +### PTYCHO PARAMETERS +p = u.Param() +p.verbose_level = "info" +p.run = None + +p.data_type = "single" +p.run = None +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False, layout="nearfield") +p.io.interaction = u.Param(active=False) + +# Simulation parameters +sim = u.Param() +sim.energy = 9.7 +sim.distance = 8.46e-2 +sim.psize = 100e-9 +sim.shape = 1024 +sim.xy = u.Param() +sim.xy.override = u.parallel.MPIrand_uniform(0.0,10e-6,(20,2)) +#sim.xy.positions = np.random.normal(0.0,3e-6,(20,2)) +sim.verbose_level = 1 + +sim.illumination = u.Param() +sim.illumination.model = None +sim.illumination.photons = 1e11 +sim.illumination.aperture = u.Param() +sim.illumination.aperture.diffuser = (8.0, 10.0) +sim.illumination.aperture.form = "circ" +sim.illumination.aperture.size = 90e-6 +sim.illumination.aperture.central_stop = 0.15 +sim.illumination.propagation = u.Param() +sim.illumination.propagation.focussed = None#0.08 +sim.illumination.propagation.parallel = 0.005 +sim.illumination.propagation.spot_size = None + +sim.sample = u.Param() +sim.sample.model = u.xradia_star((1200,1200),minfeature=3,contrast=0.8) +sim.sample.process = u.Param() +sim.sample.process.offset = (0,0) +sim.sample.process.zoom = 1.0 +sim.sample.process.formula = "Au" +sim.sample.process.density = 19.3 +sim.sample.process.thickness = 700e-9 +sim.sample.process.ref_index = None +sim.sample.process.smoothing = None +sim.sample.fill = 1.0+0.j + +sim.detector = 'GenericCCD32bit' +sim.plot = False + +# Scan model and initial value parameters +p.scans = u.Param() +p.scans.scan00 = u.Param() +p.scans.scan00.name = 'BlockFull' + +p.scans.scan00.coherence = u.Param() +p.scans.scan00.coherence.num_probe_modes = 1 +p.scans.scan00.coherence.num_object_modes = 1 +p.scans.scan00.coherence.energies = [1.0] + +p.scans.scan00.sample = u.Param() + +# (copy simulation illumination and modify some things) +p.scans.scan00.illumination = sim.illumination.copy(99) +p.scans.scan00.illumination.aperture.size = 105e-6 +p.scans.scan00.illumination.aperture.central_stop = None + +# Scan data (simulation) parameters +p.scans.scan00.data=u.Param() +p.scans.scan00.data.name = 'SimScan' +p.scans.scan00.data.propagation = 'nearfield' +p.scans.scan00.data.save = None #'append' +p.scans.scan00.data.shape = None +p.scans.scan00.data.num_frames = None +p.scans.scan00.data.update(sim) + +# Reconstruction parameters +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 100 +p.engines.engine00.object_inertia = 1. +p.engines.engine00.numiter_contiguous = 1 +p.engines.engine00.probe_support = None +p.engines.engine00.probe_inertia = 0.001 +p.engines.engine00.obj_smooth_std = 10 +p.engines.engine00.clip_object = None +p.engines.engine00.alpha = 1 +p.engines.engine00.probe_update_start = 2 +p.engines.engine00.update_object_first = True +p.engines.engine00.overlap_converge_factor = 0.5 +p.engines.engine00.overlap_max_iterations = 100 +p.engines.engine00.fourier_relax_factor = 0.05 + +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML_pycuda' +p.engines.engine01.numiter = 50 + +P = Ptycho(p,level=5) + diff --git a/templates/accelerate/ptypy_i13_AuStar_nearfield_pycuda.py b/templates/accelerate/ptypy_i13_AuStar_nearfield_pycuda.py new file mode 100644 index 000000000..21846666c --- /dev/null +++ b/templates/accelerate/ptypy_i13_AuStar_nearfield_pycuda.py @@ -0,0 +1,117 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental nearfield conditions in the hard X-ray regime. +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +### PTYCHO PARAMETERS +p = u.Param() +p.verbose_level = "info" +p.run = None +p.frames_per_block = 20 + +p.data_type = "single" +p.run = None +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False, layout="nearfield") +p.io.interaction = u.Param(active=False) + +# Simulation parameters +sim = u.Param() +sim.energy = 9.7 +sim.distance = 8.46e-2 +sim.psize = 100e-9 +sim.shape = 1024 +sim.xy = u.Param() +sim.xy.override = u.parallel.MPIrand_uniform(0.0,10e-6,(20,2)) +#sim.xy.positions = np.random.normal(0.0,3e-6,(20,2)) +sim.verbose_level = 1 + +sim.illumination = u.Param() +sim.illumination.model = None +sim.illumination.photons = 1e11 +sim.illumination.aperture = u.Param() +sim.illumination.aperture.diffuser = (8.0, 10.0) +sim.illumination.aperture.form = "circ" +sim.illumination.aperture.size = 90e-6 +sim.illumination.aperture.central_stop = 0.15 +sim.illumination.propagation = u.Param() +sim.illumination.propagation.focussed = None#0.08 +sim.illumination.propagation.parallel = 0.005 +sim.illumination.propagation.spot_size = None + +sim.sample = u.Param() +sim.sample.model = u.xradia_star((1200,1200),minfeature=3,contrast=0.8) +sim.sample.process = u.Param() +sim.sample.process.offset = (0,0) +sim.sample.process.zoom = 1.0 +sim.sample.process.formula = "Au" +sim.sample.process.density = 19.3 +sim.sample.process.thickness = 700e-9 +sim.sample.process.ref_index = None +sim.sample.process.smoothing = None +sim.sample.fill = 1.0+0.j + +sim.detector = 'GenericCCD32bit' +sim.plot = False + +# Scan model and initial value parameters +p.scans = u.Param() +p.scans.scan00 = u.Param() +p.scans.scan00.name = 'BlockFull' +p.scans.scan00.propagation = "nearfield" + +p.scans.scan00.coherence = u.Param() +p.scans.scan00.coherence.num_probe_modes = 1 +p.scans.scan00.coherence.num_object_modes = 1 +p.scans.scan00.coherence.energies = [1.0] + +p.scans.scan00.sample = u.Param() + +# (copy simulation illumination and modify some things) +p.scans.scan00.illumination = sim.illumination.copy(99) +p.scans.scan00.illumination.aperture.size = 105e-6 +p.scans.scan00.illumination.aperture.central_stop = None + +# Scan data (simulation) parameters +p.scans.scan00.data=u.Param() +p.scans.scan00.data.name = 'SimScan' +p.scans.scan00.data.propagation = 'nearfield' +p.scans.scan00.data.save = None #'append' +p.scans.scan00.data.shape = None +p.scans.scan00.data.num_frames = None +p.scans.scan00.data.update(sim) + +# Reconstruction parameters +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 100 +p.engines.engine00.object_inertia = 1. +p.engines.engine00.numiter_contiguous = 1 +p.engines.engine00.probe_support = None +p.engines.engine00.probe_inertia = 0.001 +p.engines.engine00.obj_smooth_std = 10 +p.engines.engine00.clip_object = None +p.engines.engine00.alpha = 1 +p.engines.engine00.probe_update_start = 2 +p.engines.engine00.update_object_first = True +p.engines.engine00.overlap_converge_factor = 0.5 +p.engines.engine00.overlap_max_iterations = 100 +p.engines.engine00.fourier_relax_factor = 0.05 + +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML_pycuda' +p.engines.engine01.numiter = 50 + +P = Ptycho(p,level=5) + diff --git a/templates/accelerate/ptypy_id22ni_AuStar_focused_pycuda.py b/templates/accelerate/ptypy_id22ni_AuStar_focused_pycuda.py new file mode 100644 index 000000000..d14913301 --- /dev/null +++ b/templates/accelerate/ptypy_id22ni_AuStar_focused_pycuda.py @@ -0,0 +1,119 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental farfield conditions and with a focused beam in the hard X-ray regime. +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import numpy as np +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +### PTYCHO PARAMETERS +p.verbose_level = "info" + +p.data_type = "single" +p.run = None + +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False, layout="weak") +p.io.interaction = u.Param(active=False) + +# Simulation parameters +sim = u.Param() +sim.energy = 17.0 +sim.distance = 2.886 +sim.psize = 51e-6 +sim.shape = 256 +sim.xy = u.Param() +sim.xy.model = "round" +sim.xy.spacing = 250e-9 +sim.xy.steps = 30 +sim.xy.extent = 4e-6 + +sim.illumination = u.Param() +sim.illumination.model = None +sim.illumination.photons = 3e8 +sim.illumination.aperture = u.Param() +sim.illumination.aperture.diffuser = None +sim.illumination.aperture.form = "rect" +sim.illumination.aperture.size = 35e-6 +sim.illumination.aperture.central_stop = None +sim.illumination.propagation = u.Param() +sim.illumination.propagation.focussed = 0.08 +sim.illumination.propagation.parallel = 0.0014 +sim.illumination.propagation.spot_size = None + +sim.sample = u.Param() +sim.sample.model = u.xradia_star((1000,1000),minfeature=3,contrast=0.0) +sim.sample.process = u.Param() +sim.sample.process.offset = (100,100) +sim.sample.process.zoom = 1.0 +sim.sample.process.formula = "Au" +sim.sample.process.density = 19.3 +sim.sample.process.thickness = 2000e-9 +sim.sample.process.ref_index = None +sim.sample.process.smoothing = None +sim.sample.fill = 1.0+0.j + +#sim.detector = 'FRELON_TAPER' +sim.detector = 'GenericCCD32bit' +sim.verbose_level = 1 +sim.psf = 1. # emulates partial coherence +sim.plot = False + +# Scan model and initial value parameters +p.scans = u.Param() +p.scans.scan00 = u.Param() +p.scans.scan00.name = 'BlockFull' + +p.scans.scan00.coherence = u.Param() +p.scans.scan00.coherence.num_probe_modes = 4 +p.scans.scan00.coherence.num_object_modes = 1 +p.scans.scan00.coherence.energies = [1.0] + +p.scans.scan00.sample = u.Param() +p.scans.scan00.sample.model = 'stxm' +p.scans.scan00.sample.process = None + +# (copy the simulation illumination and change specific things) +p.scans.scan00.illumination = sim.illumination.copy(99) +p.scans.scan00.illumination.aperture.form = 'circ' +p.scans.scan00.illumination.propagation.focussed = 0.06 +p.scans.scan00.illumination.diversity = u.Param() +p.scans.scan00.illumination.diversity.power = 0.1 +p.scans.scan00.illumination.diversity.noise = (np.pi,3.0) + +# Scan data (simulation) parameters +p.scans.scan00.data = u.Param() +p.scans.scan00.data.name = 'SimScan' +p.scans.scan00.data.update(sim) +p.scans.scan00.data.save = None + +# Reconstruction parameters +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 150 +p.engines.engine00.fourier_relax_factor = 0.05 +p.engines.engine00.numiter_contiguous = 1 +p.engines.engine00.probe_support = 0.7 +p.engines.engine00.probe_inertia = 0.01 +p.engines.engine00.object_inertia = 0.1 +p.engines.engine00.clip_object = (0, 1.) +p.engines.engine00.alpha = 1 +p.engines.engine00.probe_update_start = 2 +p.engines.engine00.update_object_first = True +p.engines.engine00.overlap_converge_factor = 0.05 +p.engines.engine00.overlap_max_iterations = 100 +p.engines.engine00.obj_smooth_std = 5 + +u.verbose.set_level("info") +P = Ptycho(p,level=5) diff --git a/templates/accelerate/ptypy_laser_logo_focused_pycuda.py b/templates/accelerate/ptypy_laser_logo_focused_pycuda.py new file mode 100644 index 000000000..0274aefa2 --- /dev/null +++ b/templates/accelerate/ptypy_laser_logo_focused_pycuda.py @@ -0,0 +1,99 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental farfield conditions and with a focused optical laser beam. +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy.simulations as sim +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import pathlib +import numpy as np +import tempfile +tmpdir = tempfile.gettempdir() + +### PTYCHO PARAMETERS +p = u.Param() +p.verbose_level = "info" +p.data_type = "single" + +p.run = None +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False, layout="minimal") +p.io.interaction = u.Param(active=False) + +# Simulation parameters +sim = u.Param() +sim.energy = u.keV2m(1.0)/6.32e-7 +sim.distance = 15e-2 +sim.psize = 24e-6 +sim.shape = 256 +sim.xy = u.Param() +sim.xy.model = "round" +sim.xy.spacing = 0.3e-3 +sim.xy.steps = 60 +sim.xy.extent = (5e-3,10e-3) + +sim.illumination = u.Param() +sim.illumination.model = None +sim.illumination.photons = int(1e9) +sim.illumination.aperture = u.Param() +sim.illumination.aperture.diffuser = None#(0.7,3) +sim.illumination.aperture.form = "circ" +sim.illumination.aperture.size = 1.0e-3 +sim.illumination.aperture.edge = 2 +sim.illumination.aperture.central_stop = None +sim.illumination.propagation = u.Param() +sim.illumination.propagation.focussed = None +sim.illumination.propagation.parallel = 0.03 +sim.illumination.propagation.spot_size = None + +imgfile = "/".join([str(pathlib.Path(__file__).parent.resolve()), '../../resources/ptypy_logo_1M.png']) +sim.sample = u.Param() +sim.sample.model = -u.rgb2complex(u.imload(imgfile)[::-1,:,:-1]) +sim.sample.process = u.Param() +sim.sample.process.offset = (0,0) +sim.sample.process.zoom = 0.5 +sim.sample.process.formula = None +sim.sample.process.density = None +sim.sample.process.thickness = None +sim.sample.process.ref_index = None +sim.sample.process.smoothing = None +sim.sample.fill = 1.0+0.j +sim.plot=False +sim.detector = u.Param(dtype=np.uint32,full_well=2**32-1,psf=None) + +# Scan model and initial value parameters +p.scans = u.Param() +p.scans.ptypy = u.Param() +p.scans.ptypy.name = 'BlockFull' + +p.scans.ptypy.coherence = u.Param() +p.scans.ptypy.coherence.num_probe_modes=1 + +p.scans.ptypy.illumination = u.Param() +p.scans.ptypy.illumination.model=None +p.scans.ptypy.illumination.aperture = u.Param() +p.scans.ptypy.illumination.aperture.diffuser = None +p.scans.ptypy.illumination.aperture.form = "circ" +p.scans.ptypy.illumination.aperture.size = 1.0e-3 +p.scans.ptypy.illumination.aperture.edge = 10 + +# Scan data (simulation) parameters +p.scans.ptypy.data = u.Param() +p.scans.ptypy.data.name = 'SimScan' +p.scans.ptypy.data.update(sim) + +# Reconstruction parameters +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 40 +p.engines.engine00.fourier_relax_factor = 0.05 + +u.verbose.set_level("info") +P = Ptycho(p,level=5) diff --git a/templates/accelerate/ptypy_minimal_prep_and_run_pycuda.py b/templates/accelerate/ptypy_minimal_prep_and_run_pycuda.py new file mode 100644 index 000000000..26226aae4 --- /dev/null +++ b/templates/accelerate/ptypy_minimal_prep_and_run_pycuda.py @@ -0,0 +1,53 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 80 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_DM.py b/templates/engines/moonflower_DM.py new file mode 100644 index 000000000..8cebace19 --- /dev/null +++ b/templates/engines/moonflower_DM.py @@ -0,0 +1,54 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM' +p.engines.engine00.numiter = 80 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_DM_ML.py b/templates/engines/moonflower_DM_ML.py new file mode 100644 index 000000000..18fd7603e --- /dev/null +++ b/templates/engines/moonflower_DM_ML.py @@ -0,0 +1,69 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 400 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 600 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM' +p.engines.engine00.numiter = 60 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.probe_support = 0.5 + +# attach a reconstrucion engine +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML' +p.engines.engine01.numiter = 20 +p.engines.engine01.numiter_contiguous = 5 +p.engines.engine01.reg_del2 = False +p.engines.engine01.reg_del2_amplitude = 1. +p.engines.engine01.floating_intensities = False +p.engines.engine01.probe_support = 0.5 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_DM_ML_pycuda.py b/templates/engines/moonflower_DM_ML_pycuda.py new file mode 100644 index 000000000..8ea2584d7 --- /dev/null +++ b/templates/engines/moonflower_DM_ML_pycuda.py @@ -0,0 +1,67 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 400 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 600 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 60 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.probe_support = 0.5 + +# attach a reconstrucion engine +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML_pycuda' +p.engines.engine01.numiter = 20 +p.engines.engine01.numiter_contiguous = 5 +p.engines.engine01.reg_del2 = False +p.engines.engine01.reg_del2_amplitude = 1. +p.engines.engine01.floating_intensities = False +p.engines.engine01.probe_support = 0.5 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_DM_ocl.py b/templates/engines/moonflower_DM_ocl.py new file mode 100644 index 000000000..4a8119fe4 --- /dev/null +++ b/templates/engines/moonflower_DM_ocl.py @@ -0,0 +1,56 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="ocl") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 300 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 1000 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=2) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.1 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_ocl' +p.engines.engine00.numiter = 80 +p.engines.engine00.numiter_contiguous = 10 + +# prepare and run +P = Ptycho(p,level=5) \ No newline at end of file diff --git a/templates/engines/moonflower_DM_pycuda.py b/templates/engines/moonflower_DM_pycuda.py new file mode 100644 index 000000000..e09aca720 --- /dev/null +++ b/templates/engines/moonflower_DM_pycuda.py @@ -0,0 +1,56 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 1000 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=4) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 20 +p.engines.engine00.numiter_contiguous = 10 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_DM_pycuda_nostream.py b/templates/engines/moonflower_DM_pycuda_nostream.py new file mode 100644 index 000000000..4970bfe06 --- /dev/null +++ b/templates/engines/moonflower_DM_pycuda_nostream.py @@ -0,0 +1,57 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 1000 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=4) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda_nostream' +p.engines.engine00.numiter = 20 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.probe_update_start = 1 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_DM_serial.py b/templates/engines/moonflower_DM_serial.py new file mode 100644 index 000000000..fdc352d4f --- /dev/null +++ b/templates/engines/moonflower_DM_serial.py @@ -0,0 +1,56 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 400 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_serial' +p.engines.engine00.numiter = 20 +p.engines.engine00.numiter_contiguous = 10 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_EPIE.py b/templates/engines/moonflower_EPIE.py new file mode 100644 index 000000000..12931b06a --- /dev/null +++ b/templates/engines/moonflower_EPIE.py @@ -0,0 +1,60 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'GradFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'EPIE' +p.engines.engine00.numiter = 200 +p.engines.engine00.probe_center_tol = None +p.engines.engine00.compute_log_likelihood = True +p.engines.engine00.object_norm_is_global = True +p.engines.engine00.alpha = 1 +p.engines.engine00.beta = 1 +p.engines.engine00.probe_update_start = 2 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_EPIE_ML_pycuda.py b/templates/engines/moonflower_EPIE_ML_pycuda.py new file mode 100644 index 000000000..c9635b82f --- /dev/null +++ b/templates/engines/moonflower_EPIE_ML_pycuda.py @@ -0,0 +1,74 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'GradFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +p.scans.MF.illumination=u.Param() +p.scans.MF.illumination.diversity = None + +p.scans.MF.coherence=u.Param() +p.scans.MF.coherence.num_probe_modes = 1 +p.scans.MF.coherence.num_object_modes = 1 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'EPIE_pycuda' +p.engines.engine00.numiter = 200 +p.engines.engine00.probe_center_tol = None +p.engines.engine00.compute_log_likelihood = True +p.engines.engine00.object_norm_is_global = True +p.engines.engine00.alpha = 1 +p.engines.engine00.beta = 1 +p.engines.engine00.probe_update_start = 2 + +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML_pycuda' +p.engines.engine01.ML_type = 'Gaussian' +p.engines.engine01.reg_del2 = True +p.engines.engine01.reg_del2_amplitude = 1. +p.engines.engine01.scale_precond = True +p.engines.engine01.scale_probe_object = 1. +p.engines.engine01.numiter = 100 + +# prepare and run +P = Ptycho(p,level=5) \ No newline at end of file diff --git a/templates/engines/moonflower_EPIE_pycuda.py b/templates/engines/moonflower_EPIE_pycuda.py new file mode 100644 index 000000000..4ef869bb6 --- /dev/null +++ b/templates/engines/moonflower_EPIE_pycuda.py @@ -0,0 +1,58 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'GradFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'EPIE_pycuda' +p.engines.engine00.numiter = 200 +p.engines.engine00.probe_center_tol = None +p.engines.engine00.compute_log_likelihood = True +p.engines.engine00.object_norm_is_global = True +p.engines.engine00.alpha = 1 +p.engines.engine00.beta = 1 +p.engines.engine00.probe_update_start = 2 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_EPIE_serial.py b/templates/engines/moonflower_EPIE_serial.py new file mode 100644 index 000000000..785f04933 --- /dev/null +++ b/templates/engines/moonflower_EPIE_serial.py @@ -0,0 +1,58 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'GradFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'EPIE_serial' +p.engines.engine00.numiter = 200 +p.engines.engine00.probe_center_tol = None +p.engines.engine00.compute_log_likelihood = True +p.engines.engine00.object_norm_is_global = True +p.engines.engine00.alpha = 1 +p.engines.engine00.beta = 1 +p.engines.engine00.probe_update_start = 2 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/minimal_prep_and_run_resample_ML.py b/templates/engines/moonflower_ML_Gaussian.py similarity index 64% rename from templates/minimal_prep_and_run_resample_ML.py rename to templates/engines/moonflower_ML_Gaussian.py index f0d5619f9..9e1334c02 100644 --- a/templates/minimal_prep_and_run_resample_ML.py +++ b/templates/engines/moonflower_ML_Gaussian.py @@ -7,27 +7,33 @@ from ptypy.core import Ptycho from ptypy import utils as u +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 4 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None -#p.io.autoplot = u.Param() -#p.io.autoplot.dump = True -#p.io.autoplot = False +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) # max 100 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() -p.scans.MF.name = 'Full' +p.scans.MF.name = 'BlockFull' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 -p.scans.MF.data.num_frames = 100 +p.scans.MF.data.num_frames = 200 p.scans.MF.data.save = None # position distance in fraction of illumination frame @@ -37,23 +43,19 @@ # Gaussian FWHM of possible detector blurring p.scans.MF.data.psf = 0. -# Resample by a factor of 2 -p.scans.MF.resample = 2 - # attach a reconstrucion engine p.engines = u.Param() p.engines.engine00 = u.Param() p.engines.engine00.name = 'ML' -p.engines.engine00.reg_del2 = True # Whether to use a Gaussian prior (smoothing) regularizer +p.engines.engine00.ML_type = 'Gaussian' +p.engines.engine00.reg_del2 = True # Whether to use a Gaussian prior (smoothing) regularizer p.engines.engine00.reg_del2_amplitude = 1. # Amplitude of the Gaussian prior if used p.engines.engine00.scale_precond = True -p.engines.engine00.scale_probe_object = 1. +#p.engines.engine00.scale_probe_object = 1. p.engines.engine00.smooth_gradient = 20. p.engines.engine00.smooth_gradient_decay = 1/50. -p.engines.engine00.floating_intensities = True +p.engines.engine00.floating_intensities = False p.engines.engine00.numiter = 300 # prepare and run -P = Ptycho(p,level=4) -P.run() - +P = Ptycho(p,level=5) diff --git a/templates/minimal_prep_and_run_ML_Poisson.py b/templates/engines/moonflower_ML_Poisson.py similarity index 83% rename from templates/minimal_prep_and_run_ML_Poisson.py rename to templates/engines/moonflower_ML_Poisson.py index 374622217..7c638d65e 100644 --- a/templates/minimal_prep_and_run_ML_Poisson.py +++ b/templates/engines/moonflower_ML_Poisson.py @@ -3,28 +3,32 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ -#import ptypy from ptypy.core import Ptycho from ptypy import utils as u -#import numpy -#numpy.seterr(divide='raise', invalid='raise') + +import tempfile +tmpdir = tempfile.gettempdir() p = u.Param() # for verbose output -p.verbose_level = 4 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None -p.io.autoplot = u.Param() -p.io.autoplot.active = True +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) # max 100 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() -p.scans.MF.name = 'Full' +p.scans.MF.name = 'BlockFull' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 @@ -60,5 +64,6 @@ p.engines.engine01.smooth_gradient_decay = 1/50. p.engines.engine01.floating_intensities = False p.engines.engine01.numiter = 300 + # prepare and run P = Ptycho(p, level=5) diff --git a/templates/engines/moonflower_ML_pycuda.py b/templates/engines/moonflower_ML_pycuda.py new file mode 100644 index 000000000..9d86f501e --- /dev/null +++ b/templates/engines/moonflower_ML_pycuda.py @@ -0,0 +1,62 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" + +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 400 +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 100 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'ML_pycuda' +p.engines.engine00.numiter = 300 +p.engines.engine00.numiter_contiguous = 5 +p.engines.engine00.reg_del2 = True # Whether to use a Gaussian prior (smoothing) regularizer +p.engines.engine00.reg_del2_amplitude = 1. # Amplitude of the Gaussian prior if used +p.engines.engine00.scale_precond = True +p.engines.engine00.smooth_gradient = 20. +p.engines.engine00.smooth_gradient_decay = 1/50. +p.engines.engine00.floating_intensities = False + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_ML_serial.py b/templates/engines/moonflower_ML_serial.py new file mode 100644 index 000000000..6f4cccfbe --- /dev/null +++ b/templates/engines/moonflower_ML_serial.py @@ -0,0 +1,62 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 100 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 600 +p.scans.MF.data.save = None +p.scans.MF.data.block_wait_count = 1 + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.1 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_serial' +p.engines.engine00.numiter = 20 +p.engines.engine00.numiter_contiguous = 1 +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML_serial' +p.engines.engine01.numiter = 20 +p.engines.engine01.numiter_contiguous = 1 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/minimal_prep_and_run_ePIE.py b/templates/engines/moonflower_RAAR.py similarity index 72% rename from templates/minimal_prep_and_run_ePIE.py rename to templates/engines/moonflower_RAAR.py index 28d8bced8..c01bb236c 100644 --- a/templates/minimal_prep_and_run_ePIE.py +++ b/templates/engines/moonflower_RAAR.py @@ -3,26 +3,34 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ - from ptypy.core import Ptycho from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None -#p.io.autoplot = u.Param(active=False) +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) # max 200 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() # now you have to specify which ScanModel to use with scans.XX.name, # just as you have to give 'name' for engines and PtyScan subclasses. -p.scans.MF.name = 'Vanilla' # or 'Full' +p.scans.MF.name = 'BlockFull' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 @@ -36,17 +44,12 @@ # Gaussian FWHM of possible detector blurring p.scans.MF.data.psf = 0. -#p.scans.MF.resample = 2 - # attach a reconstrucion engine p.engines = u.Param() p.engines.engine00 = u.Param() -p.engines.engine00.name = 'ePIE' +p.engines.engine00.name = 'RAAR' p.engines.engine00.numiter = 100 -p.engines.engine00.average_probe = True -#p.engines.engine00.obj_smooth_std = 2 -p.engines.engine00.probe_center_tol = None -#p.engines.engine00.compute_log_likelihood = False +p.engines.engine00.beta = 0.9 # prepare and run P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_RAAR_ML.py b/templates/engines/moonflower_RAAR_ML.py new file mode 100644 index 000000000..5745a6c3f --- /dev/null +++ b/templates/engines/moonflower_RAAR_ML.py @@ -0,0 +1,70 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 400 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 600 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'RAAR' +p.engines.engine00.numiter = 60 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.probe_support = 0.5 +p.engines.engine00.beta = 0.9 + +# attach a reconstrucion engine +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML' +p.engines.engine01.numiter = 20 +p.engines.engine01.numiter_contiguous = 5 +p.engines.engine01.reg_del2 = False +p.engines.engine01.reg_del2_amplitude = 1. +p.engines.engine01.floating_intensities = False +p.engines.engine01.probe_support = 0.5 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_RAAR_ML_pycuda.py b/templates/engines/moonflower_RAAR_ML_pycuda.py new file mode 100644 index 000000000..75f0e7b27 --- /dev/null +++ b/templates/engines/moonflower_RAAR_ML_pycuda.py @@ -0,0 +1,68 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 400 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 600 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'RAAR_pycuda' +p.engines.engine00.numiter = 60 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.probe_support = 0.5 +p.engines.engine00.beta = 0.9 + +# attach a reconstrucion engine +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML_pycuda' +p.engines.engine01.numiter = 20 +p.engines.engine01.numiter_contiguous = 5 +p.engines.engine01.reg_del2 = False +p.engines.engine01.reg_del2_amplitude = 1. +p.engines.engine01.floating_intensities = False +p.engines.engine01.probe_support = 0.5 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_RAAR_pycuda.py b/templates/engines/moonflower_RAAR_pycuda.py new file mode 100644 index 000000000..404f2925d --- /dev/null +++ b/templates/engines/moonflower_RAAR_pycuda.py @@ -0,0 +1,57 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 1000 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=4) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'RAAR_pycuda' +p.engines.engine00.numiter = 20 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.beta = 0.9 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_RAAR_serial.py b/templates/engines/moonflower_RAAR_serial.py new file mode 100644 index 000000000..7f43c8112 --- /dev/null +++ b/templates/engines/moonflower_RAAR_serial.py @@ -0,0 +1,57 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 400 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'RAAR_serial' +p.engines.engine00.numiter = 20 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.beta = 0.9 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_SDR.py b/templates/engines/moonflower_SDR.py new file mode 100644 index 000000000..f0b4926aa --- /dev/null +++ b/templates/engines/moonflower_SDR.py @@ -0,0 +1,62 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# Frames per block +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'Full' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0.0 +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 1 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'SDR' +p.engines.engine00.numiter = 300 +p.engines.engine00.sigma = 0.5 +p.engines.engine00.tau = 0.1 + + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_SDR_pycuda.py b/templates/engines/moonflower_SDR_pycuda.py new file mode 100644 index 000000000..819315492 --- /dev/null +++ b/templates/engines/moonflower_SDR_pycuda.py @@ -0,0 +1,60 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# Frames per block +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'Full' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0.0 +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 1 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'SDR_pycuda' +p.engines.engine00.numiter = 500 +p.engines.engine00.sigma = 0.5 +p.engines.engine00.tau = 0.1 +p.engines.engine00.probe_update_start = 2 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/engines/moonflower_SDR_serial.py b/templates/engines/moonflower_SDR_serial.py new file mode 100644 index 000000000..f72d388a6 --- /dev/null +++ b/templates/engines/moonflower_SDR_serial.py @@ -0,0 +1,59 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# Frames per block +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'Full' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0.0 +p.scans.MF.coherence = u.Param() +p.scans.MF.coherence.num_probe_modes = 1 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'SDR_serial' +p.engines.engine00.numiter = 100 +p.engines.engine00.sigma = 0.5 +p.engines.engine00.tau = 0.1 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/nanomax_zmq_run.py b/templates/experiment/nanomax_zmq_run.py similarity index 83% rename from templates/nanomax_zmq_run.py rename to templates/experiment/nanomax_zmq_run.py index 78d8625f3..f23109cd0 100644 --- a/templates/nanomax_zmq_run.py +++ b/templates/experiment/nanomax_zmq_run.py @@ -2,18 +2,23 @@ Loads data from a zmq stream published by Contrast at NanoMAX, https://github.com/alexbjorling/contrast """ - from ptypy.core import Ptycho from ptypy import utils as u +import ptypy +ptypy.load_ptyscan_module("nanomax_streaming") + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) # max 200 frames (128x128px) of diffraction data p.scans = u.Param() diff --git a/templates/live_processing/moonflower_DM_delayed.py b/templates/live_processing/moonflower_DM_delayed.py new file mode 100644 index 000000000..fb2230335 --- /dev/null +++ b/templates/live_processing/moonflower_DM_delayed.py @@ -0,0 +1,57 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" + +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=True) +p.io.autoplot = u.Param(active=True) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 1000 +p.scans.MF.data.save = None +p.scans.MF.data.block_wait_count = 1 + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_serial' +p.engines.engine00.numiter = 60 +p.engines.engine00.numiter_contiguous = 2 +p.engines.engine00.probe_update_start = 1 + +# prepare and run +P = Ptycho(p,level=5) \ No newline at end of file diff --git a/templates/live_processing/moonflower_DM_delayed_pycuda.py b/templates/live_processing/moonflower_DM_delayed_pycuda.py new file mode 100644 index 000000000..6e59390c2 --- /dev/null +++ b/templates/live_processing/moonflower_DM_delayed_pycuda.py @@ -0,0 +1,59 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" + +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 1000 +p.scans.MF.data.save = None +p.scans.MF.data.block_wait_count = 1 + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.numiter = 120 +p.engines.engine00.numiter_contiguous = 5 +p.engines.engine00.probe_update_start = 1 + +# prepare and run +P = Ptycho(p,level=5) \ No newline at end of file diff --git a/templates/on_the_fly_ptyd.py b/templates/live_processing/on_the_fly_ptyd.py similarity index 100% rename from templates/on_the_fly_ptyd.py rename to templates/live_processing/on_the_fly_ptyd.py diff --git a/templates/on_the_fly_rec.py b/templates/live_processing/on_the_fly_rec.py similarity index 100% rename from templates/on_the_fly_rec.py rename to templates/live_processing/on_the_fly_rec.py diff --git a/templates/bragg_field_of_view.py b/templates/misc/bragg_field_of_view.py similarity index 98% rename from templates/bragg_field_of_view.py rename to templates/misc/bragg_field_of_view.py index 4376e6ebc..dea88e759 100644 --- a/templates/bragg_field_of_view.py +++ b/templates/misc/bragg_field_of_view.py @@ -2,16 +2,17 @@ Example script which uses the 3d Bragg ptycho code to calculate and plot the 3d field of view as compared to the incoming probe. """ - from ptypy.core import Ptycho from ptypy import utils as u +import ptypy +ptypy.load_ptyscan_module("Bragg3dSim") import matplotlib.pyplot as plt import numpy as np # Set up a parameter tree p = u.Param() -p.verbose_level = 3 +p.verbose_level = "info" # illumination for data simulation and pods illumination = u.Param() diff --git a/templates/bragg_prep_and_run.py b/templates/misc/bragg_prep_and_run.py similarity index 95% rename from templates/bragg_prep_and_run.py rename to templates/misc/bragg_prep_and_run.py index 40ab60f9f..d6ced5020 100644 --- a/templates/bragg_prep_and_run.py +++ b/templates/misc/bragg_prep_and_run.py @@ -1,16 +1,17 @@ """ Simulates and then inverts 3d Bragg ptycho data. """ - from ptypy.core import Ptycho from ptypy import utils as u +import ptypy +ptypy.load_ptyscan_module("Bragg3dSim") import tempfile p = u.Param() p.run = 'Si110_stripes' # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # use special plot layout for 3d data p.io = u.Param() diff --git a/templates/misc/moonflower_DM_object_regul.py b/templates/misc/moonflower_DM_object_regul.py new file mode 100644 index 000000000..7b8e7d7a0 --- /dev/null +++ b/templates/misc/moonflower_DM_object_regul.py @@ -0,0 +1,62 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +from ptypy.custom import DM_object_regul, DM_pycuda_object_regul +import numpy as np + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +ny,nx = (492,492) +xx,yy = np.meshgrid(np.arange(nx)-nx//2, np.arange(ny)-ny//2) +mask = xx**2 + yy**2 > (150)**2 +mask = np.expand_dims(mask,0) + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockVanilla' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_object_regul' +p.engines.engine00.numiter = 80 +p.engines.engine00.object_regul_mask = mask +p.engines.engine00.object_regul_fill = 0. +p.engines.engine00.object_regul_start = 10 +p.engines.engine00.object_regul_stop = 60 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/misc/moonflower_DM_object_regul_pycuda.py b/templates/misc/moonflower_DM_object_regul_pycuda.py new file mode 100644 index 000000000..5ac11fe91 --- /dev/null +++ b/templates/misc/moonflower_DM_object_regul_pycuda.py @@ -0,0 +1,62 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u +from ptypy.custom import DM_object_regul, DM_pycuda_object_regul +import numpy as np + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +ny,nx = (492,492) +xx,yy = np.meshgrid(np.arange(nx)-nx//2, np.arange(ny)-ny//2) +mask = xx**2 + yy**2 > (150)**2 +mask = np.expand_dims(mask,0) + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 200 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockVanilla' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda_object_regul' +p.engines.engine00.numiter = 80 +p.engines.engine00.object_regul_mask = mask +p.engines.engine00.object_regul_fill = 0. +p.engines.engine00.object_regul_start = 10 +p.engines.engine00.object_regul_stop = 60 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/probe_sharing.py b/templates/misc/moonflower_probe_sharing.py similarity index 94% rename from templates/probe_sharing.py rename to templates/misc/moonflower_probe_sharing.py index caa2a4dd6..8cec73cc6 100644 --- a/templates/probe_sharing.py +++ b/templates/misc/moonflower_probe_sharing.py @@ -1,16 +1,13 @@ ''' An example showing how to share a probe across two scans ''' - - -import sys -import time import ptypy from ptypy import utils as u from ptypy.core import Ptycho import tempfile u.verbose.set_level(3) tmp = tempfile.mkdtemp()+'/%s' +tmpdir = tempfile.gettempdir() def make_sample(outpath): data = u.Param() @@ -40,10 +37,10 @@ def make_sample(outpath): p = u.Param() -p.verbose_level = 3 +p.verbose_level = "info" p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) p.scans = u.Param() diff --git a/templates/pars_few_alldoc.py b/templates/misc/pars_few_alldoc.py similarity index 100% rename from templates/pars_few_alldoc.py rename to templates/misc/pars_few_alldoc.py diff --git a/templates/minimal_prep_and_run_probe_modes.py b/templates/model/moonflower_blockfull.py similarity index 83% rename from templates/minimal_prep_and_run_probe_modes.py rename to templates/model/moonflower_blockfull.py index 8dbcb4dc4..e83cc2538 100644 --- a/templates/minimal_prep_and_run_probe_modes.py +++ b/templates/model/moonflower_blockfull.py @@ -3,25 +3,31 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ - from ptypy.core import Ptycho from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) + +# Block size +p.frames_per_block = 20 # max 200 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() # now you have to specify which ScanModel to use with scans.XX.name, # just as you have to give 'name' for engines and PtyScan subclasses. -p.scans.MF.name = 'Full' # or 'Full' +p.scans.MF.name = 'BlockFull' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 @@ -34,9 +40,6 @@ p.scans.MF.data.photons = 1e8 # Gaussian FWHM of possible detector blurring p.scans.MF.data.psf = 0. -p.scans.MF.coherence=u.Param() -p.scans.MF.coherence.num_probe_modes = 2 - # attach a reconstrucion engine p.engines = u.Param() diff --git a/templates/model/moonflower_blockgradfull.py b/templates/model/moonflower_blockgradfull.py new file mode 100644 index 000000000..fe97d1c94 --- /dev/null +++ b/templates/model/moonflower_blockgradfull.py @@ -0,0 +1,64 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +#import ptypy +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) + +# Block size +p.frames_per_block = 20 + +# max 100 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +p.scans.MF.name = 'BlockGradFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'ML' +p.engines.engine00.ML_type = 'Gaussian' +p.engines.engine00.reg_del2 = True # Whether to use a Gaussian prior (smoothing) regularizer +p.engines.engine00.reg_del2_amplitude = 1. # Amplitude of the Gaussian prior if used +p.engines.engine00.scale_precond = True +#p.engines.engine00.scale_probe_object = 1. +p.engines.engine00.smooth_gradient = 20. +p.engines.engine00.smooth_gradient_decay = 1/50. +p.engines.engine00.floating_intensities = False +p.engines.engine00.numiter = 300 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/minimal_prep_and_run_resample_DM.py b/templates/model/moonflower_blockvanilla.py similarity index 79% rename from templates/minimal_prep_and_run_resample_DM.py rename to templates/model/moonflower_blockvanilla.py index 08c12540c..346e3fcc3 100644 --- a/templates/minimal_prep_and_run_resample_DM.py +++ b/templates/model/moonflower_blockvanilla.py @@ -3,25 +3,31 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ - from ptypy.core import Ptycho from ptypy import utils as u +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() + # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) + +# Block size +p.frames_per_block = 20 # max 200 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() # now you have to specify which ScanModel to use with scans.XX.name, # just as you have to give 'name' for engines and PtyScan subclasses. -p.scans.MF.name = 'Vanilla' # or 'Full' +p.scans.MF.name = 'BlockVanilla' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 @@ -35,16 +41,11 @@ # Gaussian FWHM of possible detector blurring p.scans.MF.data.psf = 0. -# Resample by a factor of 2 -p.scans.MF.resample = 2 - # attach a reconstrucion engine p.engines = u.Param() p.engines.engine00 = u.Param() p.engines.engine00.name = 'DM' -p.engines.engine00.numiter = 100 -p.engines.engine00.probe_support = 0.05 +p.engines.engine00.numiter = 80 # prepare and run -P = Ptycho(p,level=4) -P.run() +P = Ptycho(p,level=5) diff --git a/templates/minimal_prep_and_run_blockmodel.py b/templates/model/moonflower_full.py similarity index 89% rename from templates/minimal_prep_and_run_blockmodel.py rename to templates/model/moonflower_full.py index beeaf7342..6347b243c 100644 --- a/templates/minimal_prep_and_run_blockmodel.py +++ b/templates/model/moonflower_full.py @@ -3,28 +3,28 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ - from ptypy.core import Ptycho from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home = "/".join([tmpdir, "ptypy"]) p.io.autosave = u.Param(active=False) -# Block size -p.frames_per_block = 20 - # max 200 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() # now you have to specify which ScanModel to use with scans.XX.name, # just as you have to give 'name' for engines and PtyScan subclasses. -p.scans.MF.name = 'BlockVanilla' # or 'Full' +p.scans.MF.name = 'Full' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 diff --git a/templates/minimal_prep_and_run_ML_Gaussian.py b/templates/model/moonflower_gradfull.py similarity index 79% rename from templates/minimal_prep_and_run_ML_Gaussian.py rename to templates/model/moonflower_gradfull.py index 28c9daa1c..641055a04 100644 --- a/templates/minimal_prep_and_run_ML_Gaussian.py +++ b/templates/model/moonflower_gradfull.py @@ -6,28 +6,34 @@ #import ptypy from ptypy.core import Ptycho from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 4 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = u.Param() -p.io.autosave.active = False -p.io.autoplot = u.Param() -p.io.autoplot.active = True -p.io.autoplot.dump = False +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) # max 100 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() -p.scans.MF.name = 'Full' +p.scans.MF.name = 'GradFull' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 -p.scans.MF.data.num_frames = 100 +p.scans.MF.data.num_frames = 200 p.scans.MF.data.save = None # position distance in fraction of illumination frame diff --git a/templates/simulation_test_OPR_scanmodel.py b/templates/model/moonflower_independent_probes.py similarity index 91% rename from templates/simulation_test_OPR_scanmodel.py rename to templates/model/moonflower_independent_probes.py index f98e9f8ac..00df68ad9 100644 --- a/templates/simulation_test_OPR_scanmodel.py +++ b/templates/model/moonflower_independent_probes.py @@ -1,23 +1,23 @@ # A simulation template to test and demonstrate the "OPR" version of Difference map. # Note that it is better to use "ptypy.plotclient --layout minimal" otherwise 92 probes will be plotted. - from ptypy import utils as u from ptypy.core import Ptycho import numpy as np +from ptypy.custom import DMOPR, MLOPR +import tempfile +tmpdir = tempfile.gettempdir() p = u.Param() -p.verbose_level = 4 +p.verbose_level = "debug" p.data_type = "single" p.run = 'test_indep_probes' p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home ="/".join([tmpdir, "ptypy"]) p.io.autosave = u.Param() p.io.autosave.interval = 200 -p.io.autoplot = u.Param() -p.io.autoplot.active = False -p.io.interaction = u.Param() -p.io.interaction.active = True +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) p.scans = u.Param() p.scans.MF = u.Param() diff --git a/templates/minimal_prep_and_run_probe_from_array.py b/templates/model/moonflower_probe_from_array.py similarity index 87% rename from templates/minimal_prep_and_run_probe_from_array.py rename to templates/model/moonflower_probe_from_array.py index b089bc443..6895c4b60 100644 --- a/templates/minimal_prep_and_run_probe_from_array.py +++ b/templates/model/moonflower_probe_from_array.py @@ -3,27 +3,29 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ - from ptypy.core import Ptycho from ptypy import utils as u import numpy as np +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) # max 200 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() # now you have to specify which ScanModel to use with scans.XX.name, # just as you have to give 'name' for engines and PtyScan subclasses. -p.scans.MF.name = 'Full' # or 'Full' +p.scans.MF.name = 'BlockFull' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 256 @@ -39,8 +41,6 @@ # Gaussian FWHM of possible detector blurring p.scans.MF.data.psf = 0. - - # attach a reconstrucion engine p.engines = u.Param() p.engines.engine00 = u.Param() diff --git a/templates/model/moonflower_probe_modes.py b/templates/model/moonflower_probe_modes.py new file mode 100644 index 000000000..c526c96b8 --- /dev/null +++ b/templates/model/moonflower_probe_modes.py @@ -0,0 +1,54 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=True) +p.io.interaction.client = u.Param() +p.io.interaction.client.poll_timeout = 1 + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0.2 +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence=u.Param() +p.scans.MF.coherence.num_probe_modes = 2 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM' +p.engines.engine00.numiter = 80 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/templates/model/moonflower_resample.py b/templates/model/moonflower_resample.py new file mode 100644 index 000000000..5c6567ff7 --- /dev/null +++ b/templates/model/moonflower_resample.py @@ -0,0 +1,62 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockVanilla' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# Resample by a factor of 2 +p.scans.MF.resample = 2 + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM' +p.engines.engine00.numiter = 100 +p.engines.engine00.probe_center_tol = 2 +#p.engines.engine00.probe_support = 0.05 + +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML' +p.engines.engine01.reg_del2 = False +p.engines.engine01.reg_del2_amplitude = 0.1 +p.engines.engine01.scale_precond = False +p.engines.engine01.scale_probe_object = 1. +p.engines.engine01.floating_intensities = False +p.engines.engine01.numiter = 100 +p.engines.engine01.probe_update_start = 0 + +# prepare and run +P = Ptycho(p,level=5) \ No newline at end of file diff --git a/templates/minimal_prep_and_run.py b/templates/model/moonflower_vanilla.py similarity index 89% rename from templates/minimal_prep_and_run.py rename to templates/model/moonflower_vanilla.py index 470ffd011..88c70b586 100644 --- a/templates/minimal_prep_and_run.py +++ b/templates/model/moonflower_vanilla.py @@ -3,17 +3,20 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ - from ptypy.core import Ptycho from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 3 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home = "/".join([tmpdir, "ptypy"]) p.io.autosave = u.Param(active=False) # max 200 frames (128x128px) of diffraction data @@ -21,7 +24,7 @@ p.scans.MF = u.Param() # now you have to specify which ScanModel to use with scans.XX.name, # just as you have to give 'name' for engines and PtyScan subclasses. -p.scans.MF.name = 'Vanilla' # or 'Full' +p.scans.MF.name = 'Vanilla' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 diff --git a/templates/notebooks/moonflower_dm.ipynb b/templates/notebooks/moonflower_dm.ipynb new file mode 100644 index 000000000..85341b9b2 --- /dev/null +++ b/templates/notebooks/moonflower_dm.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: BlockFull\n", + "#### engine: Difference Map (DM)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'BlockFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# difference map reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'DM'\n", + "p.engines.engine00.numiter = 80" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BlockFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "BlockFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "BlockFull: loading data for scan MF (initializing probe/obj/exit)\n", + "DM: initializing engine\n", + "DM: preparing engine\n", + "DM: Iteration # 80/80 :: Fourier 5.32e+01, Photons 1.55e+01, Exit 4.71e+00\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "The difference map reconstruction algorithm:\n", + " Thibault et al., \"Probe retrieval in ptychographic coherent diffractive imaging\" Ultramicroscopy 109 (2009) 338, doi: 10.1016/j.ultramic.2008.12.011.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "

    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/notebooks/moonflower_dm_pycuda.ipynb b/templates/notebooks/moonflower_dm_pycuda.ipynb new file mode 100644 index 000000000..db0a1094a --- /dev/null +++ b/templates/notebooks/moonflower_dm_pycuda.ipynb @@ -0,0 +1,218 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: BlockFull\n", + "#### engine: Difference Map (DM) with GPU acceleration" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ptypy\n", + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Import pycuda engines (DM, RAAR)\n", + "ptypy.load_gpu_engines(\"cuda\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Nr. of frames in a block\n", + "p.frames_per_block = 20" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'BlockFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# difference map reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'DM_pycuda'\n", + "p.engines.engine00.numiter = 80\n", + "p.engines.engine00.numiter_contiguous = 10\n", + "p.engines.engine00.probe_update_start = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BlockFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "BlockFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "BlockFull: loading data for scan MF (initializing probe/obj/exit)\n", + "DM_pycuda: initializing engine\n", + "DM_pycuda: preparing engine\n", + "DM_pycuda: Iteration # 80/80 :: Fourier 5.29e+01, Photons 1.58e+01, Exit 4.87e+00\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "The difference map reconstruction algorithm:\n", + " Thibault et al., \"Probe retrieval in ptychographic coherent diffractive imaging\" Ultramicroscopy 109 (2009) 338, doi: 10.1016/j.ultramic.2008.12.011.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/notebooks/moonflower_epie.ipynb b/templates/notebooks/moonflower_epie.ipynb new file mode 100644 index 000000000..8ea2c1575 --- /dev/null +++ b/templates/notebooks/moonflower_epie.ipynb @@ -0,0 +1,200 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: GradFull\n", + "#### engine: Extended Ptychographic Iterative Engine (EPIE)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'GradFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "# EPIE reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'EPIE'\n", + "p.engines.engine00.numiter = 200\n", + "p.engines.engine00.numiter_contiguous = 10\n", + "p.engines.engine00.object_norm_is_global = True\n", + "p.engines.engine00.alpha = 1\n", + "p.engines.engine00.beta = 1\n", + "p.engines.engine00.probe_update_start = 2" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GradFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "GradFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "GradFull: loading data for scan MF (initializing probe/obj/exit)\n", + "EPIE: initializing engine\n", + "EPIE: preparing engine\n", + "EPIE: Iteration # 200/200 :: Fourier 1.26e+01, Photons 7.42e+01, Exit 3.96e+03\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "The ePIE reconstruction algorithm:\n", + " Maiden A. and Rodenburg J., \"An improved ptychographical phase retrieval algorithm for diffractive imaging\" Ultramicroscopy 10 (2009) 1256, doi: 10.1016/j.ultramic.2009.05.012.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/notebooks/moonflower_epie_pycuda.ipynb b/templates/notebooks/moonflower_epie_pycuda.ipynb new file mode 100644 index 000000000..34041850e --- /dev/null +++ b/templates/notebooks/moonflower_epie_pycuda.ipynb @@ -0,0 +1,211 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: GradFull\n", + "#### engine: Extended Ptychographic Iterative Engine (EPIE) with GPU acceleration" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ptypy\n", + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Import pycuda engines (DM, RAAR)\n", + "ptypy.load_gpu_engines(\"cuda\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'GradFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "# EPIE reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'EPIE_pycuda'\n", + "p.engines.engine00.numiter = 500\n", + "p.engines.engine00.numiter_contiguous = 10\n", + "p.engines.engine00.object_norm_is_global = True\n", + "p.engines.engine00.alpha = 1\n", + "p.engines.engine00.beta = 1\n", + "p.engines.engine00.probe_update_start = 2" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GradFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "GradFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "GradFull: loading data for scan MF (initializing probe/obj/exit)\n", + "EPIE_pycuda: initializing engine\n", + "EPIE_pycuda: preparing engine\n", + "EPIE_pycuda: Iteration # 500/500 :: Fourier 0.00e+00, Photons 2.03e+01, Exit 0.00e+00\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "The ePIE reconstruction algorithm:\n", + " Maiden A. and Rodenburg J., \"An improved ptychographical phase retrieval algorithm for diffractive imaging\" Ultramicroscopy 10 (2009) 1256, doi: 10.1016/j.ultramic.2009.05.012.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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iIiIiIiIiIiIiHuP7wV+Q+RORO52zf77gA0A7/1HvPf//BVZWcSXLWLPXURERERERERERMS9xTKhk++nReQnCMXn/wewCNwEvgf4OuD9hK66nywVuveVj/kLhIL0bwTeKCL/GngWaHvv/5aIvB/4dsADf9p7/7SIfBL4ReBJ4F947//6vXqzEfcOkdxFRERERERERERE3FscA58Cvsl7fy4iHwD+N+/9vxGRPwd8G3BAIHLv9t57EZn33v+giKwBP+69/70i8hHgA977XxaR7wUQkQvAtwC/B7hCII1fD3SAvw68BDxT3o54yBDJXURERERERERERMT9xRPAO0XkB4A54EcI5O4T3ntfPua7ROS7CUrehS+xr6vAp733DrgmIkvl9mPv/YsAIjJ4Fd5DxAOASO4iIiIiIiIiIiIi7j0mQFre/jXgn3vvfw5ARDKC8uYqj38/8EaCpfOjL7MPxTXgLeWc3hXgpNzuiXjoEcldRERERERERERExL3HTwEfFJEPAn8V+Aci8j+U933/yzz+/wN+jjA31y23/TTwQyLyFLAL4L3fFZH/B/gYgdD916/eW4h40CBTpTciIiIiIiIiIiIiIiLiyxWxCiEiIiIiIiIiIiIiIuIhQCR3ERERERERERERERERDwEiuYuIiIiIiIiIiIiIiHgIEMldRERERERERERERETEQ4BI7iIiIiIiIiIiIiIiIh4CRHIXERERERERERERERHxECCSu4iIiIiIiIiIiIiIiIcA/z+7ahcUK4iEbQAAAABJRU5ErkJggg==", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/notebooks/moonflower_ml.ipynb b/templates/notebooks/moonflower_ml.ipynb new file mode 100644 index 000000000..076acccee --- /dev/null +++ b/templates/notebooks/moonflower_ml.ipynb @@ -0,0 +1,202 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: BlockGradFull\n", + "#### engine: Maximum Likelihood (ML)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'BlockGradFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# maximum likelihood reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'ML'\n", + "p.engines.engine00.ML_type = 'Gaussian'\n", + "p.engines.engine00.reg_del2 = True \n", + "p.engines.engine00.reg_del2_amplitude = 1. \n", + "p.engines.engine00.scale_precond = True\n", + "p.engines.engine00.smooth_gradient = 20.\n", + "p.engines.engine00.smooth_gradient_decay = 1/50.\n", + "p.engines.engine00.floating_intensities = False\n", + "p.engines.engine00.numiter = 300" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BlockGradFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "BlockGradFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "BlockGradFull: loading data for scan MF (initializing probe/obj/exit)\n", + "ML: initializing engine\n", + "ML: preparing engine\n", + "ML: Iteration # 300/300 :: Fourier 0.00e+00, Photons 6.88e+01, Exit 0.00e+00\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "The maximum likelihood reconstruction algorithm:\n", + " Thibault P. and Guizar-Sicairos M., \"Maximum-likelihood refinement for coherent diffractive imaging\" New Journal of Physics 14 (2012) 63004, doi: 10.1088/1367-2630/14/6/063004.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/notebooks/moonflower_ml_pycuda.ipynb b/templates/notebooks/moonflower_ml_pycuda.ipynb new file mode 100644 index 000000000..720748a1b --- /dev/null +++ b/templates/notebooks/moonflower_ml_pycuda.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: BlockGradFull\n", + "#### engine: Maximum Likelihood (ML) with GPU acceleration" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ptypy\n", + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Import pycuda engines (DM, RAAR)\n", + "ptypy.load_gpu_engines(\"cuda\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Nr. of frames in a block\n", + "p.frames_per_block = 20" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'BlockGradFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# maximum likelihood reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'ML_pycuda'\n", + "p.engines.engine00.ML_type = 'Gaussian'\n", + "p.engines.engine00.reg_del2 = True \n", + "p.engines.engine00.reg_del2_amplitude = 1. \n", + "p.engines.engine00.scale_precond = True\n", + "p.engines.engine00.smooth_gradient = 20.\n", + "p.engines.engine00.smooth_gradient_decay = 1/50.\n", + "p.engines.engine00.floating_intensities = False\n", + "p.engines.engine00.numiter = 300" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BlockGradFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "BlockGradFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "BlockGradFull: loading data for scan MF (initializing probe/obj/exit)\n", + "ML_pycuda: initializing engine\n", + "ML_pycuda: preparing engine\n", + "ML_pycuda: Iteration # 300/300 :: Fourier 0.00e+00, Photons 9.00e+01, Exit 0.00e+00\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "The maximum likelihood reconstruction algorithm:\n", + " Thibault P. and Guizar-Sicairos M., \"Maximum-likelihood refinement for coherent diffractive imaging\" New Journal of Physics 14 (2012) 63004, doi: 10.1088/1367-2630/14/6/063004.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA30AAADpCAYAAACQlV5xAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAk6AAAJOgHwZJJKAAEAAElEQVR4nOz9d7wlyXXfCX5PZOa1z9Yr11XVaIcmLAEQICiCBCVSpAhBJCFDihw5SpSGmllppNEONdodaaXVzuxqNZJWGkmj0UhLjRw96AmCMPQAaGAbrkGgAXShu6q63LPXp4mYPyJP3Lj3vVdtUI2ubuSvP9X33jSRkZF574tf/s75HXHO0aBBgwYNGjRo0KBBgwYNXpgwz3UHGjRo0KBBgwYNGjRo0KDBs4eG9DVo0KBBgwYNGjRo0KDBCxgN6WvQoEGDBg0aNGjQoEGDFzAa0tegQYMGDRo0aNCgQYMGL2A0pK9BgwYNGjRo0KBBgwYNXsB4QZM+EbkgIj8nIo+IyOdE5J+LSKte9xdE5H89Zr/feobH+2Mi8vIvps/P4Jh/X0T+Zv3+fxSRb6nf/w0R6T2D9oa3u48NGsDz4/t4q340aPDlBhGpROQhEfmEiLz16fxNab5LDV7oEJGzIvJj9d+zh0Xk7SLyFc91vxo0OA4vWNInIgL8NPCzzrkHga8AVoD/z5Pt65z7umd42D8GfElJXwzn3N9zzv1y/fFvAE+b9DVo8Gzgy/H72KDBCwAT59xrnHOvBHLgv45Xikjy3HSrQYPnFvXftJ8Bft0594Bz7uXA3wbOfIn70XwHGzxlvGBJH/AHgalz7t8DOOcq4P8K/MXoaeXdIvIOEfm0iPw/dcdY7RKR/15EPiAiHxOR/1e0/HvrZR8Vkf8sIl8HvAX4x/WT0QfizojId4jI74rIR0Tkl0XkTL3874vIfxSRd4nIRRH5EyLyj0Tk43Xfsnq7iyLyP4vI++t/L14+YRH5DyLyXSLy14FzwK+JyK8dcU7fJSL/oX5/n4j8dn2O/9NSe0eee4MGzwB32vfxP4jI/y4i7xGRz4jIt0erz9X9eERE/lG0z78WkQ+KyCeXjv0P66e8HxORf1IvOyUiP1X39QMi8vW3YQwbNHgu8R7gxSLyjSLyayLyI8DHRaQjIv++/pv1ERH5pmif477Tf7b+O/aQiPybZuLa4HmIbwIK59z/rguccw8B7xWRf1yr4x8Xke8BqL83vy4iPykivyciPywebxaRn9A26u1+oX7/rfX87MPilfaVevlFEfl7IvJe4E+KyB+p23yviPwLEXlbvV1fRP6P+m/QR0Tkj9bL/4KI/PQxf+f+cH28j4rIr9yqnQbPP6TPdQeeRbwC+FC8wDl3ICKPAUqYvgZ4JTAGPiAiv+ic+6BuLyLfCjxYbyfAz4vI7we2gb8DfL1z7qaInHDO7YjIzwNvc8795BH9eS/wtc45JyL/JfC3gB+o1z2A/wF5OfDbwHc65/6WiPwM8G3Az9bbHTjnvkZEvhf4X4B4ohqf578Qkf8O+Cbn3M0nGad/Dvxr59x/EpG/+mTn7pz7zSdpr0GDo3CnfR8B7gX+AP7792syf5DyGuCrgBnwaRH5l865x4G/U7ebAL8iIq8CLgF/HHhp/d3eqNv458A/c869V0ReBLwTeNnTHbQGDe4EiEgKvBl4R73oa4BXOuceFZEfAHDOfaWIvBR4l8xD3A59p4ER8D3472shIv8b8GeA//SlO6MGDb5ovJKlv2k1/gT+b8irgZP4+17nTV+F/1t4BXgf8PXAu4F/IyJ955x+N35cRE4C/w/gW5xzIxH5vwH/HfA/1m1NnXNvFJEO8Ajw++vv449Gffk7wK865/5i/bfp/SKi0WCvYenvHDAF/v9RWydu1U7d3wbPI7yQSZ8A7kmWv9s5tw0gIj8NvBH4YLTtt9b/PlJ/XsFPOl8N/KQSKufczlPozwX8F/kuoAU8Gq37pfqP38eBhPkf1o/jJ6aKH41e/9lTOOZTwdcD31m//8/A/1y/P+7cG9LX4JngTvs+AvyEc84Cj4jI54GX1st/xTm3X/fjYeAe4HHgu0XkL+N/N+/CP6R5GP+H8gfrCe3b6ja+BXi5iOix1kRk1Tk3eIp9a9DgTkBXRB6q378H+HfA1wHvd87p37A3Av8SwDn3eyLyBXz4Nhz9nS6B1+EnwwBd4PqzfyoNGnxJ8EbgR+tolmsi8hvA64ED/PfmEkD9vbq3fjD4DuA7ROQn8Q/6/xb+geTLgffV35MWXhRQ/Hj9+lLg89H38UeBv1y//1bgLVL7PgAd4EX1+6P+zm0Cv6ltRX9Lj2vnU89siBo8V3ghk75PMiczAIjIGnA38Dn8H53lSejyZwH+v865f7PUzl8/Ytsnw78E/qlz7udF5BuBvx+tmwE456yIFM45bduyeI3cMe+fCuLtO7dYpzjy3Bs0eIa4076PR7Wvn2fRsgpIReQ+4G8Cr3fO7YoPj+4450oR+Rrgm4H/Avhv8KGsBniDc27yDPrVoMGdgolz7jXxgnoCGj/hF47HUd8xAf6jc+5/uB0dbNDgOcInge86Yvmtvg+H/rbU738c+KvADvAB59xA/Bft3c65P3VMW/odvNXxBB859umFhSK/75i+3Orh7KF2Gjz/8ELO6fsVoFeHQmqy6/8P+A/OuXG9zR8SkRMi0sWbPrxvqY134nOONI76vIicrtv+bhHZqperBD4AVo/pzzpwuX7/55/hOX1P9Prbt9rwiL5cE5GXiYjBh6Mp3oefrIIPsVEcd+4NGjwT3GnfR/C5EEZ8vt/9wK3+oK3h/8jui8/HfXN9rBVg3Tn3drx50mvq7d+FJ4DU272GBg1emPhN6r8ddVjni5h/l476Tv8K8F3696Ref8+XvNcNGnxx+FWgLSLfrwtE5PXALvA9IpKIyCng9wPvf5K2fh14LfD9zBW83wG+XtMORKQnRzuD/h5wv4jcW3/+nmjdO4G/VhNIROSrnqQfvw38gfohZ/y39Om20+AOxQuW9NVq2R/HT+weAT6DD8P629Fm78WHND4E/FSUP+TqNt4F/Ajw23Xo5U8Cq865T+JdB39DRD4K/NN6vx8D/nvxia4LxhF4Ze+tIvIe4Mny7I5DW0R+F/hv8SYYt8K/BX5JaiMX4P+ODz37VeCJaLv/FvirIvIBPDEFjj/3Z9jvBl/muAO/j+Anpr8B/BLwXzvnprfo/0fxYaWfBP4P5oR0FXibiHysbku/l38d+Grx5i4Ps+R62KDBCwj/G5DU38kfB/6Cc05VhEPfaefcw/hcpXfV35t348OlGzR43iD6m/aHxJds+CR+nvcjwMeAj+LnW3/LOXf1Sdqq8POzN9evOOduAH8B+NH6e/I7zFMQ4n0nwF8B3iHe2OUasF+v/p+ADPiYiHyi/nyrftzAh4b+dP23VAno02qnwZ0LmUcSNgCo1YIPO+fuqCePInIR+OqnYMzSoMELBs/W97EOz7yVyUuDBg0aNGhwx0NEVpxzw1qJ+1fAI8652+X70OAFhBes0vdMICLn8PL2P3mu+9KgwZc7mu9jgwYNGjRo8KT4/toY5pP4iK3Gi6HBkWiUvgYNGjRo0KBBgwYNGjR4AaNR+ho0aNCgQYMGDRo0aNDgBYyG9DVo0KBBgwYNGjRo0KDBCxjH1un74Ac/6LIsQ0QoyxJrLUmSkCQJRVGQ5zmDwYDZbIa1lm63S6vVYnV1lXa7zXA4pCxLAFqtFlVVce3aNSaTCcYY0jSlLEum0yl5ngMwHA4ZjUZsb2/TbrfpdrucOHGCNE2ZzWahzdXVVVZXV9na2sI5R1mWDAYDJpMJZVkiIiRJQqfjy9HpvnmeY63FGIO1lsFgQJ7nFEXB1tYWp06d4q677sIYQ5ZlrK6uIiIMBgOcc1RVhXOOTqdDq9Wi0+mQJAnT6ZSyLOl2uxhjEBE2NjbCWBpjKMuSXq9HkiRhmfal1+uxurpKnueMRqPQr2vXrvGZz3yGxx57jLe97W1cunSJ2WwW+qLjmCQJIoK1Nqxrt9ukaYoxhiRJsNZSliUazltVFSKiNZcAwv4AaZqSZRlZlmGMwRgTzrnT6WCtpdVq0e/3w3hlWRb2dc4xmUzI8xznXDjvNE2x1gLQbrdpt9usrq5y9uxZTp8+zYMPPki32yXLMlqtFsYY+v0+ei9qP0ajEa1WizRNqaoqtKnbWGvD9Y7P5ezZs7eqafMlx4P/4J+6UGXHgSnlUJUclzicAZf4bcThKzgCCIgFnPhXwKbObxu1Kw6c0fbACSAuLEP8P2ccJPWyUvyxnB4DpJJ5m/h+mEJIZiAVJFO/rfbFGf8+yR3dm5aVz+1jdofY7R1cVeHq777vg0GMgETPopzF2XpAbMU7rzwUVhWuwtYDYernVxZLFYWsJyJhnUGw9eAahEQMlbNYHEM7o8AxsI7PFZv8lZ/9S0glmAJa++LPw0HVno+jWDDFfHzNrB7DxK+zqd9ex0Mq/+oS30ax6jCFYHIwZd1ufK3qima6n1SQTlwYW+z8usVjrdfLFJBNLL0rU9LdMe7SVX7+936dscvZqSoKhLFNyTEkODKxJDgqhIFtMXUZU5eR4OhIQc/M6nFPqOox7UhBC0vPlGQ4krov91544o76ngGISJPL0OAFB+fcHfVde93rXuceeOAos+QGDZ6/eOtb3/ph59zrnut+fLE4lvQpmRKRQHZEhCzLwmS91WpRliWz2SwQCCUXxpjQjpKbVqsVSICSyJWVlUAcp9NpIBBpmgbClec54/E4kC0lUaPRiMlkwnQ6ZTKZBPLTarXCa7/fD+ckIkwmk3Aes9mM6XTKzs4Oe3t7DAYDsiyj1+tx4oQvT1JVVSCvSiattRRFEc4B5mRGyVRRFOE8lNTGhKQsy3D+ukzb6/V6tNvtMB5FUYQ+KMnRvuj1KMuSPM/DuriPeuyqqgACEdTxUqKo6+JrqdddX6uqIs/zcB2UNFZVRZIkoe3ZbBaIt14L5xxpmpKmKa1WC2tt2D/Pc/b399nf36coCk6ePBmOAVAUBb1eD2sts9mMqqoC8dVx1bHRfYwxgcTq+dxx0GloTLpcTbbw5MwZ5gSu5oQSEbqwfb2PWPHMYWkqILYme1oeud5sgWQmgnOAuIV1cxLiwPrj6W625WrCIdgsIjlK/ATKSnDGkMxWaXcy0rLEjcbYqvKkzvmNnXVIJhAtw9lABN907jWB+GWSAEkgbkoALdF1DkwIFq++oXQFlXOMXcGNSti3Hd4z/gp+7tKrSWZCOvKDXXX0RCGZEUialI5izRPCmIAFcm3mBC/J5+8X1huHTYWY4Hly7V9N5ccvEEa9DvU94ExNGMVvO78HIClqgiiCyxIkMVgsM+dHqHJChWCdIZEqED/jhL4UdKRk5CoKl5C7BGwbgNwlWAw9M6OFxYgjqQ9cNbSqQYMvazzwwAP8xE/8xHPdjQYNbitE5HPPdR9uB44lfUpa2u02xphAJnSyrRN8VXWUOMSKkpKgdrsdiERMWuKJeh498VdlJiY8s9ksqFrT6TSQlOl0GrbR9pWkOOfIsowkSWi1WiRJEghSlmV0Oh1WVlaw1gYyWFUVRVFQFAXTqS/bpedjjKHdbrNsfpNlWVimREaVqHa7TVVVCwRGldNlJU4Ji5Lj6XTK9vY2+/v7gbDoq56TttFqtRb6FBMeHSt91WunfdXtYoKkn/W9kkz9rOcSE6mqqsL11H17vV5Q6ZTUqiqs/YmPt6zaKUnU66n/tP96nznnFohonucLKmZMbO8kKLFykWID9eewgS5kQdlZaCR62CsOwi0ak8P4c7yMmigqqYOaUSwfRwmnQ+VJMfWxUqByOCuH+qddq1pC2TWkPf8whCyt1T2Ls6Zub3HnoPLVBFAJ35vOvabuo/PL3ALVo5pTUhKSoP4l9T2hiqDFUjjHju1yo1rjs+PTTIoUnCd4NgOb6Bh59cyUYIqa9dbKHTUxt4nfR69VTMrC+NfvpQIp5wqtrldyroqu1MvELW4zH6TF20RcTLyjmyFJMLXuWSEUGKpDNxMk4siwZKiaapiShPvB1ipfskT4/LoGDRo0aNCgwZ2IW5K+TqcTJuwaKgdz0qLIsiwoeLPZLBCCmNgpSZlMJmHSrkpQq9ViOp0yGo3odruBuKlaVVVV2A5gd3c3hAwq8VH1URWsJElCO0mSsLq6GkiYKpOqhK2vr3NwcBDImSqMsKjgKTnTcEddr5jNZpRlSVVVTCYTsiyj3+/T6XRI03RB9VJSEhMfJWAa9vroo4/ysY99jMuXL3NwcBAUT/ChkTomvV4vqJ9xSCUQSFRMSvXa6bJYoYvJuV47Pdd4W+1nnudhOyV4Su46nU64nrqtqpNKoJXUdTqdoHDqOOnDAVWO9VySJAkEWR8oxKqhPphYJqSqyt5J0HBJkQXetszj5iGWZnFZeGscojsskzUXkQSJCIWTuUJVH5NSIHXzbY6AV6qUjNUvVrwYJy6EfGqoYdgvAZsKLhEwAmmKtDLcbIYsX5tY5XOuZj4yJ3t6Twq86fxX8c7LHwEHJf77ZJ3D6P1Qk7sKh63HyGAoqLDOMXDCQ9N7+NToHDemK5zoTTi4f8KoEtLHOiQTmYdyaghtJtg6jDOZ+deqU4d1tjyhc4kPza3S+Wcp5xfV5Ivhs3rNdexjYjdXEwVTuTC2ply8vuJcUAiT3JJMHSavkKICk/DtF76an3r8tymcY+oSxrZNJiUGr/oZJyTiMDhsfWPkLmFQdcmkIhFLT2Z0jFcCgzpYdy938Q3aoEGDBg0aNLhTcOxf6GVFR0mBhl/qxDsOIVQoqdPtYiIRq2RKzGICpaF/8TrdX/ukKpyGharSpcdSpVBJXxyuCIR+KRlV1Sg+X+dcUPxi0qPH0/aV9CkJUoKo25ZlSVEUgdDFIZMxAYvb1RzFnZ0drl+/HnIZdb8kSUL/4rw1JZfLxFfJjhKjOHRz+ZorodXrvUwWNdxVzzVuU88vVnp1XPQ+0fVxOKleL71+8cMAHY/4PtM+6b56n8SKs/Yzvv53IukDgmIU1BkV2zR0U/O0NJcv/gwRU+Bw2Gb4J2Ez3R9b873lEM9wLFlsI4aGMkaKZAhnXPq30C/rvPqk/5YRKX0uVqlcLF96RTD8SxJPBiEoeoHwxSqUc1T4f0r4ChyVE57IN3hsvMmV4TqXdjaQi116KzOKdSWfi8TMZrWiByHfzuSeGIoSsTrc0iY+v0/zKHX7MH76T0lcdB0WXqPtxNYhppXvkyeCzvejAlM6TO4whfWEr1xUuTWsM6/z86zTnMijlD8b9qkiUufJ3uFreFQbDRo0aNCgQYPnFscqfUroVJ1Rla0syxDCp5N3zdVSkhATHyVMGuapZh86AdcQzJWVFfI8J8syiqJgNBqFvDcN85zNvJGAmnIURbFg0qEKkfal2+2GYwwGg6AcdjqdhdBUPV5MjoCQO6bLY1LUarWCyqgKX7vdDiQoJq06lnoMzWdTwqhjqarqZDJhZ2eHRx55hCeeeILJZBIUrna7Ha5RURThmsCcNCvpVgIUm+YsX+M4BzAm3zFJVlVNiZ6uW95Or41uP5vNGI1Gh5RivUanTp2i3++TpinT6fQQ8YuNYdQ4aHV1NVxzVVTV+CXOjdQx0LaOCoG9kxBy5JSU1e+lXig62V8KAV1W4wIJc4LgAuETVfT01UXbax5aTO5Cbtg838wJYHTFvI9zLuawBk8WxXkCZAU0P62AbGxJBzluOoMix+UFrixBpFb76jxTOBTSWTlLIoY3nXsNrqrq2NL5Nsv5ZKr2qcqnKEJOm6MAdmyXi+MtPr+9xeh6n2wvIRsKwydWvAJbK3WxWqphnElej0E6vxbJdE4IEys+L6/O1VRS58SrgCaPFFHnCaPotVf1TsM9rQ8rNaWSvogEO79fMnOYwpFOKrKDHDPOkYMRlCV2POZtlz7I0HrJ2IglEUtR5+hlrgq5fXHoZ4KjY/xvjcHvk2DpSEWGo0AoEBIcyVK4Z4MGDRo0aNDgzsCxpE/D9GLFRgmLEr1YTYsJjCpxmqcXEyclTbHyp+RFw/1Go1HYt9/v0+12F1QmVd9ilajT6dDv9xfcRjVEUxXCWP3SPLzZbMbBwQFAyO0zxrC2tkav1wv5hnEby8eODViUGMZjpH1SUqZjpYRR29EQyEceeYSPfexjvO997+P69euB1OnxlaDptdE29F9svKLtaugrzFXMOPfROUee56FvcZ6dXncly8v5mXE/jDGBXCpBVSKr90JM/kejEVVVsb6+zmw2C/mL6+vr4f6KleP4VUNv4/xEXab76Ng55w6R3jsCS/lYgWxFBEwn/AvhnnI4BFTfh+WRIYxXh5Q5RvskS/vLvKEFl1CnHEsQ4zzXOpRbWPdVHE7ZqZ2rUq0DR3t7htkfe8I3neHKgndeeSgoddhaMaxVQE/mbHDdxFnedvlDgHfg1HDOwplg4LKs7lk8+TMRY/YGLjByKR+Y3M9jg01G1/skQwPOu252rqdUbRdCM20GJgpqUCfOcG3q4XXpfGxcrfCZmWBb83F2qSOZLOZhIsxjL9z8GFI5klrVS2aOdObJfDq14fokuQUH2cEMmZaY4Ri3s4edzaA2y3nb4+9naGeMnCVBaGFZNdPgxjl1qc/1c/791GUc2A5Fvdw6TxT7zDzBq906ExyFEt6G8DVo0KBBgwZ3JI4lfTrZVsWuKIoFA41YqQKCiqI5e7F7pJKP2OkyDquMTVy0LZ3YK8FQVSfO31JCEudyaV/jHK9Y+YrdHquqCs6fqsrFhE6VL1UwtT0loEpmY0VNc9LivDolVRraqCQyds7U88jznJs3b3Lt2jW2t7eDmpokSSAwSl5hXg5Cj6fXZDlsFeYkTJU4bScuR7EcgqrHiMNGY/MXHY/4HplOp+G66njqstgsZjweU5blQvmLg4ODsH3cX+2/Xpc4t1LVvDj3bzlX8k5FIHe3yKF7UqP5aAMfvikLnwOh0AV67eqIUFcTT6cq3lG5gccN4ULby+vm5NGUXuVLBjNkmmOLEldZn4uHJ3dvuvA6xFiw8xDCZWdOrzXN3Tg1nLPiyU16EmSBEFYIU5fw+ckpbg68wucEio0KMzGkNSmztYpnba3ExaG2EY+2Zs6ZF/Iz67BQqXxbPjdwPjaiIbvMCX4gfZWrjWO8spfOHOnE+vINwwKpHOIcZlriREj2hpAX2L193vaZ9zBxOQZDIsLMVUydpXAsGLh4AleR12YtFsPUZYxci4HtUriEjvgHcxoGauo8Pr1S+ppIU/z1eQeJv+8NaW/w7OEfveP3OL3a5i98/X3PdVcaNPiyxLGkr9VqMRwOA2FT18s4Z0vVo3hiHYeFqopzcHAQcrSKogj7xOUN4lp/mp8XG41Mp1PG4/FCyCHMQwVVTYoNPLStOOR0NBoFZW53d5cbN26EsFIlFFojUJ06tTahuoiquqckQwmtElUlMnHuXkyYtQ01dYn7tr29zUc/+lF+67d+i9FotFCyQM8jrp2nx9vd3Q3lJnTsYrdQvV5xnmNsvBOrurHLpYaw6n5x2Gis4KqLamy8Mh6Pg4qrobYwz3XUcht67/T7fcqyZG9vD2st999/fwiZVYOd2JVUr4feI8PhMCh9ej5x7cQ7EnaR1C2rebd8hZBLB/P8OVfndi2UfYhIpdb1c8azFTE63xMfvqmkpg77XHaGdG4hQnRRrbSyULJBc93SsaN7dYbs7OOmM+xkyjsvfYiZK0jrwoDvvPShuVELQK3SaT0+g6GiIhEJZC9W92Lzlvh94SwmhCoKFgm5fHtVj7d98itJr7SxmcOUgpkZ0PzE+pylhLLrJVQ1btFafDoGsUlOrPzp0AanVuPbW4DM8/lCWGdFIHzpxJHkjtZ+SWtngoymsLMHhf9uu+mMn370vTxR+Qdo27bNh3J/xn0pycSS4Zg6w8wl5BE1M1qfzwmFS9lzLUa27d9XPQB66YwNM6ZjCrbMhI5UbBgTxnjmvBtqRwyZNLTvjkSIG64/K8k7ZPV7J2HhlyZaxhHLG9zp+Oz1IeP8znPRbtDgywXHkr6YzMWmI/H7ZQt8JQNq9BG/VwVITTqWzV9i5UkJgoZywtzoJO6TKlBK7jQPMDZsiZ0mVcmaTCYURcH169fZ2dlZIHlKLuL6fPG+sXoYG6uosqZ91TGMQ2O1L7qPjqeSLiW2N27cYHd3l06ns6DixfmC8Xnp9YjHSMc97k+cU6fHVPOXWHHV/WKDnPiY8dioQqgkMM7t1GOlacrKysr8pqsV0djFU/ut5HZ3d5e9vT36/T79fn+BQKvLamzwog8GYuOZo8yE7jQs18kTWMyhq3FU+d2gKNWvoWh4HSboIhVRm9VjBqMW42J/lLBBqBgfl2fQ9upif6FLIYRUAtmMi5EnM0jHkO5NcXnuc/hsReVUsXPePdJZ3n75w/yR868NeXzFEfO66lCH57BL6+LPsco3L91gMDdaJFNPVvMTlSe+VjCzxOfvpY6kFEzp6xCaEmrhqx6jpXFYHMrw6mq1L1ynWOXTNpZKMczDY+tcvXGBORjDeEI1HOHynLdd+iCPlRM+UbSZuhUSbAjJrJxhagpaVHSkxIg7VKYhDsmsEKa2xdRmFK6OGsDSkYKNZExHKtZNRUuEnslISerQ2xyLoy1pXT+xwR2DY3/2jvoeyZOsb9DgmUOEQwZyDRo0+NLhlqRPVR6d+Gt+23L4phKpqqpCcW1VfVSJUtMVrVunCpEassSqnhqWxDl2ACsrK8H0Iy75oCRSzUKUQKhCGR9nZ2eHwWDAeDxme3ubPM85f/58KHugx1cCEStLWZYF5SgmQjH5BEIdwVhhUtKipGi5mLsqXFevXuXGjRuMx2PW19cBGA6HoS+aE7esYCp50qLmeZ6zt7cXHE6VMGktOz0HzTVU1VEJrpZaUIVXSbxeE73emifX6XRYX19fqJUnIoFQ33///YG86bmoUry2tkan0wkF6bXdixcvsrq6yoMPPki73Q7XKCbLk8mEJEmYTCYcHBxgrWV3dzcY9pw9e5Zut8vKysqd+cdmSUVbqIQQEbYFoxc3Jw6HoCRwKTcsPobm44n17yXeT6INF3aaM0dT1ZQv6htRCKNYwcwgG/kctN51S2cnR564jt0/wFUVb7/84ZrsqWvk/Hhvu/yhQPZUyTORMrVYkc8jqfsTkzx167R4tS+JJrQG6BvLqpmQjsWHT+ZC1UpwqZuH2zqoOg6XQjKtj5EtjrdN/XY2Yz4mkfmKftawzUD6qnnJhQXUZDApHK2RI51aepcnJDf2qS5f5a2PvpeZK9mzlqkzfCTPuFGdZGpbwWlTUTnD2PmyDH3J6UiBERuKqmv+XuHSkL83sq2aNBpWzYSOKTidDNgyM3oCp5Me33bP12BWVyExUFlcHdYP4IqSdy8rmQ2eQ8RqWfRdhtvH644S5G4Ljmr0Dvwdb/CUYNSkq0GDBs8JbpnT1+/3g5oXO0PGyknsTKl15LIs4/Tp04zHYyaTSVin+Vq9Xo8TJ04Eha7b7QaiqOQwNuLQkEZVfdSKX0nobDZjPB4vEKKYTJRlyf7+PuPxmCtXroSwwyRJWFlZWTBjUcKiqpfmyBljgqkMEFS22BUyVvjyPA/9AQL50vXdbjeEpGr/PvWpT/Hoo49y+fJlhsMhZ86cYW1tjdOnTy+opmr8oseczWaBpG9tbYVQ2ps3bzKZTBgOh4AP1VxbWwuEVcdbCaley8lkwurqaqidp9d1d3eX8XjMwcFBUGDVOfXUqVNsbm4GJ009njGGTqfDuXPn2Nzc5MyZM2H89Drp+LZaLcbjcbhXhsMhg8GAa9euceLEiaBK6kMGEWE0GoUQzxs3bjCbzRgOhxRFQa/XYzQasba2xvr6Or1e74v8utx+KJ86UvFbrq2nZG4hX0xCvKWu05puhx7wzzfF4ev6iYOQDmcAkfmcMOT4+Y76fspSOGdNXiqZl52ooDWA3rWK1sDSuTpChhNv3FJVvPPyR4LTplnq5WL+nkciQuGqBaUOWCBxy1CyN2/Xn0gmhqwOJ/2z978RecWLkbeIN48x3nkT4z+bwo97OjML5+vMPM9PSj9uoVxFfQ20XEPI+XNzNTbO4wyunVHobTJzmBx610v6D1/F7e7z7z7xdq5ULfZslw/mLRJSBrZLXjtvKka2TeWEge0yq9npyfSAjilIjPOhnHUtPh/Omfj8PdtmYP1vW8cUrMkUg6VvZvQl51w64XxN9pKTWyR3d7DrfWzLj2WyP8GMp7jx2DuzNrhzcOhh11OZdj/VqfmzxvYavABhRA5FYzRo0OBLh2NJX2zqAQSCE4fIxa6PeZ4HItVut4OqE6tMw+GQ0WgUirSvra2RZVkwbolrsSlhiJVAJZqaB6ckSMsRaMhknuchH282mwXSMhqNQp6hqlAa/hcbrcQ154CFEgJ6/svhgnGuYZxPp0pcHA4bh8UqAb158yaXL1/m8ccfZ39/PxR1V4MTHW91Lo2dQ0ejEUBwMVWyFrt2qgIaK6larD6uaxc7XMYF2uNQWYBerxceBGgu4erqKt1uN7h86nXqdruBsKvLqvZFx3A6nYbQWj1W7NY6m82YTCZhX82xHI/HjMfjYP5SFEW4BiLCbDbj5s2bjEajoJw+L+AWowblGHXPK4C+PEPgEImDpCZ4VhYJYEQgfeP1G+uOYInLB2NOHI/KPaQORawgmTpaA0s2KJDRFJnMwtTwTedewzuvPEQSvQdCuOdy+GYi4oupu7l8ZGSxs4sGLYuEr3IuhHNmJPRMizddeB3Jy+5j+1XrC8YpphKsjqX48YtLNRzlWBpeldxF6468djFp1hDPKB/Q5NAaWXoX9/jp3/pprlUzHilXGNt27a6ZYrB13p133szqQumFS8hdysxmjG0Lg2PqWiTOkbuEjjMLaqCGgFoMRW3l2pGCTEoyKvqSs2pyTpoW3/4V34DZ6uM217CdFtVKi6pt9IJg2inGyNLN0eD5gWdyzZ4FxbDBCxtydHnWBg2erxCRrwH+Uf3xLuDtwO8AfwOYAH/eOfe4iLwc+LdAAvxd59wvi8gK8MPAFvA259w/rNv8J8DvAx4Dvs85t+h2+UXglnX6FNbaMLHWMM/YJGQ6nQbisbm5GWqpadji1atXuXr1KhcvXmR3dzdst7Kywkte8hJe/epXB8KgxCYuHRAXJo+PqwRO6+RNp9NAWlSRU8Kwvb3NdDplMBgEormysrJAapQk6LGVVGmoo/6LnUE15265iLgqanGhch07JcrT6ZTd3V2GwyEf/vCH+dCHPsTu7i4vfvGL6Xa7nDp1KpAfPZcY/X4/hDaOx2NEhG63S7fbZX19nU6nw2QyYWVlJah4Sho11LTX69Hv90ObqjoOh8MQBgqelGmY6blz59jY2Ajqq6p5W1tbgSDH10pVUiXiIhLIrJLvtbW1QD7zPGd9fZ1utxuI3MHBwYJDp6rI165dC2Y0qnDquAAMBoNwf169epXv+I7veDrfj2cdC7X2oofmMXlQAhKWR/tIBSTeOdIBNnO4rZwktYg4rBOqWYLZzUhmEhp0Zin80Hmy45LoeEF98qYvToCkppjauVrZsjikEqhLC6RTRzosSIYzT/jq0gFILUPiCR8mCcRPVb44pNOqtFajwnkzllqy1KfGSvoOB3560tgWQ0dS/thL/yDcd57ym/rceLBF1ZVQKF1Kr9YZBFsPsObdHWWw4+rcyQWDHEuoqaj7x9dZnTz9QkKOYGi6cvSul3QfH/DD7/qPfHDWYWC32Kt65C6hJRWZVKG+ni6zzlDUil+Cpa119cT6e8DJAtlTglfV+xgsmVR1Ll9CB1+UvSMlPan4zq/4ZuTe81QrbYqNNjYVir7Bpl4trroJJm+R9dskg/nvSYPnC57uTPwokniL5NYGDajDO5vbo8ELCM659wPfCCAiPwj8LJ4EfgPweuDvAn8Z+AfA9wHXgHcAvwx8P/ALzrkfFJF3iMgPAaeAs865bxCRvwN8F/Ajt6u/x5K+2O5fSUesfgELLok6Gdd8taqquHr1Ko8//jgPPfQQe3t7IXQwtuv/7Gc/y5kzZ3DOBdIQW/BrX/QY2g+tBaikTcNAldwoudLtDg4OQj7isrumnqOSoWV1U/dRcqdjowpeTIbj8FRVrZQALZeWGI1GXL58mevXr/O5z30OYwynT58OhEhDQHV/JbeqwHU6nUBgtZ9qSKOkW69TXG8wJqdamy8O2V1bWwsKYBxmOx6PabVanDhxgq2trVBqQUtF9Pt9sixjMpmEEE8lmTp+0+k0hGkCC6/qGKvb63moGqpushqaqiQe5k6tGuaqaq2qxEoS71hE+XtHrYuDqCTmQRo2mEHVdbhuxckTQza6E6wTBrM2syLloOpjbmaL5R9qBimOhXp9YdVSh44sHVGrWSR+A5fPw0PFOqS0C+FlYgRnzaJL55NgwaGzPhR4wlfUo6LqoCp68ee2GL77whswnQ5y4STD+9aYrSVUbZm7byo0P7GSxTqF8fqFMay3ia/fkpKnm8XLY+IeG7uYCtq7M+SxK1yphJFtM3UZFaYO46xqda4Oq6wbyZcvIATi1zFF2M6PoyfesYGLFlxPxFJhKFzqHT3VxKfdxvbbVP2MsuvJns0EmwjiHFVbcMZgyhRMo/S98BHHojeKX4OnBtMYuTR4gUJEUuBrgX8KfLJW595Xq3YAdznnHqm33RaRk8AbgL9Zr393vf9p4F31snfgieKzT/o0j07VFw3ljMmREjMlMXFY3m/+5m/yIz/yIwyHQzY2NoKJiBqhqNq1s7PDhz70IR544AFe9apXBQKjZEqNRGazWSA9s9ksELuY9Ohy3UaJZVxeod1uh5ISBwcHpGlKnufBEETLRsQ5c2pKk2VZIJox2Y1Vs9XV1VCrTxXSOEy0qip2d3eZTCZ85jOf4Xd/93cZDod0u11e9apX0e12Q/kGPV8Nc9U+aXiqKnGdTidcJyWZ8TXp9/uB4KmaqWO1v79PnuchJBM8gTp37lxQS4fDIePxmEuXLrGxscGFCxe4cOECvV6PXq8XQmc1Z05Dbvv9PidPngwETMdOyzSoa2ee54HgKmGOz3dtbY3hcMh0OuXKlSvhYcN0OuXmzZucPHmSXq/H2toaaZqytrYGEEi+KrXLSukdg2WCsOS1sABViiIykm9VtE6PedWZ62y1R7STkpYpyaTi8mSDaZVyvTPjcrVFMkgwuSwSOCU64ifwc9J21PHjWE4WJ3lRqQabCLaV4LJ5CK9zzhddt/7efOflj/Cmc6/h7Zc/TOEO5/ItF1vXtXH4Zu7m6wye7C204hzfffcbSB68n/L0GoN7Okw3DC71Kpvm4sVqnhaTVx4VSJmSuyUVL6xbiMeN1kXXc0HZrdfF+YI4SJ/Y5Z99/Jd4vFxj7NpMbcZe1cM6Q2UMHVNQ1Q0m4oJaZ2tt1Iglq2tCJOJC6GeCz+kDKFxKARzYTuhLxxQYZxnbts8NFG/kUuC/N1UvpewmNekDV5M7h4TPLhFMr3HvfE4Q/2Y8Z/PqWxx4Icb86XTwjjixBrcBTU5fg+cxLojIT9Tv3+qce+vS+j8I/AawARxEy/UPYvxDtg+cWNo2XnZladltwy3dO2Nzk9h1MZ7EqaKk+XEAOzs7/PzP/zynTp3i3LlzHBz4c2q1WkEd0wm4c47Lly+TpikXLlxgY2MjGLto/p7m5cXGLfpe2+t0OqyurobcPiWg6uipJGk6nQZXy8lkEnLgFEr4VIGK99VQUlUDVVVSUhkXbVclTVWqqqq4efMm29vbfOpTn+LKlStcvHiRl73sZdxzzz2sr68v1N9TIxwlOUqu1ZE0DnuN8y+VvAJBAdWQVw1jjcmkKmQa7qptXLt2jV6vF8jr5uZmGKv77rsvmLbE+ZuqoK6urlJVFb1eb8HRU/urJjn9fn/BlEbPL86dBEL452QyodVqBZdTVXC1HIS2p8S2qipWVlYWiPSdhoXQzhpu6fMCydAQQV1l4Du/7v30kpzCJhhxDKt2HeJXcqI1pnCG9daUG/sr5FWHZJL4XDU3JzJKWqSE2qk/cDoBnJZpWCY6zodzmkJ84fJahaw6MDmVUXUSOtYi4xbGOexwhCt8TT4t0RCbtyzn82n9vThnL5C/muCZaHnl5gRwwxg2kx7yuldw8yvXqFpQdn3ZhWC+oqYrEimomp+nZixJtCyEwzJX7pZI+8I1i8c3Umfn5S+WlgkU507QE8de1WfqsigUU9iveoxtVRO7CpwP5/TrDS3xZF+JHuDf1x2f1uYuBUlYP3UZ1pkFU5jKCZUIU5cxtoUvs6E3hZ5fuC8dNvHjZBPBpI3Sd7txRwdOPpNJfFSLs8GXD4Qmp6/B8xaXnHPffYv1fxKfn7cLrEXLNVcufh69AexE2+7Vyy7iZxxrS9vdNtwypy9WlJZrzgELJRvi0gUaTre1tUWWZYGcHQVjDKPRiP39/aAoqsoH81AAVYv0HxAm+ErANERwOXww7p+WJQDvqKnno0RvOVxyuTag9kOhYxHXrouLz2tY4+7uLp/73Od44okn+MAHPsDe3h4XLlwIBKrT6YQxgEVTFi0foSSz1+uF846VM+2Xkk59H9e4033i66VkU11IASaTSSBnMcFVw5W1tbVAeOOaiZpXqA8E1EgmLsmh26nxT+x+GhvkxKU3dJx7vR5FUYSyFDrG8bnHoawaChsT+TsOT3WO7BaVISdQrvi8vcL68bG1AqTqT9sUpBgScfS7M/LEu8kGtdDN8wcDQVnumua1adieO/xPa/LpTjYTip7vQ7baITEGM82RvDi2XuJR9ffip8LzbMD5tob5YzR/3h4dgT/9km9Bzp1h72tXKfqCS2p1Lz1CSV0m1jomrm50WdU8Akc6sToOK7cxIQzuO/UqgfxEiwvpCr+Fz8XLQ708T3KrOimzoCZ8S8XQKydk0fESmYdyxk6fFXWeX33sqWthnZBJ5UNCNXQW8fmYlQvXWYyOTxy/6l+PVakbfNG45W14xAOkZ7Uj8TGfClz8RnwbIk+vjcYt9HkNaXL6GrwAUYd2vgH4r/BTkpeLSAuf0/exerOrIvIgPqfvhHPupoj8DvCtwA8C3wL8JXx45w8A/wl4E/C+29nXY2fBaoqiE3edqKtSo5N2VZGUpKysrFCWZchN63Q6wYZfc7GAUIev3W4HUgSwurpKr9cLpiFKWlSZ039K1PS4VVWFfEElN+pSCYTQ0na7zcbGBnt7e1y/fj20rcRke3s7KIz6uhw2qSQ4rgOo+2u+nP6bTCbs7e3xqU99ine/+9184Qtf4OzZs5w9e5av/dqv5WUve1nIn8uyjNFoFEIZNX9OzUySJGFjY2NBLcuyjNXV1VC+QAmiEvbY7VTLQyRJEvLvlssYdDod1tbWmE6ngSCWZUm32+WBBx7g4x//OA8//DCveMUraLVaHBwcBCMVbU9dMlV11bHUe0hDSXU8J5NJCPvVfis5V9IO8zIiqnKORiMGgwFFUTAej8Px9H6LyamGmt5piA0+jiQGS4RBzUNUpSo2LHtFj9Vs7rqa25RUqqAQJVj6yYwL6/vs7fURm3kVS8MxXd2mHu8It8pAftS4ZCn0M+QY1iYxZRecCFXHAG2yUUbXOUxZ4qbTENZZUjF1JQkSjFsKt9R4Da/kKRHxv6wJPm9PiV9HfCmHP/OVb+bGn365V/ZazIvYm/n7MP42Gmc3vx7h/JdMdMJ+9WUK3jRKmCNFUMs0xATZRM+/XL2NqdfZlnDwopSv+n//Ff6bv/bTVBgSLBYhMxWZK5m6LFxXc2SipUfhEoxYKmegJn65S0LRdZgTwnjEMynpmKJ28fQ5hG975L1827ffh0sNpmdwIohRZuwVPoCkWnpq0OC2QBV3otcnJX9fBJ42tXraJDA+o6OOKvMv01G7NnhewghUjdTX4IWHbwJ+0zlnASsi/ws+1HMKfG+9zd8G/j1+2vL36mU/CPyQiHwf3tDlMnBZRK6KyHvw7p3/+HZ29FjSp7lxMLfuV8Uozu+LzVBUxUnTNIT1aV5YrMipGqN5cKoYqhrT6XQC6QNCPpqSmtlsFvINlYzFJSM0zy7us/ZLyxnERFJzzfRYce6ikqputxtMT5QIAws5ivG+mjM3HA7Z39/n8uXLfPazn8UYw5kzZzhx4kRQnuLi7+12OxiOKFFutVoL4bDxeGrOmoZmah6bXsOYbGk/VU1UcqzXVBWzNE3DddNczDzP2djY4IEHHuChhx7iO77jO1hfXw9uqDou4NU2vRc0PFjVRD3PGLF5j+ZL6vbqlqrnrqHE6hCrdRs1hDY20QHCPRzfi3calEAcN38/ivgp+QOY2ZSuTUhNRYKlckIqUNo6VA+DoaKTFJjUHtHgMR07apNYESMiU6nz+X51SKQFTOqVx6JnQIRstU3roAOVBZNgcYxtQYVjBhiqQOzUkCVGvhTOqcSvco5MhARhxbT5Y1/9bWy/5T7yNZmrehFZC+chx4z5ceOhql1ca69uU8wx+8nh/RfGcWlbTxK9KnmzXKVtijpcs6qNViLHzbox6yS89wSPQOwqZ2jVYaAVMLbtEMZpsLSkCnl+2n5L1DbGhtDRmSsxeYkpMqRySAJSyYIJTYMvPV4Y2W5HfRkUd3Rga4OnCSNy7EO9Bg2er3DOvRtvxKKffwz4saVtHgbeuLRsAPzRI9r7m8vLbheOJX2x5b6Si+WJeVxrTUmJ5nWdOnWKL3zhC5w5cybU2lsO2TTGsLGxwZkzZ9jf32d9fT20qVBTEM3Ng3kIn9acA68qqdIXh2vqMXU/LcjearWCM+SlS5e4fv06ZVkymUwCCdW2VldXg8qm6uLa2lowMVESo3mNs9mM69evMxwOuXjxIteuXeMjH/kIL3nJS9ja2uJFL3pROPb169eDWY0SE1VEn3jiiUD8lg1l4jBXrYEnIkyn05CvF+c8ajhnHFpXVRWDwSCoq3E5irW1tQXVdDabcfbsWdrtNpcvX+btb387r33ta3nRi16EtZb19fVA6pVQx/dFr9cL46PHUMIeF2lXohi7q2q+pJrWAMEl9OzZsxwcHCyovnoNYsMgYwybm5vP7FvyLGO5MLtfGBGU6P2h4uwzYVqlzBL/VZ7UKpB1pk5K86RgYn3oXqtVUma+8HgI1XPz1BpVHuOSDbh5FNaxfU3AJXUtzXp9MEoRoexClXVYSbfIdnuYwYCxy3mkzBjYDgnecMSTG0dWK1AGFzlP1g+XcAuhngbomdqh89Uv4/Kf2qRqE5S98ForcMuGLMvnHYbuFnl8YftaOXREqmnUpo5fvL+S0GUCGMhp3Yd/94vfwjf9wYfYyCa8qL0NUhOz+sxjsqdhvRp3WdVsLJMy5PFVGPaqHrO6LIOpSzK0TRGcOzW0s1WXhVADmJkrkdEE00pJZr49Z/zTChefg2mm518qSPjf/Pt7aH2Np3tNjt1+KRz5lge9VcPHqXhhu5gIxiGgzd31fIZIk9PXoMFziWNJ38rKSiA0mu8V57dpjpmqamqNr0rRN3zDN3D16lUeeeQRzp8/z40bNxgOh4GMKcHZ3NzEOcfKygpbW1ukabqg6O3v7wciA54o9nq9hRwv8ERSwwjVrTEutK7/dnZ8TqSSDT2v6XTK9vY2BwcHC4RPVS4RYWdnJ5z/wcEB3W6X69evL+SyJUnCaDTi0qVLbG9v89nPfpadnR3W1tY4f/486+vrrKysICLs7++H8VLXSXUA1ZBErTOoCunu7i7r6+uBkKl6qeRWHTE1PFPVP5gTKlX1dL3WO9T1mteohEuxtrZGlmW8/vWv51Of+hQXL17kW7/1W7lw4QL9fj8oiLG7qx5PyzgoudTtgIVSC0ruVGHUsgtKYJW8aj83NjbCtVajFyXFel/GSuadBlezl+BrEE+alpdFhEAJQzoRPvzRB9h40R4vP3mNzFTMqpSN1oQKQ2kTbJ0bdn28ymySkdZqnBqHxErYsTPFuo9xIfNAqOJ9EodzvtZd5UASv51teWdHaNFeTelPL/CnXvmH+bcf+0U+r2YlLg0qlh7DOlW15mRHiWHhEqYuI5OSVTMhOXOaq2/YoGrX5CON+kkUohqHcMbnqqGzek66XenfLxRYN0vnXY+d0zHU9mKVUccxJtV1NOShEF4gOxDe9bFXcO89N1g9M2UlmYbzDmMS5zyKxVD58g51bp5f4qicMLBd9ssehUsY21Yw++klMzKp2EjGJNh5cfZaYczE+vpawxGJCFkvQ8oEZ4SqJbX06up7s4ntfLZw5Fz5KU2gb4MeuPy79FSbWT70srS9kETsDn+pwpOmBi8EiEhD2xs0eA5xLOlTlSyuvacT+NiURT+rGqh5VadOneK1r30tn/nMZ0JtNs0RBEK+n5qD9Ho9jDELE3x1npxMJgvKlk7iNcwyzgFTu/+4nESc36YqmJ6jhj+urq4GY5eY9GmoqhKh2NwkLgEQ5waORiMODg4YjUZBOYyVNs1lq6qK6XQaiLWer4Yhahhjnuch3FUJTlVVwQBFSzBo7p8auWiopyqkWtZCr5WOpaqomr+4uroaiq7HpipKMFdXV1lbW+PmzZt88pOfBODFL37xAqGMDXK0jTjEF+b1CnWs42PFZjgayhufi7avOaGaD6jjGTvP6vnekfWBjkpriXBI7VtaLxUkQ8Pu5XUud6est6asZDMqJ1gnWIRZlWIRJkWGnSWHFC59VSJybMieYy6vLYVJLsMXcvfrLD6PzaZQtXy4Z7XZI7Fb/JcPfjP/6bO/yp6FcVQ0T+vDTUkXctCqOvnQ4sjxxckrhJ6UsNr3xFKVx0V/k8XwSu17tDwocPG5R+pemJMuj58c3U48vmF5Mh/DQ/PZ5f0cpDczLsopbp64TOESeiYPmxsEW3fG1Naj9pg8P4thZjNmLqWwCTObBhKdiMWKITcpHSnqsE7fhip9hbO4yRRJU8y4IAWqTlLXOZTF+/MO/Jo9f3D7BvCptSJIdAO74/Y65nvO0v3/1DsR/7C5o9s+so3bF9Da3KpfehihKdnQoMFziGNJn6oycagjEEomxE6YcTmD2WxGu93m1KlTvOUtb+HGjRu8/e1v5/Lly4FMAGF/5xydTocLFy4EMw41iJlOp4zH42C/H5dCUOjEP1aFtA0ghCxq2QdVgbTMgxIRDY/MsoyyLDlz5gybm5vB3VJJnxJFVcpUaZxMJoxGI4qiYG1tLZjU7O7uUpYlBwcHDIdDWq1WKB0QO2xmWcZ4PKaqqqDcra6usrq6uhBymiRJIGlKqIbDYSB9Stz29/c5ODgIximqoC4TVSVCWsZhZWUlkF3dT8dVyfWpU6coioLNzU0+/OEPc/nyZTY3Nzl//jx5ngeFdTmvT0lnvE7PXx0/dYyVyOnx2+12eMAQm+moiY0S4dlsRrfbDcqjPgSYTCZ3pHtnMPyICMqxat8RD8alFB/uuJPw2Cfuwm4UvOSeq5iuxTrDqGxhEfIqYXtnBTNMw/EWFMRatVsI34zCGg8RzpjMHDopcIkLiqRUgit9e2XX59mNznfpdFLa05zvve8P8HNf+G32bR7y9hSVyylqpdIeNTM0EzpSkUkXGY7JRo58vVaftPj8MSGdyx0PSiAsWoVGamZQB8t6vARq49QQLutSFuv+6XGXlL/QDSWDR4xt60DIBi1+Tl7DvXff4CvWr7OV+d8PI45EbB0C6197yYzEuVCYvXLGl3ZgTqSDmlpjajOmZCFvcDWZoMUb8ppkD2zFLz3yPt58/9eStFuYvOdJX5biEoet/z4shx83eK5x/MWQp3uhnozUHbX+SEIo0fLlpx7HHbi5qV4IEOHOfPjaoMGXCZ40py82/4iXqVqmE/N4Eq8Oke12m5MnT/KSl7wEay2XLl2i3+8HRUhVps3NzTCpVwOZmODFdfBUYVMFR90qlRgoEdJ9VMnT/ivpUzVKiUy/318wUUmSJJAeDUnVsNE4t1HDKZVYxn1qt9vcuHEDay2PPfYYBwcH9Hq9QJSPK3Kv5Kfb7YZ8Ru0nEIiTnsvBwcFCOKeeKxBU2OU8OiWO2jYQCO9gMAikSYmxqoZ6XdWh88yZMwDs7e1x7ty5hfsivk90TPW6wDysU5fHocOxGYueQ5xnGl87VXtjgxol+xoWG7d3J2KBIByzzn9YXOfz8QSpHMlEwGU8sbnGSjajNBUWobSGQdHBTRNMTVaIJudB5dM4w+P+KMf5OEsP5xfqzi2FNIp+rguhWwc2FWwqkCZImtKWjIwCxBuzqEmLEa82FdGJV3iiq6+ZQOEqqpvbbP7eGcZnVup0Rpn3qQ7nPKSsReqf1tILal7db+eWtlPVr94nGJfGNfl0fKLjBYJ8xDgdmjwvqYfp9YzLvXVOdYd0k4K2KX1NwujmUBKYiXf5RExQ8xbVOzsP3YysRCuECkPhUjKq2tXTUImjQBjaKXY6xcxyyFLMrMIUSSB8h/re4Ekh4f+qsT3NCfFTlquO3yhW9wQ5Xu17+k3fYl30tEjC/55+Q7dP+GvwJYARwdon365BgwbPDo4lfbGqpKpLPPHW8DslPwolWuqw2e12ee1rX8u5c+f4wAc+ENQn3afT6QTlSK32lThp6YJYkVKTk6qq2NvbCyrRxsZG6LOGimq+mkLLNcShlGoOkiQJBwcHgeypIYj2VclIfP7xqzpvan5ev9/n4OCA1dVVdnZ2+OQnP8nDDz/M9vY2DzzwAGtra8GUREM6T548GUomqDmM5jRubW3R7XbZ3d0NIY3qDjoej0MunOaxabkDVRk1103DNgEODg7Y29sLxLbT6QQ3U1X9dLw05FZDUZUQ3nPPPYzHY4bD4YK5TkwyVZmLjWKUpCmJ1xxCdUhVRU/HRxXhZadRJZaTySSEcZZlye7ubgjz1XzJO5L0HTFRWS4NEMhBNKGOFUL/XjA14Rh9bp2PDDt0ujm9dsF4ljHe62JGCVLNQ/FcwtLEa7FP8xDH2pxF3KKSowXddR8HYlXxmZ+YqxWxcBwHSe5ICgvFvMaiESHTc9Tve71bJnERdr8u09BGYOgKfvTR3+S/eFHFXe3XsHd/h/FZ74IZxkv7GJu0uHkfXUKog+jqvsamK0r+YgU0Ls0Qrp+SS7e4HOblGlQJDGpove2Csaqbr0unwuxaj8fWNmmZio1swlo6wYhjWLXJbYrB12xsm5LCJcH5M3HeiXMlmWLEK8BKmI1YCucLtbdNQUtKKmcoZB4GXCEk1rEjBT/2+G/xp178TZjplCxLQYSyn+KMwaa+jINrWN9TwnyUvki2ckvi91TI3q2u17MRBHlUeMBSV57qIb+IrjUc8UsPI8/goUKDBg1uG44lfZprp6GQo9Eo5L6psqIKkm6vhdZ1uRYN73a7oS6dumTqhL8sy4V8PlWfVA1TO/64JAD4MFMNAdXjw9xEJFblNMSv1+uxtrYWlCY1VFECORgMAEK4qJYHiOvMAcH0RMdASZASnpWVFTqdDrPZLNQc3NraYmtri2vXrvGhD32Izc1NHnzwQc6dOxdqyN11111B/VISPJvNAF8aotPpcNdddzGbzRgOh1y9ejWQzTjPUkNLlaDpOGqOoZ6DhtjGBilKAHV7PW91UY2VOGNMGOe9vT12d3c5e/ZsIFtxofs4lFZzFlU9VBMWDf+MyZqOrV7jbrcbHioo+TfG0O/3A1kEQihrt9sNob93opHLQkHvI5YDC2GJR+XZiZ5WAuIEJmCvdJimbSZZTYwqwRTRNNM4b7qhpEfDIeNjHDJVEMS6QKTAh27GO4V9rUTtMK9jV0Eyg9ZBSXKQ42Y5Ls+pnCUjwYqlchVRKTvff45H4aBw3uXzPz/2XjbNB/mOe34fq9/4avYfaJGvHTGpPWK8dbnUKp2G3DoTkeul/RbCZCPyZuNwzTlbXbjWy0YywR203iZElzowM2/scnNvhQurexSpocJQ1W0V9QGHVZuxbZFJRS/JaUtJktaunJLTSXIS8cYuvktmwelTXTu19IMWd8gxFA6mWH76c+/hj1/4Gv8dryzJagcpO1SdhGLFYJNmUvd08bRH7Ja8+pkzp6P2kAUl8ul1a5lWHtmGyt8LO8v8Ra2Dm7DA5z0a984GDZ5bHEv64tIJOlkHFhwZdZlOplXZicMxVTnTUDytJ6eulLHhhhK2sixD6CKwULA7JiaxQUhs2ALzMhNK1HQfVeV0P/DkI1YZ9TziMgCx8YmOj7YZ1wJUUqQkVENX1VhFDVD29vbY29vj7NmzC/mKWmKgKApGoxE3btwIYa/dbje4X7bbbVZWVgIZ02uj4Z36XkmpOqbG+XZKsoCgdup5tNvtQNzjto5SyzQH7+DggK2trUDiY8Oc+P6Ix1DHK85vhPlDhOUw4/ge1GusY633pIZ36tjEpSTuOERqDizxrCXlZ4FEHZEvFuZO1hNB42SBOGAJEZwiEZGpN5gTmyUWelRdvyg00XuIyCHiFIiqnf8zhfMq37TCzAqw1cJkrloI4zwM7W7lDi/DQY5jaGf85MX38t2v7JNM7+Pmq3rY9lxZi7HMa5d1jZiYLZA+N99Yx3QBUYkIt3y9ooMdpeyFibIqqdaX2EhyIR+n5DalXGCV9Tg48QYt4ihVI018zT4jjgxCXb6sPmbhUl+A3QkdU4QQUIXmBGqZDACL5e2XP8y3P/hGzHiKAdJ2ijio2oK7ExX1OxjP6FfpqYh0Acs3+S2UtkN7RmTvSUQ/idYfejb1VHnoofNyCw+9gPDdCmEGXySLWDytJl702YQgd+bf4QYNvkxwLOnTHLt4gg1zhUihipQqO6rQiAjj8TiQRyWQWpMurtWnrp5qgBKHlG5sbDCbzRiPx0H5UVUHCOYnqjxprbrJZBKOmyRJKKcQ53sNBgP29/f53Oc+x/Xr1zHGsLa2tmBAokpanKsGc1IS152L//V6vaBo9Xo9er0em5ubbG9v0+l0uHjxIp/+9KfJsoyzZ88GZTA2XIkVMC1HEPdLjW703GOSp2pcXN8vJnz9fj+QYi1A3+l0wv6qAI7H43CN1TxGSZYqnu12O5Sp2Nra4ty5c0G1bbfbgUy22+1DBivaT3Vr7XQ6tNvtcL3iUhyx8jscDg+R0JWVFfI8D2MFPjQ0z/OFe+ZOQlB9YgMRWSICRO+VJESkI6yr6v0RDL50goQ40egYIdSyVu3qbWRJidIQTV+PblniqvtpHFKJzxUk2l+JXkRCWweOlSslrf2C7PIObjLFDkf8/OUPMHOOsfPEv6hz+uJC7L5o+/w9zHP7iLaZOU9KjLP86CfewXff/XWcLr+Sm6/uU7WEumRdGIMQ5qrLIkUyEHI94BEqXxwKukDg9NyrWomVefvz8fVt6tBqbqW2Gdq2YGoCmO5klNb4/D0smamLtqd1EXWbecdWm/oSDuKYupTKClaEMT73r1XLwwZLxxRkUjt11ierBdo7pqAjRW2WA5kImSRYHHYygZsW6bTJipKk3wXXp1g7TEgbLOK20eJjpbPoqczCgZ/ehPupbq0/w96o42kd4uiDHRHlsLA8NoO5bWgeVjzbMF/M/dGgQYMvGsfOgjX8EuZ5dKr4qbKm5EwdE4EwyVfFKjaBiZ0gY6fGvb29EH6n4XyaQ6ZFvTUkVFWrdrsdjhUrizAngko0lPhpnluWZeR5zmOPPca1a9e4fPlyKIEQK0xxSKCeU6wcKrmKSzHECpcSTCU92r9z587hnOP69euBBGpZCyCELY5GI8bjMeDDO7WcwlGhinq91LlU1ULdNg5FhTl5XVtbCyQvVuGUpCuZVBKt13FZQS3LkuvXr7O3t8fp06fD+C+rwrptTPp0nFSBjdVY3U73XS5Hoa6leo+q8Y1ea831m06nrKysPI2vxpcQS0Lacdvoa1CWjvrjWZMDH2PoFgW7JafQuXlLvV2Uu7eMhaLjh1bOF4YQRVvnr1kfzplOHb2bFZ0nxpjBGLc/wM1muNmMyjkstiZ2c8JH9Pp0UNVz3sJZfubx3+W7vuok2YsfwCYCSvq0v/X74xRWJ7EaGnUozuGrSe6CmnHUejMnwk7mJDx8Zr5/HPYbisNXPszTOiEzFUZc7diZk5mKyplQow9DcOMsbAqGQJArJ8zIQi2+lvPqXwxTF2tvUdHCkomlJUImBv+fn725soQpMJ5gREhmHariVsG4DZ5TPJMJtzK65dm6Snv6nb9VE2G91w6P43fHkrn6YdY8zNM9sx+HI3CrIXkScbPB04Qx0pRsaNDgOcQtjVzUYCMOmVPlLg5DjOuyra+vB/VOywAoUdQyAMBCHTfNGVQjlfF4HIqk93o9RIS1tbXgNKlkJsuykJcX1/fb29tjNBoxGAxCqKOGHKpSVFUVN2/eDKUbTp06FRSmdrsd8hH1PIFAPoAFQxc9B1WwYmVUVSolVd1uN5R1mM1m/MZv/AYf/ehHSdOUl770peGcer0e0+mUK1euMJlMgmNnrIJq3t7+/j7j8Tjk4SnxO84Fs9VqhRqJ7XY7hN/G10YJeFEUTCaTBZdPvSZKJtXx01rLlStXOHXqVHA7VeLX6XRCuQ69f3q9XrjP9NgxUdZroGM4m83Y29tbCA0eDAYcHBwskFvNAY1DlLV+3x0LnV0cQUaOU5qW9w/kwIKrACOLRjDaThoZskSKF84FI5bQrMMvi8xJlvtiylrRUlWvVreS3CEVdHcr2jdzWhdvYHd2sXnOOx77IJWzzFzJzJUUOPJ6MqCqXjyneyY0Yuochcv5iY/8At/zhu9i8Npz7N+bYrNFchoOEM0+XQghi843Fk+OE1F0XyWMZX2MuGyE7h7PKJP5vkEhrJVbqVxdDkJIcqGVVKS1+2anNmspXBJy89TMpZf40O24ZIPW3asQqMM+tfZhyxRzsicVmZT0jFf5ThjoSUYmCd9+4atBTLgorrLIbAYiJMMuSadR+p4Mz/60d4mFqRzNMyAy9ROmhf3CD9T84/JPw7wCoPcEBXfEOhfeVXGfrTv6Sx//jt0GLBJOd+S6hvjdPjQ5fQ0aPLc4lvTFZQBipSY2TFHlTifWwIISqCqYrtdyBUBQYmInz6IoFursqSoYK0s6uVeFcX19PRQxN8YwHo/Z29sLOXpa1Dw2kNHcPjV86ff7rK2tBdMPJa16ntqX2LBFc8/03GI1T8s3qMqp2+g5b21tBTINcPHiRT75yU9y7tw5Tpw4EQxclFDGYZgauqpkSsdcyexgMGA6nYbjKsFL0zQQ9FhF0/qFGhbrnAsKo3MuhHfq8VWBVJKrY7Szs8PKygrT6ZS9vb1A1LR8h14zJaJ6H+n9EudgqluoqqZaf297e5ubN28ynU7Z39+nKAp2d3fZ3d0N4aG6rxJHrbUYk9o7Dhr2F9eF05ww3SYO7TwKy0qbA2cJBdLjXDaxstBeCOOszVdcLXXFOXp12TdP8JRclv5z68DRGlmkcqSjimRakYwKzHgGVQV7B7jJlHI85mce/10sln3rwhPfkbMLoZwxqqVp2eKyOeIwUPCkUStSTquSf/O+H2PDpHz7f/XXOHhR6ktGLD8DWFbo4smlTjbjsNC6lp/mZIbw2ehamFi2hIVZptSzH82BE6ckUa9HfQ1RMu1wIvTSnLYpQygmEJS69XQcnDhV6fNjZhbKO7Tr8FBtw4ilJzM6pqAvOT0paYmlJ9AzCd9z3x/AdDuQJCQn6z8bdp51KVkG7RbOmND/BrBMK576ui8BJDr+sV1YojwxE7KLi0JeMYu7LC4Pet6hb3E4UvT9cdFv0JHs6zYxsuVmmkDPZwdCo/Q1aPBc4ljSp0qdEgSdROtkXxUZJX1xbhYQwvB04q7GIWqwEU/0gYV6avF2wKHwv9iwQ/+p8qdqneZ2qTqlOWaqfDnn6Pf7oYyB5rTp9kogNGwzrvunfYrzA5XAaTikhjHGtfeUuGrZhDzPeeCBB+h0Ojz++ONcvnw5kKWiKEIYqhI47beOhbpS6rXRPmuoo7alY6/ul2pOk6ZpyJdUcqtKqZK+ePz1uirpUzKYJAmTySSET8ZhwTrmOi6xAU5cZF3vKQ3t1PXOOYbDIYPBgOFwyO7uLpPJhKtXrzKZTJhMJsF1Ve83VT2ttZw6dSqot3dieOeRnikwJ2VL21KvW5iVLD1mFztXm+LJUsgls/P5nqgC8GS5PpESqflupoCkcKRTSzasMLkl3Z9hpjlyMMJNp1BVVHv7vO3yhxjaGeNIzVOEfD1ZWoYEl0mEhfy9o5YndR+17bxmukk9e9yxJf1P3WB0+i7oOFwqh8f0Kc5HFgquH7HPIcLIXMXT7ZcNT45SdWVZ1RBI61y9uetmVefjqUmLVwE1b6+KGH+Fr92nhC+rFb2WVLSkoiMFGyZn3fjcvXXT5Q/f9/tITm5CuwXG4BKDVN6hxiVmfi5Zimsn3hW2QY05yTl8mzw3k18HtRlKLF8f7ovU/1mxtXJdh146/8Nkot8NB0j97Ffn9M65+vs1V+3ikOakVhArXGQ4pX0ySP3Ew/8eSsQAQwfnr0/283VMhOpRzS1+utX1a/B00fw0NGjw3OJY0qf121QpUeVLiYDm1cVW/FrXL1YGVUVSkqKTeQ3Pg3khdFV1tGxBXEuv1+uxsbFBkiQhj0tJlRp3rKyssLKywunTp2m326G/cU6fKk+qkqmBTKwCaUinEp3hcAh4Itzv90OooRJadZ4EAkmMTVSUBOoyJahnz57FGMOpU6cYjUb8+q//Oo8++iivfOUrQ//39/dDzTwlblo2Q0sUaJiklk+YTCZsb2+H/sSK4NraWshdjAu/x6RvPB6H/VdWVgKpm0wmgcwr1LxHy1LEtfNigqyEUx1H1bAH5kRZSauGAavafPHiRfb399nd3WV7ezuE/2rtvk6nE5ROPQdV91QxVHX3TsOC6Ulc1Js5IdQ5+wIvCyodYUZiqkVSEW8rLKqEy+UClv8ah3p79TFsVk/YokmbWBf6bgrri3UPxjAYUd7Y5m2Pv5/CVYxdwa6F3DmSeROH1L3YkTNW8yyCdRKcPW0IB/NPjSt8uYYi3t8JBRruWG+D49/8+g/xl9/8l9h/+QajuxJctqSeBkI271y4Lm4+7stj6oQQohnPEGOTzYUxN9H10/WuXh4C3/w2pnCYWlXFQWY8qZvZ1BdpR0IIZ0eKYNRivKVNUPn8qy/F0JE6lBNL38zoSMGZZMipxPHnXvomzNoqtFvYjRXsazOq1GAzs0CUq66h7BhPZiswlcOUzudONvji8UxFwojHzX8zpI7wrH9Uooc4C+xJxD8VygRnBFcJxtlayRbWyVgBeggZLcSlGBISUrLSy38VFSUlnhYKFfW9gZAgVFQUwAwYUzLDkmMpKBEKCnFME+dD1EP/TNTh+ksWy4JPAhf9ri4sP2JIGzx7MNIofQ0aPJc4lvTF4Zfx5F3NS2J1K66Pputih87ZbLZQRy5WrWKlJy7Yrg6WSrji2m963Lhkg6pKmuumLpCKOA9NSWUcAhmHjyqZ0FBDPXeFKppxP1RNU6IR9w1YKMug26dpyvr6OiLCuXPn2NjY4Pr16xwcHASCG5ehAIJJSVVVIfxWibK6X6qCp2Q4JryxC6uGVirh1lBPraun/ddtdd/YEEbJfpqm7O/vh2sdh1vqtdPtdJ1ut+w2quM0mUwYjUbs7u4G9VGPt7KyEsirEkh9MKH3rpapUMVzdXX1+G/Cc4Vlwhcpdvp2oVTDMqKZSjADWVYBb6EKHlp2hPIIc3JiExCdf9Vt20ywmcHkFikrXFnyzksfYmgLCmcX3DjhGJVvaVkMg/PELyJ74HPX9Nys846lCzXo6tekPonKeffK6uHPsNp5OaO71mvCS3Au1XM+VCZDx0ui9bViGnL4dCz1pW47HuuFOn0xiYz211INShi9clYrDhZm1dE/28vlFrQGX+EST/YiaB0+IwUZXuE7lTj+zN1fT3rPCezGCq6VUqy2sJknAzYVXAo2EZzx5RmqlieBpqJ2cXVPKhp/OeJLPiTLD3+ihxj+ZnO1s6/U4dzipXIDFEJmIXGQ2YQ+KW1naNdVG3Mq9lMwD5xEVtpIkmIqn2+KdThnqaSAOhfYWYtYixjBtDJs5h/8FNaR7wyxOwPMOGeTDkKbmatIbM40KtpSSfQD5fA/Qm75JJewRIyfTMx3R36q/443Ot9tgRGwyz/wDRo8zyEi3wj8XTyn+qdAB/gbwAT48865x0Xk5cC/xT8e/rvOuV8WkRXgh4Et4G3OuX9Yt/dPgN8HPAZ8n3Muv119PZb09Xq9UJ9OQwiVJBRFseCWqEoOECbmcfH15dBIdedUAtTpdIJKA570nThxIhA/VfY0XFMVIyUQSkji/ZXs3Lhxg+l0GhSyTqcTVLe4UHlch0+VIlUxVT3SY2k9PyWYcTimIq4xp8fQsdB9lOykacq9997Lgw8+iDGGK1euhPNQUqR91ePHeY5qyKKGKkq8VJGMa/Gpuqfjs7W1FdRTDedV5VNzEwEGg0Eg8nH5BCXi6+vrVFXFwcFBMGbRUhkaXtlqtUKeZZ7nC+Rfx0FEmEwmXLlyhWvXrjEajXj88ccpioJer8fJkycXylbEhFvvA723dBt94HAnGrmYuIxCTACWjUSibW6FUGtP26mJgxqtaNuORfIRCMsRKuG8LYdtiVf2BFwlWBxFT0jyBCkd2SzHTabMXMHYVeTOLRC75dp7GsYJhFpwuTOB4CmqWu1bXuYVvTmS+ntslOghGGSuHDrLf37svXzvfQnpa19P2RGszMcdtzT28bxSt8NfN1W4qnY07gvqCgsMNjbPiU13BK+k6npxYFN/7dTMxabzY1ybrHKyPWQzG4ecPKsrXRrcOStkgfBp2GeC9cYvGHrM2EpGnElyvvdV30F6/zr5+U2KlRSXeJLnakMgp2QvE1ziz9um9SQ8d34e3qh8t8StBLpDuNVGxz0Aqu9XWVoelOg6n9dYkJrwOeMfAJCAJA5TCt2KmoKlbCYdVtOM1aTF1dRxxeXc6IL76vNwVxc6CcwsDARK/Jddciid/1eVUFlIDax2/PaIX/fINbJHSno3HBtVH1clzGyO2H0cc5/Pqdj610ElzFhiP2agbgtPc7evqQb+4VGj9DV4AUFEOsAPAG92zuUikgHvBb4BeD2eDP5l4B8A3wdcA94B/DLw/cAvOOd+UETeISI/BJwCzjrnvkFE/g7wXcCP3K7+Hkv6lAzoxDquAReTNp2wKznQMLs43DPO89PSA7qdEjc1EAFfnmBjYyM4cw6HQ4qiYGdnhzzPOX/+fCjZoO0rKayqKoT8bW1tBYKm7o0rKyusrq6GkFUNIVUlTImjhpQqydEQTVXN9JzUVVPJneY3KunS5bEjpipQzrlQbqDX63HvvffSarX4+Mc/zmQyYWNjI+RNxuqZmrx0u93QhhLAmFjGxeZ1/OOwWA3R1e2UjHa73TCWa2triAj9fp+DgwMmk0kof6CErt/v0+l0eOKJJ8JYa61CJXfxdRiPx6FNvU96vR5ZljEej7l+/To/93M/x8c//nFOnjzJ+fPn6XQ6nDlzhtXV1UAi9d7R66JhxKqG6vHi/NM7DcGwJVaFYDF8cPGhs39rWZrZ1bvVT/SXSxBQK1PadjiGLks8yVgo2K6kUF0njVfWbGveb1N61aDoClIltPtdZOrLMFQ14dOwy2Vyt4yqDgWzMUnjsIIVr/Nk0ARnyirUpYi2d0lwrUQgQ3BlyepjBePTGbNN355LaiXzGOOVhRqGS3NOqeYmN84sEsflvLxbKmECVWuuJupDARXq0gm0kxLrDAZfskHHp3AJQ5tRuITCelLnx8/SNiXttMCIpaJW/6x//c7+Lm/56rdQvvQMZT9ltpku5uXVqqOSPiWgNhHEukPn96TlRxociafwTOfWO0cI7SR4giQCuavvGa+aV0YgFegIjKGXO1acYZOUU3RJSHiMnM+8dIv8wVPw6nO4V3Zx0w4Mu5CvwWYBvQKyHIoOFAZyB9duQpJBlkDbehVRDJgUSn26UsH9pym+TdifzvjEpz4Hn3Mkl/dpXzzgPCtASoVhYIcckJPXj40cdckiMYTQzydT/ho85zDSXKEGLzh8HV7R+wURGQP/GPhkrc69r1btAO5yzj0CICLbInISeAPwN+v17wa+FjgNvKte9g48UXz2SZ/micU5arEtPhBIn6pjGj4ZG7LEChwQwhCVNMX5b0pqNCRPyUCssKkzpRLQ2DlUFTlV8lqtViiNoI6gej5JkgTiEJdbWCYGSiS0dADMCVwcCqnKXjxmsOhmGpvaKDmMyeL6+jrD4TAocmVZsra2FsoyqNKpSqbuq8YoSsi1DIWaxeg1U0VSjxcbqAChCH1ZlqHWoR5H1cSiKIJqpyUjtLyCtqOqpBJUNeXR66vjpPmfGmIrIuzu7vKJT3yCd73rXeEhwL333huIpTqsauhtXHBdQ3qzLAtlJuKw3TuxODswD+1bWnZULl48M1THyEBA4vp7y1gO9aqJppLH4+ZLwQ0UvJOkqgamJkmAWO+EWbUE12ljOm0ySQ43VsOrb45EFu27NSxTVT0jx5O9GCY413jFy4g9FM4YMxNTfzfb21OKlYR8LcGm8zFZOPfwgUOqn74ukOTomsROqrC4TPc9FO7JfFtTRcetjyMVpMYeqqtXIRQ2pXAJU5sxs2lQRtumxDhXq4EV1hnGVZth1eaxyQl+/H94M+arvIpbtg1VW+p+zztkUwlkVh8O6LDGpSgawndrPF3x7igcIofRg4mwLgrnFOvq5z6+Rl4lDtr+QqYOehPHSpXScQkpwg2m7L/kAtx7muFL1sjTBHvuHLzy5VB8Cjb60NkAexIO9mA2gOnE/4iIQDuFlQ7h6dNgWpMzwE1AMshSaGdw/oxf70a4u7fAGarTG8xessW1S1dh5GAErRsz1ur+Fwgzl1OKpRKLk5SQ3/cMGcXybfvkzXxRFP3LFqae7zVo8ALCGeA+4OuBbwb+PvBwtF4nQ/HPzD5wAtgADo5YdmVp2W3DLXP6VB3TfC+doCuJUdVICY8SMM2Jiw07tMC3kislCLqN1vTTcE4lSf1+PxCU0WhEu91eMIDR19FotFC4Wwmj1veLDV20v+oSqWGqMVkDGI1GpGm60AcllXoe8T7a57hwuoYsaj81xFDPOw6P3NjYoKoq7r333kDWdIw1LHVvb4+tra1g5qJjoYY7165dY3t7m2vXrgWTGu1vXMqh1+sFAqXXTEmpEu+Y8Lbb7WBAUxRFcCBdXV2l1WqFOot7e3tcunSJXq8XzHdUZVPCqzUb9UFBt9sN4/3ud7+bX/3VX+VlL3sZr3jFK1hfX2d9fX0hD1PJnLWWg4MDVlZWAhHV9cPhMJD9+PzuSLj5JFoRQjFVpYuUJan3UaVOorDDOATxyMinSN0TW5uDaB+cj/4KfbCL+4Q+JfiQwwwk8curllf9yq0umXO85St+P2/99K8wsCUFcSimOmqWR9ZrskcQPiWBqmjlQcGab2OiMMdDhK9eXtSDOLA5b7/8Yd7yDffSyxJm6z3yNQnEytVz1aMUuoWyGgIuZaFsQyDUOsZRPKscMbNUUi9l3aaZq3shXLRyC8RxVqb+PGvFZmxbVPXn2Kkzk8Vg2sIlFJWv5zdzKZ8fn+Thn34pch/zuoA6nqXP+VowmwkPF+qxqusHLg7SoaFvcAwirva09zkEN18ffjPq8GNxYOp704on72KgZQ0dC6uFo4Mh6XWh32XQGlG+9AzuZefhdVswKmH9Ljh5Dh5/BJIU+qlX7Q6AmYOihKR+AqAOnA4f1jktoDL+CY/Nodv1HcgMtNu+fUlhax1o4wpHaWccdKawUyK7lrWiojOdYooSV4HDYZz/bSklLvf+9Ef16d+y0Y9tQ/yeFoSmTl+D5y0uiMhP1O/f6px7a/1+D3hvHdr5q8B/Ai5F+8XedYoNYAfYBdbqNjaAi3iSuLa03W3DsaQvrrWn5Gq5XIGSrCzLFhQVVe9UbdF9VOmJEatfwILyFJOOZdfNWHWMC71re3E4quaA6baq0Onxl8NYY8VS99FXHZOYyGj72mc9RkxKY4dKHUfdVwlMt9ul0+mwsbERQlqV6Gh+YZ7nwQm12+0u5PFp8XctLq9tq/qleXparkLdVGXp6ZuGsqqCGBvgKGHT0E7NrdS2tdSH3j/xNVcFNg5P1eNo2Oe1a9e4dOkSr3vd63jJS14SDGVUWZ1MJmHc9HrpGOtDBS3joGY2y7Um7zgcM+s4isgt5O3U/5RrEL1fmKhH29+SCEqtUiVL6yJ1KkRPRoRUFSCbCDY12FaKWenznRe+lp+49NtMK2/Dl9Sd1fDNw8YjsmDSsqz0LePIen1HqXwRCpcwc1BSgbWYogrkatlJc9nERR1LQ1096vmtekrE2zmOV1zja0l0nOj6hDGP39ck88aoz5neAW1TeBVP3ELNPg3nTLBhnanHWklhYRMe2TlJXdVhnm/InPji/DV90nl0/ICiwbOKIy9BtNAaQB9ulZasvt4OKDKBdoaIoXUw5S5ghZQOKR9lTPHKk/DGl8A965CugWnB9gzu6UM+gw//nr/Rrw+BMSRX4KCEsoJuBX0L4xHsjvzMXm+IBB/jXVSQF3CiD63a6vaxm4BAkkD3BFw4DYMcvnADXnEv7Oe4/YL9l51j/1OPwBM7yHbBSVbp4ahcwbAaUEgdNSowt8Y94vdeou/tMcOozRz/6xN/iY88REMFj4ExjdLX4HmLS8657z5i+fvxpi0AX4UPzXy5iLTwOX0fq9ddFZEH8Tl9J5xzN0Xkd4BvBX4Q+BbgL+HDO38ATx7fBLzvdp7EsaRvOBwymUwWQgfVZEMJyd7eHmVZhjp3wELphbi+XlVVDAYD9vb2/IHrSbsSD1W94tpuSlqUgCnxUfKjxCFNU1ZXVxdIRWwiAwRnyuFwuOAsqqQmriW4XOZASYsSRyWMqoDFoYZ6XtqHeFt1rwTC8VWts9aG4uaaN6glKLQ8RLvd5vz580Et1BBMvS4iwsrKClVVceLEiaA8apimOoIq8YzPV6+FhpVqsXu9Ftpunud0u13OnDnDysoKa2troa+q1Oq1fvTRR+n1epRlycmTJwNpjAu0G2MYDAbcvHmTa9eu8dBDD3H69GnOnTvHyZMnEZFQIH5vby9cp7iQ+87ODjs7O4FkasmK6XQaCs7rPXCnQcP7Djl0aghlpPLBXP0x1XxbJXw6WVdTkIWJvKp11K6Q0SRdHEiBN2Fw/nFU2B+8klURwgul7pO6S9rUO3hW1lF2E8xKC7OxRpIXfPfdX8d/+MJ7qICWCHldiL1wcQqjWyB7SgiV+OkyRZzDF0PVvlj1i5HgyKRi6hJ2qpmfrDJXQ7T4emg1ui5aiN0lsjCjCyS5Jr8h1NEtKnviHA4Jr8vhosFwZ2k+FEIspXbNTODgM5u85/oqaydH9No596ztsprOMOLJXiYVm9mYtikW2ipsisWbu3xo50UMPrFFtz6mJ+3zc0oqsEYW80zjPkdENA5T1XurwZPjdkx9F8I5NaTTOsSKz13F1g9iErCG7tTRt5ZzrLLDjCdOd5m+5l7Kr74fei0fdtnqwXgC+QRMDx6f+fDN7W04L0AGlcDeAFzL5+5JGyYzb9rSMdDt1DcV4GawSm3Z2fHhBKMZlDN/06y0oJ/BShvGBzCulb977oe9Pbi5A5en8OJ74MIZ3HDC7iNX6B44+pMuGyRM3JSpy8mNxYqrk8eMVxqPGjc5mvgtDOwtL9KtKeHCtWkQINIofQ1eWHDObYvIz4vIb+J/8f4i8DXAbwBT4HvrTf828O/xj8H+Xr3sB4EfEpHvwxu6XAYui8hVEXkP3r3zH9/O/t6S9MUFx5VM3bhxg+FwSJZlC7X2dBKv5G93dzfk7cX5btvb2xhjWF9fDyUV4pp/agITK1pxPmGMuAZeTDDVIEQVOlUJJ5MJN27c4MSJE6HPqjZprpsSKg0VjMtRKImKyxcoedIyAkpStU9xeYtlR08lLUoYV1dXmc1mwaRGwym1tENsjKMhms459vb2QhijjsnZs2cDMdJyFXo8PTddpjlyeu69Xi8QJCW/6iRaliXtdputra2Qz9dqtTh79ixveMMbmE6nIf/w8ccf5wtf+AK9Xo8HHniA1dVV7rrrLl7+8pfTbrdDvwaDAZcuXeLRRx9la2uLzc1NAD7/+c+TJAl7e3uMRiP29/eDq+z6+jpAIHixM6fmb8bht3ey0ncolFCWJuFRSKGGEi5sq8SxZB7qqd9sVfFioqIP4aOJjVn8aoXcrXifQE7r/mjImCdDYFtCvmpAMpJxm+SgTbK6yomkzdTVai+OImI7RptzbsGFc+62Of+sZK9w/uQqqYuM4wLRS2oCqdv6fX1Bcn88XQZuOsVMCtKZIy/Fu08qH1tWO+P8OoVdJHYwV0wD1NDF1QTOgRyRe+RdFLURIhLvPzjxhA8HrX2DHbWY3MiYCNzITsHZGZ1uTq+d08sKHli7yYu6O3Rq4qelG3bLHjt5j0cePk97VrepKm+kLNp0UeFbuO61FeuC2ltzYWdkcewaPC3EXOMpkwb9LovU10lCmKcVCb8PfZvSrywdJ0zaKaO715mcXWd6dgMeOA9UMJl4I5ZQJFRgUkFhvYJorZ+yJAZaHWi1wdTuR2VZx4fXjMokXsFLDNjC3z+txKuG1vrPSUr44hUWitqZvNcG04ZWC/ot2EhhswdFATcOKGdDZrsFyY6Q7FUYm9HG4VxBKQ5LXRhe68sow3P1GMvhsV34Kt9azGvwDNHk9DV4IcI596+AfxUt+jzwY0vbPAy8cWnZAPijR7T3N5eX3S4cS/q0/pmqKnGOWVVVtNvthZBJJQ2qXGkIY1yQXOuuxSYqsUoWO11qWYHYfCMmf0rSlGCpsYi2HZuGaDvWWobDYWgzDi2NQy7jdfE2MWmIcxL1Na5VqMfTkFBtU88hdkTVtvQ8tL9KsDSUUwmYHlPzLJXAxbUQ1YhHnUxVlZxMJgsFzPU6KHFVUqphldo/JYpxHzR0V++RXq8XQkn13rly5QpVVbG/v89oNOLGjRtsbm5y9913B7fQ7e1tbty4we7ubrg2Giaapinb29vMZjP29/eZTqcLpUQGgwHT6XRBUY2dU/XeisNq73gshRoCi6GAcbjfsgqIVwFD7buj/r7W+ztZ/ExNHI0QTFo4ahslNnapTbRmn8zZUJLQlqx28vRSo3FHu3cqjlLxjoIWGves0xxS+JQAJgsdrU1PHLhZjsxyn5tWh22G4dRzXCJ5xGOiH5eVL47eLixb2s4dcV+qWc6c+OkKT0CNVedU32huO4w7LcZrBSazlNbQTQr66SwQv6nNuDlb4YnxGsnEeHU3PvQRymM4Zs3IxUaENSJ8x55vg2eEWw1lfFs6qNVYfyHEzp8zFMZBmmIqR2tiOUtGVxxVCg9vJvCmV8DGKmwXID1Ip8AYLs+86tZPoRCwFaRt2FyH8qYnfe0EVk5DLwVnvepn6wdvhfPErptCt+UJ3GTgnxKlHchzTwQT8cQOB7MSRjmklSd6vT7sF/6HrNeCu1egJTAuYGjgVUK+NyB/fBc+PmF9ltG1KeLGTG1BaSylwZNO63wf4y+zW/xpWx5zx+LmDW4PhEbpa9DgucSxpO/EiRNhYq311+Ki10mSBEVNHSWVmGiIoJKc6XRKnudcvnyZ69evh7pzJ06cYG1tjZWVlZBX12q1AjmM3RmNMUynU0ajEU888UQIbVTDkBMnTnDu3DmSJAk14pSAajhnWZbBWVJz4MbjMadPnwZYqJ0XkyBVIdWMJjalSdM0FGMfDAaBSMWhnMsGMcvkUQlSp9MJJSX29vYWSi6oKre2thaUvfF4HEjvyZMng7umGtPA3NFSDWjUqEXLZWhZDCXHcR6ltTb0Q8NykyRhc3Mz9EFrHyrR7XQ6nD17lqqqOHnyJOfOnSPP8xCiOhqN+MVf/EUefPBBvvmbv5ksy3jssce4ePEi169fD/UC1UlUTYS0JMdkMlkgwPoQQl9VudR7Kc61vBOVviMLoavqEtv/U5cEiOv61a9i/bamcCEE0ybeUdNFrpRh31ox8gfz60zp5pP76aKCY1vi3S21j25+TF0WfwaQosLV4ZNvOvcafv7yBzwxAxIRLJUv60A9R3SGqUsoXEJOQosKI/aQclcRKXjO0MIXIUdKcOZQYfcER04SiF9Rs5kEh5tMMDPPZkIe21LY5cJY63hFxMclUquvLiLiiyrlMgKJWw6d1Ne6DwthezVMBeR+mV5PZ6BdCS5JqPYMGLh6pcOv39/l1OqQM70BLVMyrTI+ffM0g/0urYnM1d3lUFOzOBbiPAk0pZsb+tTnN79P5FBfGzx9LD9jCQsDSYnlV+pq12r042/ASt1VxdDLDRsuZZOUiwwpXns/9itfBF/1IDxxFfodeMPr4NM3wQ5AJtDp++WdugDlYOwJWr8N474vw9BpQdaF2RTKwquAnQ5IAi6HtTZktQqYlz7Z0DnIbV2/r/Khl876Mg8p/ua2CbRqkxc7gW4G3S2oNuHgMpgZvPQu2O6A6wAZ9Fc4+Mx1JpcPOMkqxo0pqoLCFcySEpckPgS1KADXcLnnEEakuQINGjyHOJb0qVIVu0zqJFrz1uKwypWVlRAmqU6cWsB9NpsFd8cbN24sKDDqxCgiwelxd3c3lHqI6+Gpqyf4UMurV68yGAzIsoytrS329vY4f/48d911FwArKyuByKjyV1UV4/E4mIloOOjKykroU1EUCwYvMYnUY8d5ixpCOh6PabVawWlTx1GLkithjE1r+v1+INNqEqP5itqO7q/lEmKzGuccRVEwHo/DWOo5KtmLzVzKsgz1/ZQcKpFXstfv94M6pgR4OBwynU5DEXjdTttUBVVDfNUZVEM49aHB3t4eBwcHPPbYY3z4wx9mfX19QeHTc7p+/XpwV1XCrA8W1K1Tz+vGjRuMRqMFAxodR4UxZqF0yJ2C5Zp4C3l0GplUv2peHcxrpYGfK6n7YpJDZ88ymxmKVaFYmbe/7Dy5/KhbHJgcsrElyR3pxGIKixSW8V1tir6hatX7Gz/Rj239fR+dJ5+lD9Vy0xk/del3GNiKwjmS+lpOnaNyBEdNNXFRQpeT0HJQiVtYnmCxMfGrXw2Gql6v0LDPVtRFLVheSeUHODHhOoh1i8YlMZaIYFzqQgmf0fIZxoXxXCRSEsYuDstdOIyZLztUMqLuhymXtlMyX3n1T6wn/VO3xsWNHpdX1zHGYa1QDNqYUYLJo/tJVc1Yhaz7KXauJnkl1J+rS+aF2cM9UO+77Hr65YWnHJR55J5HwsXraiMhPYQFcapQO3+fYUisoYOwVqvhN1vC7FX3Ud19Cre6Ct0+nD0FvdpJM3VAAmkXklWoKhhM/E1qWn7deOxVuiqBqYVO6gmesdBKYVZBK4M08wqgJs6lBmj58M9p7pXDsvQEMElgnPubpgUkBUxLOCj8Ddiqn34lzrdb1U8d+h1fY7Dbgeo67vyEMrUMHi/IbEpWf8GczSmNpRI8MXXqLrqo8B0l1D/da7Z81RtqcxgiPrK3QYMGzw2OJX2q8Cnx0pBENUuJc9li10zN89NttFacTsCV+GxsbITcK53cq3KoKpqGDWoMuBKxzc3NYDoyGo3Y3d0lz3N6vR7GmJBrBoSQRiUqy8YqQDAo0XOISyzE4xGrd9rnOJRQiWWsKOlxYhKi46N5hnFIom6v5FEJ2Hg8DmOhCqASIlW6lLwpidYi6Hrd4nBava6qbKoSqDmASpDUqfPEiRNcu3YtEEwt9aDlMEajEf1+HyAUZ49Joz4giPMiNdQ3VoM1HDh2LlWiq6VA1IBGHyrE1yjO3YtzB/RevuOwlMu38KqIScBRM5N6uakgKTxZc0awLSj6srhvfUxNo1mY7NfEMJ06kklFdpBj8goZTVkbtCk3OkxOtZitGlwKzszVLW3blF5xpKxweYGdzZi6inFN8nTCZfHqXoFZUPMUCbau5ycL4Zrgc/mqYxLH4oLkijjss8LQkYKOVJhuB9duzUl1PZ885EIZFM06t04EWSiqd7RiewjxeNefHYvHXLhO0bqFUNwjyKLUEWyquhr8AwA3SihoeyJqBcmNz/2MFbulkNTlXD0B36Awr9EX3zfR2BxSSb/s8Oyc/KIYOyfZgkNCQKc+SBBSZ+g7Q08MBx3DjbUUXnsPtFehuwJVCme2vHHLeOYLtJu2J1ZJF3b2YTz1sXgbK/47MBmBm/q8PZdCx0HmauOWzD9FyFJIW769qvQkLTF48xcHxdRv56z/LM6XdHAVdBP/1KkSryCun/LksCwhLXy71oEtfchntw3VGuyN4GyO7QiD7eusTxKyKvHOpdY/DbOmwiVZncy7FOoZD/AXcW0aPDmMCLbJ6WvQ4DnDsaTv2rVrC+Qtzh1TcgRz9UWJhBpoxGUYhsMhg8EguFoOBgN6vR5JkrC9vc19990XwhqVxEwmk4UQSzUoWVlZ4cyZM+R5zubmJisrK+zu7vL444/zsY99jM9//vPcfffd9Pt9tra2gnKn+yuhy7KMtbW1BSKoIaAxOYrLOMTkabkMgJrPKAFT4qbtqOJVFEUgtZo7p6Y4ly5dYmdnh729Pay1nD59mhMnToRjaVimflbDF1UBNe8ySZIFB9O1tbVAWOPw3Nj1U0lsr9cLSqiaviipHw6HDIdDHnvsMcbjcSDqsblNt9vliSeeAAjKbNyGlnXI85yHHnoo3Duf//znQ1+SJGF1dZUTJ04E8mutZX9/P4y1EkglerEqGN+reo30utxpSPJ6smwWJ9oYVdMIIZtq4hLm6hFRM5UjnTpaBxXtm1PSUQYuI19LDjtDWqDy7WtR8qSeuZiqJnzDgvTmAGY59mAARUEKrG+sUzxwF/laRr6eBLdGU/jwv9agIh0UyP6An3vkPezbKfvWkTvD2KVz4lYzjSIibznz9xUmkLT5smjcFqTLxXaSWhpJ8IXI43YTLOeSGWeSLpw5SXFqZTFnLlJYFSF0E0JIZiBpNqqhp9cr5oO6vZEFN1W/L0fWwQvGO0esW3YIDQW4awdSPbYFzExIgapMPFEsZaEen01V4VxU5+J7TM1jnOCVmDpvzK+sHx7ELNn5MWlwCywp7Eeuo76e9aJlmhI/O7BY/wUWQ1oa2hjaQBv4wnpC+eBZePndcHkfvvEl8NIHYFhCtgfTIdzchrvuhekYBrswvORjrm3dyWv7c7Y/vO7r7LW7ddH1xOfsZR2g8qSsBFpdaPf8/nv7XuHLK3AZ9Cx08ce4PPRfbBGYAMXIq4Zr65Ct+D7NCjg99opi1oL2ClR1e5MKtk7hfzBTeOMq+w9dpHVzxnrZRZyQVDNazBhkBU4SqBKkWnSuEv3fF3HrNuTvyfGkrqkNGjR4VnEs6btx4wZAMEhR1UYJUOxEqYqLhgzqJFwJlhKVTqcTDEEGgwGdTicYccQOk2pCAiyoiRpqqTXmdCKvuWyXLl1iOp1y5cqV4EAZtwssKEdaXkJDH2NCGxu7LKt/Smrj89VlShI1py42otH8QM2n0+3VIOfq1auhHIaOgxrqrK6uhpxEJX0aoqp17jSXUvupZisa1qr5f6qgaZ80tFNVRDWIUeIa19nTsNXJZBIMVOKyHEr0VD3V+0VVWN1vOp1ycHAQ+j8YDELoqV7ntbW1YPaihDAOhdV7Sx9MxNcL5rUhY6fVOw2mAMR58mTmDo0QE4slZYb5xFzDEqXO6UsnFclwhhSWVtsgVRKUrKDmmWhyEs8eo2NiHVReraMosHkBzuK2d8lESNdWMHevU3USXArJ1GEKSzKtSEYz3HTK0M4YO0/4ZnW+3lEOnUrsVNEzEQOJ1b9b1eCLi5KjSmB9LoVLAtnsmIITpsV33P91yMs6VL10gRSLc4skZhnLauwRE5hDu0cqntPPqpAeo9qGIvGRqnZLw5QjlEp9WGAKCWTRlCC2DgY08+1isrpc6iMombGKd8zETZw7pEI2eOpYeKBDNOQRo0iczEV7oyzfmyN1yEipqBLHzV5CdfeG3/HiTfjjfxA2V+DgAPYOICshTeHEJlzbg9HQl0yY7HglUFKf34f19fVGpQ/HzJ0vsm4MrPZ87t9oBm7iCWG/40s8DMTn7+UzuHHTk7ykD7tD32cj3uxlWnqCmbbg3N3QFf+AYVT4khHJCtgDr+ylLeivwmjgFcI0h2rFq4TjVXhiG156jvL6Hgef3aaXd0lch6RK6ZsRM2MpRXC2DvVkrrYjLJQUeIrcfKH2aUP8bo0mp69Bg+cWx5K+T3/60yFXb3V1daEwuZIuzataLsQO8/pzcejg6dOnuXDhQigonqZpcG00xrC1tYWIMBz6PwpalkDNXTQ8T8tDqFJ38uRJ1tfXOX36NJPJhKtXrzKdTnniiSc4c+YMvV4vKEp5noccvvX19VAOQYmEhoFqqCGwUF8uLmS+TKRUxVJCqKGe2p6Gsq6vr2OtZWdnhxs3bpDnOY899hi/93u/R1VVQeVSh8s45FXLF8xmM3Z2doJhDsxLY6gpy+rqarhOVVUxGo0WwkyV3KlDqOYdTqdT9vb2QiitkkT9pySvLEtms1nI21TFMiZdGuqp4aSj0Sgofdvb2+GaxuSz1+uxvr7O2tpaUBg1/1AVSiWLqkoWRREMhVRVVMKnBLbX693WL8/twNpjBTYRyr6hagmzNcG2hCoyTpEqMnFx9dNSb4Tp8/nqvKx0YkkHObLryXR3VtA+t0W+5kM9gylHNIF3UtfZSwXjnDcmMd763U1nuPGYt33utxi6eY3DjIS2pHzb/W9A0hSzuYHrtuucGYeMp/zsJ3+VzxTCyLV9Dh2+ZICWW8ikDEQsj1U6sUHbM2IDYVPCVyFUzoRi5JUz2EgNNFgKl4ZXPU5HCjKpuDst+JOv/07kJRtMzvcp+glVFimtEM28qUMaZZGIS/2mJokxtOadYtngZDmvMuTmqdrmVLVzCwqkqoix7BOX4ghqXUwQXU34xHnznrgkhZn3ZfmhgI6BRH2KSeUhtxwdq/j8Xoi4FQt4qk1EbSw3E8jHccd2IMg81LO+1uJ8gGfiDCkGR0WRCpNe5l022xl0enDhvFf2BgeeNPU63mjFZLBzw5O+2cgTNsm802Ym4EpvgjLLoV/fRJY6z8548jbNQUpf4LFlYTT1zp+F8+3tj6G03qRlOvX7ZInPAXRVPRgO0h60DXQLH9aZ9fy/fOzLO7RSv0+R1g+mgH7p+5mlsD+FUyXWOGb7A7JtSAtDUmV0qgxrSpyxVEniw0ij/D4lfk8F8U9E/PmpYoE0Ps19n88wEalu0KDBlx7Hkj51oNQJuio4SvRid0QlJRrW2G636Xa7wRBFFRat01ZVFR/60IcCWRoMBqG+X1zPTskLENwxtR9aimBtbS2UFOh2uyHsU90cYZ6bpwRxa2uLdrsdjF5UwdKwSDUmUVKnuXGqasWF5FVZ0v2VXGq/VY2cTqeBvGh+2dWrV/nc5z7HlStX2Nvb48SJE5w+fZpXvvKVrK2tBRKrxi7W2uAkqnX0NNxU1Twlpt1ul9OnTwdFs91uMxgMAsHWmoKar9dqtRiPx6Fm3pUrVwC46667WF9fJ8syDg4OmEwmgaDr+alSeXBwEFQ9vT6qhur10LBTVSCVQGvYqbqx9vt9Op1OUC+V8MV5nnqt4jIYqriqkhrnAN6JSl/309dwrQy70adYbeEko1iBqi3eqbFWawIhUNOQUuP9agWnApNbJC9xkwmussh0yurlNfZbGUVdd03nViYqKq7ujLZmClI6zGhGdf0GP/X4b3O5KpnWjKgjjspVWHL+xWd+lY44TpoWf+KBb0BaLdx0xs9dfB+fzC171ud4zkM50wWCV9XsQUmbwVI5QyZVyMlrSRUI355rk7sEi8HUT+mV/GV18GdB4l09xbcHsGEmnEtL1k2LP/bmv8T4dWtUbaHsCjYVbDJX1YLzZvTI3iWREuZYIG6BhMVP++1SW0vqmBI6JXxOau8XN78Wmh+nbYklkLZQl1HXxwpg1I7eM6YQL2qIL9otZe0CGil7+jBAz+1QbuGhEFAXzk+cCw8knrYTxvMJy5PVZzDjdy4ifsdAjvrg9K3Pf3XRusQKbRJaJAgVgwSKVgJrXbi6C1/zInjzN8LOGCbXYboPZRu2NoAErg3h2jVfjL3KffmEVgodfP7edFYTMFfn7GV1QXb8cmd9nl2CJ103cygm0O/6k33smj9x52C0608ma4FL4WBY1x8Rbx7zOQObbbinDf0C2uLJ56jv8wMnDmYD0JiBVhs6hVcTqwI21/xxTqVwssvwA58luVnRGiRsVGsYM2SWzjhoWZiZurZNdGO7OTGJxfhbXeImXPGpQ5qcvgYNnlPckvQZY4KSMhgMsNayvr4eJtiaGxbnVsXhkLpcJ+oaLtjv90Penio44/GY0Wi0YCiiipGGFCr5jAuNq/tlXDcvNgNRkqHEbm1tLbhOKpGLa7ip2qVqo6pFei4axqj5dP1+n263G4qNq/Kp0NDI/f39YDijBjZf+MIXePjhh7lx4wZnzpzh9OnTgZBqIfbYgEbrIHY6HfI8DwXb9TopsVJCNBwOgyqrY6nQnEK9zmVZBlI3Go3CccfjcSDyMcFSAhbX7lMoYV/OCY1zE3VcNDxYzyOuXRiPffxecxvjNuN8S30gEYcfA3dkUVi3uw/1NUgFsklK1ZJ5yF2stizsOCeASgxN5ZDKgXW4PIeqorU7w1zIFibtqgKJJYQ2ujq0yZMEH871M4//Lju2ZGCT2mhFvOslUNRErXKWjJxf+PxvkUnC2OZcq3IO3EogeEnEFpTgecJnMFgyWcyvKerQzBYVHSkx4mv8ZVJ6902b+oLrtbVkIt7RU0meVwsta2ZKR0oupCV/7qXfijm1xfC16xRd8UY3GfP8yeMm4m7x/WG3TQk5bHHJDYVeowXVTtU/6mu3fMg6vDImWcFcRQndEimbH/CIRY553lR0TkeZ1ril4+r2y8c7qrbglx2+GJL7dOMAXfziD2ys51kp3lW1EMtktUd1bhVWMq/C/eE3w7nTsN6Gzz8K1OrZmRcBlc/n+8SnwXW9ElfVdfJabZAEBgOv4InxoaD5zHOkDB9qmc88SyIFk3rVjil01rz753jo8/RoQ2UgF983VyuAZV343VZgBtATKFuwb+DlJ3xtvpaDzipMch/SOZzBdFAriSnMhl6NTI0nib0UygxuZnDvOaruNrNLO+wdrNMu2mRW6LenTFJfvN4X9LP12HpH4uj5ybOO8LW88/483XY0OX0NGnzxEE9YfsA590+e7r7Hkr5Tp06FkMSiKLh27Rqj0YiXv/zlYVKtxEvNOpQ46fuDgwMGg0FQ5gD6/T4bGxtsbm7yxBNPcPHiRc6cOYOIsL6+HhSimPBojpySFy0HoOGLmgO3vr4OwPr6OrPZjIODA4bDYVAPW60Wm5ubQVVU4qlkIM4f1By62WzG448/zt7eHpcuXQqKk+YDnj9/nvPnz7OyssKJEyfo9XpsbGyEcVQF7NFHH+XRRx8NTqZaxqLVanHPPfdw9uxZNjY26PV6jEajUIIB5sYlGirb6XQWCrGvrHhf/qIo2NnZCYYysbGOFk9XZ8+YRI3HY4bDIY8++iiTySQURVcCqyGr6+vrTCYTrl27FsZeCZ3WzosfACj5W65RqARdVdN4vfY7LnGh56HmPvGxYpdRVQdVLYwVPm3vTsNPPvzL/PELX0Mym5Ee9Oi7U5iiS76WIkukLw4lVAMRsd6lMZtYsp0Jsr1HORzx84//DpVzfOfLErbsi7j+NWs+TJF5dGIcauPzvxxJ4TCzEpnmjF3BwBoGtsXU+QcvvnbenBVkUoEtGLsJBhi4lLHtMrUZBXUuXX2MKpohFy6lwtKJGEblDAUJ2+UKHVPQkYIXpbskOCzijwUUklI54/P46tlZUq9bNVO2zIRVU/H993wDyeoq9itexPAP98lXDcWKHCI6C7UPlybj4hxUcvTcfkkJC8OiuXLOzQmUrcM1DcH8ZpnULR875HfWYaC+Tebum5aF/DvH/LOqgQ7fL4najsl/XHNPx2CBDCrhi9VNd/h9CBv9cuSCT5fA8RS218lxRCzdwmpXh3r6HKlSvGpd9epi5GkbvuLuWs1zsL3tVblO25uw2AT2dmFnF/IhmE4tT6feiKWs/E1jTMR+nCd11nmFrJOBmshkqSeJWL9NWcKkgLE+0KlvpFR8nh9urv4ZjT0uvdpoK7BtmALtug5Nq/Je/9bW9f3qgZH6i6O1/0rnXUfz3BNX24Z2C7uako9yjBWwKVlpyI3z30dTh6vWCqp+FcM1OHxpvmjEzwueye3zfIWJ0mMaNGjwzOCccyLyehFJnHPVk+8xxy2VPg0TVPKlIZxqfKKkL1b+YvVsMpkwGAxCuJ0qRu12O4QuXr9+nf39/ZA3qHX/4lprGtqoqo2GVyqh0Pa1P5pnt6wSxbXgdDtVnlSxWltbW3CBFJEQmnnjxo3gIFkPfCgofuLEiRBaqjllqpAeHBxw5coVbty4EciLMYbNzc1ATtRIJQ5L1WPA3BBG+6munapeqntqrErOZjPyPA+GKRrKGruhaijrwcEB29vbgWzrWMb5ieGmqa9lfD1UiYxLbGjZhZjUxT/4sbOmHi9WA9UIRk1hlHDr8eP7Lg5jVWU4Lp2xnHN6p2DsCn7m0vv5E/d+HeIcZrhKOm5hyjQ4ax6t8hFUPi3TYPZH2MGQtz3+fsauxDrHL336PfyRV3wT6VeuUUQpjSJAPa9T8qjETxyQJmTi1bS4Jl7lEgrmBc6ruuRCUbOCqUuimnoOU9fVUyx/XsbUtrhWrpNJRc/kbCRj1pgxrXMBtU1k0bwlk5KOFJxKJjyQdvn2B99I8uBd2NUO4ws98lVD2cHXGYRF0nVIaqsXW3AhJvYwYlLnxIshh9qJZ3eqKEaurMtq0QJZe4oIxi1HnQuL7R0yBYpfY2IYLw/LnKcaIQ9KFp1Njzn+CxrPYP7q3NFhgxK/qVc6mOea6XVASKJFFY7KOP970TWwP4H1E/DVr4NyCDvbsH/gXS77a9CpnTcfuwbDba+klYCknuRNJyAzoIJez6+rnC/BINYXWzcFSNcvz4z/JynBZngy8qRvan3OoLGe8KWpN2hJxH92Dtot/+WZVpBPwVlf/H2MP045gU5d/qG09TFTr+xVGVRtn1M4KWFmfbmJqoLNLdgRoA3rfaqdfWazLtam9PKUrFPUYe3Gh7HGF3T5IcYz5Cnx1+GIn3F/jC8jDtTk9DVocNtwF/B7IvJR6r/UzrnvfrKdjiV9mhcXq0RZloUi3JrDlmVZcI7UibeIsLe3x9WrVzk4OAhGIbpORHjlK1/J6dOnec973sPHPvYxJpMJ58+fD/llMUEriiKEHKoKNJvNQpFxNfDQ9tVkREmLhpAqgVFFTxUkNUbR5Uo4lUyqU+jly5f57Gc/i3MumMh86lOf4tFHH2Vzc5N7772XM2fOhLzFsizZ399nb28vFCDPsoyzZ8/S7/c5c+YMQHClHA6HjMfjoF7t7e2FsNSVlZXQNyU6p06dCgqmuoKqAjmZTPjMZz7DaDTixo0b9Ho9VldX2djYWCg0f/PmTS5evBhUUfAhrkokgYXQS83Z0361Wq2FWnw6hmVZBqMZDb9VdVNJrY6RqsWxo6mSba3FGJfS0P7o2On9ooY9GoqqZRs0dDUm03cK9q1jLDk/fvE9fPeFN5CON0jGPUzRqmNh5ttqKGGcW5bk0Nqv6NyYUF26wo9ffA9PVJX3T3AJIzfkhz/2i/zZ17fY/ub7qNriJ4fiH8hL5aclpnS+xl5J/fTd0JGUjFldAqHEYJjajKnLSMSSUZEgjGjRqXNsQk29SBFM6lw9MCRShVw+3U5z+Pqm4HQyZCMZ8SsHr+Bnf+qNtPfA5I4TD09IP/wZfvaR3+BT+YRt26NwaW3QUnIuGXMmafEn3/inKM6foPy6hNlmikuEsi2UHXBpXbg8JZTEqDsIzMlvUK9SQpmMhdBO55XWxWuzGH4ZXmvVzSVSO7T6dSZSF+MafUHVjdU6Mw8Bi483b9u/JyKgmocY+lu/miIqIq/HjcN+6+0OhbJqaQbHYmjn8na6/5crngZJOKTwyNGc2R25XN13/MUzlYNZhb25A2/+JrhwAT6/DfZSHfa4Aqfu8rl3g7FX+La3vUFLb8MrgEXuFbKE+glGrYAVNdkqK19qIU187b2qgG7PG8UY49W9VgIrG7AzrMMmM9hLwM7A5cAQWj2f05dm/hyq0hO5Ue6TW10B45FX7JLM92VW1IRSfAH5TuLPZVaAtKFd+v499gRkfXAzeOyS/+J0ul753JpQ7sxwY6HLKtl0H5KSMq1dTCs7j6ZQ7neEyvp0sMTXv+whNDl9DRrcJvy5Z7LTsaSv2+2GsE1VT5RY6cRcCaEqaaq6WWsZDAZB5brrrrsC4el2u6ysrNButzl58iRlWfJrv/Zr7O/v88ADDyyEBSZJwv7+fiAuqhxpmF+/3w/unkr2gKDodLvdoBTF+YN5nodjaL9GoxFFUbC7u7tQLF77CXDPPfcwmUwCwdO6g9ruQw89xGg0CkQRYGtri16vx/nz57n77rtZWVkJYYidTofd3d1QqkANcZTk3Lx5E/B5b+pQevbs2UB64jILqqAqubp06VJwx1xfXw9keTqdhmuqte92dnY4ODgIdexUUet2u2xubgaCu7u7G/IjNUxTyVes+mpYcJzrN5vNwljpPyCEaeo9pYqk3k+x+qs1ITXnUUM6YyMfDRnVY8Fc8VvOPbwTMHIpmbMYKn7m0vv5k68/iykqTEEgZ8DCBD0Qg1rly4YlZn/MT1x8Lzu2Ys+mFDWjqawwwPKv3/9T/F9+/59m+JVnmWz5+nreBdT/AfbW/pBMvRkM1pKSkAl0pGRUh3cmS+Gd4ImehnKaOuYwk8q7dsYlF7A1WSS4eOr+Lak4kxScSNr80fu/msFbXkP3hAukYu/BLq1zr+IP/dVX0t4tSPemmO0DSBNct0250aPsp+Sv8zmRzhBew/ipoukOh87FNQwlWhebpNjaUGXh6X2t2BnLQoimEj6byJxg6uxP55UJ8/p6lV7fWkUT8dcnYTHPLiJrgWjW1y/k+9Xr9DxcRHA1PFSLrceho2F8bpFHGvpQ93Uh1+8FPrv9IkWfQ7hVOy4eU332EymEFofgyzRUAq7f9uUTTm/A5rpX0vZ2gQn0V6Df83mdk4l37tw/8O0nGSRtr97Ncp+jlyaeaLnEm7qUNSlzeNUuS7zS1ur4ouxG/L6VeHfOoiaLRnzuX5bACK/UifV5ejqaaQvKwhM4asUxMb4PRd0nk0AxA8l9u3kO6y1fiLCTwotOwnAf9gSu3YSR9UQyF0gK/8WtDGycgNkerpwxyy0ZGamDjJKi/mK7BStbjsxBe7rX/5bXeT4SXxZocvoaNLht2Af+GvBi4BHgf30qOx1L+oCFIutxHTSdjCu5ik1CNM9KFZ3YaEMn9LF9/tmzZzl79iy7u7t8/vOfD0Si3++HcEclZLEph07sl406YqMQVeqUEGpoYUwCtF9azkGJAnjymKYpa2trOOc4e/Ys+/v7nDx5kvPnz4f6eOoi2e/3GY1GoS9afqHf7weip8dUx0slrPH5achkXJ9Ow2r13GJnUu279jkuD6HGL3rucahlURSMx+NAqlTR1PIHqqIpgY1JZmy2Eof46npdHuftxbUd9VrF4Z6qFqp6qeOlJkKaz6k5fRriqaRPzy02gdFleh/caSjqvLTCWWauxFmLlNaXYqjD6ZaJHxAm6KZ0JNMSmcwonKVwvk1vvOJfCwzWVrz1vT/Jd/2hP0PR28Jm+FIF9fCb0hPIZGaRWQlFSVlrcjlmXizdmVBqwVLn+4gFl1KJpRX9QV+onQcLZRbibTQfryOGt5x/PcmZdX+6Ebe0GeR9wXQSiq5BzrZJZ6th1mTT2IlT5mQmOF76wYtNbHQc49cFta8OxYwivubbLitcEdELx4FA+BZUtzgkFBYJVt1GcPhcZhp6jCWFMBC+yH3T1a6icWmGOLxTFUEVUOJzOXS8ZcTbEPXnBUr8ZOn9saf4FBW+4zYN5E44rNTG24WLWkcmdjNfg++B+7xqV+YwGfinEZ0ETNuboIzGMBjCcATU5EpSH1aZF17Ry2rSh4GiwpuziCdfaVYTvhRW+t7R01pP0HKp8wHFK3/qNGPcvGi7AS134nMP05r01edj6y+MEd+fNPc75TOoJp44Tlsgq7CWQi+BrTXIrFf31jueKLrE/2jI1LMMMbC6AaMpLs8p8oKMFANkrqQ01tfuq3Mk77y/FC8MNDl9DRrcNvwQ8GPATwCvB34Y+LYn2+lY0pfn+QIJiVW8eIKvk3tVBlU1unr1KrPZLChbaiKi/06cOMHm5ib9vrd1v3jxIg8//DCPP/44u7u7gVTt7OwwGo3I8zzkpGk/tAC7Eos4VFCJmB4v7r9Cz0VVs7iouprGpGkaiNOrX/1qWq0Wp06d4u677w6qoRI2Ver+T/b+PNqWJb/rAz8RmbnnfcY7vFdvqFdVqhJVBQIJJDGomMy0UDdItiwZm0nYkhvjpmkDthta2F4Ls1jI7V4Ldy8vaLllYxkDwsumJYOQxCAL2QJhyS6p5lelqjfd4cxnzzszI/qPX/xiR+bZ5977qkrSfe+dqHXqnrN3ZmRkZO794pvf7+/7TevldEzdbpfFYsFiseDi4qIRNK/bag2lxj6cnZ1xfn4evyQXiwVHR0fABlznec54PGY0GlEURZS+7uzs0O/3I6BU59Fer0ee53Es0+k0ynJ7vV5k8gaDQTR5UeOaNkhT8Kf76rjUMCWVYGptpR6j0+lEJ9b0mijYG41G8VrqtVL2MX0vZRrVWVXHpcdO6wCftrb0OaX34iJHCasVVA5bemxlcJmPICY2Jw+wbeXpnFdkb5xQP3jIxDuWIQR9TRZAmgkRBjU1c/7LH/4v+cO/9Q9Q3dlhcbdL1ZWOi4Ujn9Z070/g+JR6OuONasWp6zBxvQbQ06y883qAwzKwKw6zKRY2x8U0ohgU8EkdILGPjgFrSsnkw/N3X/9pfs/X/x9jLmGswUPWcD4XsGo8lIMsyh+3BaIru+YyRVdsQFAK/JJVfBp2r03BjDebGrYr0QZtHVcC/nz7uGH7bK3j8A3QuQ0g6tjiuSkgpQVUw/u2DHPXYu/sOjleZhoMpIa5t+dRxxK3q68yfG83A5f2aX+5+722Jethw3WsiCFP33CIYcrCw4c/BEevSOYeFgb70O1BvYJXj2A9EeOW+QKyXSATlq+ciOSyvyexDfqwqQiSgCJk/XUIRioGDm7B7EgAZr8jQeua57d3C/wlrBYwn8hxsiwapuCUUs/C0wkXsva68vpkLtuunOT+9Yywk87B/h15fVKBr2B1JDeuKeD5W1A/hDqDyY6MZacD+304c3BrH19YyotTHCPBpWvIerKLNwg41Wv1i4hP2s9d3o7N2puavpt2075MbeS9/77w+6eMMd/xJDs9kulrs3tAIxZAF9JaH7dYLGL9mtajKWDShXdqwuGcY29vjxdffBFrbWT0Hjx4EI1iptMpy+UyAruUfUzHlspNlVFUmWJqNNJ2i9T3ut1uBLF6bP1JM/j29vY4PDyMGXIqM1Wmab1exwDytJ5N2b20jk2NVtIcQT1OGnmh4CYNgJ/P51xcXJBlGXfv3o2yW2XgUtOTNC9PjWCUadTrqOyYMSaa+KSul6mDqEpFgWj0oyArNVzRsaeZfAoqgVg7mQLJVLaqQDG9F3VOlC1Maw1TkJmCwdTs5WlrZWDI8ND1NX65wixXZGtP6Uwwp9s8e46L99KTrT3F5Rp3OeEHvvBP+fnKUHrbAF4aXr72MHcFJzj8Gw8oakfdO8SM5N4rpjXF5RpzOaOezvg7n/snvFxmUaIptXibz83SF0xcH+ctPVNGUFiGmIY2y6eB6goG2+87b5k5z8BI1EQEXgFs2do36+1StisFW2GVHnP3GlEJm21Stqtdrxe3aUnsMM16ysb2JNtCNE9R58tUXrk56RbATFZ9mwy89ph8lH5eqTEkOd82MHXBe6NVi9hoEfiGBw3btnscanmbLOgSQvUXrF3p21/9XW9tD9GB1eHIMXhjqXIDd3fg2bvwzF0xaJmvBcj5jqAZb8UVc2VFrlkGuWMnHGhdC+AzuRyo22MT3AgQgsyXdQho70K/C2cz2S4v5G+fSb1dJ4d5AYupyEL7uTBtlReJ6XyFIDoPZi1h7LYQ4xe7Jt7czkld30SD2p3IRS9OJNZhNIRiFwZluFgZDPdhdyrnP3FQDWG6hsUljPfFmMbX+L0Lyoknrw2GLnm9whtPbQFr8M6LmlWZd3MdAG9fzcd8AJ6ACX+bfIS2tpuavpv2dmvGmJeAnwI+Fl76l4HfCvwJYAH8Ie/9q8aYDwF/FdE/fJf3/keNMSOEoTsEftB7/xdDn/8J8PXAK8C3e++3uRC+boz5C+HYXwe89iTjvRb0pXVyvV6P8XjM5eUlX/jCF/joRz/K+973vlhb5ZzjwYMHHB0d8fGPfzwCvve///2MRqMYsaCB6ApgtGbwPe95DwcHB2RZxuXlJaenp7z66qsNB829vb24eFcmKJX0KQhI4wBUlqj1bgpOlM1TWaPGQSiA0bGqmUyn04mM3t7eHuPxOAbBe+8Zj8cNueHh4SGnp6csFosI8uq6jjJFZaLOz8+31quFix4BaK/XiwznbDaL9XeXl5cx4zDLMkajUSN+op1zmMZcnJycMJvNopRWA9RTZ08gsnEp6NIYjjS/T+Mt1OFVAafOeVq7p0BbYzHUzVQfEiizrDmL2peaxiizmspxlflLmWi9z1Ig+bS1me/E/8ovfcF//Zl/wO9/729heDDC5UPqrqHuAvjIDmUrT++kpHOxxnzy8/yNT/ww9+qaietQ+qxRS6dZdnqs2hn+v5/4+xxkXb75V/4uuof7YA3mcoa7nFBNJvwXr/wTvlAZ2R7TcPAEAaoZnrFdkBnPXjaPWXslWYhjkO0lbsGG/TaAUIGfw2KdwxnLhavITIl3geksPX5gxVYdg9U6m5SFa7F8xm/q6Nw2YjcBc215pIIq7TfWyYW3bSAk2mOQF7gixTReTGhcbq6AN+nDb0LSdUGvweyKONxm2w0ITQa1bREZ3o71gg5MMOrR7X0W1vS+OX8bxm8zDwq8dX7i/NnNXG7LKHyrt3RaG6d1zcL9igTzmve3bedb77WPnTq0yo/BG4PPrLBweztwax8ml2JoQia0ClaAV1mK/LL2cuGyIkgtA7jLCmKhp7XCvnkn918833BDOS/umPMZDGqp0csRx81OLuzgvN64bdog1wyRMfJ3Fv4uBPThYBFAp96YlRMWsQKWlTiTVkhdou1K7d+kG8IKrfSbd2HQh50S9h1UIyiX4DXCYQVZDsOceu4wNeTkWLfGhAgHjI0or3ENTXLxr1z/N/Hflmvw4dP3X6cvf2t5k920m/Z2aT/mvf8WAGNMAfw7wEcQ2eV3Ad8J/AXg24EHwA8BPwp8B/AD3vvvMcb8kDHm+4DbwDPe+48YY/4s8C3AX99yzP8L8JuBDwD/HPi/P8lArwV9utBWUHBwcBDdMz/2sY9xfHxMWZZx4f6zP/uz/PzP/zyf/exn2d3d5Su/8ivZ2dmJJiDap7JIeZ5H0AQCzD7wgQ9QliUPHz7k5OQkMlHdbpd+vx/dIhVA6M9qtQKIACANDFdDkdFoFNmq1WoVwVhaB9btdhmPxw12rF2PqDV6GmoObGWRFGhpjaHWH+oYp9Mpk8kkSiFHoxHj8Ti6oSq7qIBPmcjZbBbHsrOzw3K5jAyrAlA975QhVGOafr/PfD7n5OSEi4sLvPdR8qkALpVd6vkDEdQpwNI5zPM8gkaV1SpoVQfV1ABIga7+q86kem/oPmntpbpzqnNomjPYjqFQxlLnX695GjvxNLXSS+5caXK+UC35q5/9h/ybH7DslO9jdafP8jAXIAN0Zo7isqL3zz7D3/r4D/Nq5fhCZXG+YOkltBw2Iej6X9jMOHqmpGcqMmOovecH/vcf4Xc/9zX8/Tf+N6ZuydzXLL3nuC6Y+aJhtpICubGVe/Z2fsnQrINk0wTAmbMO/2bGsamQbbba2whQrZUYhzUrag84CZm3lbgSei+gIyUHNbw+lV5Gw4uUAYSmhNEjbEPoYwPQFLVsjqGgMbJ7dQqkQs2d5eoqRsEYCowCU+ubx9FsP5Wo6rFid27D4KWgyqtctfW+j4tzabYKwLLaHEfH1ADKCizrTX/xvRQUGpB0ON84x/S807rCd2x7QtJHN9kGDttgw1owTvIi81B1JjWrDs5LYfV6A7j3qrBZ3YEAK+eC+2YNcyc1dHkIbs+cgDeLmLnowwb9gDiCDDMAtsyC7cDaiUT0cgoHfWHyVrXIPY2D+TwEqa/k9doI6DNGxrS3I7V3467cjPOpMHpVJrJTNXWZr2UiHLLvrBKDlnIBz+wK6JzWEsLez2DQgVvPiHtoXoicdGw2cQzTEpYzYRzHPcoT+R7LKTC1wRqPMT7kY5qA7ZJ7Pb2ub+IaX/dg5p3Ybmr6btrbtP0GY8yPAz+OMHcfC+zcTwTWDuBZ7/1nAIwxJ8aYW8CvA/5UeP9HgF8L3AF+OLz2QwhQ3Ab6/lvv/W9/swN9JOhTsGOMYTQaMZvNODg4iLK/+/fvR2bn/v37HB8fN+quxuNxdP7URXgqxUwX5ior1Pw5BRcqP0wZrzSHre0iqSBH90tr9lRWqYBktVpdcaBUAKQB6Ap8lGFSQKMsogLMPM+jFFGBhtauKUuXZtulP3ouOr52rWFq/qIAUGWL6TUCIqs4n88BInOoIE2Ncc7Pz5nP55EtTWWdGoGg1ymVqaZmLArgzJYvcj331HBG5bXKdOo+Ctx0nlIArX+nEs1U0pnW6el56P563XRun8bIBm1lyLeb+Q49V/P3PveTfOOvu003M9T9QaxjK6Y1nbMl/9nP/l1+bt2nRkLLRc65MVupMQ2WbROfIHPucFTU/K3X/heOa8fce5besPQ5My+xDCrJLH2+CWfHkRkfcvEqCuPI8Kx9zposgL88snpP0lR+Ki6gRhgGNgBCogsUndGgRVL5JrAVjDTq7lqvqVwy7vuIFvPtAght9CUnIt0oGaBgM/StbJ26cTby8ki21QVm8l4MUjet7c0jQFYLWBoXmMTEuTPtO7KcJhl2ZEOTjL42Q2E253ElvuEd1Lbh4C9bxw5UX2iAda+L2+nCrT78sq+UGjqTSfB60ZEYhVEoVMPJe3sFLJfC+hkjgC581hqM3CrU6PVyAWwuMHHVUjLxVqXcTMNdogy004FpBZkXNvHyXI6Flby8fkf6risBYfMSztbwwgvgp+CXAkRnawGoXSQewgfQVgbA5xzQgeNLKBYSGdG/I3NU1wI28xxsH3Y87Hclv2+yljrHvC/xFfUcP15TmZr1oianIKPCU1OawPhhYgFajF+5ysE+2TV8E5u+nZs15qam76a9Vdvzxpi/FX7/fu/994ff7yEOmnPg/wP8XuAy2U8XQ+l/Hi6AA2Av2TZ97Y3Wa9vaZ4wx347IOx2A9/7jjzuJa0GfsjPKUN25cycafBwcHLBarTg6OuLhw4cxk28ymXDr1i2eeeYZ3vWud8VMOCAu2lOAkoK9lBna29uLIAuIUQKaHTccDiMgAiIYUvOVtH4uBUNa56dyzRTQqHxQMwWV+VJjktVqxb1792Ktno5BgahKGBVwpHVx2pearJRlGZk7HZMarfT7/WiKkjpOKuDUGsK6rhkOh1EeWdc10+m0UX+nrFcazL5cLjk6OuL+/fvUdc3du3cjkEyNUfTaKIuqMt4UBKc1e/ogIA1M11pPBeBAw5hF2cg0BkIBtIJ0BcKpmZAyjW2Ql7qFau3fcrmMURWpYczT0pZOHh7YsHKfOLn296op9ev3sGcXjC5uiWNeVePvPeS/+vgP8Q/nL2CNY2hXFOHJvMYm1FuWnrW3lOQs8UxcydpUQMVxXbAmx4VaQM3hU5YvM64BIgtbNQAfSB6g7JPFfSKLZ1zM7rMBQMp4AsMu2k2scZw7cfR1FxPyZU01zOQJPBsm7FrNnP7pvRAVLafWttNmZN08kblKJZjtcPK2YUsEQwnbFmWZOt6UTauDlDMQJ9pnZCqDiURq2hOllAlD1z4HbwxY35SEBsAZAXPYxWVQ981WSazss2Ejr8g0fTIenXcDLtYQhjdSQ5l3YGszdO25MNe/daWPxjYpe+wJbBQi2dw9kGXFaiaAJyvA5rJhnguwc15uPO+ESSt6xJq9HAGFxgo71isEvBHAzzqT/Z2HngWfy00z7AKV7DMayWurtcgvFysBfyaESK4D0DThxuxa+U6bO1h7Yd9WCzkPb4SdzNZInWEeJiOAVBNqDPMwMWUlIBcjLqSrSsZSdOUYKi2dlrDswaorQLZb4NeeelGJj6cz8t2R+eT7I7kKX+p9/YgnAu+Uj4wx3NT03bS3anttW/i5934FrACMMf8d8EeASbKJsg3pAnQPOAXOgB3gPLz2eeRbcKe13bbWB35j+AH5GvkjjzuJRzJ9+q8CLZV3WmujoUtZltFxcTwes7OzE90klQ1LWRldpCsYSN/Tmi0NI09rtpR9S50k075TYJCOPQ0C19cUuCg4UOapqqrYt/arskU1T1HXS5UmKoOlTp7K0CnoS5lIlUHqeQ4GgwjkFFynTJcevy1VBGIf2hQw6fVSFrPNiOk4FVDqXChwVvCWZtylhinpnKfAT6WVKcAriiI6jLaz+FIzntSc5XFOm2kfqclLen/pfKdsst5vT1trA7TaSwD6xBv+zhf+F37P818XHxP59Zr/+jP/gJfLHmufiZGet2RhJS7gTFrWon+E8ZN3116e3K+9bcg41xG8bZg6HZ9TFJH26U2UdcZQdjyYirWR11yCVNRNdDMeGfMaOZe1z5j5Dr5cQ+03piMJ46dtE73gG2BL3uTqAqv1Wloj50mZPzbAryH/DNu2DFniGBPWrMGkpfun/V7XIkLbsIHbAJj0mZ5A+72rY491jo+SphnYZlivwHh7UPg15/A2bo3apMd8rbTJ2SfZ7rpjGpAsOeuFnZs66IygvBCzkmIk2XdZJsxYJwCkshaWTNkyq0WdVpYYWTBbsTnsjGE9h2oFrpSQ9ToToNQpBIhZRE5ZIjWFOyNh2VYrOJ8K+CMTgFZVMF3KB6M7kDrAYR/6Q3hds/eMyDyLHmBh4cTYJTebMPhSgajZ9G2sAEoXxr9ayLGzTMbkvMxBz8KkB4s+LOZQ5SGQ3uEuFkAvykBNFh562A3L6ZHvi2ZNWgoOHwEM2+TgI95+uzdrNs+HbtpNezs0Y8zYe68g7zcCPwj8UWNMB6np+2h4774x5v1ITd+B9/7YGPOTwO8Avgf4bcC/jsg7/yTw14DfCfzElmMa4MR7/6fa7z2uXQv6UodFlfwp8Ov1epRlyWAwiHV9dV2zs7PDwcEBh4eH0exEc9tSo5XUPVNBjrJcKk1UIKe1XPo+EOMElP2y1sbAdgWFqYOnOlUqMFAwkNYapsyQxkEom6TOpOv1msvLy1jHqKzScrls5PtphIGycFonpwBPWa3BYBDnRPPyUmMSjarQPlOwrPMJRNav2+1GIKlMnrKJGsb+yiuvcHJyEh1KV6sVFxcX9Hq9yMzqOSkAV7CnIC+dGz2+GuEo66eyUpWiKihUOXA6X3p/KeOYykv1mqUuqmmUQ1vaqvdNykgquFfG+GlqS1fQsxv3SxBAdO46ZNWK/+ILP07HGNbeM/eGn1uPuV/tUvqcjqmjNFL3iy0QXlmQXwo7V0poOhbnDTOfx1o9NVWJkQxIzR0eemZzrcqwDxBlpUtfsE5AokZELIPkFEReKn1u6g4BOsEAZu0zatdn6Qv+i1f+Cf/G7/0w2ajA1oE8yhKJVaij2xgMKiAxTcZLm7JpZsPMxZBy3TRh7hq1aQnAijV8MtmbqVa2y2x+rtQfqjFM0tK6QwLpQjBQ0fiEeM7ZhhlUsNeIc6h8oy9viSYxCkLrwmyOlTKZCdvYBr5xm+Q8G2tYZUtTp9Cb9iW39JmBXqIMyIzF0qM0Ff7WEN79Pph5yd1bXcDtsUgpux3ZqcjBrQXoTULeXh5YwKIQMJVZ2cfXIv185RRu70K/B/MH0DfQ74MZiSvmMBOm7vQcDg/E7fPeJaymAvAw0M1gaYXFm10KgLQ5+A6YAnwJ1QX0DmCeiaPo3El0hMkE/JlCPmfL4M7pELDaKQKD6ERCWvRg5SH3wEoAaVXD/WMY7UrcQ6cG1rBbQL4n53pxgeeU+vIcvwizHh+0BJbTmk1N4DsKnv3CNGNu3Dtv2tuufYMx5s8j8s6fR4xbVsCPAUvgD4bt/gzwvchX+Z8Lr30P8H1BpvkD3vvXEVfO+6FG8BXgu9sH9N57Y8wtY8yO9/6y/f6j2rWgTyV3umAHWWCPx+MGKNCF+e7uLvP5nPF4zGAwoNfrNYBVKjdMmSvYyDNTB8w0m06BUpsR9N4znU4jMFQAUNd1w5hEAcJ6vY61gWmgtwLDqqqYTqfRQVIlmWo28swzz3B0dMS9e/ci0FosFszn8wjo0ho92EQa6HmqMU0aZ1HXdQRMQKzH06gDBYNZljXq4Xq9HoPBgN3d3UaNXFEU3L59O8ZXeO85Pj7m+PiYs7OzKDHVa6Hzqy2tJUwZwpSRU2Cl8lGthdSxprWPGhiv0tjkxo3MoDqX6nhXq1UcvwLHVO6aSkzT+kJ9CABEoxkFvk+jvDMzntLnOBzWOHpGoFfpM86D9FMB4aXvcr/a5bwWGWQKntQNcyPHlPdKD4WpsDhqY7ABkSiMk/dk37XGM+ApgZkT4G6to2fLYARTRkZwW9vIN23YrkOW5PTVAXBqq7ERrJTOsqTg4+t9zMuv0PMvsNrbgY4RK/XI7hHXX6kjJXiRLmbCVsXIh3CsK7EMpgnymrV2vgmOIOjqAhugtW0BLDXYuBRkBtAlYFQ6a7tlpnV5KQCN22o/tJi/1rgjOvABMIbzdwXRUMaWZisobcxL2hTwGbOpf0zXwAqQ/ebvtyPTdy1Zc8369TqY4FvvX9fCXSYPNBD8kSPE0io3uN19uHULnj2ULI7cAgPoj6HbDU6aHZFyOieOmp1M2EFfETXGeAFjlYdyLSHoB3vCDFZOagRNN2T49WAZ9vcVDHowW0pMxNqBWwYZqAXWYsxSIZl6tQvOQgvo5hLRkHcEMK6NgKudHVgG5o9881THIpLNTiGsH+H1zMrfw478vVyLOU1VIlLQGi4MTI28viqhLqXf/bGYxhQFDArqpXyQLBm1r9sX4urFad8QbRel7Rd0+9/vIAwkTPUv9Shu2k378jXv/d8D/l7r5b8RftLtPg58Q+u1CVID2O7zSRi8rwNeMca8TPgy995/3eN2emRNX1ozlbJkCi6m0ylABAepKYgyRvp+KsdsnVyUCCpoU1YHiCAhzQps1wmmfaXjVoYINhJClVemjF+n04kgVuWemp+Xyht3d3ejY+Z0OmV/fz/2q8eADbjUcadMVRpX0Z4jBY+pu6iCZQVp+m8KZNLaO61NHI/H8XzOzs6Yz+dMJpNGcHxZlnFMsKmd1FpFPXc1gWlLPfVH2bdU/qlzq6ye7qfSzVSamdZC6lym91C6nzYFnKl8Nb3+qZlOO8fvaWu1NyJN8hu5ZuqcqU3r7WqsMGcheB1zNRcv9o0hUyklNizKQ11lsk9mHL0ABSWSwTVC1jN8BG4xliHZvzBVBJx6XJWJurDCEdbR4dhcx+ZYbez/733qx/ndv/GbMfU4gDBzZW3UADoQAaEDqXOzunR+dFNwFOMNUvOG5KAGNiuWBHClG6i8U18Xh9HEAKU13q3tWoTBhhFMxmfS8dDcd2twve7bPmaKSK4DMy1Dlyus4NsQ8LWb3wZsr5mz9FKb1mvtbbb/LTfmhhQ2lB0DwxGMd0WKOZuHyIIBdHrBkMUKUCudgKMik//a+2xT34eTTjWT0RsBgHtDuFgIECwGUhvXLYQNzIxIOMvArF0uJU5hHZg0k4vDZ+bkO81a+deEpzauErYu7wqgzLsCAle55O6ZTMZch/HFOr7AUOZBtqpzYzM597oWdq+DgDtXyfdclIgH8GhCYW0v1D5mOXRyalNjPRgycPVW64X04VG81I+7mO03UsST3hDvECB04955027al6d57z/0xex3LehLTU5U5qeL9aqqGAwGrFarWIumrS3PSxfx21wVFaykoegKIpVtVImoZrbptqm8UyWkaQB8ygqmLp1q+qKsYL/fj6YjKrOczWYRPOr2zz77LK+99hoPHz7kjTfeiJLG1KlTj5ECXWst/X4/1rZpPl9d14xGo8hwnZycMJlMuLi4YL1ex+w+vRapuY33nsvLS2azGb1eL5q51HUdox+UeVssFkyn0ziH6uDpnGM0GkWwpyBwMBjEfRUczmazhuQ3DZlP3TQ1ikPHqNspU6t9pv/Cpk4wNb9Rwxtl+9TkJo2MSN1G9XW9N9IHDHo+T1urvYAqWdiIVHLpC2prwHfI8FjhAZm5LpO6z9x12M2EDa4T2ib9XVvPlBGsLV3IazQ11jg61BSmJvPi8pmZOsgyC2auK9l7PmedsHcYmNT9IOnMcV7cQ7PAUIKwl7U3LH3ByhXRwGVgVyHzb8P2RSAK8bWZ6/JKdQwPTyimtwDbcMtsxBuYze9x4eTD2rbwjf3SMPRUvqnh5UDMzksdKbXPuDgzzWMp8IrMngciuyfv22pzLA9xwXfFWTQ5Rtu4JZ5JHOvm7QZTSHP8jRzCNttorvax7b20PlDhd8q4PjGF9XZrbYb3EevZR5Gg7W4IMM/g5VFILjjGGwOjXFgy2xWXyvooALu+XJTpXOrafA71GnBB5rkI0Qu51OLZ8LApN8K8He7DeBAknF35TqqB0Y4cfDkV8FWEur6T+wFgOem39sIwjvZF1ln0BFjNuwIUPXLs0Qh2RwIul8DBGAYWjibCMFovxadrxDHUVbC/K2yhS266GmEey0uZIGOh4wW0VaVEOoy6wkIuahiMoPBgKgmrNwF8GkvJioKMnAyrOHGbVrl9Eb0C6EcBmXfah+L6Zi037p037aZ9GZox5l3A/w0xffkjwB/03n/v4/Z7pLwzBXNtkwwgOj7qgr3T6cQFeLo9NFk4BTwKZMIJxOOkZiDK9Clwa0cdpGYdKSOZAr80ZiAdS1obpjVoeu6ah6f9pGyRRkrM5/OG9FCBZJsRTcFfykDpNlon2e/3I9ABItBW05i0jhCI9YcKBjXMXFm6VGoJm5B5PUeVdaYsnY5X4y709VRamW6rIC1lLvU1PW+VbyqL2o5lSO+DNB8yZUTTqIv0GivQb5vE6Dy13T2fttaow2PDvpU+FxBlHGBD9p1EIljjsMaTGX/FYEVdNh91rDr8Vpha3D6Nj3V8AHhh5azxEcjF/hvS0Sxur78XpsYhjJ8yhbXPcPhGjIP2UXsiE5jOwdIbfF0n4GjDLHnHlTo6BVJRarjFBMV436iVa4AWfX8bA8YGODXq3mgt9ZIFoQK+CDIfkxayzVRmq4FLe32ZsG0NEJz27Zr7p/EPjUB5v3ldj9MA1a05Sf9t5AS+A9a416rzTPPXbbeTpTmFag7i/WbqDYJphB8zdDJPXufUzgpg6fdhMIZiCNMz6OxBryfOmUWQcZ7PBOjp0TodqFfClHkj5idkG9OTTkder4ywhDkC5gyhrq0jAJFgCJMXQC5grK6FPesNZBzDw2AWg7B+47GcqAXG+8LuOS/Hr0wwhLECTutKQFvlwvi6Ij2tw83V6cHhjrCPRQbrNVRdKDNYX8L5cYil6Ii5jAsTu1qKKU1uRap6chmkr32wCyEWsVi3IQivo7+NSUi7bRfZJBtuvVHeme2mpu+m3bQvW/teBPT9P7z3tTHm94fXHtmuBX1pbVQqsVOGJq3Hm8/nESgMh8PIXgHR6VP70Ro9rVfTRX0q51SjmJQJSvvTn8FgEMGmcy4CJzUtUbOUbrfL5eVldMmczWaREUplo8aYGM9QVRWTyYRerxejIkCA1sXFBRcXFywWCw4PD2M9oYIPBUspU5rGEYzH48i6rVarWDeoLNZqtYrMnMojFfSMx+MG+NZtFPhpPWOv1+Phw4ecnZ3FzEMgSld1LnVsegwg1hmqEU3qGJrmIOr8tw1y0mxAdTHVa65gOj02ENm8tDZQ5077TaWo2mfK5qYSz/RhQvrA4WlrmnsX/04AnAu5e8sAnmauS+kllqFn1tEcpb2YyALCEMBVkHlPncgvLRLUPrYhEsVnlF6Q0jaTlYzmmKKsFEK23sa90wbjGGscS59TOvmKKYG56zCwa6xx0QRGxuvCvj6M2zJ3OX612rByWqNGWE8Zqd9zxuCskBopq2acPNDPnG+GrLOF5Utb+FvZvg2IazJrKpv01myYtLC9SkR9lM0lfSmDp8euaVy/NNz8cbfrxkGUTai67pQyhSmg02lKzo1kPuL8Jmzq5oBNINpwMk2uzzsB9GmL05POpf655cGA5/Frf7mVmiCjMCFRILPimrkzFODngxxR5cPOCYiKLp1VQI5WWDZfEi1YrRU5Z96F4WBjrZgXAqg6GXSMAC5vArtowK+k7zwTSaWeVL8vuXnDnhzPBqvYDmCC7DR3UnfYscK6eSc1iHUpoe5FIZ+JMnwAbSZjLUsZf5HDIDnOoAOLCSw7sLKwQMBi7QUQujVRj2s9UWrqVDKaiYtnL8cvDb7yGCzGOVHcX7Xs3LR4nydPPBpofts+6VOWd14zJGD5pt20m/altNx7/9PGxP9KP9F/eR8p71QWD4hsTirRm8/nXF5ecnFxEcHaaDSi3+9H+WfKQqlMNF2QK0DQOjuNa0iZK+99BGO672AwoNPpxNdWq1UcIxBlm/q7BoWr/DANOk+z4AaDAf1+H2MMg8EgAlhlqMqy5I033mA4HLK/v8/du3djHEGbnYKNbFFjELTeTnP1FHCdnZ1FEKVxF1VVcXJy0pDB6nmn9XQKivr9fmQLHzx4wNHREcvlMs6t1gsCEUCpLFTNW1KZpF5/HaNeL53LFEQpkEyluW05pd4z5+fnLJdLFosFAMPhMI5HIyUUoOvY1JFVr6de9xQI6jlq7aNKYfWeexrlnVIv16SBUkln3C64ZEJg6IynDgBM6mWkrg6a9XTrUJ83tKsowxzaFWO7psBTBmbQGkcdmDh147Q4OsEEBkKtn3F0wvFxUn8nOVpuE9FgNiPPjIuy0pQNxG4MaqzxZLg4foDX612+53P/mH/tj31NAzAJQAnfH7X8WAt1d+Os2WDIHDHnT9e68feEEYyOni0A1lyxe6ktNOCUAWmBItlO99+86YqEDdP9hMTdLJp9cNxMjh+D01vSyytjC8eNAevWbIxp6s15RSAYQGGb7YzHdc1jpLmDDbDXni+Ap+9j9sTtWgbvcS0F/Y8CCH77LWNMwBZOX92YuBg8XSSld1nk8MwteOZQpJhlHcBcMDDpdsX0pAp01SgANQuYArIKkXvWQRLagd5YmDi/hnIl7OFgKA6gXQvT++EhQIhocGvpv7AwW8nxnBf55O6OANL5ROSdeSEyVN+T43UcFHswKCSA/d4ZdMfi9nl+JsYuZMIcriHeTMsZHAyk7m80lu36Q9gbQ15uDF7WbgNws/BBr8OkdzMBrhgxcekUAaAOYK+HOy8xU4clx7gSg8dnfqNJfFNLq+S+uGmxWWPwN5Ny027al6N9zBjzHwK3jDF/Fvjfn2Sna0GfLrxhIwVsOyemNWLKyukiXIGIgrBUgqcAKZV6prJHXbynZh6pXBA2pil6jOVyGfPy9L20z26328hqS8GsskXKRNZ1Ta/Xi2ApzaFTYKf5favVitFoFPtLZYip+UlbZprWoqXASpv2MRqN4vVI4wcUeAMxzF3nVd05tT4xNUTR2sKUSdVrlV6j9DrrAwAdu4I+3VfHkhrspOdbFEUEkanstS3JbM+P9pfee3rP6LVMzXDa90d6jZ/mVnvTYPsgsF/hJWXflBnbFr6ethTwlT6LixSVh6Zsnjp16u8iKQuyqvB7J4BFbYWpoxtoHF8iM1Uwl+FxbOSb7exAG4xdFPClczBzXR7UHZFTXnP9BLQJEGMdnDvt1fDxyHTp79f1le7SXrzH2p0t+7pN7WDqttmWaKaB7ts7ao073TZl4badxyPWUVulmWGMnhaIIwF8j+gv5iReAcbX7/dOadeRPDReV/TvrgGa4iSpGXGVAzfqwmAArgcvPCfU3xdelbDxXniqYAODhRHgVJbIZ9mEzIcs1PTVUrfX7YppynkJPaDfhReeF6BUOphUwZhlJS6hzoppy3QFF0di4oIVALYG5ms5Vu4D0PSyzd5I5J/dJQz3BEj5Gg4MHD+Um+7ZO1AVEuw+OYc7XYmAWK9hMhaQWGRy3nfvitR0uQI7hL4VGekzO3Dupaax9FLL5xHGcB2etGRWJKiukmMdz8Fm+KykphKbLG9wLjmHSI/7R1xgfRD0xaDDd0azxtzU9N20m/ZlaN77P26M+UZgBnzSe/8DT7LftaCvbYShC3EFCqvVisViweXlJWdnZ1GaqOAsBSm6v7Z0Yb9YLGJtYAo2FBRp7lxZllxeXgJEkKcASLP9lKkCogGI1qspGFRGSvdLx5ZlGcPhMLJIykyp6Ym1lsPDQ9773vfGsPbT01OstfF4Gh+hc6igriiKyBYqCNLau/V6HVm/lGHL85ydnZ0GU6c5d9q/Rkeo2cxyuWQymXB2dtbI3FMmcG9vL0ZTpFJPnXcFuCnwSsGdzlO322W1WnF5eRkNYDTTL83UUyZyNptdkbkqSNMcQb3eKs/V+dJ7KjWGSceU5vK1az31OHpPPo1NwY4AIMnUE7fMDe0jYC2Ap1Afp2ygrE0cGTbm60lNnUlAVU1hKjqmZmjWAewZSm8pW+YvLuTpKbNXmHVi0uKAHILLaGbKJuPHBsxZ47BspKCFaTOaEgWhEleVkVrjOMymHNg1xWUNO+ADmxkdNhvrKo9N/nYyPMmlC8qsrTV1vvUvCdhLWDxhCJPlemDU1MUyBXyREUvGE0sfFTS2WUW7iQLT+LFU0hqdP+P5bmlt1pEWU2c22ykwjSA0zE2atXells8Ta858nAM2LGWcp6vjeKu2Rwnx3vQpbrs+sWCVZo0YyL2FvG5tUHDiROc53henznoOZgoU8tlzNkQeaL/hImu/mT6cyYQxc14MVpwN2xbCzE3noU4PqYG7mMJ6qRptAVpOb5I6sIyVGMD4hdQV9jPoOqkT7OVBdhpcMTuFnOxKvjvEHCaH7q6EwA8GMOoLc3hxARfn0BlCeR5C4UfCWroKXCkAbumClJPA8tUSP5FeyTyXbfNMAGFWy+/dHlxM8KU4e1qKICHXa9ZEeVuvfbxZ2k9AlB7XzvzVfR7Vttw3b+VmDTc1fTftpn2Zmvf+fwT+xzezzyMjG3Th3LbjV2AwnU4jeEjr2rQ+TxfrKQOUAsnU9EPrxVK2R6V9aZ9AlBpWVcVsNmO1WjGfz+n1ehFApi6XqWmKjkFBgDJzKQPZHkPK8vX7fQ4PD3n48CHe++iEqU6cyr6lsssU/KT1b7qdnl8KtFOjmTSrThlGvQ4gAHZnZ4eiKFgsFjEQXud0tVpF8NftdiPwVIZRnTUVcGq/On+pgUu7Pk6lsiq/VKCVnrOC7VTyqdddz1XnRcGrmtak7+s103lSFjE1d0nHnP5onebT2hTwKDCzjWw70/gPv9b9NWIdDNT4WG+nbF5hasnYs+sYzl4Yhw3blt6yZmO64rxk9tXexHrBGoN++lJmUMfdsyW1sxRUMU5Ct3X4K2AvbTpWhxMTGuPpmZKhWbNnLXZdY0uLzbVGrrlgiOxUyrQpMHQyLw2vnGue0l9h9rQvrnk99NPIrYO4SEsBg0Y2mDez2PEbcNl+fetiMd2shVYaIM80X2u31BSmeRLJMR63WH0LExzt0/5ytLhuTxbwjYcIyS/NYxusNUKKVQC1sFt7+8KI1Q7yOTBEH8QwXwq4M5aQ7LfpOMsFPRqkLu7iUli7GgGKFqnjOzkPo7Dge3B8IXV9NodpMFSpEfYsQwBdXQt7uC6hXApo61XQrwSkVhUxNiHLoKwCUAyB8Z2OgLlaJyIXh897FtwKugMBn9QSV6GRDtRiylKVMkm1D09OrIBCRdTWCtjs96RucF5DVgrYHA3gqJZzkFmXfVJaXT8vbWZ7272+9f6/ATqAfA/dTMVNu2m/ZO1a0Je6aaYOnVoXNp1OOTk54eTkhPl8zu7uboOh0m21DzV6SSV+ChA09kHr6zTnrs38gQCcyWQCSI3YgwcPmEwmMYfu1q1b0cik2+1GIxE1BVFpprKDWqemx1dwqXVjKoPUMe3u7nL37l2M2QTSKwun8Q29Xi+G06emNAq2FBDpeWuQvAI9rUfTQHaNUdB+qqqKTKMCnsFgQFEU9Pv9WBOZgqA0BzH90RpBBaFad5jmH+r9oP1475nP5zFXUEH7cDhsPCAwxjCfz6PbqRrKpCA7lXqm103Z0LQvZSb1J3VN1WukwFLHr1LQ+XwecyWfplZ7K3LJYIDSMXUAZ1WQW5qQmydOnW0AVfpMDFCS/5Aq2wcwMKtQw7eUzLzAAJbGBgOXrCH3rDH0rDq82ujC6QLTZpGx1b4TxygsY4EjS9g6eb0AxtmSGsPKFSxdQWmEiSxtRs9UAmK9ANQOUkN4O1uwn43IlhWum1F3bDRV8MFTvVHnpveRC4I5A7Zuri4UoDUdPZv/xv4I6zwX+o5UjIksnHceV5gGUmjLMS0i0EvNXho5zikYU8CV1Bmm7UofjTfl/CSEPTkXm7CPKaK4BvBtDG42DKbxHu82c6f9RaDst8/fW7c9hl75Ys6vxeKlL7aVwz5uY8gJhpmeUBNXiCzzZAlmAZ1K4hY6HalZOwWydRhjDr4jsQndXOroRnsikZydh44R1qwODB41+Fpq/Srg4REsF8HwpA9mLYzdqpb6uVxy7kSymUl9XxncNBXpLmvoOwFct3YlzL2qZNvZCrI+9IYwHomENOtCsQuTqbiB7ozhM2dy4xWhfvFiAqMCDm5J9MPACXhbreDBmQDPRXAq7fWEPRyP5NwLC3fuypxlmbh67gzBO3y9xFUW4424/+bJRUnvjesevrRp/utugkft9jZudsua7qbdtJv2i9euBX2wAQtqfpLK5C4uLjg/P2cymTCZTGK2m7pNprJC2ISGpzJRNWpRQ5ZOpwMQWSplexRo6niWy2Vko5Tle/jwIZeXl5yfn7NYLBiPxzz//PONeAOVbCpwSZmiFIwoMzQcDsmyLJ6HgpeqqmJunIJHdQOt65q9vb0I+mADmHRO9XeVMSoIVNlmO/pA5zPLMg4ODhgOh1FaqzLb09NTZrMZOzs7HB4eUtc1i8UinqcCUQW+vV4vAkcFuuv1ulF7B1cdPhV0KmDe29vDex+dN4uiiPeAgmZlSXu9HuPxOMpt9X29pxaLBefn5/T7/WjqovOqwHq5XEZ2EWhcy7QmVI+px9F5etqaOGoahlbA3sCu6JmSDhuWrzQla5PFHD6HT0LQLXgJPFeWzSWr7qXvQA3rAAL1GB3vmLsuS19sjFrIGoHrYyv3Y8+uKYLZTMfUMadP9pFW+oylL5jWvQhkB5nIq9QkpvaWVXDznNPBGs84W7Kfz6IMtfaWueuyjoVmZhN4npkAKMLnyoGtfARh0AJ0iWxRAJSPNX8N0BK2SZk9/Vulnfqvrby8npkrDGA0hklfD0Yz6tiJgqbNaWzGrcevdTvTAH5RPgkJw5kOYHNeaZ+pzPOKcYtJgF4SSi9z45PNdKGbMFTpsVMw+pZfyz1m9X3tgj/ZtX1vpL9swwxeyDlnaFxU40NsHeBtJqCnl8O8EjOWvhF5Yl0Jy1c5AXDWCwtnO3KwysNwKMDNGsnTO9wNtXsFTFaB7esIMNKw88xIQDs+ZP/pDRTOoDeQOsCdARDGNuwKq5gb2X7tBZA6G4xnAkOYB91zVgh4VMfPfhf2duXGXHUFgA66gSkMzF1hhbErelJ72BuIpHQ5l3H1OzDswNlpuKHrwIzqRVBJqxdZal7IuOxVUvvau+GxzLdvbrf1hnhnNQV9zsutddNu2k17c80Y871c87Xkvf8jj9v/ke6dqfQvjW/QhbwyZppr1+/3G46Lyrakph8KZFKGRvPtVHKo/Su7lkYrKChpm5547zk/P4/gdG9vj93d3QgY1MFRQaSCgk6nE8FKKkNNDV90XMpuXVxcRJapKIroiKnSQ5VRKuOZSkXTKAw9Viq3VKfS1DkzradT1k9ZNJ0nnRONSRgMBo0cv5QxSw13tJYyNUFJJZypY6ZeFz0HvW4pkE2f3rXlqqmEM5W6pjLPNEsxzflr5y7q9ikjnL6XHkPH/zTKO9uRDSlTBgQGTMbtvMAzazbRCi4EpktfdYO1S/vTWsFOiErQbL61z8h0jrwNkQy2YRYj9YUuvq7h7RIfsZGaOi8M47zuxPEXtmrkBurrzhtcEv0AxNB2Bai/87mvxv/6DNexuNxIrZ4x0WnzkdIqL6yf/O6TejWPDw6crana6kYZAWdQk6XHvS6XLhq7hPfTuIkGIEuH7J+MIYsB8OHftP7uSv1eY8cNmG0CSdPIQGwOiqvb0Tpma/sW6fyWbO1peOLz0flv72A2pJfWiJnY+9Vjyjtmgymcqh5tiFnwIos0YeVsM4k8qNbBEVYPmCB8bwTU6E1vrQCjym/SsuXJipyACyDJGgGKdSX9K+gyBgk37wSWsSvgzRNC1I3c+FnYzptQI+gRrbgVoNftCvizRgChMUAmr+WB2esUAipdV87HhjHWTlhEY6XWMc9h3IX9cdjfwGIpAfUmxFroQ6PKyTk6t5GE2s1TlQhrn+hmSJ90JEDvUZ/nJ72p0n7e6h8sZIpB6vqydyryvWk37UtrfzH8+8eAjwL/DPg1wFc/yc6PdO9UR8s0o+709JSHDx9yenoaWRc1F6mqiqOjI/r9Pnfu3ImgTlm5tE4rlVVqdIKyULrI7/f7cSHf7XYZDocRfKmcD4iSwOPjY87Ozjg+PmZ/f5/ZbMatW7di/wC3b99mf3+fPM+ZzWbR9VNr3xT8pbJTHbvGUyyXS+7evcv+/n48lyzL4nhTR1Jl0RSkKpBTpjN1xFQGDohMnjJ+2lIAfHBw0Iix0ONYaxmPx9FkRYGdSlBTcKsGM9tAWJ7nUSqqdYvKso1Go2hSk9bt6fVM4yGAK06oOk/r9ToykSor1Wsxm83ifaJ9qGGQ5jFqrl+79lTnWgHvcrmMTOrT1FIDk7YrpwA1qXXrmVKkkUGSqdLPlStCnZ6Eolvjg7RSPnM9WwZ554KekXtwTcbSFUycRJo06gWTrL7zeggIuzcIkQ8daiZ1jwflLmfVIEhTPSuXU7mMV+b7XK567HSXvH/0kJ18yThbxrGoPLX0GZlxjLJlQ7JqjUpcHbbfZ93LcIUAvrqjMQQivcpKv2HQEgBklLVKAVwwRFHQCEh+X2ASU+ZsW3afrhUbnjctALcBdspENheADSCoLwcg6bLNMRrH1anRY9nm2m8bs5bOQwQbesBtC8fWa95szqEJcM0VcJo6pW5A5ZZjvIXb1jX3IwCyvp1O+xUQ3X7pyh+yZwT26DXxEs6+qDdunApc6kAJdkKwuqkFDKpBy2weDExCaDqVsHfTtbB5NhNQdjETJg0rq/TxjgCk+UT6dGvZLu8K4CLUD+4dCqO2XMqNkSPArTeW7dcOlkZiJLLwVObFfanVm87h7Az29oAa3ASWUwFqgyG8sCNzUpZwfg7nE7icwRsn8P5nxe3TA33gYFey96oFdMbyr1kKWLaBcbyYwmohtYV5J+QNeiEAE+S3+U/rNShOAXDjQ3l1sy+lvY0wX1QH3Ji53LSb9sU17/2nAIwxv8x7/8fDyx81xvzIk+z/SKYvlc2pYcnZ2RmXl5exVktBReosqQBHpYnKHKUsTgosFMSsVquG86MCRAWAWrN1eXkZx6Ws4WAwiLJTzbc7Pj7mXe96F51Oh+eeey46bGZZxs7OTgQFCpRUDqjALK1lVICSSkudc5F102B6BU4aYaDn1+v1GmHtKptsa9u1L3UiVdY0dTTVpuejNW46ZjVWSY+pQGswGDTiMtQcJmUCUwOf9NpoSyWX7SiJtHawHf+QsprQZP8U3Ot5K+BO31d2WPftdDpxHKlhEDSlqjonqUHN09LU5bIwFQV1AFaOAkcn5t1VLO2Kni2ZlgKAB3Yd6v2y6JKpZisq77QJelG2Tev9Sp9TexvNYppxChX4nKFdSQ4gG2fRgS15prhg7rr81Nm7ZWtn8d5QecvxdMi6zCnHlhcHBaUvQ82eRDoowCsQBnBslxSmwmFjpqCMR0Ziah8lnPJCICOsgL+UwVLG4grg8yQs1eY9i8EbL2s2A2re4LUPlNnbsC5NTSObFVnKAqS/RzBpmoAxAL/IQLrNvhuH0s15p0xRO0vQt8eQANc28yag8ao0dWszzb6vgJdIhyTbthnDd3B7MzyGAv4NNjdIfZ3cnxb5bsYYMVvpD6BXS73aqAMrYG0EpXSNgCpl3Xz40KxXEp/gjIS2Zws5YK8nlrfU4tJpAojMEOno+URGlVkBjP0OFF3o7Uqt3rAjuYEzJ5/BTk9MX/qZSEVLL8A0K+S9ykodYCccMw+SzsEt+bc/hvEBTCv5u3by7/FUagB7XWEdi1zO6+RIAulNDncOg+zTwW4B3AnMYwmrM/m3rGX7VSnSTmPCd4cXUhIT/+cjOwhPBLti7e9N29Y264Ff4oHctJv21m8Xxpg/D/yvwNcA50+y02PD2fXn4cOHTKdTjo+POTk5YTKZcHFxwcXFBWdnZxGoHB8fs7e3F5lCXZBrBp6CwNRoQyMO0uOqK+d4PI5sj/6uZi5q4KKA4NOf/jRvvPEGALdu3cJ7T6/Xi4YrAA8ePIgM4EsvvdRwgYQNkAIiUwlEUPXaa68xmUy4c+dOND0BIlgxxjCdTlkul1hrGQwGjXo6jUtQcKUSyfV6HceoDKhKRZVFa8s8b926hXMusnrah243Go1i3865CJJSJ1Q9v5TlS2sRNUpBYzp0zKlDq0pmNbpBwV06l8ro6X2igDNlCoEo/9R6RwXMeszUkEeZPgXRakijY9V6T31AkbKAT0vTKIWhWVOYirEp6RnH0Br2bZ//w/O/BoD//tV/yuvVQ76wPGTlcnpWHqyUPsOGPCk1VYnNOyZ1j6UrWNoiMngKrFRGqUxeM4uvioYuGZ4MT89UvCur+dMvfQV2POaVf/s9uA7UPU81dBrtB8D9Zc4n8oq7/QnZaAP2uoHtG5iSni25nV9S+pyZ67IOrglrn1F6MOMRPqA/W4nHgyrQQALP1UhFGCapudOAc1MnBiopIAO0hs2AuIJa8AmN5a3ZGMH4xHojhJ5HQxPtM2HXGi1Z+UcGzjXf01pADM2w+Pbtug1QpQwiV8cU+w9z1ErnaG5Dwhyaze8No5lk2zge3zzu2w30JVj6ydgWvR/Cr/irZNCV7R/xosbEYa0AJM2Z61cij+xYKEPhqHeb3W1w8TRWQM1iiZi7GAE/3TIAOS9ZeCYTsOYIEQuhHm/twgnkIvXMKmHuOkVgGzsB5AWm0RswHQGnOWKqkpngKmoCMxnGxwrJzlMgm0G3kO2dh7wvJjTZCoZ9IINLJ/OwdgLglrNg/pJLv0UAp92uZA8uKwF+5MJSuhqKEbGAzzUoPVy4cgbUu+n6FvfbsINfbmru7fRxsjeg76bdtC9X+1eAbwLeD/wM8B8+yU6PBH0avA5Epm82mwE0HB4vLy8jUHnttdew1vLud7+bd7/73VFyp6yVyvqm02l0VXzxxRfp9XqNHDVl7haLRTR6UbZJA8bVRVIZxm63y507d5hOp1H2t1gsyPM8MoD9fp/j42Pu3bvHZDLhmWeeYTQaRQmkZutdXl42Yg3yPOeNN97gE5/4BM8880wEe2mEQBploYBnNBrR6XQ4PDyM0s3xeBxZTwUj6taZsl2wYUv1OqSulwrIRqNRZLZSs53xeEyWZRFsqyumyizTUHQFW7Cph9P+1FUzBVYK4pQx1HsiZWC1P+1D918ulw3gmLKrGnivPwpEdU7UjVWz/dRkKK0ZTQ171DVU2cmntVnjGJqSw8wzMDnf8p7fiN0dkz9TQK/Lv/wbnqe+/5Bf9j9XHK9GjLIVha2lDs+EqAVjGqt6h+W0GjKthR18rnvGwK7pGfmcaYj7BMvSF4ztkp5dS1JV4szZs8LWdU3NrWxI/t6XWL9rj+65pxoIKjBOFlCuAAzUVZfPc8jpzoBuVrFbLLhVbNxTCyNjP6+HwcDFhDGJ7POP/9pvYfkrnqPuZ/hMAFy2ltNzeWDkbPPHePCZwQXQZEsBbrbarDDS2jT9uxHNoJJMs2URRwKEsg0AbIAxZRXbAElX7ols0qd9koC9ugWcwq5pPV+Umobj+UATpcHp21qMsUjGegXwbTnvq+co10S3cYkrQ9t19K3eHns2jwAG/rHb6A3UAoaBfk4vCXkuLFZmYTiQpIbhCDoG5gg7V3kBNRgBPkUu21e1SDmLroCtxVxAXgH4NUxKMUXp2ZCEUIn0cb4WAxgNdD/YhXIVws9LKDuwRPa/1ZebaGXA98JDjDqwcp3wdwCMPhMglpVyLJOJDHSAjK9awnwl0RLFDsw/D8NdyHpwMhXW0K1gMge/gN2euIMqgDNWWMpsLuO8XIbIiiXgYNiDaSEfoirUPobPrQtXwRBYu/Znov0B8/rhaDs0pXdO8+Kb1l319vrEXN90Fm7knTft7daMMb8P+Mve+9vGmG8D/gSwAP6Q9/5VY8yHgL+KPCr7Lu/9jxpjRsB/AxwCP+i9/4uhr/8E+HrgFeDbvffbQqYN8l+BEfLN+UTikmtBn0r3NOMtdXRU4JFlWWTzVHJ5eXnJ6ekpR0dH3L17N+6b1pGlrJLm0ykoUEmf1nlprWDKcim40lDz+XzO2dkZ6/U6yja1ac2eOl2q2cx8Po8M1ksvvcR6vY4AyVrLdDqNZjSLxYKTkxM++clP0uv1uH37dqyN0/w3rWFLWco0lmG9XtPvSw1VCsiAuG96fmlwvNbxpe6UsKkPTN09lQ1TEKT1kTqH2tJAegV5ek3bcQ0p2FNQp3LRVIKbSlbb2YhAo24wvUZtwxWtP0yBaWqIowBQ76HUxCat70udR9N78GlqUq/n6CD5eV1jGdkedjTE9PtQ5LiBPCzIuMNz3Y9zvBoxdx0GNL8HXJBQar0fSHyBDXWDEvtgcMZGl8x2HaEaq5Q+B1NByPDDSP3h73rP1+O/eofVYWcTHl6DLQ04JE7BCitXL3JmRYej5QgQSSoAFiZOzmnpiyAjDQY8IbJi8eHnKMfhwUcwYlFGzNZ+U2NGsg4LTbqSaAc1bdE+MKGu73HLrAS4bWry2tsk37JtkOabr2/YomDyEvpsg8UY15CAu2u/yhOmDRuAX7pGba09Fez6tMMW4Ntay/eo8wofYxte8A3k8vZpV07piWm/ZPsrL+j9u7kim27lwnqEvPIQ2LKADrt9GOYwPoRDLw6WsxUcL8V5c4DIMLuFxCO4qbBhp+diAtPPhVnzFaymsHNXJJ6Vh9MpIvEOGX5lKWBzPJAYBOeF9VvPQhZeDyYDmN+Hbsjb6w+ltm+5gMlMpKhFcBwddISNyzOYLqTuLzNSo7cuhK2s1rCbw2wJkwredQtMIYHqz9yCxTFMwtgqA3YJ9YXYCdsMbC1jzXvCQNZr6TsbyE2cORmryWG6kpgJ5+T7oiENeDPX+VG04Oa9J1qZvU1bZPp+icdx027al7MZYyzwLcCrxpgC+HeAjwBfC3wX8J3AXwC+HXgA/BDwo8B3AD/gvf8eY8wPGWO+D7gNPOO9/4gx5s+Gfv/6lsN+H/CzwE+E43wfwv49sl0L+rR+Tx0gZ7MZ0+k0hpDror3f73N2dsZP//RPc3p6Gg1SBoMBw+GQvb29Rp6ampukbprOObrdLvP5PIIRBU0pAEpr7RaLBQ8fPuTll1/m+PiYyWTChz/8YZxzvPzyy5H56vf7lGXJ8fFxrLsDYkTCG2+8wac+9anowqls2Oc+9zl+5md+BoBXX32V1WrFe9/7Xn7Db/gNDIfDyFxqvVgKvjRaIHWMPDo6iud8eHjI7u4uzz77LOPxOM7LxcUFZVlyenoaQZqyeb1eL8odFdgoiJlOp5HF1PNVZ08gMowpkFQQmUZJpK+n9YwqU1UGTWWtGqSu0lEFcwrA2jV+KUhLaxDDh+ZK3acC0tS9MzWd0XNSkxZ1LtV7Ra+HzkMqI35a2tIV7Nglu3bFnnV823t+K3Y0hDuH1Dt9XEfcK70x2MM+/+hf+mq+8b//p/zTi/cwMT2G+QprPLWxArYwEBw6AUbZklG2ZGzFUKX0WTCEyZm7TswH1NYxNcsY0C5sdm0F0O3ZNfZ97+bixQFVz+CzUApkAj4E8rm87vuQTTKqZZ+Pr5+l6FbsjhYMihJjBIwB9PIy1hb+/NEh/ueH7H8c7B0fJJ1+E24ecqVNGRRqhSjF0lo3MRbZmLVoPIMAVEFSvmgyU2lrSyobzJnd7CNj8RvGjQ1gakQkpCDJboBf2CEc08fYBJWmpvLRtG4vBXJG+41j9cE1MfxNsm3iYLop+JN/4ly0gW0KSiHWGaZzo8e30Rj37bWcu/ZsnuA0t/I8CVhsQ4TE5+cKqLRYjNrH5l0Yj2GngtGluADNSwFNdS3ArbZAAE82k85dKYxfHWr2FgtxuLRW8u00mE5D3AmumApQrZVn1KXGQmQSil7WUn83qmFkZJ+dPAAwLxl5JpOfzAQmsZQHK6sSTFfGVy+BSoDmuoZ8DXYkERCDkTByVSnj0+iILIOl3WTyuY70nznwKwGK3guAxUJvJCBzVRGfsHgj51DLB8Th28a+V67dE6H+xkOX9vdNE96/U9qNkctNe5u2fxX428CfROSWHwvs3E8E1g7gWe/9ZwCMMSfGmFvArwP+VHj/R4BfC9wBfji89kMIUNwG+u567xXk/X1jzD9+koFeC/rOz88je2aMiQYtWZbF2jpdqB8cHLCzs8PZ2VkEPCcnJzx8+BDvPTs7OxHsKajQBb0CIXXvTIFHyh6pmYfKDhXYnJ+fR5Cki3rtS8Gk1pKlUQkpe7VcLlksFqxWq+hWuVwu+eAHP0ie53zVV30VZVnG4HdlztpukTo+ZarCxY1ySN12Nps1zFw041DPTbMANVpB5yoFSEAEQovF4kr0gQbdA9G8RfMElaFTwJfKMtMxt4+lrysbl76uTcGWzr+CVz1GOv8pcNV/U9MaHWdaZ5hKOBWE6lzBpkYxde7UhwtPJegLwGrPOg6yLrbfwwz61KMu1bCQqIIiiWd4Zsz/+Gvfw8EPWRYuI7d1YO+Sa5BIPDM8XVtGB820pYCvDnELGuOgfVjjAuu3psBT3h5SFwL2/BawYCsiePHeYEpDTcE6zzm67GI6Nb4O46sNZp7RPbWYSoiAbAku99gS8L4plUxiFzDi4GkUSAUAYhyy0NTXSICMUdCSLMCuYbIeGZ/QZvh0/WL91bWdbmOS/RIgGeMS1F0/nNu1bdvakYSh0+Olm7RYy8iMplOhl7LFWDacOdN+2mPQ+X8LruVahOgX13Tnay6db891uujVuTPNbdKuLEG/nAGdrtS3DWtxtrQ9OLuATui3NgKcnANfSs1eVohcsw5mK7UXoOes1OmV641RS9EVEOSqjaOS87JvB82PELnpahkcQBdgOsKslYjSUbMCO0MJczfIgxOHsI9Ga+0y+anXUqdXG3H5nC5gPJSMvqIr71PJhx4vfRW5ZAvWCzFl6VQwWcg5dyv9IgpOOAhDWhSwOg8nEp4a6byEC9Lk5dInH0qJJxfWPOLCb3a80re/AhzfLH381muR6Xv6yutv2k37opoxJgO+Famv+5PAHnCZbKISs/RL4gI4aG2bvvZG67VtbWKM+U7gp4CvQ7QPj23Xgr5Pf/rTnJ+fxxgBBWh3797l4OAgSiAXiwW9Xo/JZMJrr70W9zk/P+fll1/m4uKC9773vdHMJAUOqURUgYq2tJZOQ8FTBmq1WnHv3j0+8YlPcHR0xNd+7dfyrne9iyzLODo6YjKZRMCp4CgFK+rUmcYA7OzsMBwO6Xa7vPe972V/f5/FYsFkMmnEUoCAixSYwEYqqa9pvAQQnT0VXKkBzvHxMTs7O9y+fbsBxhQsqey00+lEVktr9A4ODiIYBhrxDb1eL0pyd3Z2qOuao6OjRraiXlutlUzPQ4Pdtem49Rgqs9QgdQ1aTx0yU5OW1L0zrYfUa6HmLWkdYKfTod/vx/pMHVsaY6EuqWr8osY1CgZ1PKPRiL29vSf5TPyitrNqyIvFCX/4pd9Etr+Lf/EO9ahDudOh7lph07INg+SKgvrr3s+v2/thPjF7F6frAYVx5LaGXOSRapaS4YMzaE3tDUvfiZEPztuGHHQecvfU3bOdn2dzxx/7lR9h8ds6VH1D3ZEYBWBjYBKYNjxkSxMlg/kiw2c+GLDkQaJpBOjNwK5oxClo3Zm3RoBfjBsLYM5tWDYwMTagUa+mLaywXGAsJONPGLF4igkr12a6Gg/pU0DmE4aOwDCGnC+XKUja9KP9N+Se+vnym/7ioQO+apxTm4lLto1uo+7R28ax668BCGtN4GNbGyHpcWwCXN/pbdscPAYUgjp0JqCcGB5AjcUXuUg2xzsCxnIL/T5kw5DVVxHp7tJLTVynE8LNh3B2CcsQt1CE2Ibaw2wNewPpqzsMzJ8Xg5TJHHIfWLY6yDODfNMi9XydKsRA1ALa5is4OZWxaKD8ygm7Nl/JWPLAIh4eiMwUA2fnsiLJrOw3d5A76f/sRMZdBhOYnZGcbw0c3oFqKszecAfOS8BBr4CLuZzr4SGcTGRe6noTdeGdsJ0pUM5qqP3mMiYPba693o/97Gz7ctrW0du73TB9N+0t3J43xvyt8Pv3e++/P/z++4G/5b13YV16Buwk++miOH3UsQecJtueh9c+j4DEndZ229rvR2Sj3wG8DPxrT3IS14K+n/zJnyTLMu7evcutW7d44YUXGAwG7O/vs7+/jzGG3d3dCEqOjo7Y39/nH/2jf8Srr77KwcEB9+7dY7lcMhgMuHXrFrdu3YqLdWWBptMpOzs7keEBWcinDJdGIGj9n5pznJyc8Prrr3NwcBAz87IsY29vL8YMpIYoGlmgwFMZQWUwFdgOBoNYG6h1jQoYU2CRAtU0a9A5F8GUMlppyLya5JydnXFyckKWZTz//PO89NJLjMdj8jxnPp/HcUEz4P7y8jKC5m63GyMyUkCkoBaITGKe5zx8+DD2BUSH05QF1df0eBrHocyjnoeeS2pko2C97crZBtxprR4QjVkU9KlkVB8WqLRTx63MprLAjxqP9j8ej5/kM/GL2gpT8w29C/7ye1/EjQeU+z0JIy8MrtgAJ5CH8i4zVL2c/+5P/E6++f/5I3zUPU/pLc4ZplWXUb6i8DWZqSLgc95w6kasXBEZPW0udN41FYWtQkD6hvUrTM3tfMJv7Z/y3b/ll7HctVHSGcFM3QInHnGhDH8KEBHZZ5QxlvJ7PveRHZR6u9BFAI8+MyGewW8WXmG9YNdegtCtkcw9IHXDkH1MMFyR7bJSwVoLXKYtHMeHBbgqwFKWLo1EMM5LtFgwRrQKRNMuU0ZQX0sSRBoREV7mwdYbgInlkYtLBZkpCNVrFOWnbMbdWMhGtjDpPyU32uuzLeu1NOz+7ZbT9wveknmX20Q+QB4vRpoEj8vlCi4m8MZ96NwH14fDTD5MHRNcL4Nhit5coS4YVwmY63Tlv/o2E8DjkM9MpxdiFAyYXviAVnB5DmUusmHvhS10FtZWgJslANAKFiXYAnolXF7AcCxmKquVOGn2B1LHV1ZhPwWnF9J3vwvLOVQrAWFFTyZksYCLC+gN5Gaez4ECdg9g5wDOFlDtyEyNumLWUtVyDBV31MD+DtT5pv6xQn5fLYStVAMc4/FpyKUClCch4kzrQ5qCm+Sz9U6FPDc1fTftLdxe895/65bXPwR8tTHm9yPSzu8EPmSM6SC1dh8N2903xrwfqek78N4fG2N+EvgdwPcAvw341xF5558E/hrwO5GavSvNe39pjPlnCCj8tPf+ctt27XYt6Ds+PubZZ59lOBwyGAwYjUb0+/2GiYbWlw2HQ27dukVZljz77LN85jOfYbFYxDiF2WwWHSZTxisFgOlrqQulNpWBprmAyral0j3vPcPhsJGJp/upE6YeV1kyZaz6/X48T6CRX5fKK9N/U7ORNIhd50YdQNPz1HGmfZyennJ4eBjns9PpMJvNIthSgKPAM83Fa8sm9dhpjqECUq0bTNm+VHarc6+gKs1ZTI+f1jOmbpltwx8917QuM2X/dFu9p3T7dlai7tuOXUjvkXS/9u86D09be2/3Id/6od+Bf28fNygE6GWQ4LJmC3O63M/5m//B7+Jr/v2fbsg564a000Vmb+UKli4A6mRRUipasuCciQwhiEy0BGpv+Lav/xcxX7sBJulT8DRrLmX94pCTS2ZqUVzZtQAdWxHdKrcxRjF8PYC5KwYsyripkyYkaDNx5gwvxvEZOW6D7TPNc0vjHuJ6z0uKl0hPm+d5tUiLa1c3TRmlvwLGZBwJ8+fMBni2WmPOY19mU0PorrJ+cXsFHHFSto+3IQ9Nxr6NTXykNPbt2trXHa6CfEiMH31jk6u3io94w0k1H361loD12SWcH0muHftQLGFQwM4ABg7MSuSTmZcoBWuBAPoiqxXkjMbI67UXlsvVGwbMeakD9E5YPH2asy6BldTPFTrIhYDBsobFWqSdvSFSx1cI25gFJ9EQMYM1wrqtK+mjUwijWDvki6AQ8ObmYgpjOwGs1gIIg2kUs2DYYrIg6yxD/a2FgdmE1ndNQM9rORd9MuMqOe+aMB/pB3vrJy65YE9wszfujQREPml7GyEkfb52w/TdtLdL897/e/q7Meafe+//r8aYfwX4MUQL8QfD238G+F6Eyftz4bXvAb7PGPPtiKHL68Drxpj7xpgfR9w7v3vbcY0x/29EcP/PgT9sjPl93vs/9rjxXrsKXq1WPPvss3zgAx9gf3+f3d3dyPLoAl7rre7evct73vMexuMxZ2dn9Ho9Pve5z0V7/jfeeAPvPbdu3YrySQUhmrWmgK+qqggA0igDzbYbDAYNt8yDgwMODw8ZjcQhUNm81HREQYsxhslkEuWFi8UiArW9vT12dna4c+dOZPImk0msv1NmUkFVGlCvjJtup8A1BThqTJOyUSlgOjk5wRjD3t4ezz33XDTCmc/nUcJojOTmDQaDCNj039lsRl3XdDqd+KOs5mg0ikzk5eUlk8kksoO6rUo09fzUCCeNlFDWNb0uKtFMpZSpKUt6bXUu0vrATqcTjWE09kOvm46v3+/HGkW9P9KQ+jSHT8c0n8+ZzWbxdQW+T1v7q9/w63Hvu02128XlBp+bTX2Wp/ngOMgYvYW6Y6iM4evHn+Onp++O7J0Gs5c+Y+lzpnWP0mWBwds4eYJIxioXjJOCVnNed7hVTClMzShbkuF4sThl8cFnqTuWfOWpvYksGsH7QcGfD2xXVrJh3BIm0NRIlt46gL9KDFowUPWS89ZSm8Js8u186CujsUreyCgD45cuuk260A7MobJhBRtQ530UXxiIUs20RUC4DfDpOLyQIleYsm1rnDbYS99KgZwRAGiC3nQrgEv+jTmCyvRt2T6tj4z72WQfu+lr6xhb/V7J63sLtS/L8rMN+B9znDYubIxB38wMvjYBI9WYSYkfFMACXvl5YbWKD8P4DXhmR5wsX+3Cg3ti7FLWsI9IJDMnNXguALz5euMGmlkJYK8qGDqYrsHPoJ5KdIIdClBb18IGugrKuQDKyorscnYGxaGAtIuZADjnBFDuHcoxyxLmM4ldsAFcVUthGb2V/XJCuHpP+povxbylmwszVxgJo6cvrOD0Qr4UekPpd3Ym/XYM0JM8Q5uFL6YF+LXMS7dDzDDMfQC2ASjW9QaYWaXM44f/6oVqMeaNC558N11/N1xt6dZvJ3gUH/TfgL6b9jZs3vtfE/79G8DfaL33ceAbWq9NgN+7pZ8/1X5tS/vl3vvfFH7/K8aYH3uSMV4L+nZ3d7lz5w57e3uxbittyrgowzUej6mqisPDQ87Oznj55Zcj0JlMJgwGg4bLpIItZZp0Ya6mGwqg0qiC+XweGT0FG4eHh9y6datRd6hyxeVyyWQyaTheKphJ4xJUPgrEc82yjLOzsyjx1C8rBSbKwCmo0H5Ulqg1iNpXGmWQsmF6XM0pPD8/j9l7e3t7cX8gMlZpfIIybwqIdE5Tp0/Ns9MaRp37NApB+0+vbwrklP1LW8rUKdBL5aXW2sb+CoQV+CobmUp79TXNOBwOh40oiNRkRvtX6a7W8inwS0F6W8L6tDT3wh3qXh4CwpuLCBNMAtJ6K13MS20afN9XfyVf979csHRFBHx1cN90GOZ1B+dNBH2FqaMMT1/X31cu57yUz9duvuCFzgkDs+JXdqZi2uI9pjbYKqjC6iSOwzYBqnGIo6fR3zdAyTgBfKb2EQQKoxdyyRLppZ5zrK8LYCydAwHCphHcTsCkbRljO1R9Y6CS/B6YryvRBm+GwYrX8E3s87imBjZbAOLmuJs/Yl1gel10Iaqf22RhqhJab80GFLaHoPNnaJnH+CbTe9Me2bZOUbhn40MH7zHGknmPo8KUGcxrOH0DxntibPKpc3jpEiZrIIOXbkM9g3MH6yVcLKHKYBzq5Kjk4N2ehKb7wOQVY2HczEKSpcqFAMVnnoFsD5ECVHC5lqcaOVCdg+3K/i5Q9IURELe7D3sH4jKKD/ENS1jNIe8Eps9BXsN6IWMfjUX+aa38rC8AJ6B0PIb+KLw+F9awXsv3weggSDVXYv4yyATU5WPYKYIbqJOnRcsJmLWwiKUXWed6EVxKrRTkIg8chfWjWYnTvnDxM7CFGWwDG932Kq37jmnxeeY79Pxv2k37MrbKGPPbEabv65Bvwce2a0Hfr/pVv4qv+Iqv4PDwENi4XaZujalpye3bt+l0OrzwwgsA3L9/n3v37sXasNlsxv7+Pjs7OxGYpYBDF/CpW6PKNxVIffzjH+fll19mZ2eHyUSMar7yK7+S5557jsPDwwgWbt26xWKx4PT0NAIbdY/UwHmVgeZ5zs7OTmTQ7ty5g7WWo6OjCGKm02kEPOoCurOzQ1VVkVXSkHZjDP1+vwFiFLhpnZ6elzJYCnwXiwWXl5dcXl4yGAx44YUXuHPnDkVRMJ1Oo4NqW36aBsQrE7tYLGKNnJrAaC2ghq0rc6hgSMGosqhaU6ggDjbgLWU4dY71PBXY6j56PVVO246DSF08FaCpyUzK6Om9ltZmptdTGdX5fB5Nd7Qm8Nlnn2V3d/dJPhO/qG15py/gx+tCj+aCoAU2BOgYqGSRvfzIh/jW3f+Mj62f4aPzF1m5nIu6L4HnAeiJvFM+6s5YrNnk+ynrV/mCRV1wvBxxUfb48M49fn3vAXeyIb/7N/8Byl+RBSfRjQGKcYG1K0WSSmDiMICDfOEjEDMeshWx9i54yGzC0w34pQ9B7PJ6Q+JpTMJ+BdYrMHsKRHwDEILXOkCTREs40LgIb8B6j41gJ4xfGcXozLcBOsaGGkF8BFMes93ZUhd3LfAXgXtaL6Tb0wZx4Z82w5eaynC1f70+29jKxu/J/SXg2mz2jbVMG2AfayDjnIdNaiOminCzoPti25V50xdsrCcVVm0Bu+Epy+kc7i4BC50B9DLJ06tLmPiQs1eJyMjmcsGsldgHYwLdHo6zroRdWzphu7pG3C6zvuxjKliWEgdRGgFs5TJc8EJuoDqEsftC6vtsBtVcjpXnUBfJyj98ELIgAe31QD8TZZBcggC8vENAisEhdBliI3IxlVkshUm0udQw5nm4r42MvcgEIHZWkK9k21UZQN96IzcND3/ipXgU5WbSF9sfpG0fgjf75Ojthw1jTd/b7cRu2k37xW9/CPj3kUzAz4S/H9uuBX3ve9/7ODw8ZDweN2rulD3SBX4qp+x0Orzvfe9jMBhwenrKJz7xCc7Pz/nMZz7D0dFRZKiOjo748Ic/zGAwoNvtxuw1oNG/xhloFuCnP/3p6Bha1zUf/vCHefbZZ+n1epGJUjZLgUQaQ2Ctpd/vs16vI+PX6/UaYebL5ZL1es0rr7wSpZ3p+aZxASBARI1oUumj9xJRsbe3F4HYvXv3GjlzvV4v5hjWdR0ZUZWO3rt3L7qPjkajKCtVmaiCJz0PHQ9s6t8UVM/nc1577bUoj0xZNgXYacRByuSmeXlAZNMU1GmWoGY6qtQ1ZVBTQxqde2VHU5ZW+1cDIL3HVHKasp66bcos6n2jDOx4PGZvb4/bt29HCfDT1FxhsP7qIiM15FB3xc0ifsOa+dzwJ/7Av0U2XfNX/85f4R/PX+J/n73IwheNWr+0qcSzsHU0dpmGGIWD7pzfsv9JfvfwZb79I/8qbm/E4v1DAXy5iQt9lQKK46aAqpiTZ8F0wC/lPFyok1Gg15BmOrClgKs8AFBlCPV4CvSsArFAX7nc4DITAV80L1EwZmkwgTK9iiR9gwFLaxP1bxmfbi9Ax+IbYEcZWW9CLabZnGMEd8qsJTWG2qXW7zWA/ZZ1ZKP5zTlEQGaubpuuq+I8GFoPGDZsaQoiFZRuDaRXMN064JWayndqu4IYtqxwU1ygn+XGZh68w/sMj93Md4W4bd4NT1nOZsLoFT0xQslr2B0BdcieKyWOYV4GSWNXpJednoS12wpYyQ1d1lKvV9dgcqnDs4UAxDyTTJXBBKYE0Gfk2M6LQUyNMIfOC7uoT2RWS8gHAgCtgTI+QQFficlLpxeiIyoZw3K9yQLMA4BUJr7oBYOZSsBgbyTS1KWH7kBYu0wBaJC1dgph+no96C7FXGa+lHy/ZXgIpvXD24yIHglSrn4er92/8bm5vtM3Dw/fGk3FLPUN6rtpN+1Lat7714B/+83udy3oUwmmApbU7bEt4UvNQnZ2dlgul9y6dYvDw0N6vR7379/n4uKCBw8esLe3x2q1igHle3t7jTiFlJlLJYGXl5cYY3jmmWdiWLi1NoKm8/PzCPS0H2Uilb3SWrs0WD1lncqy5OHDh1FKmrpiKtOZmrWkElStM1NZJBBf63a71HUda/Fms1lkEVMAqIBstVrhnGOxWHBxcQFsIh/SOU9r+lJQngJoBV7r9ZrpdBoDzIuiiCxqCmAVJCrjmrJ4KcObumgqiEzHoO8B8f5IzWgU9OkYlDFMTV/S81FQqu8pq5pGQej4dP71PNo1gE9dS4AKbEBeZKYSVW1bOudyQzXI8HmX/9Nv/8P87X/w1/n51R2O1mNKb8nCDpnx5KYORi0C+LqmogwRMpnxdG3NTj7lV/e+wLe/77diPjSkGnUiu2e8FxOIdCw+oaASVgoiThLWLjQNHze1mrgkssCKGP69Yc8EnDRCyVtzsm0+00WT8QTTChrrrAisjWmEjjcUWsFARo/d+Em3aYG2Ry3aGjWK1619tuEED/66hWIb8Jlm31fAZrLNdcYraV2fHj8dn3EhZqPFGL5j25U58Fffhyv3YfqW1z68Ayds/RpVZAcgc3oJOw9g0od8BL++C/sjkTPWI6gWwmQN1rAyIo/sdoJEckdq31YVdEfC5vVrqFewqGFewWwa5JoWJkvJA1zVUM5geQHzQn7WHqhDzIKB6VLyA/fGAtQWKwF1BcKmZTmMdmFWg6+DvHMMox0Bfr5G8vqCBDXrSn1ftytADy/s3INTYCHnNNgR9tPVArgO70B5AYWFW/tSg9gbyM8brwk47XYDGF6KM+jlXObIeLB1IOr0Kcp1H9BtN3vyPeihYdryRXw23o6wSJk+596OZ3fTbtovXjPG/HvA7wPmhG8d7/2vf9x+14I+reVT0KQ1dNCULKYgwxjDzs4OWZbxwQ9+kF6vx3Q65bXXXuPs7Iyf+7mf4+joiLt377JYLLh79y7vfe97I6OjcsFerxdD1ZX1+qmf+imMMYzHY37Fr/gVjEYjzs7OuLy8jEYtIEySykeHw2HM2JvP5xFoWWsZDocR7A0Gg2go8vDhQ9brNYvFAiDWlim4UuZP3UgvLy+j5FDBjcpgU3DZ7Xa5desW4/GYyWTSYBt1HMpEdTodLi8vOT8/54033uDy8pJbt27FOAmtGQTinGnWnvalgHe1WnF0dBQzAdfrNYPBIDqFrlarCCCVoUvrE5fLZby2yvSl4fSaIdg2+VGgmQJmBWUqN9U6PAWjCuS0DwVyaQ1k6r6aSoB1vHqtVeqqrqxqdvPUtS3sSAQhCWNgAsOk27rMiLGLNZiOFYOS53b5pm/7TvJPvcrX/4N7nJRDVi6nMI5+tmaQremaKga1d23JyhWUPpNaP+CX91/jz/7mb8F8aMT6cBDy7SArPb42eOvxgYmr65DV54V1A8iqAMiSNZMJGNw4yNYi58yWPgC+8K81G6bDe/G3qgzkPtaYxRrHlGVzAbh4L+vkPGGrMunTOGEJN/V9fmuOXjq/Wq/WDoeXaVJ2zMTxNNhE57HqCxHreMyGXWQL4GsDBl04ttZGV+r57HZ2QeclZR+UtWyzem2GT1nK9lxH8J3smspwG+N+J7d2HVja2myecs+B6Qv/5Y5PJByeGkfhDaauYeUw5xXerGA9hbMjmL4bnh0J8HuYS1ZdWYYMvjyAKwJ4CqzX3r6ANu+Cu6UX4NPrwMwHNjA4bnY6gb3LYW9PpJLdLDBkDqiF8esEeeZ0Cf2ZsH5ltnEWLXS5kQfmMCmirRFmrwzRCUUur9lcfvIC5lNhDb0Xhs8Ec5d5yCTudIUlNEEG6rwwfasyhLevYHIJ5+fCai6X8hoOCMYzviZiPpDXtiL0MO7Gl0j6mdLttr2mvz+e8Xu7NRu+I2+MXG7aTfuS2zcDX+39m/swXQv6NBBbF9hq86+mIKnsThf1ChK63S7PPPNMDBN/9tlnOTs7o9/v88lPfpI33niDi4sL7ty5w/3792OO3N7eHp1Oh7t379Lv9zHGcHp6yquvvsoXvvAFvuIrvoLnn3+eZ555JgKrsiwj8EullRp9oGBMwY0ySkVRRCfRvb29GJh+fn4e6/20tkxBhbKECh5TKaVm72VZxng8jtuoy6WyWQqQtBZN50/lkiqX1dfKsmS5XHJyckKv1+Pw8LAhswUaLJi6dKZ5f5/97Gc5PT3lwYMHlGUZa+UUQCoAU3dLla+moFTBoMZQpMyc1s4NBoO4fcr8ad9prWZVVfH6KCDvdrsx+xGIc6Bz3g5+TyWdqVRVr0G32+XZZ5+NdaRaB/o0NW82LFqDMUoX/OFfW21khLIvsjDPAITxqwYZ5a9+D//09x3y3/7wf8X3XnwQ5y2jbMlhNqVn1wxDTV+NCYYvFovjMJvxXV/3jax+xR28BVdYcU6vIas8xrso83S5weSBuTOQLzf1ehIiD3VPzs2WAexUG+CkzFoEP7rwREGcAA4XwJICKq/mCrqv24BCjJrChG1V7mkMDr8BQiapyWMzlrY5Scr+qcmL8R5TBWBupAbRZQZ14NBj6DhclvTRAnENls3QZNR8a/vrmAIXbpC05hFQY5VNh5t5FylsSgP6K0AwjiHJEozr2i2s4luZ5fsFWXo/Kfjd8nmXuzNcU+9xeCwWQy3XdJGBX4Ofw/wSlgNhxUa5AKzhWOSOzgk4q8PN2yHUx+WwOxaXzjLIMWMopReA1QuMWLcQgFbVsk1vICAss7CsgBChUK6l1q52wvDNpkAHfEdAo/ZVlgIacwT0GScgrfYbaad3kgFIIWPJc7k3VarqkRpAb2T7xULOq+gKg5mHG7IMX071CtxKJJ3TGUymAgQXAQgajzxBcwHksfksPOrBzLYLHOv5tr237fd3FvjJlOl7Z532TbtpvxDtZ4DngNfezE6PDC5LZZ3aUhmdtrQGTOWNWtMGRCnhu971LmazGScnJzx8+JDlcslwOOTw8DCyPqPRKAI2gFdffZX79+9z+/btaNgCRAlkWluYmqfoj44ZiGBJwY5GGVhrubi4YDabMZ1Oo/mJRhkoq5bWsWlr15hpHEOaE5cyY7pPKgXV80j70Vo3Hb/WAs7n88jidTqdBhjVue71epE19N4znU65vLxksVjEWrfVahVBXirL1HEo0NOx6/2g11+vbcq6ad1fKqdNpba6bWrykt5D2k9bKpq6j6bMaOpeulqtInu6Wq3igwntSwH4U9cis7IBKnGxHh8m+8aCEGcagMCHmjltDsv67ohv+6bvwP/0J/ivv/A/cb/OGJuKLHx0aw9lsoLZs/AHXvwGzK+6I+xeAq5iCyDAWKLDZhz3loW/y4A8nM8W5iPKLhvsl2+5Qob9r1BcSR+6ZvUkIAhxGUxBlTIwCThqyDrbpxsY1621fmzcPVV2aUKA9cZIxRMnfNu5xz/S36WPJ216zt76Rp8pkxhxo09eSBewyjCJqFbeSsFjZPjEXfVKqehbHPT9kjQT/+/6pg8ZcFhyOoXH5JZ1tiPsVn8Ad25D/gKsprA8g/d9WALHTVdq5AYVXF7C0an0mXWQ/DvgYE/utcVC2LXjCzifw7teBJyAs4NdCYSfL8QZ0+Yiq+zZwLBV8t1TGDg7FbZtMIaTY3AjyHdhsBdAWQHjEez2xeylXsJoIDl7i6X8VCFDL7Mw2JcavE4O5VSYQ5wwlEUmrpuq3hj35dirmRSOOSeGNwc7MJnBybmwfYulSE3PL2ReFovwXWc2n4vwYGjDtm++o+K1uwIAH9O2bv/OQz6xpu8G9d20m/ZFNWPMTxEefQE/ZowJX+547/3XPW7/a0GfxhukNV/K8KSyzjQvTgFOWqOntX5ZlsUsP83cU7ZuPB4DxMX62dkZi8UiLvJfeOEF3vOe9/D8889H50qtwdN6uNVqxXQ6jUCl1+tFJk3ZOq2v29vbYzweMxwOmc1mHB0d8dprr0Upozp5DgaDaP+voEEZO+1T2S0Fe2Hm4xh1PlarVWTK2sHjq9UqMoA67zoO3X4+n+O95/z8HIDBYBBNYrrdLmdnZ1H+qsD585//POfn5xwfH3N8fMzp6Wk83nw+p9frRaMevb4KpNJQeAVyKsFUwKdj0/NQR011/UwBcxq3oP3MZrM4p2VZRkCr561zonOYAmoFlQpqZ7MZDx484PLykul0yuHhIcPhkOFwSFEUnJyccHJy8iSfqV/0lsoK9e+G4ck2gKDrdxs2SiV6FspRTjXIyH7Lr+Jf+/1fRX62wMxXmNrhp3Mo1/jaYbodTJ7jd8eYr+lS7vZCfAQNcxQJMjeRTVPGzOReZJRWzFxsvXnYvant28hT1WlTsvt8ZOs0J89nJmHPaBipiAuob86Baa7ARB4XZ+KKsydmM3ZvA5rxTbYvHC6AOQHU8XoYwG2AqM+C9FQH5NM+TLwuMYaixdy12TxvfbTrR/u9jvFTUOCJ7GbaNud0tWaxDZbjsH0TPMYxKguk40nHnszZtnG8FdqXYwnaIIe20Yeta9c+fnwGEvfdbLQOTrwe5GHM6/ehV8KoBz/1WWHR9juws4LdASwcHE8hL0WqeWtXOu4MoegLA7daCSB64xjyM2G6CgNvPITdHQFkixUsLoU1yzow2g9gayLbLkO4+lLr8DryoONgDIe3YHdX6oBNcOAsujBdCWgbjAW0zQIYW6yEsRz24HAo8RTOwXIl+X7WiIR0gMRS1EFnnuWyzWIJqAdhfAAA1PFJREFUkxUMg4S125P6x6oStnIyh9MzODqGkxOReq7WMmYfqfyErUueGF3RWbd+SZm7R34EtqHHd05TeeebVKTdtJt200Lz3n/tl7L/taBPF/a6oE+dINtMTMzqCgv7tmumtk6nE+vatNZKZYZlWXJychIX+gogdnZ26PV63LlzJ7JZCih1LFq/pbLBNuhSSaKCwcFgQFEUEXhcXFxE58mUyUrjBxSopAAyjRpQIJKyZcowpQyXjl2NTHSe9D3dt81grlarCC41LkGZRwVJGneQ5zmLxYKHDx9yfHzM2dkZ0+k07qdzlDJ26ZypvFNrJfW4qYRWzVyULdSx6rnpeeq9ow8HtD916FR2TuWqq9Wq8ZBB2boUQKYsn96Dup0ynYeHh9F9NjW2eeraNf/ti3V97XVBCgB00U4Ajglu8VlYgId6PzPqYosMHNi+1L7YqsYXOb7IccMudTe7umCJC3m/Yd3YgLa2iQsEMBAC2TXWIUordeg27OT8Brcp4EivU1h7CSg0yb7St553Wq9m3GYwAaM1JlEzmNMn+2len4KirZlzCmZDrZyAYR1PegG4OpfXtcb60iT/PtnCyKTs5pZdTPvF5HhpjaGClW3GLPH91j4NMKo/74AF3RVM9whAd21T1xaz+dNvuW88UCH1r1LnVsLlAuwYuh5efR1O7sJqCCxg3IVZAHa+EufLrpcbddAXYHXpibLM+Rz8TJ4bZ5lEMizXYML3QbmWQWWZZNstQ9SBRT6rLpOBa/RCXsDOGIZDkYN2BhL90OnI+xlyf3sjn+ksC2xeBr4AcvkCI8Q/lJUA1Cy4gdYmxDn4IAMNEtMozbSBufMC+BZLOcfFUuSdlxMBreVa9skKAX1evymSC9L4Un3EtWy871v/tjdMrvG18Q5vz6ZGLjfunTftpn1xzRjzb3rv/4ox5rtpfXl47//dx+3/SPfOTqcT4xFSR0VtqWxPQUD6BEcNQRTI9fv9CCB2dnbY3d3l4OAAayVI/ezsjPV6HeWdRVEwGo0i86aMjxp/KBBJawtVBqjgQfsBGI/HPP/881Em+cYbbzCbzZhMJhHEad2fOlwqWFHQp+cFzdy4FAClY1Ngl2VZzJNLmTIFdZorl2VZHLPWFKY1g1qfqNl8Gvuwt7cXZZ1lWfKpT32Kj3/845yennJ6ehoZNZ2nyWQSHVp1DApQFaTpORpjmM/nDYOV9XodpbEK+IAIkls3YqwJ1X1nsxmnp6csFguWyyV7e3vxeAqu9Tqkrp7pfdi+F3X/w8NDbt26xf7+PrBxWn0aIxsAorwzWUnGWrVUkpe8pyzUxn5fzEO0uUwZJllX1b0cn0mNXmW6mMqRrcSpzmcWn9sIhIwTlokMWZTkPtbSaTSDcRsQaEvEmKVjGsAQL0YfWSibibV9JkRVlGLgQuLuiQNT++2ySF0jB7DnctMEKYS1l2lJHRNgprVvgLCKYY7S9V0K/OL10RzCKgF3AfxZNaqp0oNKXaOMNYBVvcQKNJPvSk96/ZPxJ9s1nERbLeYjpnOlY7fJe6G/BiOXAECv22h2YVrnlx5QxxXmSu/Ft+YC1kSm9onx2pYXFDRfu9HWzpM5bGAFqXXTKS1xFDXYuoZ6IjfT7Q68uAs/81l4uAvTEexPYH8IdSZRBjbQxSY86ejmAtQmC+j7IM2s4XgNvpTP/DNjAUmzFSyGsl2eCeg6Pg8ZeUEq3+1BF6CWiIdeD7pDYfl6QXJ5cBv2etAN+urdESzmArw6IWrCVdCxcG4kJ/BkAp1MtpvPBNTV4bti5YXltEaA5NqIW6fN5UOnD68UKE6m8Mpr0DEC+C4vYXqJravwEbbgS4yCvm3y5fTf9sOO9nbb9m0Awqufvbfkx+aLaFrTdyPvvGk37YtuPxn+/cEvZudrQZ8ambTdIBWgpSYeaqRR1zUXFxdcXl5G6Z4xhv39fXq9HvP5nKOjoyjh6/V6HBwc0O/3Yy6bgh/Np9PFfGrRr8dVIKOgqNfrNQLLAXZ2dqjrmsPDQ7rdLqPRKMoAZ7NZNDFRcDMajej1euzu7kaQYK2NJi0KhhSgKWBSgxIFOGm9HBBBo7papvWGytrpvCpzp8HiOtca0K6gU11Avffcvn07grjpdMqDBw9iVEZqGKNjPz4+BuD555/n9u3bcT51TLBxAtX5TmMvdL7VBVWBlYJGPV+9PikLqj8KFNNrmUYspDWLKTOo8wkiCdZrqQ8NBoMBVVXFsHvd72kMZ4cm4EvNWUwdFtPXSIbUtVLZIZc1WRpvoO6aAEosPt8AHhNcLtPaLZzB4rBl+DsLQeTG4EMmnuuEvL4AymzlyVeOYm6oO4aqF+SZBSGSIQGJ4fx8ZhLDlQ2g1PNQJk4yABOAtY1Ba5NYScxAZKScgE+fsmcBDEUAmLCH7X71+jTcL9PrEBwss7ULYw9GN5E9bA5S+4lMZcq6qQxUt03NZJJ6x621iP7qPnL85gOF9glGABjmLH3QoDWNbeAo503cT2tKo6vpO7W17883uV8KvMmsGKN4j6EWaGos5A6KAzibwj/5R+Ceh0/sC6v2m2/BnTMYjeAr3wtf+KQAotEALkMgeVbBnVvw4HW4WEA9lzq9KpfjTmewfxjy8NbQD+BtOAR7AYsulB24NZaIBmsEnPX6IuHs9kS6+ewdcft0AwGNahAzPxPQ6AAKWM4FcO7uyDbOQ7aC1+9JTl9vRySruREG8o1jkZN2MhgWAgRj4bDfPKGpV/D5V6UeMcvhwX04OoLjI7h3H+fCfDoxcPE+cHzKWOt1QF9sPYB75MW/5vPZ/u56ZxF9UcRxQ/TdtJv2RbfSGPMh4OiL2fla0LdNZqjyuXYwtgKfuq5ZLBbMZjPW6zWdTicGb7fjHXRxr3JE/d0Yw3Q6bYCUdmacZq5ty2pLGSuVdKYB3svlMga8w8Z4ph25oMBUQdt8Pm8Yiei+2q8CzzbzmL6vtXGp5DGtfVQwp/WMzrk4J71eLwKpNBxer8N8Po/ztFwuG32keYJ6XJ2H2WwWpbPKxsFG1puyawr2UsZQGT+tewTi+BSstQ2B9DVt6/U6Aj91IE3HoH2mDGral/6t27adZ1NznKetpQv56/LStLUNTholKNrHRpyEulS6zGCyjWOamtWlpiExA84ZTO2k/zpIL43B1wZrDbXPtjKT+dKRrQ3GW6quiXV7el7Gb8CX7nMFuLTBVAu8NBgvc3X/mBnX7j8FQ401WXIOCei+WtMms6qGOXGGI2A0IbNu00fab1sOGcFgwlpuZRFgK4BKGbat7yXnLEPwV/qMGYU2mdctHUY57zYzG713fXIe267rU95U/vqYJfsX03Gzo0exgFubb/zu2DB/VCsxY1nV8HwB01P43Ofha+7AcAG9Ap59F5weQnUJ1UwYNQ1IL0L4eqcvEQ/VJRLo7kW+uV7JtkUm4M8WEn9QZOBzkWPeuSsRD0UeWLklmEzko/lQ+u52YIXk/lUG1k5Y/G5HQO10JSyfccAaXEdYvWoN/V6oEexKFt96BeVKQOzShrFWYDtECr3oyAewqmR7V4o8dLGGy6nU8c0m4kZqC7koXqn+LZclMnrXXbD0s5Wi9kdcUpP+8c5qUd55w/TdtJv2xbY/fc3rHvgjj9v5kUYuqSQwDWvXhbcyV+qkqWYqCjjUNr/f7zcCyrU/BTR6PF386+JdQZPWlqVB8ApCUwml5rCt1+to2NLr9fDec3Z2xsXFBWdnZ5ydnVGWZSM7TsGaMnHL5TK6dwJRyqhjVdCi++k2Ol/KUinQUmmn5gG2pYn6tzJaKnus65p+vx/jDVSaqqyc1kReXFxwcXHB7u7u1rrGNCohy7IIeh88eMCzzz4b2TEFXmkungLT9XrNfD6PrKwC0clkwmw2i2NUoKkmODpXOh/z+Tzm800mEyaTSRyfAshutxslpXqdU+ZX5yr9W/P4bt26xd7eXmRoVXqrzq9PVfOy6ExDrhsZfS2JHRCNSdJ6tgj8jJFcZefRujNvJUcvq30IRfcbBkplfMr2RBDoMZXbvBb6tquafGFxmQ2h7ZuxGXys48sXRIkgXli/fOkiODA10dHT2wA4rNmYgaRGNoS1lL4W5i3m4oX5aoA6ndvkGKDAMJyjk/O6EpdBG4xv3tiY3GzAmytMALUWUznSyAk5zpbrmlzLeDxMAhCbADc9pzDZm3FlvgkmkwcC8dqlzaTg16Csp61bg0uO1xh7Ciit38SObHFofUe3bcxOu7UuTSO6rRXkWOkONVBNhdHKuiKdnJ/CqxWcfj2MQmbeM324/0yQTM5htxfuBQMmF9atGkFWS/6dKwUMrVcbANffCcHoRgxbLML65SO4dVvkk70u3DmAB/dCzWFP3s8UZMrDJNYeqlLonn4H6gKOzuU8rRNwZirwFfiF1AWajkgYZpewmAhDmGcytroWI5lhTrzpOx1x59QoB4MAwMk8SDsvpC8PkkdjNvWBsPmsRHSdPETZ9lCufU39thdbF7x5Wd9RLQvf2Tc1fTftpn1xzXv/7fq7McYCt7z3D590/0dGNmwzwNDA8VRWqcAgtcRPWa7UdEVZuvQY2p8CrtQRUoGC9qV9K5jqdDoREGjmXCpHres6hqhfXFzErDZlLbVPlQ2mLFUqJVTgoqBXGbe6rhvRDikrmp5/lmWNuseUfWqzk9r3crlsBMJrjENq6qLMpwLF4+Pjhmtqyt7puNPIg+l0ynw+j/Ooc65zp68pa6fmN6mxjYI/la2mEly9nnr+yraWZclqtWpIQNN7SucsZVFTk6D0HlTmMDWPSVnGLMsYjUYxBuRpa1vZnGsW0JvsubCdrkss+GThsnHOJEr2TKiX03o8AXI+9rMBjmE/Y4L0CVm4eY8twTuPsQ5bhu8FC3U3w5nU7TMEorP53ZbhWGGNJbWDAdwEKWk8t5Qde9QcbF100Vigpe6o17FQCrquvKagK8xhBI9W5m7DZm4G03CxVACmNZp4rkhJt4zlUS0F2uIDoQtJv73PtL9wbzTWnQmA3Wz3yCE8ut2s5zbtOrBwzabNF+Qpx6YLLxi+Qv7Lnef4bACf+Dk4vA23PfzPH4OdD8PdMQwW8KH3wP1deK0HezMY7Eqe3uUFdE7FYKU3hPUEmIVMOyfArTuE2/twNhdp5SiD10+guy+yzpN7ML4lLN4bp/DMbWHw8i5UewFMAnsjAXxlLTl6FnHeLNcw6Ilrp7Nwdx/uXwowzLtQ5kAFbi65fy5HTFsCC9jtwLAr9/c6ZA6ua5iey/bnE3EanU7g/AROz7HHx5jLM+o831wY71r3vn55vtkb+Qm2/7JSyW+9pkzfjXvnTbtpX1ozxnwb8EeBA2PMVwP/jff+X3ncfteCPpUbKouVgq6UpUqdLRVMqAmKMnTKyKRukAoI1NxDwWBqUqKMoG4PRJfONDRc+93Z2Ymh7loX98orr3B5eRlzAbW+DYiARseSxhUcHx9H0KfnpcygMnar1YrZbNYAp1qT1+12I8OnzFsKYnTu0vnWOUvBqv6roKfX67Fer6MxjI5fwdaDBw+iI+aLL77IZDLh1VdfjTWWOl/Kmk0mE46OjiILm4arpwYq8/mcy8tLJpMJZ2dn8VocHR1FwK3MbSrlTYF1Cta990wmkxhFkYJ/BW0pKFSZsbK6eu6r1Yrj4+M4z0VRsFwuuby8jOyhtZbRaHTlgcPT1Nqywm3MiYKdTV0XcVFpUhasoeULLFAtvzfYRAvemw3gi8cRoOOwUpOHk7o1gLrGrAODmNvNwsh18IMssHpibqKMovG+CfTYnGtaa9cIYG+fi65820yeHr/NqKQ4p8X2NUHlZsN2rEEDjOs+uk7MZO6s9xEcimOqbdTISW2fx9aBxVQ5pDI5ntY13SAy44gsXtpnrEWMkl4B1hizcZ6vfSDxNtLTKOFNziu6oibXRsgI05jvWFuqc5DMhQbWx8G8xdp1BNwXfSpbHjw8su8myb7pQ+ffhuuKl4cGYSdT15h6hqcQADU9g5/6CXhxH/q7cKeCOwayfaAD1eflJijnIuHcO4BBLYzZ4L2SXXc2CcCsEGmlRWIW6hVcXMJ4DGaALB0yYdY6wf1y4SQ6YjyCZS6SzJWTY2VWgGNpQtg6ct8NC5GB+lpq83IjMtLZQsBfgdQw2grmTmocexVUwSXUZ5Lht65k3Iu1SENdJZM4WYisc3oBp/dxi7l8rqwNFyJ52mHCxF+58NsuUOv6PtHN8qU8SXl7tE1O3y/tOG7aTXsbtP8z8BHgH3rva2PMnSfZ6VrQl7owKkMEm7qotN4qDWEfj8cURRFdP1X+mZpxbKvPSvPc0pq5NLw8rd1TpkhNOpRJUpfLsiw5Pj7m4uIiMkDeey4vL2M/ej7KYKmEUR0yZ7NZZO000F0BkwLbqqqYTqdkWRYlsPq+gpzFYsFisYiv6X56zLSeUQGlRjOkeYbqlLq7u8tqtYrXajqdRiCl12KxWJBlGYPBgJ2dHTqdTsw41Pk3xnBxccHnPvc5Tk5OeOmllyLYV6ZSJZ1nZ2fcv3+f+XweDWR0blLWETY1dVobqUBY6yTTrEc13wGRkk6n0wiSlZVVV9aURdSfyWTC/fv3KcsyyonLsuT0VPIq1ZhH5+VpazF43DUX/WndV9q2s4K6DGwDJmX5tC+DyyFTpk/BkAIC3S/EKXgAYxPQ4DcLH/3dWlxucYVIpbLSQwn53GEqJwxeHsxgMrOJSghS0ygBxWBLF0xQiHl9PpFDtevKttYEXrf4CmC0XYuY7pu69rXNdbYalIRFuW5bDkTyWvUE4Nka8oUcxGUkMk/Zve5YAcYkx66bgfC2VNAXxpLLgAzgswQQh3/bGYggADCO3Mg6OQK/pM7SJHU23m4eMqTHSK+BT/9WGenNE/w31x7FBKYPGdBPpI/3ufcO69b4LBenytmlSBo/+xocHML73wU7S8m929uD467U9rkQwTAYSZRDVcJ+LkYwRQ9mS3HEtEH+WHTCd9RKJJflAFxXTFuMjcCUEnHYzAvInNTwOSNgrJ/Fzz2FBZMJwMvCSVaEYPZwtnUNCy/AsXDyYSorAYQdhI2sw2vrtbCIVS1gsVzLObnw92wmbN/0HKoyMPqGTS2fv/4aYJqfgdZ3Rnz9idujdnr704Aq73Q33xM37W3SjDG/HPgryLfYFPg24BuBPwEsgD/kvX81mK/8VcQj+bu89z9qjBkB/w1wCPyg9/4vhj7/E+DrgVeAb/fer7naNh7rxmSEldvj2rWgL5X1peBMzU1Shi0FZWmMgdboqZFKu++UsUsz2JLJbOyT1nClRiFVVcUaOpX/LZfLBjCCpkxVgWxa1wdEhlHrybIsYzweR8YtrfNryzG1/iwdv4IQZc7aeXappFFBdrqt9pMaqCjAVVmlc47FYsFqtWrUPur4lP1M5ZA6B8vlkuPjY1arFbu7u7EuTgHnbDZjuVwym80iyNa5Td06dXu9RxT86o/GUIzH44Zxjt5feo9ovZ+CeiDGfqgjajq/q9Uqgvw0EkTnQlnc+Xz+VBq5bNZxmwV8WgO2zVwjbVfMSvB4nzCBsc8NYIu1cC3WLQI/x+brI9TyYb3U66TAx9oAzgyuEMfK9Hi29ngHtTWx9isMMvTTBGGp7FBzBjcmMF7ASnKu2+ayAUrS2ju2A2adwyu1c9pHAvzUp6ExBmXqLKHGMYzdyDRKxIU4qEb3T5vsExxXN5LNlnunSmQD8Pa5if0ryMoqYdr0PIxL6jZpgjky01y8KvvrPClzrOcWJbfx4UESJxGiQhoOsLBxDHpLts2Tgy+J7bvaXWxX+ty6/jc0wySTDmPRqJi7sCrh5AxmBfzqr4Kf/V/h7ALe9W6RRvYOoHsLTj3YFXTXcPsATsLBu1149lnYP4DxsThejkYC6Nx5CH4fw8G7BXBNgLmFwQBKB3YNtwawzmBlYFKK4YrtQ1HAcgmmFMA3deC7YgiTd2C9lEiF5VKOs1oDTrIGy1qcN8slHBj5IFuCcYsJtXsroAr3YgUP74MvoV5LHePkHE6P4OF98tWC2uT4vBsA6DqZ220Pgx4BBn9BSbsvy533VDZdI7i39HfETbtpjfYp7/1vADDG/AfANwP/NsLCfS3wXcB3An8B+HbgAfBDwI8C3wH8gPf+e4wxP2SM+T7gNvCM9/4jxpg/C3wL8Ne3HPc/Bn4MeD/wD8Lfj23Xgr7FYsFkMokgSA1RVqtVZKLUnEUX7KlMT2WaKWMFwgauVqsIqjS2QRmuwWDAfD6PbpmpTFABnYKutiukMkEpA5Vm6vX7fcbjMcvlMsoQtU/9NzUyUXBV1zXT6TSyZtqPZuopIFOwq+PSMQ8Gg8i8pYCu7aqpIEqBo54PEGvj+v0+g8GgEYugTJmOOcsybt++zenpKZeXlzH6YW9vj6OjI+bzOUAEa/P5PII+ZTSVnVXjlpOTE87Ozlgul1xcXDCbzRqMrbJxKTsMRJCsEQw7OzuMRqMIBtN8RZWsqoxVTX/0Pnjuuee4detW3N45F2MslN1br9dcXFwwn88js6g1fZ1Oh4985CNP8rn4RWvGe6hNBDQbk5XwfkRkgDNN9oWwKA+gUfPoomTSJMdI/hvrcoPxBmM8tnZ4ZdxgkyvniKDCZwaDxScyT58ZXCfDFRaXG1xHWC7pw0tmXRiXrQWIitFL8z/2ChpTealk//kNkApgJ4Lhhnw1ZZtorJdivV3W3EYA1GbOqcMcJn4OMZswXqekfzb9GSfxFC6Jw8iWUHdk26ofFAUriRFT8KfbuiKJdjAhX9HLNcznjs4/+ij/+cv/kB9fvMTf/J2/js9+9x7jwYraGVZljveGl/7MAnM5BaB66S4+s2SzNWYlphmul28Y2W446eQ6GNe6RwyNlIfrGDytyUxzG9/6zW/5rd0esyhPAUGbGXrsHLV2jt8FHq9PYpwP7ru6XS1AyGXwsY/C8x8As4Cf+TS8+9dIDp+5DwcDOC0l6qE+BTOUvo+XcmOu58KS5RlMJsKy9TMYD+V4F1M42BdwRRnCzZFj5x0whYxpUYnss6pEZlnkAg6xMOgEBq6Uuj6/ljFkHanHM4EF9AYGHiorP1MnDGI3yER9LkwdNayB6SUsZkAF0wVMp3D6EL7wWTh6CCfHVLWRMHdjwNcBOLugOdSnO9uovEe0NiZ/oo239ZuM4e3xQbq2ZdbcGLnctLdN896XyZ8DhJ37WGDnfiKwdgDPeu8/A2CMOTHG3AJ+HfCnwvs/Avxa4A7ww+G1H0KA4jbQ93PANwC3gGPgQ08y3mtBn7JwCuhUGpiGYytzorVnWmOl7pIpkEkNNrRfZZRUKqj96nFSs5CUEVLA1B5jyuykLCAQx6LgIq09SyMJUtMW7TeVmKYgJgVgqYlNURQN5jE1L0lNZnROdHzKLKbmNymLqueQgsTUlEW31zFqvaT+aHC7Xqd2W6/XcQ4Xi0U0bVkulxEYrtfryPLBxogmzdfTBwAK+tMaPB1bu2ZQJaUK6Hu9XpR6aiyEzqFKNhWMK/uoGZDj8Zjz8/NYEwpE0Pk0NgV+gGQyB0ZF3ty4dL65Tmlm1TXWMgGgWAVjbALSTTi21osFRskHoKhoR9xDw09uI4BSlknkhQFcxbWMv8ICqelJzA98xHlui1NIZZlXzEhIXmf7e3GtFdZjaQxBZLdM6710TAl5rGydrT2sg0w1fMPWHXDekC9DWD2bS5zKa+O51GBLx1/6zP/Ej8w/wI+efJD7/68+Y7Ni2FmTWUdhazpZzQf/5n2+de+fAfDnPvLNuMMd7OVcpG9Fjsn6+AJgw8wqWFPZrnE+5J2Fc4CYn5gaBOl4vZFcQp0X3b8N6t/KrYnR0r8ed44pVdp66XHbpYfSm+RRh4tYwclDn/lK6u/m5/CJj8FXvxfqHdi1Eoq+Hsh1PrmAkVLNRthCdb3sDaBcCAjLegHYGanFs4WEqHsL1GDyIOGsJfw8sL+YPDBuTm7maIgUvgP0vGwmcQ/WSdZffHgQtlfRkrPE3BdvBKzVlbB160rGXVby+molks7zUzg5gckEU1YigyWMzydPeLZRr/G1IF/dSCkam73p7+XG9r+gdOFT2zJj3tpigJv2Tm3PG2P+Vvj9+733369vGGN+O/CXEJH73wEuk/10lZB+4C+AA2Av2TZ97Y3Wa9va9yDS0YfGmF+NMIm/83EncS3oUwZI3SnbUsu0zkoX9/qjEsI0905z4NLYAJX3LRaLCGTS0O2UDdPjapSExiooeFPwqMYei8WC8/PzxjgUvCmDl56XGoGkYFYBldYhqvFIyrSNx2MODg4awFDrBAHG43EEXRcXFw3DG2XWnHOcn59HSeJgMGg4aCojp3Oh5jFAdCxNr4sydOoWmgLSw8NDjDGcn5/H/RSUvvHGGxE86/wq26vh8Tr3CjQVuOmxU4CaSk0VwC+Xyzi3FxcXcd/hcBgfLnQ6HcbjcQT2QGTyZrMZRVGwu7sbJboayq5GLrPZjJ2dncjezudzut3uVqD7S93a7pC2dBvzuExsG4wLoEiBRypFAmJdWVhRNECU25hsmNpLzZwlxA5YWLsY2RcX/pmVhX4mK0oPkBlcCGpXSalTaWcIbgcpvYmslQ2B8NbE2kKfmQR06iTIP1rfJuyeadTYRQC2bd7ieYf1ZfoUeRtITGSm3hjIfJPlg1gXl47BBDONyBK22UGzea1YO7w1rHYMdU+Y06xM+mst8kXOKuOxtcNncPzLe/z7X/U7+Gsf+3u8sjrkXf0LnDecl30y43mud05hap7vnPK/LV+kMBV/5X/+m3znV/8evBMWw/R6uNs71MOCbFGRLWtcx8ZswYapi/MY58B5fCWLXV9YvJFrrLWAtnTJvaUXT94zVf2WA37bcZlczCci567sl/a6/c/t+7S3TW/IlAoPb0XptcHUpXy+8p5IHo9eh888gPd9ABYvwocO4PYIbIhB+OzPi9tnvwfDsTBu67WEnw8PhFUrS6h7AsYGhZi4FD1h2no1zC+RpwkZXKzk+XRm5MdkQC3garUC05MPcLWS962FrBCjltoLaJvPpE7PuVDjHD70OcIirlci+TR52GcFq2mIsAgyh9pLXeP0As6O4fQIsyqxGGobDGd8HdhKnddHga/wZGTLy2++PQnIe2t9dr6YZsyNvPOmvSXba977b932hvf+R4CvNsb8u8BvAnaSt9WxMXUl2ANOgbOw7Xl47fMISNxpbbet/ZvA9xpjvgf4NxAZ6GPbtaDv8vIyLrCBKE1UsJPW8nW73bhAT4PYdT/N92ubwaQ1YQoUFWDosRQgqeRRmTiVnCqIW61WHB0dMZlMePjwYTQcUXCXso5q7KGgL5ULKthQAFlVVZQyau2YRinUdc3Ozk6URPZ6PWATlq51ZppVqKBDAdxqtYogU0GMji8Nslfgosxmmvmn8tPBYID3nqOjo0ZUQp7n9Pt9yrKMUs3U+CVlIe/fvx9lqfqaHit1X1UAr9dZwZW2ds5iCg51blIHUwWFeh1TZni9XjObzeL11wzI1157Lc51KhPV+1MZ1TSyY29v70k+E7+ozYeIKVv5yPKp/LDdjN/IJLcGutvktYS92uyPsDHWXFXrbflvcARWRhm9pG4v7CNGLRuGyhUmyK/0oK2+A5iruzbW7dlyY+jSrq27jgGMckR9+J+6nT6OkLlunWe4AqivhJIHQKvyR81XVMAKBKYTbOXonSkwFslq57KmmJSsDrqsR5Z8Ja+968++zNFyhPmuAybv7vPwG1cMRxNWH3s/f+g37vNr/oeX2c/nrJx8Drq24oP9NxjbBUeV/PehY2p6xuAXS+z+ntRcdguM8+SXK0xZUx4OqHqZEBiVmOl4DBSWzIFxNaZ08hOkvVhL3c3wRainDsDfrt0m59EHMJ/ZBvv51mnXgLVfzMNfy0Sb5nIh3t+BMXNrvLF4m2EXFe7V16QG7yu+Ev72fwuf+Cr4F34T/NY+5DM49PD+D8HxfThbQnEKZiEGK+MRnC/kxjcWekaAYq8L3bGsGKpgFdwbAmrm4uH4AoZrOfZ8JSyizeFgRxw0ay9s4eWFvJ53wC2a55Jlwg4ullLLl1lhHqtKAGq1htOLYPBiJbZhWcl9uFjC6/fg/utwdA9eeRlWa7yz1KZIwJ4LpGPrS+BaTPaLdU88KZP81m6ZNTdGLjftbdOMMV3vvTIKF0jl84eMMR2kpu+j4b37xpj3IzV9B977Y2PMTwK/A2HtfhvwryOPz/4k8NcQ5u4nWsf73cmfPwz8ZeDfQWoI/+7jxvtIpi9la9I6ueTgjQV/avySbpOyhKl0MWXx0iiHFFRqPZcyfLABCp1OJ7JO3ntms1msB5xMJlRVRa/XiyAzPbYeX89B6xPTWAU1FVG30DRuIWUbB4MBsAkLT81e1GRF56/f7zcAbQpQ1OlSgXYq7dT+VbqqP6lZim6j4FlNdxSgKiurwEnHpA6rClR1XrXGUo+r86UAXgE5NIF8eu3T665STj3n9JoomFSH0jQUXucpBb7KGqf3ocZ0dLtdptNpdEnVHwXlT2PzxsT6vesMRyAAnEz3Ca89gdqswfw51Y4FKWKUkj3i2Kb1r+fqIskYXBaMSCyyUFWGTmVdwRjEZ4EpVLBmDHUa9B7uoyvGKWkLgK9R16djVDWWHlt3saZhkHOFFTS63abvxoK8Je9S8CkGKnIsrZEzNeSrENZuBQx2jhbYn3+N8uvfT9XP+Pt//j/lZ8sB/3z+Xpbjgh+ffjUnXzUgyxwHgwUf/ksv80dv/Rj/6YPfzvO9M+pAjzpvmNQ9eka+Ew/zKXt2zq7tYfo93K1dTOVk0ZtbXJFhy5q6Y3GdgFgLg11vjGN8bvCVwZY1pq4jc+c7BSa3+EKAItlmHkztNg8bMpH6vtWYvk3zW37f3PBvmvV70o29/t+VD5Tcu8aHt/TmTEeSfnhrYegrC7M13HtdTFyOXoV//E9gnMO7x7A/gOfvwKIPF5UYqXSdMHC5h2opTJy1AsC6XamFW603rzmgNxIA6GqRWVZOAGpmJP/PdoTNqyoxdFG32TwXn7vlGlFDIee5rkJfXsAeuXxnlCFWYrUUV06tMXVGfspKahBPT+DhAwmJPz/Crpc4I/WsYCTYPXyoNx/7bVf1mu/At+pt/ZQ1awz1W/Y74qbdtCvttxtj/jTyrXgE/OHw748BS+APhu3+DPC9yH9B/1x47XuA7zPGfDti6PI68Lox5r4x5seR+sDvbh3va1t/fw+ber4vHvSpU6M6P6asjhqMKBPTBnKwAYDL5bKRxac1Vso0KVBqywKVZRqPxxEkqVxRZZg7OzusVium02k071DAt1gsGA6HDQCmY1MWS0GCMonqdKkASg1Y1CBFwZ2erx57d3e3kR/Y6/UYj8ekERDK9qUh5CpJ1eNrpEMqa03ZVGU2034VJOrYR6MR0+k0OmgqQFIgqazY2dkZVVWxu7sbz02jEtbrNbu7u9HQJa0zVEZzOBw2jHTaeXqp46uOU1m3NtOrP+ryqfefSjbVRVSBYAoa9UGBtZbDw0O63W4062kDvqdV3pkCDzFZ0fcgSiFTsKUM1xZA4tPt9DWbLGO81IqpHNNUm7B2H6Ri3pqG+W80W2nl6Jnab3cYDX9qFICcmNSQOUvDPdRWm3Pd1B/6OC/ROEbliGxh9PR3E0CYMqaVoykJJRgfyi/qmukTxX1DUprOnwIjdRSNx5BtbLKQTF06+8dL7Muv8d0/83f5THmbmevysflz/PTZPW57w3fd/y0M8xWFqXHe8KHv/TS31yOmZRfnLQ9XY77r1d9DL6t4sN5hmK04L/s4b1m5gn625tnOBb+69wU+UBi+6eu+CZ4ZM33fDoPX5jDs4Aph9lxupQ4PqHuWuhOkt7UnWwlzl81WmNcf4CYT7GDA3/3k/8TcrfmXPvgv8AOf/Id80wd+M7znBci1PjA4GVdugz8e8dDi6W0NOrrx95s6m2s33gYu2h/gFtCMb7cAYZvFNwbNm/M4iVNYlPDGffiKHZg+gJ85hjvPgnsBOnfhOQtHfZF0TmbizqvxCPUS6MuBvBO2z3hYLiDLw8OcTOr96iX4UsBYGcZiPXR6wtrZDFYLMWrJjNThFR1h7NZrCXDXGr+qDgFugfHLCgGGZS37zRchU7AjoNAYEU4tV3BxDsdHcPQAjh5gZhfYusZlhXzwnRdAmU49bHlituULtPF36+V2f4/a56YB8ozohum7aW+X5r3/QeAHWy//jfCTbvdxxHglfW0C/N4tff6p9mvJe//RFz1YHgH6UkOTlLFR0JCCr1Ty2TYZSSWaKQuU1pqluXBAg1FLTT8UVGgen4IplQumNYEKIkxrAaL9pZEUClgU+KVAShm+FPSmssX0vNIaRd1O2bYUOKcyVx1jGu6u7/nWF2PqzplKGrX1er0IyjT3T6MPNAYhNVtRIK2AVaW4+q/OnbKBypp2u106nU5kCFMwpnOc1nimLqip4U6b5UvrP9N7R38UrKdsbJrBqA8K9EGDsr96DmmcxlPZUpAHTbBnuCJ9vNISILjV9CQwU56rgeyN1qqli2PRPhJ5aQOABnar7RYKRNlqZCqVwHCb8cp2fsOg6Zyk8xNYs621e9edT9KVdz6SJho4LoY0yTF0twD2IuDLkmuSLro9wSn0Kih1ucVfXPL5ap/Xy33mrsN5NWCvu2BedVjU8jCmn61ZuZzC1FROUGjlLZWz2HAhS5exMjlrl1M5S5lb+uFQt7KSf/F9/wL2hS5u0MGuPavbPUzlKaYV1F4AWrKWtTEOQq5NNltjL+f8Bz/9w3x89RwvL+/y549/GQO75j/66D/kb0+f4fs/9Q/4tl/ze2HQx/c6uH6BR9g9U1e4Tv4mUdLT1r7EwXv/iC6uA35P0lpAIu7mA+gL7BdInh4Z2AH87MfhmTvw4ovwI38Xjr8G3vgg/BYLX/E8vHAAX7gNH/s0uFIAnF0DXQF3KwfuQoAcHpaZALq8A/fubWr4CoM8bTGQd6H0UC/ALIEVzFQSmsOoF5QApTiFrhZBvhmy/6pK6vWycN7eCEDs90MtIDDoiXvoYgr33oDXPi+yzgevUVycQllSFl0kdqICt97g50hTXzfHN+0Xsom885d6FDftpr01mzHmP/fe/1FjzE/RfDrlvfdf97j9r10F6+IaaAANpOcI2tKaLTUzSWWaqWNnmj2nEQgqV1QAkMYfaL9pfZcCPTWCUVBWVRVnZ2ecnZ1FMKNgKq0py/M81uopKFksFnEfNa5RhlHZs1TuqoyXghTtW+WgRVEwGo1if0ADpCjo07rElB3Tukg9Tirb1Nbr9Rpgcj6f0+/3qaqKvb09dnd3yfOc+XzOYrFogDy9jsp8rdfrWJOpbKz+rpEUys4p2Nrd3WU4HEbpahrfAESH1FSqq+BSjWeyLGuE0yuTqZEY1lpmsxnr9Zp79+5F4Le3txf7t9Y2QKea7ej1TZlUZWOfttYAN54G+Gjk2l2zXzNWwG/AjzJS6T4usGCauWeDk2NyiA3gNMH4BdmnVrBmIvDTXD1TC9Ol4AGEEau7oe/ai+prHZ7oe7OJlUiat4AzkSFss27ehNiHunWuATymElYI56eRAgnYxXlsYBPlGJu4iXROBRAmoC+Abm/EbNB43xwHYW6cAld5KRsNmdR95q7DvO5Se0PlBNAdrUZYPPvdOf2s5Lzss6xD/WpVUNYZ3bwKp+PYKyrG+YqVy8iMp/QZc9cRk8N+Dz/sgXNiCFSLk6pdlphVDRbqYVeGmclTAW8N2Uq297nFD3r8u5/5l3HeUHvDusrJs5r/wf5K7gwmfHT0Av/WP/kxxnbJS/mU73jpN2E7Bfb2LcgzqncfvmXXznK5fAuGbZ4mpHj/iz/Ck25qNgeLD370w+la22p+SHII76AKDNvlHH7+C3D3eXjl43D2QOSYv9ZIXt+LA7i4DZMLmE9hsI9IK4MxiluHeIUSsp0gy1xDPgCTyXErD1gxf7mYhRyTlXwIxoUwd95If7NFMGTJxHRlFWrycicfLBeeDJVLkZQuS9k2z4Umqqsg4TyFkwfwhc+JacvFKeb8hLr2eJPLeOo1+FDD1yjm1adVJF8cyVtpaOjW9uVk8760u+qt1m7knTftpn3xzXv/R8O/bZnnE7VrQd90Oo1SR3XH1LqwFBQokNGFtgZ360I8z/OG8YjWjq3XEjCvLBtsWMT1ek2v14vsjIJBtdxXY5bLy8sYJXBycsLFxUWsV0uNSoDoCpnKDGezWQSmGrCuABKIzJa6ac5mswg69/b2GoBPDViARi2izpfumzpdKnhVIKjvt2MQjDHxvHQeUsdOHZP3nr29vQjAFIAqAFO2bDKZMJ1OY82ezqsGyacRGEAjLw+IZis6l3mes7+/32D7gAjs1AwHNrWbCpL1uMoGqqRTf9I4kDSGQ++VTqcT506BoPahDLOCz6eS6VOWqMXkmUcyBs394/bpywEwiRtkiFConJjEeNNQ4Kl8s90Htg0q5XhpxIBKKk25GQsGfEHMEMycgIuqnwWZKE12LX1WpeA31I3FHEIdm4Iqkvd1cJ4IdPS8dG6M85GhNLWAT2PMhtV0NoIhg2+OKV14h8uS5tIp4EwZUFuLNDX/+Of51n/2ST6xfBfzusPK5ZytB8yrDrWzlC4jM46LdY+Z7bCuc5a15O85b1j7jKq0OG9Y1gWVy8htTWa8gDKXs3QFP7ve57v/tx/iT//qb8TcOqDrwDiHma/wr9/n//eZH+ebfv034YsMrMGGfEUAU4m001QON+zyyr0Ro90Fdb1RHYx7K+ZVhwzHeT1k6Tqc1wO++efuc1yNmdRLPtx/jf/yj38Tb+W2fSn65VigPqqPaxb9nsDihU2usHzpdu3XvDBceSEsWVXCwaVk8c3m8NFPw7CA9zwHL70AB0MBXVMTVgUuZOtVCb0dPixVJT+ZhU4hbF0HYdVqBNgtVwI8M5D/SwpB6/CFZwtgEWruvOzng0zYW+nPZpA5YQBtLXV50ym8/jqcHMFpMG6ZXgpgLVc4Qnh7cOo03ieg7xFzqfMYn2Y94pJ5n4Dw6zb6/7P3p9G2ZGd1IDq/FbH7vc85t8vb5M1GTUqghkZ0EgKMn22EMAYbZBmX/Ywp21S9VzVq2AWuGmUM9itXUR4DXtn1PKpqlE0N3rMB2wgKEJ1ojC1AIANCHY2kTKVSyrw3b3P63e+IWOv9WGuu+CLOPufezLw38ygV3xg39z6xI1astSL2zjVjft+cz+Xph74H7iWgPH1hGiGXJpp43iEiP4Bjfhycc//NnY4/0aePi369mCfI0ItozdDpWiwCD80UUQCFC3+m5em6wCzLohk8mSnWDurUPqb7EWTqNEGdGsk2kiSJgIlggMfX/fu0x5+uVyQzRbCkLSDI2k2n04qxPP+xJpGhP9dgSauTdjqdOFaOpa6GyfRLDcJarRbm83kE17ym0+kU+/v7SNMUw+EwGqVTOZPnZ//0P/aVIL6ekqvTNbWvIu8X9oGAnNeS6bCaxQVQAW88/3K5jPNIcE0QSlZPs7Ksk9Ss6WmLdXVxRwRV1HqAapX+T3dkH+2Nx/WNEPgFAFaeXB0DqXrRqab1g+91aaM6xVPX7IkXy/O7EegJgj1Brb3YDlk4WZumWq+DrP/8xVRU/TSZZIjRx7mSWTSumk4bstEiGCfwdGHGA1j3lg30vJMSuFoP+r7zdz+AD88exkHew8qmWBYpjDikJtgehM7mLol9NOJgjEXuDIrgT7Ysynu3m2RIA/3I191iiAwJ/vWHfx5/+Q1fj2QwgJvN8VN/8Mv4+ze/HP/3p/4Mfvq3fhrf8G1/Ey4tr69ZWZjCekGWIJX/8L9J8Ol39pF0ChTLBEmnwLnBDA8N9tAyBVqS41wywU4xxOPzi8hcgqVN8XvFK2BWtqrw+jIJUYvyO/Iy6z6M3yn9peK+tQNisa76yNnyAcU6fHCkzfBHPgeSDuC6wMceBx55GBh1gA/9OvDkx4FXvgL4mrcAb/5C4PIV/+X55C2fOumWQH7gzdXTVlgtLL3twmzprRx6Xa/sudnz4CxpeXZxPAa6bX+cW3nBFZf6zzttf6/nADrOg8us8AxiUfjPrAE2+0Cv7/eTFNi7AezvAZ/6JPDRPwbG+8DiwLN8kwmwWMClrfDjYoFihSQwtxbwILL865hr58oJvBPwOzrhbKT2+UnHHvc3T/7yA0fep+/lN64mmniRol4/+JzixPROnY5I0AWUQiZc/PP9crnEdDrFdDrFeDzGYrFAq9VCp9MBAJw5cwbj8Tim4c1msygKwn00uCCAq6c7LhYLTKdTTCaT+DoejyOY4+JeK2cSjJC1arVa2NjYgIig3+9H1gmoglidOsi0Tp1aqevQCFiYispUTS1YwjHq9EaCFwIjpkKyL0mSoNfrxVo97s/aNc4PQSzFWDhOztn+/j6uXbuG69ev48qVK+j1euj1epFtJGDkqwZ8WnyFxu3sK2vwOL+cN235QbZOM6AEZDR/J4Djfaave7vdxmAwiOwswXtRFBgMBhUPQACR5dRAj/fYaQrPEpUgRgOysqYsfMan/jyOa5jYGKKHHFk5mqSb3KM2V1PItMb4NVJCBKaM1gtAclsRZNEplxVBlsIpw2/vLVieMxxQBE+/lkTGTAM/gi7bKgFfHJd+Si+BrSwU4xaf0gf2LvTFdxRqsc3PNdXpxW2ckbgcpKeht6pAlekj4FOprLF7oW4xySzaH7+O3568Grk1WNkUk7yNlU1hnaBtcuS2jXYSHoZYgzys6o04tE2OTBKYoHi4zFMUTrDMUwzbBqmxWNkEqbEwMsQf4CpaUuCDM4uveO9t9JNn8Kn5BXzHU1+HncUAh6sOXv8b34H2W3sYXHfo38qRLG3w2C6vr1nk6D+1j8/7X1p4/K9sQroWdpbi69/4R3jHxgfRF2DmgAKCL+5M8f7Jq3BjvoG2KXCuN0WyyGGHpy+N+iWP+iL3jmAi3ND6aYsGIWvwwBF+iOfMV4DkQKsH3LgN3N4B2h3g0hZw61PAD30M+N0vAR57CHjsKvB5Xxh88HLg7BR48lPAZAW4BBg6oNv1YGwhHuClqWfkhv0AEBNgbwXMV15Z88wWMOgAruWVRRMDIPd9ym25HwRodTwwTLtAZxFEXJbAJz4GPP1JYPs6cPNpYGcPONgD9m579g/iASYQahMdRFw0yPKg2/o5iSn1J1wEV79IJ4AUkaPXt4ljw0jQ62miiSaeczjn3qv/FpG/7Zz7p3d7/IlCLtoUnWwUGTIyLUBp7j2fz7FcLmMKJ4EMGaR6OiiBj7ZO0K9aKIULfwLM+Xwe1TppNUB2j0yfZtAIsBisLyNDx75qRlKLhJCFouIn50czUzyWgIxzp8VldMon0zy1GAuACgjUlg4EqGS0CPo4N2RAtYWG9j/UhvYEr1rZlECMNXW8tpqF1WPR85nnOXq9XgS5gE/DJfDX86BVYDk2AJGVrAvaaHEdXYtora3UIHK82tieAjVUBT2VoUBNZPDq/1N0VVaPFgGVmjOmRAY2MPrHhfqzCIoAREVAQbBRKA3TWd9mnAMSqZYQkfkK7/VrBJqATxnM+fsRzNZRgkFvPF9mU5XjgrcOiCDqhDlLUBrPE/g5dxTwodzHFa4Ehnp6WcfnUILX4EHHAZb7cNGImKpaTz9NZjn+p/f/DP5/O1+JzCWYF60owJLbBBaCbpLBiIN1Aiu+zo+RB+SthVwAb9Vgw+TYUBs4zTtohYmiMMwgXWJZpLAQJMaiZSx63Qzj1y1QtLvY/GQGyQrYTopkWQC5rwFE+O2Q+QqP/OIKn/6zLdiuw39+5iPInCBzDmeNgRFBV1J85EuAn3z65/Fj41firb1P4oPz1yPpfFYa9a0J9QBG3Uh3zWOehBnuyCLVbtz6cfX7e11bmkl0wVrBWp+CaQ2we9ODtqUBPvlJYLoL7FwHXA84e8YLp/Q7wMWzwGQJTCwgPk0fJgFGvfDwhP8SxC9WKsFSAd5HrxPgVy7AKgnWDMF+od3x4i+SABsDv22ZAfNDn7a5t+dZyRvXgP1tYPcWsH8ATCeekeRYRUI9YFAArc+NUz9Qrn5x1KQeq3ClJ/a5xhFIfkKs+fF6GUWT3tlEE/c0vgnAP73bnY8FfWTc6uqZy+US87k3VNXCLoeHh5hMJtje3sb+/n60BGi1Wrh8+XI8llYQs9ksio3wHDxvt9uNzNZyuYxsDoBoz7BarTAej7G/vx9Bn07RJNuma8F0nRrTG3u9Hvr9fkzx7Pf7EQTq2kQCDoIJWgJoM3rWDfZ6PUwmk5g6yrREbV1B4KNrF7n/4eFhBMgU1KGhPFk1pke22+0jIIcsHM9LJnQ6nWKxWODixYvY2tqK6qQEaQRbDzzwQCV1Nt4s6noTbGoBGs6ZZlp5DxG4aaVTfd5erxeBNGvxNOgnKNU1iiKCwWAQ00c5ZxTlMcZgOByi2+1ic3MTk8nkbr8XL1pUxEqkuv047zm/gwJ5QQ2zktZpPeAzWTDgFoFrJyU4sWQFxds3hPo92w4G7M6nZnpg6aIwDC0OdPqpyTy7RwzpJNS05Q6snXOpwMJAVjYCJZsquwSH0AfFILqSTasKppT7WKCSYsl/lfTWsCB0CGAumMJXWNWkmkrqJLCfUk15jYxsgsgWerZMgoUFkCwL/MRP/Qv8r3tvxNy2sSxSjLNOqMkLvz/GYmFb6CZB1CmceBnq+RiZNfHSF9ZAVKpnmnqbh5VNsJ/1YMQitwk6SR5Bo4FD2xQ435vgbHeG7oUMr/6C2/jP/+Zv42++8mvRvnQRrucZcMlyX1/lHLDK0PngDl7zQQDtFv7yP/hGuIkXbPq/PvkbyFCgcA7J5gbe+Ya34af+8FfxTQ99FdIHdtGaD/HyCnfCX8/p0Lv/jDvUmPyjx/m742QYEkBZsfCCKkiBhYP5zFNwG0O4Bx8Ftp8Bbj0F/HEC7OXAGz8fuHoFOP8A8PBFYFoAN6bedSrP/b1/ZssLreTBW68wAfNZb5weRJ8wzgC3AMQAKwCu7fdxhU8B7QZPP6TAxXPAau4VOXf2gZvPANc/DXzsQ95aYjYFJjvA3q4/b6w1JLAtBc+isyKBr54PQ3auRqPWr0mcczm6LZ7oboHccVdpDW37MgV8gBdysY2QSxNN3Kv42eey87GgT5uFA4h1eEzt0/YMq9WqAixmsxkODg4q9XhkmShIolkmin1wQa/BBQFhnufY3t7G3t5eBEWr1QoHBwdYLBaw1saaOqBUuySzpf3kCJi63W40IacfIcfNNEUCm06nE9MLJ5NJTG0lqOp0OtjY2Ig+fbRmoIALgRhQsmUEgQR9ZA53dnawXC5xcHAQU1+ZhknBGNbs8RpoIMZUT6bmbm9vYzwe4/DwEP1+H1tbWxgOhxApvQZ13R2Dc0zAqoGbrqMj88l2tDgNQa/2QySI0zWTfLCgayDJJOrrQMZT1w8ytLoqgXC3243A7+Dg4Ll8N160qNTYQTFJwNFFnyYAFNNHAMjUw2RlPeiyrvTZI6iJaZghlVIxd9732acomsx6Vo7bWduqUi+jRQTHENp3AejBBkBJU/bALDIN0qZSrtlEgS8b+qHmohRtCX9HltKPUYLKqGgwh5ByKfQURPQojCmt+lqIP77ikRjAZx2ER+uJkOqZLH19XDLL8QuzixgXXSyLFPOi5e0Z8lYEdNZ5Bi4L9gzLPMWqSDBdtjHoePZimflUUCO+/4lxSIxVbRjkzsA6i7Yp0DEFekkG6wz2Vz1YeCawn66QmgIb6RJLm2JpU/zHxRX8vU/8Hv7HV30x0oev+mu2WEISA7RacNMp7HyBn3/yt/END74JptuFdDsQMfiWR78SP/vp/4iPrAr8ww/+Kq4kSwAd/NIzH8DXP/LlkPHpe7jysoyTMEQl+B0yvnYTSzjksKYDzArg8SdgsgXc+QfgzlwGPvr7wAff59MlL74SePvXAJcfAs5dAh55pbdxmK2AyRTYSjz4OpiGNEuHKMISs3xbHiwaF4RZUu/V12oD5zb8voUDDpbAh/8jcO0p4Ik/Bj7+hPfem+wDGy1g+xYwGXs7h7wIX1ZBBJA6hfvY+VC/U0d2Ck+tnhMb90LijnTvyzISIygapq+JJl5QiMjXhLcfCO8zAE86526edNyxoI8pi0yPI4jQIh5k0ugBp73ydA0YgCjqopU/NQggKCPw0v55ZJPG4zEODg6QZVms6SITB6DCGBII1evpCBTqfns6pZSiK1pchOBLp4EStGhxGS2cwv4wlZLgRadLcs4001m3FtD9r9fbaWVLvrKekKwq/y2Xy6jUyTpFAnKdqjufz+PcMmWXQL/dbkePPoI7pvzq+aj7JtaZVoI+rVZK0KavE+8XDdx1sA2OifuwD3o/bXtxaiKCp9KQPEZkneQI0xWPi351pYl7tGZwKM3WgQhg4gNtkSpYc2WaJNNDj6xLaiDUIazxNJsWnqLT5F1bHawFWnzqLuUYjuxDkRR9eo4l1jq6inWDFqkx8EQAFGCt1PSQgV63FlHsaaWmUV8vTQQsM8ysZ88sSpDXNgUsBPOs5btrDbIigRGHzBrMVy0s5m1c2hgjKxIczrrIc4NWq4AxDqvcA8XUWBSqI0YsNloLtE0e0zvp8WedwbnOFFc6BzibTnFQ9NAxWbB66OAnn/5tvPONb4fLc58a1+1AkgSu8L9VFg4/9czvoCMpvvFVXwkZ9CG9HgwEH1g8ioFZImk/i5EJ3626ncBnaYRvzAtb2j/fg4/DAmu+/zwJs7fjd7yyT9gvfk/CTkVg7ODgrIGbzAHc9HV03Q7Q7QN7t4D3/TowPAP0LwCPXgDOnAc2zwFnLwKDngdxG5tK0TMUwdpgvG4FcMzDFl+jt1oA0ylw/UlgeggcjoGnbwP7N4HZrjeUv/0scDgFZnPI4QxuMfUqpAUQnxRZC+GPlaDEfeCTo5Mm2ZXzUfl9UQyhrf3grSPlXnB8bgEgb87+UveiiSY+6+O/BrAF4PcBvAnAGMBARH7VOff9xx10IuhjPRjBAVk+ghzWahE0EDzRGJyLfgqvEAiSTdN2CfSLYxol69WYLki26/z58zg8PIxgAEAFWBBE1NMHaaeg6wdpZk5WCEBFqZPAb2NjA/1+P7Jd/X4f/X4/1jYSCJHVms1mEbhRhKXT6UTQq8EQt+laSaZ/cl4IMPmq2TACMrKlTJ3d39/HM888gxs3buDZZ5+NQi6bm5vI8zymyM7n8wo7mKZpBehNJhPM5/M4HjKWZAo5V7p2k6FrEnVaJwG5FsNhEOQTnPL68dw8hwbrBMC0fKBgC+eHTKVmW09NOIAWARrwVbzojks5YhMxHdKrgJrMIVlZz+S1TVlzxjVOjTHT7ZLRkgLx/8zRqiGkR3pDdcWmkW0TQCCwBjC2TDmLNXpSpnMeSa9Uyp6xO64cWyUN1pWfAygtJIzAtkLfa3PmsyprA47AVrGJa+YknpNjD/0Rp9g/C6+CmVnI9dsAgJYU3pPP8UGGg4lrSfHKnADaSYGHRvuwQ8HBP3kIf+Of/ybevf1FePrmGdhVgkwASSwgQKeXIW9nyK2Jfn791CL17vBIxGGctX29n0vQNjneuvE4LqUHsDBYuBZakmNh27iVj/Br8wTf94Ffxdlkgde0Bnjb1S+BtKb44cf/HfatwW8v+wASJLB41xP/Ae949KsAAJ/IFvjVnc/HI/1ddCXDlrmOkbGQdhvm4gU08SKEW/PniYvp+gGF32QEDqm3WVgtAbcCkrNAKwEmu8Af3gCkA5gN4KkzwMOPAA+9AngsBexGMFXvBKP2YLPgrP+NMAFcZllZbzffB6YLn675zBPeN3B3F3jiOrCYAWYJtBbA4QEwt8Aih4y34YwNv10tNZ4CLNOrZmeumYhjiMAy6ojZHf34BYVG7W7NNr395RtNemcTTdybcM59LQCIX4z/DIA/BeD9AJ476GM6HtMhuYBnSiFBofap0+CJYJCMIH38uFjXsvtkpJgWSYVHzQzp2ra6OqRmjXQ9GPvJWjhdU7Zuu7ZkqINGjpd1ewAikGB6JudIe8Tx2Ho6K5k2rZBKEKwZSwCxTk+rbGoQQ3ZPC5wURYHpdIrDw0NMp9MKMOQcE9iSweQ8sn0C0nq6bbjJjtToaXZP1+IR5NW9EzVbyfb5qm029Hn0/Va30iBY5PXTPn9kKk9b1NmjuNZQa44jQiciVQEXvidwjMIuUqZVQqref2TIdLjqq0uNV9cDfEqoEaWFcPz/tGNtXe6ikiYUw1f3/gu26WWLtfFWuljLwKqwei60nbgSoCm21J+PG44CPr+D7qfqj0PF7qI+32IdJLMwqwJuMsXKpUjEomMK5DaJzJyBQzfNscxTr3IKoN9a4Qs2ruHto4/g7+9/O27km7AQmMR53B0oHElCinph0E0FG+0FUrGY5d7/r5dkeP3gGibdLg7yHp6cnccDnTEebW9jyyxwuxjg0HZhYHE7H2HhWmhLgVUyw8Il2CtmePfT70fmCvzS7AoWroVpYCwfSA+RuVtwhU+ja4vFZmuBw7yL69kZfEX3OobSgnQ6KM5+9tf0HUe2fVZF/SvqFHNJZisMMoH1JXEOPu1yMgfGM5jlZ2Avnfegb3kIYBd45lOA/CbQPgec3wR6wUOvNfINOuuFWHpdz/qtFj59M8uB2RxoWw8wpzNvKYGVV9wcZ8DNA+8liDEwGng7h2UBQeJ/K53z1hAhnZPXyFYejp0A+CrBOViXWmAVC3g3k31cPJc7qfYD/DKNpBFyaaKJexEPisirnHOfBPAqAJecc05E5icddKJ65/nz5+OCmSIgTP0DvAWDthYAEBfaZPeYlsnUQAqXkOk7ODiI1glclC8Wi8jukYkDSrP04XAYAQ1ZNoJMzf6wP1T9JJjS3noEpVqJkrV2FATR9XIEplqls35slmXRAoLgZTgcRkDS7/cjyNP1fVrJlGCM2+bzeQS7BGY0uTfGRIGd/f39CkPHekua2GugStDHfpKtY//r6Zrsv67LI8CaTCZROIUpouPxGEDJ1GnhGV4bMsGcX83CasVV1hxy3ggIAVTq/Agi6dE3m80qqbunLYp2qEUL6Z1WfSOTDBVrAADlQk2AutiLZ+gCy5cIbMvAtv3jb5Ov9+eLwIVrk0Li+6IjkDQpa/vUeVzi+1Zn53ybXjxGcs9OmQAWqQpapku6wEICEKaFhs9cyeCxn6xZJPBzqi29tvLKpoo1rafMxjYVkwkCahwNp66BvhQO0dJCcodkuoJMF/gv/uBD+LWD1yFzia+5g2DUKu1EUmNjjd9me45LvTHOpFP84fIKdr4/xy/eeANyZ5CkBdAHxFh0Orn390sKPPC9KWQFTMc9uPkcbj7B//aJ9+EbP+9P4PGNN8INerCbfcgyw2FxAf94eRVSWPz8+34Gf/Yrvhr2zBBmd+xBwKAH98yz+Fcf/xXcKARtu8SHllfwdHYWk6KLvllhZts4l0ywYbqQJMFPPPWb+HjWxp89+2H8i6/9Gjy9cRW/aN4MlxoUn99GNnp5WDaEuwP3ehF+Vy2u26FOEr2g0FkEFgVTHA28GEruHyha0/LWDbYA5jmwuOXtGnp9QFbA7me8aGcnAVpd/wWiQih/G1ITDN0LD+La4pm/5QpYzr3Sp4EHhau5ZwbFeOBYFIAt4Jhq6wDYwj8iUlmYR+bojvO0LtshHHAsC1X/sblX98XnFgASERQN09dEEy80/haA/0VELgC4CeA/E5EUwPecdNCxoG97exvnz5+PHm9PPvkkbt++jd3dXQwGg+gZx3ospn7SroGpgwAqAIdAgeDQGBMFQLTAB83PtfUBUDJ0bJN1f8PhEP1+v1KXR0aLxzjnYj0agAgGuY8GBqwBa7VakSljfR4ZTaZ/6toxsqMEaASFs9ksKpKeOXMmsoXarJzHkO3T7dOgfTweR+A3n8/hnEOv14vqqbu7uyiKAoeHh1FhlDWXnA+2q/0AdbqoFuEBgOVyGVVOybSR9d3Z2cFoNMJjjz2GV73qVRARHBwcYDab4fDwMDKPh4eHEeBy3vnZxsZGBNME5Jubm/HhAdVZNVvMOWM6MAGn9kwkGF2tVpjNZhUfv9MSi7MG7bFDyM7DcsMgH/g0xdbYIZ0DncMiWi/UGSeCRqBU6jS5RdZOS6ER645YQRCsmUyxYkDJfAlKpc5C046hzaJ81ewiVTT9sURh4aW2aBXrVT5ZryfBTqECvPTa6g4PzcW6CouoGdC1D9FFPPCRkp2EdfHJv6ut68TVjqfgzbKAWRUwt/bwY7/7U/iRw9egY3IU1mCcdZBbD/6WhbdsYG3fZnuOH37lu5FA0JM2/uyjb8b2PzmL2/0Cklq4QiCJQ9pyWHxiE6/+0T1AEn9tUgMMe8CwBzEGf+7r/jLkSgEslpDlCmbPes80EbhhD8gKvP3tfxnuYnjgcvUcXBJuivND/NVvegWS3UNgleFdv/tu/LO9iziTTnEumaCAwTcN9vDnPv9PQnoOf+krvsWrfCYG9kwfSA1cK4ETIJllMKuqINTnRNwlIKsR08fs4U7a4Xmd90i48j4/khdqbbDuEA/YslDvma08EJMAEG3mQRqsN1xvtRDr4Kxqs2U8kCucz7NuwYPALANWK59GmhgP5uwKcNYfmRcxJbQyvMDw+WdV+unV3Q5+HeA7dqLU06UGqNyLSAwapq+JJl5gOOc+JCLvAHAFwDXnHOur3nfScceCvps3b+LMmTO4cOEC5vM5nnnmGYzH45hCaIzBo48+in6/X7ELILBj6iEX8trgnABLp/XRt68O/ngu7d1WT41kGxokke3RKZ5k7rTtAfvINE+CPu37RvEagjEtRKON3wk0WVNGQMm5Yc3fcDiMfdVzoceu+6ZFXCjIosVbaGMxmUwwmUwqKqucH7JzHGe/34/zACCmh2p1VdpIcFwEa2QDF4sFtra2cObMGbz61a/Gq1/9aogItre3MZ1Oo+gOgDiHIhKVUeviKrqGtNPpYDqdxtpGEcHm5mZkUJku2+v1sLW1hTRN40MIrfiqhX3qNYenIVYbgmQJmNyDlOUZwfK8Q963aI0N2nuC1jzckwR+UmP4Avgg0+d3DvuFzyoppDzupLV5AIL141xloViyhPUUVJgATICq0TqPQViwkWmUkC0gTsmp6/OsH8ORshu7ZvGnwJq2c6gE6xeNF6M5kvKlAGI5DtV+4WAPDnE9d+hKhpYpAAvkNsHKJlgWKaYrX2vX60+x2Vrif374Z/CXHv1a/NunfgO3ihn+r0/9Jt7wk28CFgaOgkcA8qyF1/7oHiS3fk656CY4TU1YCCdwg14AzoE9EYLvJAjehHEWzttscAgtAzfsA9biW7/+r8Hs7MMejj3TkiT40eGXQEapn9oA+CDibR5c4ttNDWSZQSanUDDpecdzWJzeKwbuOCS37qHHMedUl5pbju4s6o1z/l+hvyzwPwLZEigKiM3g0AJyF+r/Dvz9AQFWmQd3AAALrGyo7zPAJA/3ogFg/PF5AHwOANo+wyDPYJzn9Fz4TjHntAr6ymdMa4cnx8xfZdKea9bHumPuFgjGjj2HfV++kYg0Qi5NNPECQ0T+OoD/AsDjAB4TkX/mnPuXdzruWNB3cHCA3/qt38Lm5iam0yk2NzcxGo3Q6XQwn8+xu7uL3/u938Ojjz6Kxx57LII/ptcRrJEJLIoCvV4Pw+EwsltAtS6OLA731ybjZHcIZKiSSQCxWCwiO8bUP7JpupaLbB+BFhU3tTIltxOYAYhsmWaZyCoRmGn/PYqwEDgR/K1Wq1iLSHN4PW9aEVNbEPAfLRYIiPn3jRs3Ym0k4EHWzZs3sb+/H4EVrR5GoxGGw2EEh2TCDg4OUBRFTLflfBVFEeeGIIx9ePTRR3Hp0iW8/vWvx+tf//rIDC8WCxweHsZrqEV29JySASaLx7GnaYrd3V0cHh7i5s2bUZF0MplU0lJHoxHOnTtXEcJhfSXVRwEvPHMaY/zaDKuNFsQK8oGDeWSCbidDnidYrVJMLqaAaaN726F7YGFWYdGeh4cCwXNOCodkWUByB9sySrFT1ta4CS2u6iwc/PZYA1cEoBQtHxQzp1RC/XtXAX95LylN0J36V2s/pqrGfUrgVmHXFHCrK4uKdZVU2EoNnwalzsEcI+onDpDMhppJB9dSvxshpZT2E2I9ehRnYbICZjLHj378V/F0YTCzHSxsC+OsWwF8y8z/3D7YP8A/uPxL+Fuvfzt+6MlfxAeXGzBi8eH5I0iWAmsNXMvBdQvIPMGr/80SZrKAa6VwvVb00nPG+OuRiF8FGzX2APj8wIJITu3pupfvL8O20zLN+NwWZDSIYEAKC5eYcJ1Z52khyxWQGW/DkSQeHOSfg0zf81jEBv3J9Yfe80Xxmgad/mKY9fu5wqd3sh5QALgcKKxn+mzYaIrgm0egJqVnHkVdREIDgcFjwWqR+3asf/Ll9+KDz+NYzxPyOO+KRVoD4lzth2ZdvCDC705g83MDCTXpnU00cU/iPwPwFudcLiItAL8O4PmDPtbNWWtx5swZnD17NoISApNnn30W165dw7lz53DhwgW02+0KmANKS4J1dgVkzwiOCCrImtWFQZieSBBFOwUyfzwnGTi+J3OmlR6BUhSEIFGzdwQPBIgMslF6HARkZK7YBuvyyDyxdpDjYWi2kCCStXIMMn9krpjeuVgssLu7i93d3QiiAEQQx/RSPTadakvLBgIu1lvyOpLl06IsBK/D4RCDwQCj0QhnzpzBxsZGrMcjo0ngqtvnPLLGkICQ9xcB8HA4xHg8Rr/frwBJLWaj55DsLOdHf85redoiGWZYXhTAOCSDHIPeEiJMXxKYgcPsUgpnDJKVoG1DGmcAWi4XIFX/AzXw/ng1UBSjni6pt4fXuj1CtFHQ7BfXYwRpBHwEkUa1Fdo9Upuo/7SAVwCVtesqD97Ws4yu3i+glIWhQmdsB2trdnTffbqqVOoK46mFCqTix2wdzDyDTGYYO4vdwj9cmBctLEOBpnXi1TqdB3RLm+CsacOtVrhZtLFTDHEumeDf77wGRc+Ga2hhugVsbpDePPAgPrBrBHlUawUAJKJS3VAxuC+N6hXjys+L2r3jizD9VBj4usy8CAv+ICAU0nAh4hf/zkGsEuI5hbWzLyyebw7l3bV8tNUTznOXXbg73FP/Yqzbxwbgzw02AD2mXgL+u0DJ3MAYWg1p+UViWqitAizafLgXMMN3faBm3da8P2ni1t3Wa2+Nk+6Xu9328o3ENOqdTbx8QkS+BMA/hf9lvAngrwD4FgB/G8AcwLc7554WkdcB+OfwVdDf65z7VREZAvhRAOcA/Jxz7h+HNn8QwFcA+AyA73DOrbA+RgD2wutd/Y/3WND38MMPwzkX7QqYbrlYLDAYDHD27FmkaYrDw0N8/OMfx3w+P6I4aa2tpBlqAEeAd3h4GEEYt1F6n4Cs2+2i3++j1+tVxEzofUfmjYyWtjXQoFOrcmoGrd/vYzQaRVaOQEWDMW4jeNUm79oIXatdsm0AsX88HkBljGQoCWQ1uGNqKNnHPM8xnU7x7LPPYjab4fr169jb24ttLRYLfPrTn46gnfYKrGeczWYRRLJmjswp51vbZdDcXNcvdjodbG1tod/vYzgcYjQaRZEeWm9sbW1VhGo4l2RNCfo4Pxosiwjm8znm83msqeT7xWKB27dvYzweYzweR9Cq7y8eO5l4o2iatJ+2GA0WsP0lEmPRTgskxsI6gXOCXmcFdIDJFYfpVht5v4XRUwk6BxatafAjXBRxAU9DdCp2VoCWINb4mbzKhsUQxDo+AGX6pwFsElglJbZSObROWWgbCT7kCdYRGgj6D8IhmW9knX0DyCa6krGM3oaBDYgG6qFeqZKKSSYygCUdTD8V1Rf/gStZskBQxI8S34/WrQn+2Xt+GL8yfS3+152vQt+s8MzijPfJC3V8qyLBIkuR5wmcE2y15ti1K/zsk7+FJ7IcIzPHDz79NtyaDmHOLeGsIE0sBv0lrnzvLlyalP1h302gR5kqayR4EJaDFudgFYAncKuMT7GzscYPgBjAIfH7G+MZvTTxIC9JyoW7c8Ayg1ssgCSBnMIHKy887s8C9VQse516ox78xPxJG276CAptydJFaVmUrB7Cdhc+i34vAfjp1O146vJvJnKun5v6muaFzOAahHki4DsGyN2JvGviSDQ+fU28zOIagLc552Yi8v0A/jy8h95XA/gyAN8L4DvhbRS+Ax4YvgfAr8KLsfysc+6HROQ9IvIjAC7AK3F+tYh8D4B3APixNef9HgC/KCIGfnXy391NZ48FfZq14WKcdWHL5RIigvPnz+Ps2bNYrVa4ceMGDg4OoqUCgMhqzedzdLtdzOdznDt3DkBpQ0CrBop28Lj5fF5RehyNRhGw6dovLcCiLREoXEKARmNyAhPaKPCV27XICAHXcDiMn2kTc4I1glSCE6pvsm0COqBaj7hYLCqpqxwPQV+SJDGFk+Pa3t7GzZs3cXh4iBs3bkTxFtbMsfZyMBjg3LlzlbTR0WgUgTFFXjhXBJScM16Ps2fPxjHqfl+8eBFbW1tRPIVpnwBiSq8G5ayxBBABJpk3PV9MQe31erh92/udEfASqK5WK2xvb+Pw8BBPPfUUdnZ24oMAMra9Xg95nmNzczMqifK8pylaaQEjDlJ70t5OS4TR62ZotQrMANhWC529BGc+4WBWAJwgWdoIZGxqfImVqn8TC9hUpTvWQy1cXIIjJvGVVFHNhoEsoFcdFYdQK4YSeEIBP/FMUoWhYlsBzDGF1L+vgkOCPvZZrPO1S3FtGVRDHbwdgjal1+CmBnZ9DaJnqipMmMgRMGsyC2cF2TDBzucbvP6/28Vf+cO/DusEg/YKF3oTtE2BlU2QB/P1rPC/YUliMeissJEusF20cDkRtMXiDxYPYW/RAwB0uxmKwiBJLC5vHMKl3XBiA5ckPrVT95tjMdX5JIMncLCpiUyt03MJD/RI1sT7wCEqqjoYCCzQboVU4eQIcJS8gKRpCUDzl1NN3/2P2tS/eCdluOqr4ovX9Meqe65+vA33mSn30WQag6nGJTUcd3ScCY2xjnTiBc6SvuGfE2BrkMq9CCOC4q6o6CaaOP3hnLuh/swAvAbAHwZ27n2BtQOAy865xwFARHZE5DyAtwD47vD5rwB4M4AHAPxy2PYeeKB4BPQ5534t7I/Q5ncB+Pd36u+xoE+Lk4QTxBQ82i2QAdSpkTT7psE6GRwyXWyP4EnX/BFM8nxaCZMsk/b8Y12bFj0hyODfbEvXDta95rQ4C8VN+EpAwlRN7TVHcKFTUslcadsInkunINJKQl3AOE6CSd1Hjn0ymWB7exvj8RjT6TRaMUynU0ynU+zt7UV2lmCM3oqsQQQQAbb2suP10XYJ7AvrFDnvZ8+exWg0iucicCV45Pzr+4bXnaqdmt0jiG632xXQq9NydW3j1tYWOp1OrO+bzWbxHHyIQEZUK5metjDiIrsH+FRAI86XaAWUkiaF394qkA8SmFWCvGuQOutTPa0rWTisYfDuJvQTa4IzvShS/48+AhyZFqgN2e/QhcjScfFXYdjUeUJf4vb4WQn0IqGggJ04wFnna81027V+RcAXGAgJj6GdAoxMkXSpwKwsxAGzv7KPrc4KO4sBllkKEQfnBLO8DZtk8Tr20gxGHPLCQMSh18qwmc7QCko814ohnpg/gG6aIxGHLE8g4tBKCvyjR38a35e9wwO+VNVH6sfk2uCe80mmJVwnKcIYBREIr78o5WtMIQW8qIzI2vtKiiLUEIqv5Sts8xj/hFiHgdb9rbffl9k8iaGqnTBedmZz2tqXNbZTY/B5ML+3IfvgaO52+I/UgGAFGN7rqI3hrs5zzNVw9X3usP/neCSm9EBuoonPorgqIj8e3r/LOfcu/aGIPAzgTwP4e/BsHSPhLmrbAYCzALYAHK7Zdr227W7ibQD+33fa6VjQB5QgCkAEbUwNFJGYEsmavDRN8alPfSqKhWibBQqkzGazSjonUCpH6rozpvgBJStIsLm9vR293TQDRNBBxsc5h8ViUREMIXtGoEaBksVigSzLKoI0ZL5GoxF6vV4UPKkzUlrsRAu+aPsIgmECKWNMbIPzy/ORVeMYaX8xmUzw9NNP4/r169FagXVut2/fxnK5jPPO2jeCSNbVEcxSYXR/fx8HBwew1sa0Tip48pqyDpB1dY8++ihe+9rXYjQa4cqVK9ja2sJgMIhjJcAiWwsgsq3z+Ryz2awioEPTe9ZnUpSH+1KlU4v90O/w4sWL6Pf7UZBG+0E657Czs4OdnR1YayNzeJrCOgn1L2UUTiDiInBIjENqLNrdHMstwbJrcbBoobsj6G97Dz2T2SjgIpZsV7kI02wagMiUmcDMQUrj9Vjekuh9w6sGaYIobMIFndd1YMpXOD4Mz9eDrWHamPap6u8qOE0t/rQgCwKoAzzDGNmrAHBcIrEvghIE1tVEYx+dAIUHfpJb2LbhRYLAi+V0n53gp375R/B3rn815oX/7s6yFlxocH/RQydtoZcG1ttYbKVzPLZ5G4/0dvBNGx/Cf/v6P4Vfcxfx04+/F48vr+DGfIRO4h9I9DsrnOvN8MVbT+Pvv/5rYS44z8a1Es/0ATENkwBPCutTMJlGVziIqpvyfolSzgcZP6aDrln8M100pvEZBeQTiayqi+8TuJb16p1Hm/ucijXPSZ53aGz2XNpbR+Q9n3NX2go4rPrlBJgGXR5EIHi30Pa4jq7r+Z1Gs+4cxxxzLy9UE3cdRhohlyY+K+MZ59w7130gIhsA/hU8K5cA2FAfM21LK6dtAdiFr8fbALAftj1VO5773bM4FvRpwQ2maZIJyrIsgolut4vRaBTr98j21WvbuMjngl6LiWhhDs38aKaMLB3ZP9YKkgGqC48QQAIl40dAqNNUCXbINOk+6LRWspkEKbr2j21x3pjmyLGz//VaQq0Q2u12I5Oo54Ls2nQ6jfPKNljrx9o41hdyH814auNyAJH1Yt0hQauui9MWGgSytEgg06cFXAhuae3RarVw/vz5eF4+LCBbSECr+6lZ3MPDQ8xms+jLOBwOK/YbHAvZXp2uSxEZwD90+PSnPx0B9mkKFwQ+kpqkpKsBP2MsjLFIUouiAyweSGBbBiY3vhZurTiJqp9zOGLRENkxgqAA7rySZgBQtVTP2BbZL52CKTgqIMJ9FIukmbx1qZYSQFbd109NTvnWHN2pwt7pFSvBLWv0VLtlbWAAMbYEyJ6ACABqf4zdYonX9G/gqcV5zIsWWoGptU6QWwNTJJHha5sc3STH64fX8O0bf+QNztttwAhuFkvMbAfWGWQ2gXWCUXuJL9i8hrcMnsDvJF/u0ymNTpdzx6TouurnzinQXTJ+XuQnzK91oXZPjs0xdCJ+HwJmEQC2TMXlfgkgRPesQWziJQ39lQtOL0fwGoAjfpSo/1neepXj2KDTH+oWhI3rzU4dVDuZ0xvUye4HPqizkZX+HRd305E7wPSG/IMxTXpnEy+fEJEEXozlv3fOfSIoab5ORNrwNX0fCbveEJHH4Gv6zjrntkXk/QC+DsAPwbOEfwM+vfO74JU434aa756IvAvr8yXeeDf9PRH06TQ8goZ+v4/xeByZKZqNs6brypUrmE6nEJEoNLJcLnF4eBgX3WRsyMrQ3kAbn3e7XQwGA/R6PYhIbBPwwO7g4AD7+/vRMFwrg2ofPgIDMl3j8RgbGxtxfEVRxHYIJtrtNgaDQcXzb7VaodvtotfrIcuyWL/H8WlFUYqaML2SQfsAit5w/lqtVrQd0IqaBKUUJZnNZhgMBhgMBnEeKeyiQbEGeWQUqSLKtEcqifb7/WhkDiCysbzm9GUEgCtXrmA0GuGRRx7B1atXMRwOcfXq1Qj4nnnmGezt7WGxWKDT6eDs2bPY3t6OQIwPD3g9eC273S7OnTsXRWGKoojCNARv1lrMZrP4oGFzczOygrwXCUz7/X60nWBN6ObmJg4ODu7mO/GihiKrKqFr/Jj6mSQW7U4G1xbk7QLLMymKbht5N0Vv16A9PiqVX7JkAYzRx47nNApMMEVTLcJE/7boBYvTi5uwSTxQ1RRBRRhG+f6JRUzbPI7JE55dr8kA308AtuXZK3EeHGt7CRBocngmNKSBaU1NtPSx82eWXAHxYDT/vb/xs3i66OAbhn+I30pegd8ZvxJb3TmmWRuHC19/t4JPze0mGV49uI0zrSkupQf4o6yLC8kc/9+P/BwMgGfyFj4yuYpFkcI6wZnODP/Vg/8OX95Z4Fse/UokF0dwrdSrdjqyd56F9PV1YXDGVBarsfaujr0SqTB44gDk/qGeC21Vrkloi+cSDThFgDSkaQVm0SUA0gQyP31+mPc79DJ/3XL2bkmldZjghTB19a95/eu8/o9yv/iVUF9rhxqxty4qD4PkZCx0XJ3xkTbDf54LYNCMo3N3GPS6cx23m1RaWFf9uDYarMNEgSaaeLnEOwF8JYCRiHwvgP8dXs3zvQAWAP5a2O/vAfhh+P8zf1/Y9kMAfkREvgNe0OUagGsickNEfgNevfMHauf7bryAOBb0sQYuz/NY0yYiFQsAsi5ku7jQJ3NE37jd3d1YY9bpdGJKJcEADcU3NzfR7/fR7/cxGAyiCAr7wkX9bDaL3nSj0Sj2QZuNk0HTCp4Mgi6CIs006TRSKlaSYWO/yIplWYbxeBxBEllBql0S5DBlk2mk3Mbax42NjQoI0rV+0+kUy+Uynne1WkXwzGuwWq1iXZ0ed10YRiufkm3UwjtMi10ul3FuxuMxWq0WXvOa1+CNb3wjNjY2sLGxgYcffhhbW1txHiaTSUx9vXjxYkzHpFgNGV4A8dztdjvuR4DPe+3y5cvY2NjAarXCeDyOjCxZRy0GMxgMkKYpZrNZZEU5doLhXq8XAeBpCq8X4iKwY5pgYmwEfoX1ap6JOEhS3sfOCbKzBSZJgsV5g41PCdKlQ7KykLzG1ikVTy5kSkYMa5+8k+mKnwfAxLaizQLkSPooxVxcotg4bQZuENi06lN/zx6FVQGBqCvBou9DyR46A+8JxhRSqPPpFXhMMRUlciPx88o2cbAtiSqnJreQuUO6P8cnVhfRNRkWtoXdfIhlkSKVqhhPN83RT1c4357iwc4eWlJg5RLcyLewUwyxcC2Mix628xFuL4boJjnedOZp/NnND+EfvfbNMMMBkvNtoK2YaasGZPx3KNbaJQrdBvZOx5FUzjqYj/spwEdQmOAIi1ypkSQQLMJrmpQ+fp8j8VzIm+NghiJy72kQsxznyHDH49c9T8ZdjlczaHXGXTci4Y87NXosAHvOO1V3r/fvTvvedaN3dcLPuWjSO5t4OYVz7l8D+NdrPvo3tf3+CMBX1baNAXzzmjaPBXbOuU8/v576OFHIhemLWr0RQEX0hACJzB1rxebzeWTsWC9Xr8HjcUy7ZCqpBoY6HZLHEPxQ+VOnDdYBDM+hGUT9eZjE2DemShJIcqwAIujUPnoa+AKlHxxTRfm5rtdbrVaxzpGqoUCZSjubzTCfzytm6wCicAlBD/fhNdF2D9pSgkHWUovraNVTXe9HAOycw+bmJh588MHI7nU6HZw/fx79fj/W0YkIBoNBZOdYw0mGlemkusaQNYOsjWR6Ju8/zhFZWY6TQfBNsMhtZP+YQmqtjdYZpy1EnBduCTYNEZs5nyzH7QSDFHcRcRBjgU6BoufRz2rkF1RSCJJCs2fH/w+29ODDsRSDrgOsFgs572en2T3W9xiUgI8LxfDE/9j+qMVgWdunFklS3dfVwE1JSxxtd50IiWYdK9vZfwnAp3AwqwIyW+BmvgnrDGa2DQOHTHWCKbqtpICBg4XAOoO28WmcC9eGdYLdfIiDoodnFxtY2QRnOzN8zejjeKw1h+l1IZ020Gp58ZbEeHBnEAzY/TgJ+OoCK168mZPn2LEotkKQFkVwGNaVzKEtP5dC7SdSzdYLdX2w1rOQDFO/MC/vuBfL1xeL+ah8zfXDlvDhkW/JnTDUkc8qN0h1+zqwpx5ElEIutR3rHT4WZa/paP3YOJ6TBlXrh6uP5V7F5ybwMUZgG6qviSZekjgW9JHZYr0bUx0pdEJgZq2NbFS73cYDDzyAoihw69atWNe1u7uL+XyOw8NDDIfDCujQqo6TyQRJkuDSpUuxfovAs9PpYLlcYm9vD5/61Kdw48aNyFppLz4qbwIlcNUMGIEGgAjcWDtHZo4AkrWLrCsja8XaO7Kgm5ubkUVrtVpR1IRAiICToIj7EcTpVMwsy7C/vx+tGGazWazny7IsGrHv7Oxgb2+vMq9MF+X5iqKopFVSYIX1lhwr56PeD8D7NV69ehVveMMbcO7cOYxGI1y+fBlnz55FkiSxTQJqArFOpxNB43Q6jWBdg1/eZzoVVrOSBG58sDCdTiNg5EOE3d3dSq0eVUwJNNvtNpxzOHfuHC5dunQfvkIvLAj4CktQ59ffeaHAbRB6IftHACgCmJaFHRTI2w5Tl2A1FvSfNeiMLaSQ4H3n46g1Asp6HkUWmdyVLJko5gyI/nSeylOG7BQNcZrlk9JOAYhCLgCiMEwlFVTKxZ5YeHGWcCp/vEoZ46tqo2JhoEPtXwGKsV3E1NP4PhVYMRALmFWoF57N8cTsAaxsimnehhGH/WUPzgkMHPotX8c3ai2QO4PDrIub2QZuZhuYFB0ksDDi8OxiM6Z0brXneLC3j3/y6s9HsrEB6feANPWMWZrAtZISzCbwYC8h2AvDKEL/UgMH79UooaYvCvqY6rXQNYCeUa19xuNtmTLsjPGgUnkdShZMuvOiBKG9ztFr8Dked8JO93MJfDfckv4qrQV+d2rgSIN1ZKZa5mdHBq2+qDr74AgYZIfvdtZq4POu973TfqL+28TdhhE05uxNNPESxbGgjywUAQMX0s45bG1txZo7gi7W3tELjiwXweDh4SF2dnawu7sbPdTI2hAsAB6osQaw3W5HMMNaL6ZT0t4AKNk/XS9Wt27QAigUZNE+f0xf1W1qOwqd+slUVQCRxcvzPKYQ0utPg0+CPO0pSIVK9lHPPdkv1vEZYzCbzdDr9SI7uFgsMJ1O4xi0712v14vtEGADiKmqWjKZAE/Ugpkpk5cvX8aVK1dw9epVPPbYY7GesN/vo9frxVo7jovAjswwQTAZS14HjpMpvFRvJchnPaFmao0x2N/fj+qw+hpyDobDIcbjMQ4PD6OwDNu9evXq3X8zXqQolHKnq6zKgdyaCPAAwIT3RdhOUk1SC2ccsrMO+cjAthPYZw3SOdCaWpjCeWuHvPRvQ2rKtM0awKPJOQ3Nbct4ARD203DRVj4JjxYKrBmEqO2u9PhTq8toyaCFXioArpwnKULfjcAmvo/OhDq+kxYQtTWeZvWOZfq4PbBcyTyDmS7xp//dE/j5G2+I18w6wcG8i04rR7+VQcQhFQvrDIwU6JgcS5vG1N2OyZG5BLPcP6Q4057j9cPreKS9jQ+Zx4BWCnTacO2WZ8u0gAuDTJ+eGxtuBOuisqeDACbwpFICPl7TI+0SWEaw6Hy9Hxm88KAOInCpidvisRRvyY/WlTbh407Ab13UybHncq479eO4fe54rjqGkvqHcnRfgrQKGKzNRoWVq4083q+aza+DwMqb9ZOnWT4NGl1lh7sM9UBt7da7O/ZzLZJGyKWJJl6yOBb0keHThthMsSMI4WIaKEELU/SY+kdTcKp6EmCwxo1ghaqaDA0ICJgIPAmotEInARtBA4DIZOl6PoJU3Xdd46Zr69gWUwSZUqrTTXluAhyCRq18qRVF+artLBjaE5GG7lrFtC6CQrEXpsJSlZTnozgNWT8ymDqdle3qVFBrbRSM2djYwGAwwNbWVgSSWgCmLsDDMWl1V94PBH4E5OwzU0B5X5CdpUIqXzudTnxAQBZRq4xyX51KrMFnr9c77nZ/yUILufjaPsR0TmtNZV1tvVxmINaqCx4xDi51cGKx2hSYlUFrAphMgMwDmPhEmmqdQsbNHQES64Ig0dHegP1epxyqBWHq7YhUn45LybCVD/ul4ksXWcnYSBgT2UsNFu9RiAVM5mDmGdwzN/DRyYMYLztwTpAmBQprkBcGDilcMGfPncHKJkhtASRA5hIksEhg0TE5jHNoJ/57fLl7gC/tP4kLZgbTfp0fszI/L4VlUL4yxZN91OycRRRvIetZmbdj02rVItiiBHo6VbQIptxGIJnzjKM+PqSZelD5uVXTdy/iOHCnSfgXskxe1+66PrjjPryb7XLCjnWkWScB1+1fv19FN6TPV3tfn60INI/7cTiOCXw+ML2JO4URqZcJN9FEEy9SnCjkslgsKkwYwUKapjF9jzVdZJQ6nQ46nU5k+7rdbqzrGwwGMXVR++wxPW84HOLy5ct49NFHI2DRNXif+tSn8JnPfAYiEsVPqEbJfrKv2mKCDJQWedFKkrQJIGhhXV7dbLwoChwcHETwyno0+hTW7Rg0ANUAUAM+pngSVGrzeW1uv1wuMZvNcPPmTdy4cQO7u7sYj8dI07QiZkMvPIq0aM9DnV5KZVbWD7I/xhgMBgM88MADuHTpEt70pjfhypUr2NzcjKCNwIvKpZxTKpsSzPV6vZj+ScXWw8PDmDar1Ur1PJBV1TYYgAe758+fx2q1wrVr12L6MGv66A1IAMk5ozgQr/dpCqZzuhpaKQKT58GfoLr+l8p6SCimklogEdjUYdYzSKYGRdszfp19i86BTyf1wh4AHGAyGzztALjSnoHiH/6f31erb7q03BYZPfq28UFMzg56wGkDW6hr+rSwSvQFhCihF8Q1XCV90yEKrejJiFYP69Zqag2n0zy1SEy8LplDsiiQTjP81C//CL7r+lfhI7tXkOVJTMctrEGeJ8hWKZbGG7AbY9Fr5d5mQxzGeQfWCbpJjnOtKSwE59tTbKRzvH3jw/jSToFvfvRrkZzb9HV8KpVWnCuHRhyV+PFqsRsCQXHiwRkQ6vgQLTBKBrW0YIiA0VoP9oqiZPmsjWAOzkHywvfNivcEdC6qhjrjfQRZEyir0rKmiecWJ4G758v6MY4DeifucKfzriHl1qZYH3eetbQjQR1VaV113+cTon4U6k+P7tjJJu5lGJEmvbOJJl6iOBb0MbWOIEWzYFyw65RKAgaCHgIinR6pa8oIOgja2u02RqMRNjc30Wq1MJvNsLGxEVnA6XSKw8PDyGzxPLQeIDtU7wOZRS0QokVEtA0EwZhm/bTdAFB6/ek50Z+RUav/0+fX/nFkHPU/vZ+2tCCIIZNFgMv50Cb1FHlhii49/Opzo9U8Weu3tbWFxx57DJcuXcLFixexubkZU0rJUtLuQqdX8nprdVfOLe8H51xMB+W1IajjQwVtdq99GAnG2+02NjY2sLu7G2v3OI9MCSY7qIGjTqE9jVFYU1GB1EEAeCSk/uTbQVIHJw4FgNWmoOgKXGKQLh2w8OCMKZZH0gRdyfhFMGYk7q+B35E+SvnPdy0AyCDiEO0iHI569AnKvxMvIOIoPAKBwxo20h1lE09i+sS52JcTI7CIZlVA5hl+c9HFM7MtZCqt0xZJBN/W+sXpKk+QJoKVcZjnLQ/84AVdAGBS+Fq31/Zv4GJrH69rLfDNr/yTML1O8OOrpZ05dW3rc15LxXQBBEpBcOxKYZYjc6SYViXGEgGfc0Gx01X6FK89+8V0PYNYZ4hEvMVEE885NB91IhP3Qs5RvyXWkGkveDm+tvPHfOk0s8/76siu3KZ+5+qUpDaNXFsvqJvj/Vtp7ITOr9uviecbiWnUO5to4qWKY0Hf/v5+BEJFUUTVRopuAIigg6Ib2tOP6X1AWfcGIAp+0KaBgIntiggmk0ms+yMouXXrFvb396OYiq6LY2iGjcCG/WEKIvuswZoGcDrFkcfoNEumXRKAaFVTAsLlchnr/tgGgY82KifA06IltBsgG8YU28lkgr29PRwcHMS2aPewubkZ54EeihqQkwEjezubzbC/vx+vDfs4HA4xHA7xmte8Bq961atw6dIlXL58OTJlBHZU09SG7/QD1Gms2luR+1LZM8uyyMpx7judTlTZ5L2nFVlZq+mcw0MPPYTLly/j4OAATzzxBLQgzMMPP4x+v4/t7W0cHh7G/gyHw+f/TblPQZaP9Xt+LeNTO/0/gbN8qOBggtH2kfROXcci8OmexmH1gAMKwWrLIO8l6O469G8VMCsHk9mY/hdr/Qw8W2eqYM8lod3cAyKd2hlTCJNgH8D1UWAANSsYgWSo46NRugaArBmUIvTHoYLmSmVJF/+OzCSBSC1jKwqPOALecgFZAbGRvXRIZhnMeIo/WngvPYruAECe+98JYxzgbEzHLcRfm2WeIhWLd175PXxt/wn8Px/9avzdz7wfY2dROOBCkuIdr/hamEHXC7eEm0FCGqWfexNSNtlXsq6ukubpRMLfYYwEZCS268Ce52EqJ+exnpbpHJAVft9YQFoFdV4kxsQ6PwHgTqE1ymdLrAN+JwHBF3Ietl3fVo+Tz0uwpv6+Ixt3HACsb3dHd31eEyFrjnlhs/l8CccmwPLjJppo4iWIY0HfwcFBpW6NIiU0KOdCnSmCVGjkwnqxWEQQByCmHrJObzgcYrFYYDwe4+DgAOPxGNvb23DO4eLFi3jggQei8iXNuslyAaiwP8cOLgAzMnvGmNh3/k1QSMDCtvmZBpHcVhRFTFMcjUYAEMGe3lfXHGrLAbarbSi0hYKuxdve3o6pqUz5ZHrp5uYmBoNBBG08D9uiOAqZwtVqhb29PUyn08ii6pRXAshLly7hzJkzEYAxxZVAttVqYW9vr2JJoevq6MnHtMvlcomDg4OKBUi73caZM2fgnKuMSd9XZBEJ0AlemTq6sbGBBx98EJcvX8ZkMsFHP/pR3Lp1KwLUra2tyIoy3fe0hbUGharfK+8JU11LAXDWlFLXR4rcQsTMKAcRoGh7qc4icZi1HbINg2SRoHNYIFmVoI3HWSC45yKui0omSYEmoLJuchGYKCaJgDL8H36tCigCiDSuuhIlERWAZ6Vu0HrAF5VDRW0P+1emRtcKovbelSDWmVK51GQO5mCK4vpNbOfeS2+34AOJMvW23c6RS0m8SWBqO2mOq4N9/I3NG3j7Y2+D6Tn0TQuFXaFlDL716pthuoaov0yntNanT1r4FEzj4AjejINoyq9Qc68XzIExiX570QdEvDhLUfhUXKbhamaR7RjxYFA8S3ykblOD9AhSub1Z0ZWxDl4dv1d925qv2gs6V/z5WPfZXZ3jLo5a1w3N4h05OXe8073DH5Tqg6XKOet9ONKPO/T9hJA1A9M/P0dabL4KayMRQd6gviaaeEniWMREpokLcaZhakVKvjLdj8wMWRqdPknGhmCIvmwECdPpFAcHB+h0Otjc3IzAiGl7rAHUrBzf634AVeZOe9DROJ3btegJwaCuKyPgAEqmivswrVCbpTOlkDVpWlBEA1QtMKMZPwI2rTDKNEqtBsptOi2SoI/Mq/YcJHN4cHCAw8PD+HkdBJFt0355ZPa0kTrPyXuETK5O96ynsNI6QV8Tzmev14tzxocHrNfTNXv6gQMtQ5xz8eHAG97wBsxmMzz55JPY39+PXoZMIz6NQi65UuIEABtZvXIbGUCBYvg088UH2er/o5ExEPjUyJa/ZnnfoWgrOwXuvCaYgukFOtQHCneyK/Tj8yBSrSzJ8JFts2W9HlB9raeNVlQ0a+c+8v5I32ttxQ+qrGIcJ7wSKG0OTGHhDsf4kSf/Pf7Z7pejm/qHTUYcrPJKTI2FS3zdZZJYtNMc7aTAo8Nd/MXzv4u3v/LNkF4bbrnEwvnvW1/agKmxYcxYqIM3F+ZTAuOrB7YuZS7s60Gd2hbM3SUrArtnIUaqrFy9vZhyh6gKGh/TG7V/7bodYQybuM/x3HmnOli5Z3GnRk888bovdB1SqQYi+Dvmy76GOLwf0cCX5xaJEayK5jeiiSZeijixpm+5XILKh1RMnE6nMfURQAQXy+WyYuhNRobMHkEAPezIMpGRyvMczzzzTPTpm0wmEUxStIQqkJrtI8DQrBDBJtNRGZT6Z20aUCp3bm1txbrD5XKJLMsi+GGaJueBAJcAkOmNHAvryNgnzgMZMPZzuVxWaiMJoghGp9NpTMdkPWSWZZhMJhVbCqasMsi+Hh4eVoAd+0UmTKeaFkWBzc1NnD9/Hq1WC5PJJAI++jVyTBpQayaOc6TtLVi/R4sFzrmeG4I2KnQyDZXsK8+njdeZuksLCt5b/X4fly5dwng8jgwngfepBH15EgGeCGIqJxU843pGPeQmy+ecZ/SMYXqhxDVRPDYa5/n3tmNRdNKYDsn/9UYAl9QAUbBGiOEU7ogpf1i/NlPtigNcqBmL9Xr6PK5Gz6mH6hLqyXjO41RBoyqpZqzU+OhJGNlKfY7ATJoiWFtkFj/x4ffg1xdnMSu8ETvggZ4Rn2brnDemH/UWSMThC85dxyBZ4vN6z+Jdb7yK/8/wzZBRG3AWkqbInMXItPHnHvkKmLa/f1EUkFaKmDpZj8LGujrOEc3j49gK+Mn1HQTgwbpQWCW3Efh5sRZX/o2izLUyejEtZVqp/gzw+wczd9dKIhMYz9OEiuc2H8+N2QNO4Jnu+lxrm3yul/G5dOM4UHbk5JrR0xRl7ct77InVfu64fXRH6p8dPxHNXf78Q6Sp6WuiiZcqjgV9VGZk6iBTBQkuyMKRlaGYBoHFfD5Hp1Oa9Oo6Ns1gEQj1+308++yzUUmSxyyXy8jy6VpCXedFAESxEW1QrtM2aZCuUz4J2DY2NmKtWpZlsRaMQIa2EZwHbXyuhVAo5kKmSgNUreypxWII3gj2mCo5n89j6qM2odeG9mQcCYam02m8dgSh+jro2sl1lg0Eo+PxuJKeq4G1Tl/VNhTaNy/LsnhtdCqtFrXRNZK6bQJILQrDftaDaZt81cyuBoenFfQ5B9jCwCtwkqkOtgzRIi0s3sMeTDMi3oIEc23R7CA8ULTiFReLkpWxHQ/unFEJSzoFsLYGEueCN16t86KEVfRq1SGmA0awJRLTMSPrhyrrplNHCQqj8qQGE1p5UuPRYxiEmv3hiSGFCz6FDrt2hZ1iiMwlyJ1BEQA57RoAh8IanOnOcbYzwxcMnkYBg0fbt2G2Xg/pduGyDFhZuKJAEjrrrPNgzlkv4HKkE7WBaGbNBcN6GxakusYPCGmy/ro6WloUrhRoAYJkbH2SQhsa+AG+fk/X8/kO+UNMMIovJWhLi4cmnhN2uksC+w5H3sMQlE9Jnks39BdNH8q03zs2p8HdSad31d+ESuEqKg+e1oPB+s4n9OVOXanHHfv+uR2JQVmm0EQTTbyocaKQC4CYppgkSQQTg8EA3W63osbINDoutMnkaV84KkqKCBaLBRaLBSaTSfwb8P58/X4/HjudTqM9ggYH9Xo7pgl2Oh1sbW1F5qiu1sm+awBLA3HtyUfmT/vAEZwSnJHBpPgKVUiTJMHGxkb0tVutVhiPxxGQ0MZia2vrSM3gbDaLvnWTySTaW3A/LUwym81iTRxtEgjAyfDpukYCLIJRAneCWiqVasEWABVABvgHArpO7jg2jrWAunaS7TJVWIvBMGWU4E77EpIRZb8JIJkOynu1LuzD8dM0/rSFLQzEHE11cVZUKic8wwMJi3oHMYAxNqIqrygZAF5YeDkrR5GQBcwKQcFT/Y9XK0euYdxoj0CRE6Zzxt1s9fNKzZ1BNQVQpVjKmpWRWA4aFWZRC7ccu6DSQDTsW2Eb+Zle6xGougD6cgszz/DxbBOFEzzQGuOZ2RYG7QxZUFjttnKs8hSzZQuvHt3Ga/s38FjnBi4lE7y6leJ/7na9bcLBHHY6gxjBtz30lfipZ34Hv/TMB/D1D38pIAbSbnkhF5HyGjA90kqp6mldhc0TINTdIVon+LGUTJ8pc4aryglG/MqL6ZtJeNUMHgFeqi5cZAcRmUANziXUCzYqDc8/dfLUzFwEcfLc2NuT8JPUNjjUN4Zzrjl+HQt+BLDVv9T6PE2clmgnCVZ5c1GaaOKliGNB387OTlwkM0WTC3cCOAI7Ag2aY9MTThuo06ONYJLpiuPxGCKCvb09bG5uxjqtxWJRSc0jeAQQgQHPpUVQAGAymRyxkGAtGFMlaRpPBo/AgUCEYAVAbJuATlsz5HkegaAGP2Q0CTqSJKkocxKwEMQQiM3n80otmvYCnE6nMb1W+xACiKIqBFuaXSOQY1oq2weA0WiEra0tjEYjXLx4EWfPnsWDDz4YxW20wigZTh7P9nSqK9VWtek8GVpdF0kLCX0/afsHXiuqcpI95LyMx+N4T+qaSLJ+vV6vwl7qms/TFCaxIS0zMKo0YCdDBxx5eu7Cwt56c7awP+DyoPYoARwy3TMcBycwS4POvi097uqpfUBVtEPEK3cGfBk/U2spqmkCqIC9qJhZAAjCIc7Ai60EAHc0zVP/UXa9MgVMO9TO9tCsov/n0x5r11xUo+E1itlUBGIcWlJg4dqYFB20TYF+a4VZ1kZirAd+aY40KTDOu1jaFr6qu8A3Xf1KJFtbkL6Da6VAluEnP/M+fOtDb4G02vgLD32FB3utBJIkfgxF4YGZEz4GL8dlAsNm/DVgnR+AklWLYyo/86DQ+DRPnQ4aGLuYIhruMe+7F5ppJdW6R1N7SMDfRWNirZ/JrU/vbMzZK3E34O/U/Spphmxd555XRukJIG/dyR3/w9/A+i6KORR1nH57x77dBZMp4T+xP3cRp+6Cnq5opwbL/PR55jbRxOdCHC99iSqQIIui6+EIPrj4Z1oj4IESFT/JCi0WiwjYJpNJrFHjYv7MmTOxHmy1WlVAhk7dI+CjIiZBE4VVAMR6MYIxAgAyTQQDZCy5D4BYI0egsM7Sgfvp9EPtR8cx63o3gjSOg2BWz8lkMolKmwQ9WniFVhc8H9M08zyvMHlMXaXxOYEo0z61J6AWW9H2FpqJo6+etlFg+ijfc0ycFzKIOpWUqag6nZW+gs459Pv9I3MIeDBu1WJSbydoJzPJPvDe5XlPozk7o0zprNeg1HcMKYIBpESBDwcPCqxi4FSNHwBILkjmgnRpj6ZqhqYJfir1cU5KwEdA4rx/XtwWogL4IsPGN/4/DkFlEyiP1YyCyPrxh2NsIhXF0XjeymCOmb94AFT/UKaMGsDBC5w8nZ3DXj6I/nqpscisQWYNhu0VsiJBXiS4vRiis5Hhm65+OQwtR/IckiVAu42MDJ0RD/jqKZRAVO2sdNGGhF+mVooLaaFrBqe3hXap6umMKUGbFl8RiWbvzkrc5lJzhFlx1lbX3QSNah6dyPNmuF5ucYTYOmaf5xrHtfnZgzPumgoMm/X2+Jip2k7l6dOa08RU1TvFGhDo6p+vbfwu2m6C0U4NVnnzYKiJJl6KOBb0dTqdmBLJxTQX7ADia33BT4A2nU4jw6NBH1NEt7e3YwriZDKJ5uxpmkYRl3a7HevZCFq06iXBkq7P04t7rRR5eHiINE3jOTiGXq+H1WoVa/2YJkkQxPNpUKjnhOfRtWh8zznTrB8FZrQ3X1EUmE6n2Nvbw/b2NnZ3d6Nozu3bt7FcLnHz5k1sb2/j5s2b2NzcjGwez8Ux61o41idynllfyHnS9YhUx6RNA201CATn83kl7ZRMIYHYwcFBnEMKv1C8RhvP7+/vHzmW4wd86mi73a7UIWqbC84XHxTodF0KDBEs854gq2xqi+rTEEXwe1u7bBDn1xRU8tSKnfDgBOLgChOZvBgEgU6AxLeTHhoMn/Z2BJUTRqEQBIn/8Mr1TEztDGwdQg2hK+vfyNiRIXIiMJZecGSIymE5vX6jv57eT1BJ1aQlAJk8bxIuYFomASr7iPBx9Kxbl6bGPxV7yfNPXjXEz9z+IiwKn+2QSoFp1sYqT2AEmGctFE7QSXO0TY6lbUHabc/esc2irNn78ad/C3/pFX8Cpt2CdDs+pZP9ShIP+GJKZbgnQgpmmQqL0iPPhfaBUuxF1d25NHjnhdRPVwTlTudKiwcN7BKU5xKquyrbh8gKhklKlJVDqOeLdYNrU/GauJ/xkkOP590Bd8z7Nbs1t9XLIjqpadQ7m3jZhIiMAPwqgNcDeLNz7g9E5C8B+NsA5gC+3Tn3tIi8DsA/h/+/7fc6535VRIYAfhTAOQA/55z7x6HNHwTwFQA+A+A7nHOre9XfY0EfBVEIcAi6WC+mARVZPjI3ACq1V0zRZB0fBUq095wWGNHeanWGjXVaBD1k7LTVAPvC4LF1FowCMbpekOmGus6MrJFOVwUQx6+ZLS0kAqAyBn7Oc+V5HvssIlExk+D44OAA29vbWCwW2N7exs7OToVB1GbzZDgJ8LTXHVlTAifNULKekj55rLMkgOP1pVKmFu7R116LsxAoMt2XDCGBGs+tmUMCMwrPEPTXU4R5X/H9crmMtZME6BowapZRTuFi1K1TGYnpScc90db71ukuiSbd9RKX1ljQmnr1TNoAHJHbJ0sHHBH78McgLuoJ+MquSHXfejoipOL5d2Qs4fzOupCeiJJxPIb8OxIBrLLm0PsESgSAEXDWbwWyl7nf4Tv/h5/EL+x8AcarDgpr0AuWDYN2hlZS4ExnBgDIbYLUWGwmM8A6uNUKMhxA0gRwDnYy9YbsFvA1ecaDPJ0xQMAXAVf4O02OMm78zdEplM4BUoJGp9qK6Z4EZUAwfD86dZG9q02Q8AGC3o/3QO7N3SW3QF74Y0/hw5WXIup80AuNdT5xL3nImvfrundXXV+DHCu/dUferP983W9LPH/9qtzNlbnDfD/XusfP8eg0TF8TL6+YA/hGAD8AACLSAvBfA/hqAF8G4HsBfCeA7wfwHQBuAngPPFD8WwB+1jn3QyLyHhH5EQAXAFxyzn21iHwPgHcA+LF71dljQd/Vq1cj0BGRivE4GTECI9bJMbRBN5kn1m0dHBxEk/WiKLCzs4MHHngAly5dwtbWFnq9XoW50QIqBJo8lvVd9NnTAjCAtybQoIoKoKPRCHmex/pC/Tm98pIkiWMggKMAiU79zLIsMlvafJwpkwQwtDDQrCjnh3WF0+m0UkfHFM/JZIJPfvKT2NvbQ7fbjTWPFJMh6CVo1Sm1vEaandTG8HxNkgTj8TieczgcRkEcAnSCZII0pvzq+WOqJq8f01EJ2vgAQdtvEPyxbdZynj17Ns4lgAgagTK1lUwqhVoWi0W0pGAtJwG+Ts09LWHzsPCorT1KERZRVFb40zgP7Bx8HR9DAMB5RsY4z/C1Qgrvdhubn7JoTVU9X6UjNbarvvaiiIchz1cyeM4ILO0BaMugzxEYO88wqfYQ2Eql9Mk22CcRz0xZdWzlNR7jSpbSC2uWLKF6rRi6kzRLJJqyS2GRHC7w1b2n8CUPPo0PLa/i1w9eg3nRwsqmONue4WxrirPpFAvbwq1shDcNnsKXdT+Dn+g+BklTSKsFJAlcluGHP/lr+HTew9nE/z6S5as8gDCmZPAIqIxi+Zh2WZcq5WIzpHBWALwLQCy8Bxk+VPehOItX4UQVZOo1OJnVBJFBFBvYw6wAsrxh+U6IuwGAdybLTv50LT56KWLNAKX2Zj1Gqv0Gxf3d8QN6ocj6CBh9rpTlCX1rYm34mr4G9DXx8gjnXA7gtvp/+mMA/jCwc+8LrB0AXHbOPQ4AIrIjIucBvAXAd4fPfwXAmwE8AOCXw7b3wAPF+w/6HnvsMXS7XaxWq4rRt1Z65GL72rVrFcVI1nr1+/24uGFKHgVaptNpZIwefvhhXLlyBefPn8doNMJoNIrt0zaCojKajSO40TVpBB9pmuLcuXMRaBEAUd2TaZ1UJqWcP0EumTFts6Br6QBEgEPg0u12jwBktldnK+tBS4Hz58/jFa94BSaTSTRUJxCiDQbHQbP0NE1jGieVKrVFxmq1isfoWjrGarWKwj2DwaCSpksQzGtP9o41khR4YV3ddDrF/v5+rM/TKq5kQXkPEdy2220MBoOYUsw53NnZiQI8ZHRZ60e2l/0kmOz3+xEIr1YrLBaLIwD31IYiWCRRDFlAK0zx9GSKqwCbeDzg7RkcPOgrBMgNtj4BpHMbLQnicaxlqy9c9NpHPDgT42Kape9WySxJSOOU3Jaef4bMFdmho0OO5uy1VWBkDR0ghYuYo2SzeBw8wCnU4iswhTEt0rojx8W5dojgicDxv3r3z+C3Fo/gy7qfwcjMkdsEi6KFRdGCdYKeWeFqfwcL28KV9h5e1b6FrlgvyJIYOGt9jd4qw8ikeHVrgZYEhq8O+JjaCdWPLPfz1kohacnu1edHtJ0CUGNmHcRZn47bSkoVztwGAG+VzUM5r+xHTPM0aj4T8Ywi4MHeKi9rDAlUm7hjrCOa+Xd4XrEWRxxDjq+N5wpdnlOcxOSdEPVd7qp/dzrXXbOKrvrZicfc19n7nI920jB9TXxWxlUR+fHw/l3OuXcds98WgEP1N9kG/etzAOBsbV+97Xpt2z2LY0HfmTNnIuhjSl9RFJGF0157/X4/qnBq0RcCHa0AyW1MzyPQOXv2LDY2NjAajSLbxcU9QQeZJLYFlHV7OtVS2xBor7z6/ltbW5VaOII0AHGMPJc2UCdgYt0Y9yGYYkolj+f5qGqpFSx1dLvdCNwIeOlTOJvNKsCXaY981WwZgCMgnGNg37WgDev6tLeiBrpaHbWe1qnrHPkZWV+CUj2P69RNycYRWJLJJPDV6Zt6zvR9SNDP+ebCmr6Kuu7xVEUAdML6PWDNoiW8KUxQ7Zcaw1dHfyhXjoVAMkF3z8JkToGs6mvZVgB4GoTVQB4QAF7h2SMye34bPdwCS2lqgK/2ZD6e5y6e2IsD6GUoGqwdYSXhU0SldpxICUiPNF6e9z0Hb8S8aOFcMsGTqwcwL1qY5W3M8xZyazBp++/IVjJDWwqMzAotAVxIsRbW+xYFvvWht+CXrn0QS5f5er8kKQGStUeZMf23BnQ6haxOk6x5mMH6OoFn5mxqIIU9OrV1wIeSYI41kUaADKVZO+sDF6sI7NFKy741lg1NvNjxQp/nNbfsixadVgP6mvisjGecc++8i/32AGyov7nw1Df9FoBdte9+2PYUPEjcqO13z+JY0EdBjuFwGIEH4MFCr9eLKY7L5TIyMEw5XC6XUZiDIiC6rop1XMvlElevXsVrXvMaXLp0CWfOnEGr1YredrQF0EItTCclaGSfWNdGMMHUQhquTyaTmALJvrNujWMiQCWbqAEnWSWqjnLMw+EQs5mv79FedOwba+0AYDgcVgRgNAjRIiMEeYeHh1G8ZbVaYTgcxjTFTqdzJM2WfSQbN5/P49xwvnkdCRgJ/lg/12q1cP78eVy+fDmmdxL0E9AyzZPMGgHfcrmM4yZTygcDBPtMadXKobwWvJfI3h0eHmJnZweHh4fY2trC+fPnK2br7Df7w/nlNaMlCIHlaRRycblAUsBTWn6bGOc9+CIgDGvpxMHmNdGW4xTrGFaQTgxak7xMgdT7OQ8Q3Brg4JsPDzgCcPOsnot+dlJ4gGfywCJm/p627aQEfqJYtxqYrPzNbESdysl+FC6AEHU4xUfC3DETLE6lMgqXULNXZskGxjKARpN75uvgH87xxPgClkWK3+68GntZH4siRW4NFrn/7s6DuEsiFjfyTbQkx1lzgJ946jcBAH/x1V8LtFpwqxV+4ZkP4G1X3gTpdGA2Bj69s7Al8KuldTpdx2e8oEq0XgAAa0slToohHHdbi1fiZFpn1Q5DgFTWpnyKW+OeaMrPzSqHzJdw0xlk0IfrpD7lE/AppfU2m1gb62bpbrDH3RJtd0z3FP7nHqcoakpSf911h+7YuaOxlsB7vpPIHdc+/WnifkY7SRohlyZezvEEgNeJSBu+pu8jYfsNEXkMvqbvrHNuW0TeD+DrAPwQgD8N4G/Ap3d+F4B/CeBtAN53Lzt3LOhjahzT5Zgmp1PlyKxsbm7Guj6yYQRbBCDaf00LwhBs0AuQ77lw12IxZHCY4qfTKLVgDNkgAiSt+AiUIicEb+wXWSiOT2/XYJB/J0kSWUnNYvH8OsUSKNlJzbyxJk2rYhIo8RpokRYtKsN56nQ6FZET3a7282O9HlkxnZbKWsfRaIQLFy5EQEwgTEAJIAJNvudYOUe00NDMIAVWtE2DDm2XASCqiVLFtNVqYXNzs3Ktub82byco5BxyzrTK7KkKG/wDnUDgVRLjsiMAPhEqaTogYZqng3MeCDimcwLlyiooYEohaB0SNR5N5Yym6qgBPw3IWMungJtm9aK3HGvECAyV5cCR9kM7dbGYuL0GTP2YjqZ2xpRPg1IA5hjmT3v6MSU0zgeN38VhWfifxYO8h2neQe4SOCewTpCKRccU2C8G2C8GWNoWznVmmDmBtTkKB7zrif+AjrSwdBm+4cE3w4xGkMR4cReRKtBb9yCCQi5H5qQ2XzRtB44CvwKlSqd1ELjon1exVtAMopobqqLGz1hrGD8P3pyJgWOK6mn8fp3SuAti+8Vt74VmNN7hWMf/HIdOn8O5K3jx+fT5ro5pUjzvV7RTg2V2CrNummjieYaI/AKALwLwWgD/B4B/CuC9ABYA/lrY7e8B+GF4Ju/7wrYfAvAjIvId8IIu1wBcE5EbIvIb8OqdP3Av+3qiOTuZGy74CQC0Z5qIoN/vY2trK4IoLsBv3LgR2SYyakx1vHHjRiWdkgt21qpx8U+FRq0GqtMl+V6nYAIeRBDgsN6NqYo0ZQdQEV7hcWT56gqlBBNkH3newWAQQRdTSwmqNDjhOAmisiyLYHk8Hkfz9d3dXezs7GBvby9aViyXywh4ee5ut+svYpg3a220K2DNHeeQIEgzcBRn2d/fx2g0wsMPP4wLFy7gzJkzkTHTQTEdXl+CaA1iAcT+zmazOI9M4QQQHyAwJZXAUafWDofD+FCAY+JxFPMhYKW4Da/ddDqtMNPaxP60hSxNIJwcnDhI4oGfgYGYcoUv4o3cAXiwF2gb6wTW+TaipL4gevZJLkhWQLIK31eKr9SYN8rz69TO0iogWDWIwBQWsrI+VTCzwQLAHk0/1MChcKUNA8cTAExV/TO8sR4kxmBKaqgpPMJO8VjaDKi6Qn5WVQX1QMgUIS01t1G9c+sdN3D114Cnp1t4ZraFlU1xuPTfs0QcWkmBpU3w1OI8AODBzh4eSQt828NfhXc/8zvoSAsfWS3w//jYX8SDwwO8+9p78C1v/dZg1J5X0jVduxXG5+cz1kDSJy8AU6cfkAQfQdhwnYLQjQdenhEFAIcydVSy6vEQgTUmgmahyipFcADfHxjl9SflgwCmnXY7cJ22rxlMJFzn0/k9+1yLu7oKx4Gw+xn1jj0fbHVfbzFOSnMf369oN5YNTbzMwjn3DWs2/5vaPn8E4Ktq28YAvnlNe99d33av4ljQd+vWLSwWC9y8eRMAonfaaDSKAI3peDq06MZwOIwLfqaC9no9dDodXL9+PabwkUEiWNL1W7QJIGDgYl8rUjItkXVnOgVQ1731+32cP38+pgEy9ZHBdEBrLYbDYUz5JLM0n88xHo8j2CBY5Cv7lWVZRTVUs3NACSwJprUaKUGeZjkBYDAYRMBTB6N6TgjGteCONlQnCFssFrHejf9GoxHOnDmDra2tCC6pijqbzTAejyN44jXW/oVaWEVbLhCgA4jj43wBiCI6VAVlm7QM2dzcRJqm2NjYiLWOWZbFe4XXXrOSZAT5GRVFT1ukU4PcClzL+gV94uASB7QKwCY+1TMgGFev2wurdg8UXVT8jE4PTuASh7wbUjBXpZqjiGeCojALCR1XSvtHb7yozOki0BMHCB9oRPkJlICAINLCr6EKRV6R1XVQ5uyIdXoOLiiQqqFaz3CKpZDkGoYKAfwZD0AqpY4EoFKyfs4BSOEtLAqfNmnnC7x+uINn5xs4WPUwy7x4SzfNcbY3w1Z7ho4pUMCgJQW+qPtpfNvDb4UZDvEXvvDrIZ0OfuT9P4E/f/XDyGyKHzl8CC5NkF3cQGt3VtoakPELLC99DiMbKSWrWU2x5T8HV6D8O6ntr+sv16XvGvHzwHO5AMyNr//jNYF4IR5AIJmFg8B225B2Gj5TfUrgr9sxqcJN3H08j+zH57f/C30YVsNH9ZLUNbvUzr++qbWpnPciTmpQ1E4N5rtv0UkNssLBWgdjmt+KJpp4MeNY0EcQMZvNUBQFBoNBrB0DUAFVTOMj6GBana7fIqM2Go2iNQEX/qz506bodTChUyR5Hm2doBlIvmrLCZ2+SDCk/QfZZ7KCuqaPoI/sGPcFUPHmY/0bGUmdeqjFRdimBnUcK+eJdZMEbHV/QP7NsWsWlHOlx8c5IRglQGu329jY2MCZM2dw/vx5bG5uRgZRp9jy2vP6c355Dchy6nESfOm0T95Pes608I9OnSVgZpopU4Y5fgJ+bTtRfwjBFFuavp+2MCvApIBlimIQdHE2LOKtBHN1jWB88C9hdidzG5WRtms55KMAsiLLV28AFdAXQy/I1jBzACrskAZ8kWVyZb8dVD3huhROUW24APxcrc9hX3FBSVT3p27yDijBGjJgEsfN1FBnfDoqnMPPPf07+Nfji9ieDWIbqbFIjcVWe4at1hxGHOZFC0liMZAg0GItIAZulWHXWjzS3oZ1BgUMfuI//Fu840/9J9W0TTnaVz+fgXGrUJUKnNWjPvdrYl29pgtAUQob3B9K0FhBHAaKFUaZ0hsYSQ1WfX2naWr6XkCsI9+Om83nj0vuHlLVAdsRAFcDbevOsva4Y/a9qzvnHiLCys/KS8F8fg5GOzxUWhUWXXP6HsQ20cTLOY4Ffbdv345sHlki5xyGw2EFlBG0aXEPAJhOp9EXj4t2YwxGoxGyLMP58+ej3cGNGzdw5swZPProo9EUHihTKrVNA4DIJGmfNgCxNo1MH4PnHQwGsa5NA0OmGYpIHAc/4/jn83kcU53hYv9GoxGWy2UEJqw91L54QCleon3uOp1OZCC5fXNzs1KnxjGSmdOKnVoYhfOjwRrgGbXRaBRTQ/M8R7/fx5UrV3Dp0iVcvnw59oHXURvUE0CSoeN+bJ+AkufX9ZwEmePxOLJ5nU6n4i9I9pVgj+moBHd14MxXzS5rNVOt3Mn04NMW6dSzc0XuAVqRBnYtL4v7isIzeRVwFtL0RABnFQsYa/oC2Eoc8qGNoAYO5aJcZTIdEVRxrkyxZKohmbm40DeecdIAT1T6ZawX9NYBsu5cOpx6NVJRt9R2DZVaQ5KXBHyJRBZL20FE30CKvyiG0bUMrAiMdfjGq1+Kv/axz2A87yBJLDppgV4rQy/NMExXONPyok23VyPs5330TQ5XFHCzGTCd4ueufQB/nKV4ffsGMmfQNzmWTuCeeRZmaxNIE5/WSTDM+jgbQGxRBMGWMsWzAuj03BmURuvWT1xMv4xzGo4lONS1eQK4xFRrKI1UzxXal8IFsRsLs8r9tckToOsZP2tCam0iMcW0iecXGnvUMbgi5ePnqG27132pxzoAd7+w0gtq94XQpU3ct2iHmuZlbtFtNaCviSZezDgW9FE9kmwTF/Cr1Qq9Xq/CMGlDbQKD6XQaP9MMEOvPmD5JZo37E2BojzwNsviqz6VFOjQTqFkfppDSdmIwGODcuXOxVgyoMmjasmA+n2M2m1UULAkuWOtIRqrVakV7C23NwPnSLF899ZPedfQQJNhhuivnkgIuBF8a9AGIbdZr8rQPHgVoOBebm5uV+dLzrgGYZmS1XQUFU+q2CgS/bFMfAyA+OGCbOlWWY2V7+jozfVWDPx6v6ws16D6NPn3JEnAGcAlgjZQMD1d3QbLSKSVPwAM9MT7lsTRyXxNhBWkTieVW9bRHAEcYu6qIR7nd71umGwrK49xxMpLW1ym6+hKuLq4C9i+I1WjAVzm/lPVrsc/rGa0IaDgm4+dSVLvOCJACrjBIz55B4Z5GURhYa5CIQ9v4e8nAYWFbQdClwIOdfZw1wLuf+R1krsDY5ni2AKxrYSvJsXAWfXHoSAsuz+HCd6RS+4iSPY1MLFQKaC3dszIPjMLFGk/UH2ycdM9rbCi8L2r71+8Fnj8vfH8TbwwvNaDfxN3HcQDqToDu+f+a3f01uhNDB1RvseP6fE9B4t10fx3tuOa4Y5u6m4E38byiQ6avsW1oookXPY4FfYvFAoeHh7HeijYE29vbsV5LgxltBM7UUIII+s11u93IMl28eBF5nuOJJ57AbDbD3t4e9vb2MBgMYkojo67gqUFFu92O9VwAIjglA0hm7Pbt2xG4XLhwAZubm5EVJLCjSTiAaCTPcVPwhGwSTdBpQq7TOI0xMZ2w3+/HvhKw6DRYrdLJueE5Njc30ev1MJ/PK2mPi8WiIhLDa8FzaOVLPY8aSBO4njt3Dp/3eZ+Hs2fPxnHzGM41gTWBNNumOirvFwAxPVenAXOO+LBAX18+LAA8i0kmkm2xrpDiML1eLzK6nM92ux1BJx8I0EaC49Qg+TRF+8AhWQImExQ9wLYS2K7138y46PArEBFU0v5cYdamfZY7lG1ML7ex+UTuQVDBYxRIc867yYiUi/zIyrko9FKJ1JS41FWbQ7Wr/pzBpD22FdU4a/1XIM+FureIhZkCypo/spKBkXIC7x1oQm2gE6XS6SCZg7GAS6WyWmW9nAwHeLi1i047x2LZgjEWZ7ozPNAZwzoD6wQdk+NLBk/hCzvXcD4Z4DP5pDLMvslhAGwZoKUnZZX5c+Th96vO3vEBRl5SmC4Jpu4mXIuiOl9k9lyOEgg7pwzdHQTBiF1CvacJtgzi0zthFDtaWH8fEGw6PygpbDRnd8Z4Bmq2gKwyuDSBK9qNOfs9jLslqfjZSc8EXlAfpPY9PqkP6xpwRzfpz9axlmv7seZ8d9WpOzX+XPdp4gVHJ/W/f42YSxNNvPhxLOibz+eg5x792Fijx/RFAhQAsd6NbI5mtAiOlstl9Ex74IEH0Gq18Oyzz+L27dsYDAbY3d2Ni3wyjEzrow8f2S6ek8CEPmwEUazPI2PEIACcz+dxDGx7Pp/j8PAQQJmqqD3emLpJdjBNUwyHQwCIDCTHv1wuo2gJ6w85FoJl51wUi5nP51gul3Fci8UC0+k0pikSRJMRJWDUqZAA4txTFEXX+tHmAvA2G6PRCI888ggefPBBbGxsoNVqod/vo9vtYrVaYblcRiVPzdQy3VSne2o1V4Iu3fdut4tOp4PBYBBtKcgQ8qEA0317vR4Gg0G8LnzIoC01mBY6mUwwnU7jfQMgeieSDWQq68HBwT362ty7SFYOYn1NWbES2JagWCUoBjaKuxx1HwdEaN1wTMNOAxqHg1cLNj8JyKrwaouuZJY06PBASQE+2jzoLBxTamuWqp+1IHulWSIb0lLVcfVxsR9l5xXTFABpBfjVghYMVPMk8HWpQHJ4U3Hr4KxB0Un8XNqgTmoEbraAEYuzgxmuLTYxW3TwzHgLN6YbaCUFHh3u4otGn8FXdJ/GyAj2ihkezzbRkgJvbM3wbY+8FT/59G/j07nDyBQ4k3TxjQ99OUw7ANzCQvIgV54mJVDKlYS5ZvcC2xdTaMN3vj5PBITcXhFvISPKLAoYDw71fWXVPDM9M6TKcrtNfL2euAACF/63RBITQKEBWqkCnE0837gbYHM3DOBzJcUqAE5TjncBovSxxxJl+qu95vjnirvueqwnfdikgb6o0W6YviaaeMniWNBHgMC0OTJXxpgItgBE0KFrsepsG1CyXFpYpd1uo9fr4emnn0a328Xh4SE6nQ52dnaiSqhuh0CH/8jq6JquQi2KyChxm7ZZIMgjq0awqOvIOH7NKmnGi+yRrgkEEM+p0y5Z30jmjKB43XwfHBxge3sbk8kEk8kkMnvaKoPzSECsTdEBRMaV6Zja8oCm8gScTLkdDAbodDoRJBE8clzdbrcCqrUaKIGsBmZafEc/BGCKp543zivngaygiGC1WlUEc3Tby+US4/E4qstqIKyvOef4tIU3BndwKw/8TCZwxsEtBUUSlk40FnceNEnI0xShYucxjavt+cAiG7XQWRXeBiBdUyOGY57qs7bLSCUdUzNORzz46pJ+AbzdcU1VT+m82zgJZ3CoEkodrQtsVwmMnAFsy8DNZhjbLi70JnjWbKDIDQ4XHTgnGHRWsANBVzJYAJlzmMGhKxnOmgWGpgNJEuzbHAvXxiYKfOPVL4UkIV0dAJwF8mDdENU7XfU6iCkBXx3YAWXqJ9/Xt60Lrq+Mg1jrU1wdohBLye76zwF4cKjrCsNcVnz+8hzOGQ9GAThjG8LkBcaxzFktKp8fg5julCK67rx3avNex0mneF4sXxOnOgj6lvnp+/9xE0283ONY0EcPN+3txhS+fr8PKiICHmD0+/246CZ7Q7aQIEinDTJV8dKlS9jf38dHP/pRAMDDDz+MRx55BGfPnoVzDoeHh5VaMWttZNAAVMy4CYrIotUNuVk3x/RA1iLS903bQGRZhlu3blVAz2KxiKmWBLnj8TiyY7RVIBglMCUwy/M8poYSSJM9JNuVZRmeeeYZ3Lx5Ezdu3MD29jZ2dnaO2ENooNvtdjGbzSK7yTo5Pd8AInDs9/sRwPM6dDodXLhwIfreaZVOgm+yjUyn1WmYvA4aJBPIca7J7NGDj4wfWTxeH841rwVZQX5OCwgyfaw9pe3EYrGI7OD+/n6l9vE0hlggXTrYXNA6BExPYHKBFAKbOriOgzMOaFsgKQ3by+xP/845DwAFUmKIgJ5sx+GZP9lC/2YbF98/jT5slOWP5urqET0VHgFbZZs0i1QfSw1ElkqhUrFo0IxftAY4LkuVfVx3TnW+yHapVFMho0nlUTKHwTzez7+D7SdYDRO86Tdm+PXx5yG3Buc2psiKBMssha+ndLAQfGp5AY+2b2PLzPHKNMc/+vy3eJGWVQazKfjON3wD3GoFGANJVnBFARHja/oKgctzIEkgxgQUquYsqaFXawMQU+BOg8V43QIg13W5Tn3GY+nJR/avncY2pHCVmkCx1qeNhraEaaOpgWulkDTxYyFIBapCME28oLgbzFVh1I5BROs2azD4vBiyEw5y4QSV5xjH7KM33AWZeGIcyy6u23FdNIjyRYmmpq+JJl66OBH0aWaMZtv7+/uRpSOQI/jjwpvHsa6KAIVKmBosjEYjXLx4EZ1OB48//ngUXNnb2wOAClCi2IdWpyTjQ2AAIJ6fbJxWANWf1z9j/7UNBUEZ9yWoIgCmuA3N2HmsZsA0mNHsF8GhZjMJRFkzR1aNqbRkrwiAeTy3sV/ar284HEZxFIq5aMZUzxvHT2Cl7TKY3ktmkXOkVUx5jeo+g0wr1deGIJaptAR0BHjaokIzdnULj7p1BYWEAEQwTOB5KiOsVsQ6pHOEeivAtACx4rP2UviaqkTgxMGkPodRr1M8EOTSR46cw3YdJo84bD3RQXs/C7Vjtf1c9RgAPm1P/R1FPY+whEzjrK2eWLtna955x03Hun0iqEDJWumgUqfmSSorW1e+si0b9ree8br9JsHT8zNomxyLohW67pAmBYwAw/YK07yNa4stPN6+hIfaO3htax/JmS2g0wa6HV9/l+XAfO5ZsCwHUHjgVxTBX9AEhU5tmm7KuePXxlqI9UDO6TmIY66xhLR60CBPM4EKOHrW0cAR/BEM6nCuyi4GhtQlAkkTIE2BtoUYAxdSVWkS38S9iZOA0HFZiXcCP1J7faFga21f6rcS+7Xu9+UegK36r96xTa4bXAP2XtTotRKkRrA/O3019k008XKPY0EfgAgAdE3e7u5uVJik+MhqtcJ0OkWSJNjY2IjHaHERpjdqINTr9XD+/HnkeY4HHngAzz77LA4ODvDe97432jYMh0OMRiOcPXs2mrefOXOm4jeX53kUWel2u+j1ehEkarCm0y8BRNBDBkwrQTLNkFYNGljoVErODVnGVquFwWCAwWAQwVPdT3A6nUbQPJlMIuPF/pAZXa1WuHXrFs6ePRvns+5FR8ZMp1aynX6/j8FggK2trXideD2oOloURUyrpYiOTh3VNXRJksTavOl0GpnFeo2hZgR1vR9FZnq9XrTm4GcEzBTeYV3hfD5Hr9dDp9OJNYJU6ORc0PoBQBTkuX379hHFV47/NIUTlD5xANIFYDJf52cKgU2ApOMZP7swKAYWtm2Bnr+H6ecn4hUyBd4Q3RUIPnfhPMbB9S1gHJ75v6VIp10MnwZaU4dk6dC/sfIgEFCrwtLDjiDMpwSy8/Wx1Or01ihzHgVz5XmiCuhJD4Ap3BKAYVnvhwjspPBA1aVSMomVc4Y5KyyQe7Da/dkP4rXfdRmrIkFufUe6aY7CGow6BQbpCo8Od7C9HMLA4WPzy3g220JXPubr9AC4VurZvVXmwd8qLGrEALbw7F+SQJLET11eQNo1MR2nEG9MA008yBLx4KoIYJ2gcQ1Yq7CA61bVeQGIhcmL0G8/t1EYJlWWEYAXb1FMrUsE6HU825cYoNOOzHBT0/fixPPBKs8pZfQu97vXmOmFpns2GO50hzGCS5tdXN+fv9RdaaKJz7k4EfQBqDBHXKDv7e3h8PAQe3t76HQ62NjYQJqmMR1U+7wxFVAv0LXtQpIk2NraAgBcuXIFeZ5jd3cXi8UCeZ7j8ccfx40bN6LYCI9n7RnbIuPEhT3BFsGYFhpZrVaRGSSQJBOna8xo0UCBEM1OMYWRQjdUrSSgJRAiK8g5YHoqwRPtF+gByL4tl0vcvn0bw+Gw4pWnawHJQnJ8ZNk450xnJOuqffUI2BaLBa5fv44syzAYDJCmKUajEfr9fgR/7BP7LCJRUVOnbnI/bVOhgTaBpGZd+Z6gkqI5w+EQ0+kU8/k8tk1j99VqFfuhlVN5/Q8PDyspv7r28rSFM0EYg2t9B5gCSKde1dMmQNEV2LagaAOSG9iWQVGIN203DpJ4UMZaP6Z5AkDF6Cv1G23XImsL9oeCZCGAFXT2exhctxg9tfB+bADdInwzoX8l0ELJHMW6sOrfcYxknrRpd1TuRGAvVRuR6UItfRMlYwdA9Hk0sHEB0MFUBWj0viHdUxwgqxxX3j8EMMZWa4b9rI/cJpjn/jehk+S42t/Hm4efxO/iFdjNBhhnXYyzLn7ZvhHFzi7S9IJP11xmcJMJYJ23achW+KXrH8LbHvxiz3SiKFNV89yzE4mpsn4UnkqSct+gdBfBpXOKHVTgT9fJWFvaQBgPHmNqaF5A8sLff4kpgaa1gdEzpaehiE/pFJTXPPTLtVIgMeX1ywqInITam3ghob/WOu4WhK1LGb0boLeGoKtsvxugdT/A2HNhJSML2KDClzwe3OrhWgP6mmjiRY9jQR8BhAYaTL3TKYmsiaOaIpkizYCR9arX2BH4EaQQxJEF4wJ+e3sbi8UC3W4XIhKBANMoNaNkrY0AQvv7AaWfHBkp7eXX6XQiE6n93Ngfhk4z1d51TAVlfZwObWnBIDDqdrtx/ARN8/k8egMy5ZFMJQElwZg2QtdBllWnNPb7/ZiSSUBI4Hd4eBgZx9Vqha2trdgnzh9TWdl/nkdEMJvNICLodDrxGDJydYEX9pfsINvh+XhNtZ8hx6P9DwnstS8f3xN08p6t+zaepvBslKsANLEAcnh7AQNf82KAZCEwuV+U27Y3dHdifQ2NJeOFoyssmvSFVZ8TByTwQNICq00AzmDwbBJBnzjPGsY+xXysWmpn8OE7MiYdij30n4f3prpvBfzxkLiydOtXjoqRquS2OQcpam3oNgOrJascnSTHvGihY3KkpkDuDLIifIcC89eSAqnxczMvWrAQnAfwfz71XnznG74BkqZAlsGtMqD2W3dkggBPadrCd9fUfCQJ9ghsCeasHquUr2sYPdYwxolJjGfwOE1kC2vXUv/tbRrEv/Lz+GH9fNaD07pXYBP3PNaBN25/Lm0833Pfi7iTFcSdzvV8wGtDA56OePBMD9f2GtDXRBMvdpwI+oBSBIQLEm2GThaQjI/2pSMQIhvW6XTigpysWr12jCDx8uXL6PV6WC6X0Wvv5s2bkUViaiFZNvaB/eaiv9PpxHPleR7PQ3BHARGOTacecsx1do3sIUGIrmMkANbAUqtO8rXVasW0yizLMB6PoyDJ4eEhdnZ2sLOzU5lXCpIQUHE8TE0FSjDF1Fl9nTY2NipAbjabwVqLw8PDCLKeffZZOOdw8eLFeA11Wqpm8siOEoiTRSOQ1wIu/JvAVFtW6JRRAnEAFdaT/SD41jWenGOmyvL4fr8f60HTNMVkMqkuwk9JVERRHLyYBhAEMXyqIlYOJgdMIUhSz9q0JgKbAkXXIR8Y2JaDHRSe9bMCFJ7BO+6xvND0vRX2F4flWWC5lSKZ5RF0ifZSMqW3W33BFpuuABf1gQZ7/JuATxN2TBElruHx1lWAYDlnJZg9MpcBBB9JKWVdn3OQrAC293GhPcZn5mfRMgVe3b+FXpLhmWQLk6yD8aqDZ2Zb+IXiC7C0Kc62p0jF34ujZIH/uLiCf/bRX8B/+Ya3+64uljFH9ZeufyicM7Cn1gQBF1XDhwIwQQiJ2ynKEoCUC79vHisTiJGRM2XKp6rB86m55XhdO0XRa/m2rYPJLcxsVc5JAGxSWKCVwpqQumlE1XUGLz/nqqIv3OZcaUnRxH2N41i/exX663uv2gOwlh28l2OoPPe5h+02ce/i6lYPv/xHN2Gtg1lTBnA38bZ/8uv4rq97Db7u9Zfuce+aaOLlG8eCPtZbkYmrWy/Upfk1i0dWhQCMgIx1WFzAMxVSG6tr4ReCCloIsD8EPgRbBBBACf4I6lgbSPBJEFmvcSOLplki7U9HYNPtdisKnASQWhyGdX8cn2aq9PyxHo/gWAu7kNGqC8tolnWdMAmvi7at0EwYASLFYijQ02q1MJvNKvWFrKMj6OLY8jyPXorch59TvIf3BEEm96EIDOeG9wnTNDUbSwaXQjVkYwnkdH2jPlandWr/xFMJ+ii+UfkbHgyIF3TxC3df68ecS5cDkiIimqItyFvOe/shbK+neBq31tsOQp86wLYk+vNBd41ATNz6J/TWHa3h00AsgBDf6BpgyI80m3dcPRoUC8ljQh9EM4eh3WjMvo4hAwBncSad4obZQGYTXOnuI7Mppu0OUmOxO+8jNQV2lgNc7B3igfYYhTPYyQbIXIKFa2HftuHCAxHYIoK9wll8w4Nvwi9d/yAA4G0PfnHoq/VgjWqXnL+6oItm3qwDiiICPa/ACt9OYo6wdABK4Ac/Ly41kbVziUCyBMiDzYKRKGrjEg/KCfSdnjKLUg0VgFPKOmLXX68m7l+snfE6wrpX7b7AeDHvjuZOPL3xji95CP/iNz6FN/9P/w6vvTTCK84P8MrzA7ziwhCvPD/Ala0ekhPA4CIr8PGbY/xv/+GTDehroonnECfW9GlQpwENQUBdiZHbdK0cQQNZqaIosFgsKkInBJi6Lk8zWSISzco1w0djdZ4b8CBnPB5H0MC6OVomLJdLHBwcRDDENESCCJqUE9yxzoxBYEbAMxwOIwPF/pD94txR3VMzVdrjjmCGgI4KmxsbGxFIbW1t4fDwMDJfHB/PwbmsM3OaTdVG9WTC6Au4XC6xubmJVquFW7duodfrRWsFfW05X/1+P1p3GGOwubkJAJX6Pl77OjinH2G/30er1cJoNMLGxkY8V1EUGI/HUZSH2+nFx/5wDjXQJGClIAxBIG0cTlsQ5DnWngUQJBEwITBxgCsAswoLbWKDNmCnnvXLFqln/nrO1/sJ4BKu+AFkJoq7OHGe7VPhDLAaeAASGT6yRxYQV6p9inMx9c//DX8cAUIEC0J6qmwPCIxQOLDO2gHQ9XsEa9EIXqTCRFYM4B2ARAG/CF5dyaIiYOdg5QAx+LLek8hcgidmD+D3J4/gqzc+gdf1rmG/6OO3W6/CQdZD2xS41DnEMFlgOxvBiMOsaONcMsFDaQYUBax1ePe138XSAQYGBoJfuv4hZK6A4Urc2hLcmSQybzHIqtmQKlkU/p9zcC4ANJcCaRKM5l0AfmX7Yp1n5wKYR5rAtYK4TQB1kggK1/bX2jqYZR5URR1st1UF8VKiCLE2evnxGnAfZwTSyHe+9MEHKTW6bh3xfzc8y3H73K900uP2vVP/T98vfBPr4uFzffzy3/ka/PaTO/jkrQme3J7itz65g0/vTJEVDu3U4NFzfbzi/ABvffV5fPMXPYjNXise/+kdrxi/bGwfmmjiOcWxoE8zadrwXAt0aO84LuB1nd187nO2mbrHY6y12N7ertSdETgaY3B4eBiFTW7cuIFbt25hZ2cnWhIQjFHQgyBJ13dpwMj6NwJK7k+AxFRQmsUT0NG8fDQaxTRRwLNpg8EA3W4XW1tbESByP9ojkJEjCKvbDhAYM9VzOp0iyzJsbGxguVziwoULMZWTabP7+/uVtFP2RzN8BET1OrrpdBpZyn6/X2Hi+DkFeiikQlDb6/Uq59Aef91ut+KdCKDC4DLtldeGaqPsx3A4jMCc1h6AB9EPPPBAxbOP95lmL+fzebQDIcvK1E4CPgoMnbaIDEpg80pmDXHRRvDnVSmlwoyZArArB5cAJhfYVJD3BPnAwaVAMYBn/4wDcnN8TpUBnHGwbYFkflHvWv76af82fYg4pdRo4CX7yawVejAoGa2aQ7twsMes3CqsI46+inVAoQzFjQHE+M/cmtRO608gzkGW3kZBEoNXpjPcaN/GxyaXcbsY4rHWbVxMLHYt8Mr2LTy1Oo8/nF/FmXSKrmQYJQucb43RkgIPpfsYSgu+bq9Qp7IwSFA4CwNB4OXgQiqkwAMrMd77rjyQzF5I67QuCrkADi5xkKII7J6FZDmcVV55cc5DamYrgWslsO3EG9AbDwQphCO2vC9MmEPbTqpgPKh7OuPTRJ0xMIU/t04pbQDfKYoXyPLdCQzeS4Clz/dc23XHvG/idMdDZ/t46Gy/si0vLK7tz/Hk7Sme3J7ik7cn+MFf+jj+wbv/EFc2e3jobA9Xz/Qxz/zv7DO7M7znD57F1TN9DDsp+u0E7dSglfBfrVa6iSbuQ4jIDwL4CgCfAfAdzrnTJxUf4sSavnVfFi2gov326umEdc887kfVy+l0WlmgEzyKCJbLJcbjMVarFSaTCRaLRTRGp/UD99UgiimZmtEhSGFdGevdCNTYb7ZJkKPBJMEc20iSBIPBIO7HVE8yhkwdrQu31NNhCcC0XQOACAZ1zaH2meMc6+vD8+o0U32NyIIRDDG9kimrDNYAas87zi3no26BwdRT3T9+RpBGwRsygDyOr2y3Xr9H0KvPrf/xfmFtoxbdITAki6yB+2mMqIoJ1qMFhs+oBRhT6lQKo8lDyZg4SE7mTTwLGJgdKnfWV1bOeHsIZ5xnyBJTmnpXBD4AGES2TLN8MDgKOGxgpDTjRuaItYG6fYdKPWDd86+SZqqjqNb6VdJD+R3R7GIcWzWNNRFBEvIgVzbFtXwDV5IDAA7nzBz7yQx9s0LmErSkwGYyQ0sKXEgPsWkKAEkAcCm+6cEvw89d+wAsbAR6Fhbf9OCXQcI96PtVq++rM55FEVI2a0+zdeqnCeNk3R7VOeOEhJRRY4JKagDC4b6KAi0OPvXTJtVraQAgXK+QHmxbBpIIXJaW527isy7uBtTdC4bvbuNugWbD7L18I00MHjk3wCPnBviTYdv3fePr8NFrB3jy9gRP787xzN4Mtw4X+LYvewizVYG/828/HEHgumglgtQYpImgnfjXVmLQTgzaafiXGHRbCTpp+dppeYJjsszxyLk++u0U7cTAGEEiQJIY//8NAyTGhGx9QWIkbPf/TO3vxEh4BiqxRpvJMAIB/5e17r3/X6hwaRCPNSKVZJr69nBY2WblM4HV/w9lO6WC25H/vdfjpI9PAt0nH3fS+U5o83li/G5rndT3nUNEvhjAJefcV4vI9wB4B4Afe369uP9xLOij8ApBTj01Ti+660beTPmjiAkBGlkh1tVRxGQ6nUYRFQpyHBwcYDabYTwew1qL4XCIW7duodvtot/vV6wTCDqHw2EEdNpInOCM2y5duoR+vx/tATRgoG0B29cqoGQQaSmgU0d1zRhfCarqFguz2SymSZJp29nZiZYEnU4njlErThKw0nhcq41qkRjup4E42TKmmur5YC3j5uYmut1uTIFlzWOr1aqYvHM8FFohoNNKnQTgBK/sz2q1wmg0qoi7LJfLyDwCiGm2ZOk4z51OB3t7e9FeAkBFyXUwGGA0GkWmU9dqst3TFtr3zglgCqfAnzfRtmkAf4JYn0XvvPgDV3ihFycO6QIoOv44szSwHYO872BbQe2TqZ9gCqiDwB97+CqLwY0Rhk9NYdtJSIEsYKwNqZAEcSjrvjSo0j/GFqGOECVgNCaygPX6r3i/8v81lUf4TqWZuuo2gP+nC0woUz/9RFYUQXMFUvLCs2RZhnHoSy/JMC9aePfemzDe/GNcSA/xe7NXooCgJQUmRRddyfFgew9bZoYv7kzxzqtfBYjA9BJImsIlSUjlNIHtC4meaQpppd53TwRot47aLlC8xXnLhzg+pnzqsdeviUhg7VwpfBPTcy0kNTCJgW0FgE0mNBGf0tn2wi2S23CtArPHVYIjSDSAONh+CyYROA28ra2mqjZxqqJ+ZZ7P+uh+Xt27abu5uz63ottK8GWPnsWXPXp27efWOkxWOSaLHPOsQFZYrHKLrLBY5hZZ4ZAX4dX67VnusAr78XWRFViq1/EiQ144pIngQ0/vY7YqsMotCutgnUNuHax1KJyDtUBurdfCcg6FVf9cuV/zfOx0RisRPP4/fsPzPfwtAH45vH8PgO/AZyPo0zVcOjT7U2f7tF+eboNASXv0aW87MktUAyW7py0J+v1+rHmj752uU+M/ADEVkKCv0+lgc3MzGoKfP38eg8Eg9kfbIVC9k2CVwjF1MZPlclmpq+MccOGqGTaOYTabYblcRkEZPT80aSdIrNtQaHEXzm0dFAIly1YXj9F2GjoNlJYaBOJa6AVAFGrh3LCmju3WPQS12qkGWWxXp7/WhWk4Hs0KEnwzPZR9pzUEj9WiN+12u3Kf6rGeuiDAW7P6WpueqI4THurKjQLAhYOk8HV7xRIwK0E+AIouYLvwlg08P9sr/OJ++wsMphc3sPHpHMnKor27iGAjmnYDpZ+ecyV40WFqr7WxxWPrnx23FNWA78hBUmEKIyB0ShGTTB8BVgBXyHKsnEHhDCwEFoJbyyF2iiEyl2A39w8YljZFPymzNkZmgSQ8BpW0VQFf3/Dgm/AL134fS1fAwKEjqU//FIG0NdsnVfBKwFdn62j54B/RVse+DgSHrAEYgSwBFBYmSeDaJqTAHp1jz97CK8aySRElwFPbPzWwpgUpHFwAoFIUldrJJk53nHSlmqS4Jj4bwhjBRreFje7pzuQBUILEAAB9YoqLOmnOOf9qj9nuau/VPpZZQPXtEWzq9vw+NpyHDCCPD3tX/n4+cdKx7oRfn5OPO+l8J7R5wnF3GVdF5MfD+3c5594V3m8BuB7eHwBY/3TilMSxq2Au9MmYcIEPoJK2SIDFRTrrsTQbNJ/PozAJxT/Iso1GI4xGI2RZFkEPmT6KcFCYg2IsrJdjzSBZM/5j6mK328Xly5extbWFq1evRgZtNBpV/N+Koogsm7W2Up9Gdo8AhUCG6an9fh+bm5sxzZMpmdrYnG0sFovYb4JBbWtB5mw8HuPg4ADj8Riz2SzOK0EMQQ1Qgr96f/VnTJfUYEgrXFI5U9tbEIDv7e3FOXLOYTgcYjgcHjGg571Qr/fjnAKoMKJkOHmNp9MpkiSJDKdO1SXgZR0kgS4/1wCY9xnHoWtS6/6JpyFiGmdk9/gBjqbiGX2ceg0MoNjABhbe4sEJgKkXXClaQDYUFF1BNnKwLaDo8YCAIK1AMkHRBaYPOth2imTu0NtMMLieIpksIfMV7Kjr6/cCQ1QRUmEYc3SbyFEWT8Qzf4CaAKcAY90A0FVf2ca69wyye8752jgyX1kOrDL8zB//e7xvMcT17AzmRQuzvI1p1sYvb78O/XSFrZb/ndleDvFQfw8z20ZXMiRwyBytCiyk3fYgC4C02viGB9+En7v2ARgIEjGQxECSxKd4Mu0y9MVpawy2qcYjiSlN21m/yHo/fVz4vjvnAKqJJgmkSGDSBHaVIoGvPIwefHC+vq9wJcgTKZk+U16vOI8AXGJg26F/uRfUkSJpmL4mmmiiiTVhjJSCXk18tsUzzrl3rtm+B2AjvN8CsPui9eh5xLGgjzVmGuhp4Kfrx3QdGBflTPdkJEmC+XweQRPZOoIwApfJZILVaoWDg4PI6hFAEGAS0GnxDmstNjY2sLm5iQceeAC9Xg/nz5/HI488gjNnzuDSpUuxnpDCHhzL7du3sbe3F/9mjMfjCFCcc9FHr25fwXozzS5yzBSxqauEAh446vo5pjNeu3YN+/v7Fd85XZ/GtqkSyvnhHJI1ZeomgRTTTXke1jrSWJ3Ko2Rli6LAaDRCu92OAJv1eGQFye5qs/p6uqlWSu10OhFQkp2jdUWappF9rbORfOU1ol/gcDiM96C+PwgAyRpy7k9beNVLeBbKqm0A3RngXAno9OcVMypXfuaMZ2vi/1pyDwIBIF0AyVJQdIDVZjB4NyFNk+dN/OJ/cd6bmy8uJBg/3IfYPs7+cYbu9bHvkGb9QupljKhGWgI91vIBAArrGT1bAjJnTMnEBWsJZ1CqUob2QMW2IB7j50qlmmrARHYv9+qXkgclzLyAG09QjMf44Mri9+eP4tnVJsZZBwYOvdTf55Osg0nWQT/NMEhX6Bv/b+H8d3zhLH7u2gfw5x59C6Tjf19gnQdppoM/9+hbwJo8aSsAmyRAUQSGzKFSt7cqU8Kl3fIKn2laBbSs97O1Whbr/PUXgSusB3Qi3rcxy73NgzUxhTg+bbX+EXBMCSWxaCmIE8CgYphBexHAK4OGC9bU+L08ormKTTTRRBN3jPcD+C4A/xLA2wC876Xtzslx4iq4LhhC4EFgp0Va6gqSWo2TKZIAKmCLgAVABBoU5tjd3Y2AiMcBpaE70xF1iiCVIC9cuIBer4fNzU2MRqMoumKMiWxbURSYTqdYLpe4desWtre3URQFer1eBIaU+ecCjAbqGojV01wJSPmezCYBLlNNAUQWj4zfYrHAbDbD7u4uJpNJxXOQYIpgh1YSaZpWWEoCc832aVaQbTClNcuyWGNINc1Op4NutwvnHEajEay18drwvtAAj6C27glIIMb7gfeCfnBAERgtDMO+co453voDBZ6P9YIE9WwTQByHBqKnKgJLVq1f8y8S0j4liCuKPXr4WqDoqtvgT4EkA1zB1E5BshC4tOxDzBUNrzYRSOJBICCABVYbCbrX4QVUYMs6OqZeBtDgHKKaJ3Rd2N2EBYRgpi0lqZdIBMAaYFYAnw7NCtpgOl4UHlStVviZP/73+Ew+x/vmj2I7G2JetLGyKQwc+ukKs7wN6wTzvIVZnuN819fSZi7B1HYwdS3MXIalyzzIS9PgH+g861cUwVQ9IOAkKVVMY794sY65NynWApRpnfo+qINcvR0IrKFnFKXwc1CppeTx2jZCM8rOJ/9QSVYLCAlciQz4mSAkGTfRRBNNNNHEyzuccx8UkRsi8hvw6p0/8FL36aQ40bJBAz4umCl9r73smH6pRV9YD6fVPJnKx/a5MCfQAoDhcAjAs2zGGMxmswhaWL/FdMRutxvTJ5966ilYa2O6KFM9rbWYzWaxVo4s13K5xI0bN7BYLHDz5k3s7OxEFoyAkLV82opAgxNuZ50bAQ0VKynyAiCqgA6HQwwGgwhe+G86neLg4ADT6TSqlOrUWW1TwDrGfr8fDehns1nF7J3jAEoAVa95a7VakfUkcBMRjMdjLJdLbGxsRI9CjoWCKpwLXU+pmUn2j/eABoIagOnaR4JxMnQ6VVQrfep7h4wn53+xWGAymRzx9TuVgA9euMVpw/SwuCbr4kQ82wLAOYmgrrR6CIyQQ6y9cgIYmmeT+XMS9hVv81Ao8JSo1FFXtu/aoW0LFG2/ff9VBpsfzj1I4EG5lL5tERQYuJZ4gZg4NtavATACK0H+XwFCs7KQrEAyXXlmKi+Cx56F3egH8OIis+jPgTCHDjAOkttyP2t9O8vMp1Eulyh29/Gjn/51/Oj4Ffj08jxur0aYFm1M8zbGqw6cE3TTDIu8hWWRIC8STKWN/UUPnzo8i3ZS4MHBAS52DvEtZ34PV9PSMB3GeLsL14Kw/K8oSvGWNI1pmrH+wJSy4kzzrCieGQFaafTdK2sBM8Aaj+RV8DySpn6e8ry03MgtzKrw1g31+sA6qAtzLMYDv8jYSriqUZxHAb5Empq+JppoookmPmfCOffdL3Uf7jbumO+mxTh0Ch3TGMlCEfzVF9jaLoGpfkDJ+Glzdb5mWRZVN+lPp33/COiYrkfWj4yX9m/b3d2NLB9TCSmmwnTNZ599Fjdv3oyAgSBI15KxXQJPiooQcHB/1r1tbm7GOjqmHy6XS8zn8+iXpwVNCPp0/aBmUvX10ECK+3M+CJC0Nx7rBgnSaHvBVE3OF/+eTqcYj8cAvOJop9OJ156Kn1pYhkCO52LdoRYC0mqdBGS8N2gjwTGx3bplBNtirR6ZQt4Dmu3Uwj4AYnunLXwapoNNpWo3oF4jw+dK1odiJxGgxQW5x2Je1FFiumfREq/aaICi498XHQeX+nRAl4ZzO6/i6ZFj2GbDtsSheHQG+VdzuG4baPmUQ6krNhoWJLo4RiSATQ2c+PQ/awR5L8HsQgqbepN5sQhehMD0QWB1zuI/eetv4e0bH8ZZs8B3fck3Qno9D2KSBK7XASizHHzkJLce9Fnr1TkLD/qQZT7dcb7A//nUe/Hh1SY+tbyAg7yHadFGZhPk1qCwBtYJnBNk1sA5QWEFzvntaVJgvmohKxLM+i18cvAAXtv6NOxiCcO6vCTx6aQKGEsrrQA+JAaSS6VO019jW7JuRvx+BHmhDtC1Uv9sIM+r6ZkGJZsaACJ0DR4BXkh5FRse9DjE9M54X65J0ZTjsFxQZ41DaIi+Jppoookmmjh1cSzoI0DjYp5phTQtp+UBF+NkgvS+TL+s11oR1BC8MI2T7BrgF/bdbhcPP/xwBJgUTyF7Q1GU+XyO0WiEj33sY5jNZhGMERRqdinLMkynU0ynUzzxxBNRPEWzbkmSYGtrC2fOnKmAGQDRB47gicIry+WywuBdv349jpOpkxR8Yd9oMeGci20QrJCV5HnJGHY6HRweHgJAVBAlSGZqqWYRsyzD4eFhTAft9/sRbGkmUwMr9rcoCty+fRvWWly4cCHaVHCf+XwegS3vAbJw7DcfBhDcLpfLSp0f0205d2T8NFgloFsul3F8mnkmW6mZZJ0GzPGexiAbZ0IGJaBYNy6e6+ma4gkeALAtiYDRicAlgE2rbdGPzbb8Z3kPsG2HvO9BHwj6xHkxF+vBAre5JLA6xqHfXeF7f/3d+H+97q0wZ7YCkDFeIMWIfy0Kb10w7MF1Wlie72G1mWJ+3mBxTpD3HIqeg+06uCQUH1JMJnEw7QJigEQc3vXxL8ZPt74A54dTJP/WIhWLW5MzmC9bWM1bSK910BoLujsOo2s5+o/vAnuHHhgWBYr5Au/59O8AAApnsWPn+JXZI/jE4hI+NTsH6wxWNoERBxuAXeEE06yNyaKDxFhkReLBXxFAoRXk1mCZp3hP8kZ8dPYQvvUPn8VfHf023vGFXw+c2YSkCZC3IQGgohV+agsLpIkHpa2W/5wAK0nK99w3STzANRLN1pGksf5R8gJuUd7bQrAHwLVbkSX0+b0JnDFegRUe2JkcJasX77Nafab15hMVddR14Tzz14C+Jppoookmmjh9cSzoo1AKwYG2IyAw0Sbg2hOPLBiPWSwWkZ0hwODinIt7EcFoNIrn39jYQKvVwubmZqU2kCCPDNB0OsVsNkO328WlS5cwn8/xvve9L7YBoFIztre3h62trQi+qESp6+oI/LTMvwYqSZJgOBxGIEHbCY6NaZdkFDudDubzeQRfBEpkw4AynYugSbN4nE8eQ1Gcvb29eDzVTTlPmlUl2Do4OIhzyTmn4Alr87gvmb6Dg4MIuOfzeQR2Wk10Op0iz3Nsbm5WmN1bt25hNBrFc/MBgDad13WguiZRK5aSFeW14j4c+3w+j+qnxpiKITzBM+DFX1hPeVqiaNeongpYK9M1IYBNPMhzJJCM3+b3k8jyudRv16qfLgGKrlfttJ2wMDcloENqQdUYV0jZlwDEJACzw3Ef/+nvfzsGP7HCOx/9AL6y/zi+sL3Crs2xcIKxbWHf9vDvDl+Pf/ORL4VbJjCzkM6YWLi2BRIX25fUQoyDJOG8AqStolaOJ1gWCTaSHImxGHRWyK1BtkqRXV4huwzY4QoTKxDp46sfuYHXD69hZBa4kB7i52ddnDNT9E2GfTvAzWwTB3kPK5vCOsEgXSEVi7bJsb/sIc9THC7bEACLVQvnR1OsigSHsy6yLIFzQJoWSIzFokhxbb6F3dUAn1mew9t//ZP4xbc8Apw7U4KtoghATw1KBFTpdK60lZAg2OIKCyTwSp/Ko88laUXABtb6Y9fV9aWJB3idVph/gW2nZUqnKx868KFBJTQAtcGcvR46LTiIf1eEd5poookmmmiiiVMRcpKvRRNNNNFEE0000UQTTdxNiMgHAHzymI+vAnjmRezOy/l8L+exvdjnu5tzvco59yUvRmfuZzSgr4kmmmiiiSaaaKKJ+xoi8uPHeJ015zvF53q5n+/FHttLGadT0rCJJppoookmmmiiiSaaaKKJexIN6GuiiSaaaKKJJppo4n7Hu5rzfVae6+V+vhd7bC9ZNOmdTTTRRBNNNNFEE0000UQTL+NomL4mmmiiiSaaaKKJJppooomXcTSgr4kmmmiiiSaaaKKJ+xYi8oMi8hsi8qMi0r4P7T8qIrdF5D+EfxdE5C+JyG+LyK+JyEP36DwjEfmPIjIRkTeEbUfOIyKvE5HfDNv/9D081+NqjH/mXp0rtPMl4Rq9V0R+XERa93Fs6851P8f2BhF5Xzjfz4vI8H6N7TRHk97ZRBNNNNFEE0000cR9CRH5YgDf5Zz7qyLyPQA+5Zz7sXt8jkcB/KBz7h3h7xaA3wTw1QC+DMC3O+e+8x6cJwVwBsAPAPhBAB9fdx4R+WkAfxfATQDvcc595Qs9l3PuD0Tk95xzX1rb7wWfK7RzCcChc24mIt8P4IMAvvs+jW3duf7b+zi2lnMuC+//AYAnAfyX92Nspzkapq+JJppoookmmmiiifsVbwHwy+H9ewDcr4X0WwN79P0AXgPgD51zK+fc+wC88V6cwDmXO+duq02PHXOey865x51zhwB2ROT8PTgXAAwDW/VjInL2Xp0rnO+Gc24W/sxw/Bzei7HVz5Xf57Fl6s8+gM/cr7Gd5mhAXxNNNNFEE0000UQT9yu2AByG9wcAzh6/6/OOZwG8GsDXAHgAwDercwJAch/OCVTHps8jatu9HPNbnXN/Ah48/8P7cS4ReRjAn4ZnMO/r2NS5fg73eWwi8mdE5IMA/iQ80Hwxr9upiAb0NdFEE0000UQTTTRxv2IPwEZ4vwVg916fwDm3dM5Nna9Z+kkAX6zOCQDFvT5nCD02fR6rtm3hHo3ZObcT3r4LwBfd63OJyAaAfwXgOwDcwn0cmz6Xcy6732Nzzv2Kc+6LAfwEgD+BF/G6nZZoQF8TTTTRRBNNNNFEE/cr3g/g68L7twF4370+gYiM1J9fA88cvU5E2iLyVgAfudfnDPHEMee5ISKPBWBz1jm3/UJPFM7RCX9+TTj3PTuXiCQAfhTAf++c+wTu49jq53oRxtZRfx4AmNyvsZ3mSF/qDjTRRBNNNNFEE0008fIM59wHReSGiPwGfC3VD9yH03yViPwPAGYAPgXgewEsAbwXwALAX7tXJxKRX4Bnol4L4P8A8E/XnOfvAfhh+LTB77tH5/ppAO8UkSn82P7Te3kuAO+Er7ccicj3Avjfcf/Gtu5c/819HNufEZG/C8/k3Qbw18PrfblupzUa9c4mmmiiiSaaaKKJJppooomXcTTpnU000UQTTTTRRBNNNNFEEy/jaEBfE0000UQTTTTRRBNNNNHEyzga0NdEE0000UQTTTTRRBNNNPEyjgb0NdFEE0000UQTTTTRRBNNvIyjAX1NNNFEE0000UQTTTTxIoWIPCoiPyEif11E2i+wrT8vIg+E918vIn/h3vSyiZdbNKCviSaaaKKJJppoookmXvz46wDuCvSJyP+/vft39TmK4zj+fCUDyY+S7iSzuzAZJCmDJJFR4R9gcZNJGXQHd5BsbLaLLpZ7lRgui/wsA9uV0h3UlZRB3bfhfMRwmdzv5XOfj+l0Pu9On7N9Xp3P6f27b/ZDwCaAqpqqqom/8mbqHfv0SZIkSYO1gdaDbzLJTVqz8mvAWuADcBzYBYzQ+svd6k709nc1Z2lNzPcBw0nuA2+BNVV1JckIcAQo4GRVPUvyHHgCbAduV9XooDarpWfokyRJkgZrDngJHKiqL0nGgMtV9SDJaeAw8JEW8HZXVSVZXVUXk2wEblTVniRTwFhVvU5yAiDJEHAQ2AlspoXJvcB6YBR4D7zoxlomDH2SJEnS0toK7EhyDlgFXKeFvqdVVV3N0STHaCd/Q39YawvwqqrmgZkk67r5uap6B5Dk6yLsQf8wQ58kSZI0eN+AFd34DTBRVdMASVbSTurmf6kfAYZpv4Y+WmCNH2aAbd09wM3Ap26+0LJl6JMkSZIG7y4wnmQcuABcTXK+e3ZmgfqHwDTtXt7nbm4SuJTkHjALUFWzSe4Aj2lB79TibUH/i/w8MZYkSZIk9Y0tGyRJkiSpxwx9kiRJktRjhj5JkiRJ6jFDnyRJkiT1mKFPkiRJknrM0CdJkiRJPWbokyRJkqQe+w7mB57sf6nFowAAAABJRU5ErkJggg==", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/notebooks/moonflower_raar.ipynb b/templates/notebooks/moonflower_raar.ipynb new file mode 100644 index 000000000..60f7f4fe1 --- /dev/null +++ b/templates/notebooks/moonflower_raar.ipynb @@ -0,0 +1,194 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: BlockFull\n", + "#### engine: Relaxed Averaged Alternate Projections (RAAR)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'BlockFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# RAAR reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'RAAR'\n", + "p.engines.engine00.numiter = 80\n", + "p.engines.engine00.beta = 0.9" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BlockFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "BlockFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "BlockFull: loading data for scan MF (initializing probe/obj/exit)\n", + "RAAR: initializing engine\n", + "RAAR: preparing engine\n", + "RAAR: Iteration # 80/80 :: Fourier 3.97e+01, Photons 2.19e+01, Exit 2.67e+00\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/notebooks/moonflower_raar_pycuda.ipynb b/templates/notebooks/moonflower_raar_pycuda.ipynb new file mode 100644 index 000000000..18ebab653 --- /dev/null +++ b/templates/notebooks/moonflower_raar_pycuda.ipynb @@ -0,0 +1,217 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PtyPy moonflower example\n", + "#### scan model: BlockFull\n", + "#### engine: Relaxed Averaged Alternate Projections (RAAR) with GPU acceleration" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import ptypy\n", + "from ptypy.core import Ptycho\n", + "from ptypy import utils as u" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Import pycuda engines (DM, RAAR)\n", + "ptypy.load_gpu_engines(\"cuda\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# create parameter tree\n", + "p = u.Param()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# set verbose level to interactive\n", + "p.verbose_level = \"interactive\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Nr. of frames in a block\n", + "p.frames_per_block = 20" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# set home path and io settings (no files saved)\n", + "p.io = u.Param()\n", + "p.io.rfile = None\n", + "p.io.autosave = u.Param(active=False)\n", + "p.io.autoplot = u.Param(active=False)\n", + "p.io.interaction = u.Param(active=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# max 200 frames (128x128px) of diffraction data\n", + "p.scans = u.Param()\n", + "p.scans.MF = u.Param()\n", + "p.scans.MF.name = 'BlockFull'\n", + "p.scans.MF.data= u.Param()\n", + "p.scans.MF.data.name = 'MoonFlowerScan'\n", + "p.scans.MF.data.shape = 128\n", + "p.scans.MF.data.num_frames = 200\n", + "p.scans.MF.data.save = None\n", + "p.scans.MF.data.density = 0.2\n", + "p.scans.MF.data.photons = 1e8\n", + "p.scans.MF.data.psf = 0." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# RAAR reconstrucion engine\n", + "p.engines = u.Param()\n", + "p.engines.engine00 = u.Param()\n", + "p.engines.engine00.name = 'RAAR_pycuda'\n", + "p.engines.engine00.numiter = 80\n", + "p.engines.engine00.numiter_contiguous = 10\n", + "p.engines.engine00.probe_update_start = 1\n", + "p.engines.engine00.beta = 0.9" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BlockFull: loading data for scan MF (161 diffraction frames, 161 PODs, 1 probe(s) and 1 object(s))\n", + "BlockFull: loading data for scan MF (reformatting probe/obj/exit)\n", + "BlockFull: loading data for scan MF (initializing probe/obj/exit)\n", + "RAAR_pycuda: initializing engine\n", + "RAAR_pycuda: preparing engine\n", + "RAAR_pycuda: Iteration # 80/80 :: Fourier 3.85e+01, Photons 2.40e+01, Exit 3.33e+00\n", + "==== This reconstruction relied on the following work ==========================\n", + "The Ptypy framework:\n", + " Enders B. and Thibault P., \"A computational framework for ptychographic reconstructions\" Proc. Royal Soc. A 472 (2016) 20160640, doi: 10.1098/rspa.2016.0640.\n", + "================================================================================\n" + ] + } + ], + "source": [ + "# prepare and run\n", + "P = Ptycho(p,level=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
    " + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "obj = P.obj.S['SMFG00'].data[0]\n", + "prb = P.probe.S['SMFG00'].data[:]\n", + "likelihood_error = [P.runtime[\"iter_info\"][i]['error'][1] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "iterations = [P.runtime[\"iter_info\"][i]['iterations'] for i in range(len(P.runtime[\"iter_info\"]))]\n", + "fig, axes = plt.subplots(ncols=4, nrows=1, figsize=(18,4), dpi=60)\n", + "axes[0].set_title(\"Object amplitude\")\n", + "axes[0].axis('off')\n", + "axes[0].imshow(np.abs(obj)[100:-100,100:-100], cmap='gray', vmin=None, vmax=None, interpolation='none')\n", + "axes[1].set_title(\"Object phase\")\n", + "axes[1].axis('off')\n", + "axes[1].imshow(np.angle(obj)[100:-100,100:-100], vmin=-np.pi, vmax=np.pi, cmap='viridis', interpolation='none')\n", + "axes[2].set_title(\"Probe\")\n", + "axes[2].axis('off')\n", + "axes[2] = u.PtyAxis(axes[2], channel='c')\n", + "axes[2].set_data(prb[0])\n", + "axes[3].set_title(\"Convergence\")\n", + "axes[3].plot(iterations, likelihood_error)\n", + "axes[3].set_xlabel(\"Iteration\")\n", + "axes[3].set_ylabel(\"Log-likelihood error\")\n", + "axes[3].yaxis.set_label_position('right')\n", + "axes[3].tick_params(left=0, right=1, labelleft=0, labelright=1)\n", + "plt.show()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "94f4d5375db2f655aeda185c89beeef42bb9cecdb7e33c656c383e802f18953c" + }, + "kernelspec": { + "display_name": "Python 3.9.6 64-bit ('cuda11.2': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/templates/position_evaluation.py b/templates/position_evaluation.py deleted file mode 100644 index f471c9d12..000000000 --- a/templates/position_evaluation.py +++ /dev/null @@ -1,195 +0,0 @@ -import glob -import matplotlib.pyplot as plt -import numpy as np -import os - - -if __name__ == "__main__": - - pardir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir)) - - # Office pc versions - #base = "D:\Github\ptypy\\templates\positions_position_refinement\\" - base = "./positions_position_refinement/" - - engine = "DM" - - filenames = "pos_*.txt" - - filepaths = glob.glob(base + filenames) - norm_coord_idx = 0 - delimiter = ";" - psize = 56e-8 # 512x512 psize - - show_original = True - simulation = True - path_original = "./positions_theory.txt" - - # show trajectory of recovered positions - for i, path in enumerate(filepaths): - # iterate through all iterations of the reconstruction - print("Load: " + path) - data = np.loadtxt(path) - if i == 0: - # [number of iteration, positions, (y, x)] - pos_trajec = np.zeros((len(filepaths), data.shape[0], 2)) - - for j in range(data.shape[0]): - # iterate through all positions - pos_trajec[i, j, 0] = data[j, 0] - pos_trajec[i, j, 1] = data[j, 1] - - print(pos_trajec.shape) - # show reconstructed trajectories and cycle trough all positions - for i in range(pos_trajec.shape[1]): - if i == 0: - plt.figure("pos trajectory") - pos_trajec *= 1e6 - plt.plot(pos_trajec[0, i, 1], pos_trajec[0, i, 0], "x", ms=12, color="b", label="start") - else: - plt.plot(pos_trajec[0, i, 1], pos_trajec[0, i, 0], "x", ms=12, color="b") # first iteration - plt.plot(pos_trajec[:, i, 1], pos_trajec[:, i, 0], color="b") - - # show original positions which were used to calczlate the diffraction patterns - if show_original: - data_original = np.loadtxt(path_original) * 1e6 - offset = pos_trajec[-1, 0, :] - data_original[0, :] - - print(data_original[0, :]) - print(pos_trajec[-1, 0, :]) - - data_original += offset - for i in range(data_original.shape[0]): - if i == 0: - plt.plot(data_original[i, 1], data_original[i, 0], "x", ms=12, color="green", label="original") - plt.plot(data_original[i, 1], data_original[i, 0], "x", ms=12, color="green") - - # label axes - plt.xlabel("x Postion in um") - plt.ylabel("y Positon in um") - plt.legend() - - # Distance after position correction - distance_after = np.zeros((pos_trajec.shape[1], 2)) - # Distance before positon correction - distance_before = np.zeros((pos_trajec.shape[1], 2)) - - if simulation: - # caculate distance - for i in range(pos_trajec.shape[1]): - distance_after[i, 0] = pos_trajec[-1, i, 0] - data_original[i, 0] - distance_after[i, 1] = pos_trajec[-1, i, 1] - data_original[i, 1] - - distance_before[i, 0] = pos_trajec[0, i, 0] - data_original[i, 0] - distance_before[i, 1] = pos_trajec[0, i, 1] - data_original[i, 1] - - plt.figure("Residual Shift vertical") - plt.plot(distance_before[:, 0], label="Before pos corr") - plt.plot(distance_after[:, 0], label="After pos corr") - plt.legend() - - plt.figure("Residual Shift horizontal") - plt.plot(distance_before[:, 1], label="Before pos corr") - plt.plot(distance_after[:, 1], label="After pos corr") - - norm_dis_before = np.linalg.norm(distance_before, axis=1) - norm_dis_after = np.linalg.norm(distance_after, axis=1) - - plt.figure("Norm distance") - plt.plot(norm_dis_before/psize*1e-6, label="Distance before pos corr") - plt.plot(norm_dis_after/psize*1e-6, label="Distance after pos corr") - plt.xlabel("Position") - plt.ylabel("Distance in px") - - print("Mean distance to original position before pos corr: " + str(np.mean(norm_dis_before/psize*1e-6))) - print("Mean distance to original position after pos corr: " + str(np.mean(norm_dis_after/psize*1e-6))) - - # calculate error by relativ positions - # Contains the relative shift of the position to every other position for the positions that were used to calculate - # the diffractio patterns - relativ_shifts_original = np.zeros((data_original.shape[0], data_original.shape[0], 2)) - - for i in range(relativ_shifts_original.shape[0]): - for j in range(relativ_shifts_original.shape[0]): - relativ_shifts_original[i, j, 0] = data_original[i, 0] - data_original[j, 0] - relativ_shifts_original[i, j, 1] = data_original[i, 1] - data_original[j, 1] - - plt.figure("Relativ y-shifts original") - plt.imshow(relativ_shifts_original[:, :, 0], interpolation="none") - plt.colorbar() - - plt.figure("Relativ x-shifts original") - plt.imshow(relativ_shifts_original[:, :, 1], interpolation="none") - plt.colorbar() - - # calculate the relative shifts for the uncorrected data - relativ_shifts_uncorrected = np.zeros((data_original.shape[0], data_original.shape[0], 2)) - - for i in range(relativ_shifts_uncorrected.shape[0]): - for j in range(relativ_shifts_uncorrected.shape[0]): - relativ_shifts_uncorrected[i, j, 0] = pos_trajec[0, i, 0] - pos_trajec[0, j, 0] - relativ_shifts_uncorrected[i, j, 1] = pos_trajec[0, i, 1] - pos_trajec[0, j, 1] - - plt.figure("Relative y-shifts uncorrected") - plt.imshow(relativ_shifts_uncorrected[:, :, 0], interpolation="none") - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - plt.figure("Relative x-shifts uncorrected") - plt.imshow(relativ_shifts_uncorrected[:, :, 1], interpolation="none") - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - plt.figure("Relative y-shifts uncorrected difference") - plt.imshow(np.abs(relativ_shifts_uncorrected[:, :, 0] - relativ_shifts_original[:, :, 0])/psize*1e-6) - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - # calcuate the relativ shifts of the corrected positions - relativ_shifts_corrected = np.zeros((data_original.shape[0], data_original.shape[0], 2)) - - for i in range(relativ_shifts_corrected.shape[0]): - for j in range(relativ_shifts_corrected.shape[0]): - relativ_shifts_corrected[i, j, 0] = pos_trajec[-1, i, 0] - pos_trajec[-1, j, 0] - relativ_shifts_corrected[i, j, 1] = pos_trajec[-1, i, 1] - pos_trajec[-1, j, 1] - - plt.figure("Relative y-shifts corrected") - plt.imshow(relativ_shifts_corrected[:, :, 0], interpolation="none") - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - plt.figure("Relative x-shifts corrected") - plt.imshow(relativ_shifts_corrected[:, :, 1], interpolation="none") - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - plt.figure("y relative Difference") - plt.imshow(np.abs(relativ_shifts_corrected[:, :, 0] - relativ_shifts_original[:, :, 0])/psize*1e-6, interpolation="none") - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - plt.figure("x relative Difference") - plt.imshow(np.abs(relativ_shifts_corrected[:, :, 1] - relativ_shifts_original[:, :, 1])/psize*1e-6, interpolation="none") - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - tot_dist = np.sqrt((relativ_shifts_corrected[:, :, 1] - relativ_shifts_original[:, :, 1])**2 + (relativ_shifts_corrected[:, :, 0] - relativ_shifts_original[:, :, 0])**2)/psize*1e-6 - plt.figure("Total distance in px") - plt.imshow(tot_dist, interpolation="none") - plt.xlabel("Position number") - plt.ylabel("Position number") - plt.colorbar() - - tot_dist_uncorr = np.sqrt((relativ_shifts_uncorrected[:, :, 1] - relativ_shifts_original[:, :, 1])**2 + (relativ_shifts_uncorrected[:, :, 0] - relativ_shifts_original[:, :, 0])**2)/psize*1e-6 - print("Mean distance error to all other positions corrected: " + str(np.mean(np.mean(tot_dist, axis=0)))) - print("Mean distance error to all other positions uncorrected: " + str(np.mean(np.mean(tot_dist_uncorr, axis=0)))) - plt.legend() - - plt.show() diff --git a/templates/position_refinement.py b/templates/position_refinement/moonflower_posref_DM.py similarity index 57% rename from templates/position_refinement.py rename to templates/position_refinement/moonflower_posref_DM.py index c3a348c24..86dcbdd07 100644 --- a/templates/position_refinement.py +++ b/templates/position_refinement/moonflower_posref_DM.py @@ -3,26 +3,30 @@ of actual data. It uses the test Scan class `ptypy.core.data.MoonFlowerScan` to provide "data". """ - import numpy as np from ptypy.core import Ptycho from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() # for verbose output -p.verbose_level = 4 +p.verbose_level = "info" # set home path p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) # max 200 frames (128x128px) of diffraction data p.scans = u.Param() p.scans.MF = u.Param() # now you have to specify which ScanModel to use with scans.XX.name, # just as you have to give 'name' for engines and PtyScan subclasses. -p.scans.MF.name = 'Full' +p.scans.MF.name = 'BlockFull' p.scans.MF.data= u.Param() p.scans.MF.data.name = 'MoonFlowerScan' p.scans.MF.data.shape = 128 @@ -41,15 +45,17 @@ p.engines.engine00 = u.Param() p.engines.engine00.name = 'DM' p.engines.engine00.probe_support = 1 -# p.engines.engine00.probe_center_tol = 0.5 p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 p.engines.engine00.position_refinement = u.Param() p.engines.engine00.position_refinement.start = 50 -p.engines.engine00.position_refinement.stop = 990 +p.engines.engine00.position_refinement.stop = 950 p.engines.engine00.position_refinement.interval = 10 p.engines.engine00.position_refinement.nshifts = 32 -p.engines.engine00.position_refinement.amplitude = 1e-6 -p.engines.engine00.position_refinement.max_shift = 2e-6 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.record = True # prepare and run P = Ptycho(p, level=4) @@ -58,26 +64,39 @@ a = 0. coords = [] +coords_start = [] for pname, pod in P.pods.items(): + # Save real position coords.append(np.copy(pod.ob_view.coord)) before = pod.ob_view.coord psize = pod.pr_view.psize - # print(pname) - # print(before) perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) - new_coord = before + perturbation # make sure integer number of pixels shift - - pod.ob_view.coord = new_coord - - #pod.diff *= np.random.uniform(0.1,1)y + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) -np.savetxt("positions_theory.txt", coords) P.obj.reformat() - # Run P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_dm.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_DM_pycuda.py b/templates/position_refinement/moonflower_posref_DM_pycuda.py new file mode 100644 index 000000000..e09ffbb79 --- /dev/null +++ b/templates/position_refinement/moonflower_posref_DM_pycuda.py @@ -0,0 +1,109 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 100 +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' # or 'Full' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. +#p.scans.MF.data.add_poisson_noise = False + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_pycuda' +p.engines.engine00.probe_support = 1 +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.amplitude_decay = False +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1)y + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +# update the object storage +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_dm_pycuda.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_DM_serial.py b/templates/position_refinement/moonflower_posref_DM_serial.py new file mode 100644 index 000000000..5a43401e7 --- /dev/null +++ b/templates/position_refinement/moonflower_posref_DM_serial.py @@ -0,0 +1,109 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 100 +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' # or 'Full' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +p.scans.MF.illumination = u.Param(diversity=None) +p.scans.MF.coherence = u.Param(num_probe_modes=1) +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. +#p.scans.MF.data.add_poisson_noise = False + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM_serial' +p.engines.engine00.probe_support = 1 +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1)y + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +# update the object storage +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_dm_serial.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_EPIE.py b/templates/position_refinement/moonflower_posref_EPIE.py new file mode 100644 index 000000000..80a2026d8 --- /dev/null +++ b/templates/position_refinement/moonflower_posref_EPIE.py @@ -0,0 +1,103 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'EPIE' +p.engines.engine00.probe_support = 1 +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.metric = "photon" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions in a predictible way (for MPI) +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_epie.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_EPIE_pycuda.py b/templates/position_refinement/moonflower_posref_EPIE_pycuda.py new file mode 100644 index 000000000..e25fe2bc9 --- /dev/null +++ b/templates/position_refinement/moonflower_posref_EPIE_pycuda.py @@ -0,0 +1,105 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'EPIE_pycuda' +p.engines.engine00.probe_support = 1 +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.metric = "photon" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions in a predictible way (for MPI) +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_epie_pycuda.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_ML.py b/templates/position_refinement/moonflower_posref_ML.py new file mode 100644 index 000000000..08af0d5d7 --- /dev/null +++ b/templates/position_refinement/moonflower_posref_ML.py @@ -0,0 +1,110 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'Full' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'ML' +p.engines.engine00.ML_type = 'Gaussian' +p.engines.engine00.reg_del2 = True +p.engines.engine00.reg_del2_amplitude = .1 +p.engines.engine00.scale_precond = True +p.engines.engine00.scale_probe_object = 1. +p.engines.engine00.smooth_gradient = 20. +p.engines.engine00.smooth_gradient_decay = 1/50. +p.engines.engine00.floating_intensities = False +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.metric = "photon" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions in a predictible way (for MPI) +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_ml.pdf"]), bbox_inches='tight') +plt.show() diff --git a/templates/position_refinement/moonflower_posref_ML_pycuda.py b/templates/position_refinement/moonflower_posref_ML_pycuda.py new file mode 100644 index 000000000..62cfd7fab --- /dev/null +++ b/templates/position_refinement/moonflower_posref_ML_pycuda.py @@ -0,0 +1,115 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 100 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'ML_pycuda' +p.engines.engine00.ML_type = 'Gaussian' +p.engines.engine00.reg_del2 = True +p.engines.engine00.reg_del2_amplitude = .1 +p.engines.engine00.scale_precond = True +p.engines.engine00.scale_probe_object = 1. +p.engines.engine00.smooth_gradient = 20. +p.engines.engine00.smooth_gradient_decay = 1/50. +p.engines.engine00.floating_intensities = False +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.metric = "photon" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions in a predictible way (for MPI) +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +#np.savetxt("positions_theory.txt", coords) +#np.savetxt("positions_start.txt", coords_start) +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_ml_pycuda.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_ML_serial.py b/templates/position_refinement/moonflower_posref_ML_serial.py new file mode 100644 index 000000000..acfc34885 --- /dev/null +++ b/templates/position_refinement/moonflower_posref_ML_serial.py @@ -0,0 +1,112 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="serial") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" +p.frames_per_block = 100 + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'ML_serial' +p.engines.engine00.ML_type = 'Gaussian' +p.engines.engine00.reg_del2 = True +p.engines.engine00.reg_del2_amplitude = .1 +p.engines.engine00.scale_precond = True +p.engines.engine00.scale_probe_object = 1. +p.engines.engine00.smooth_gradient = 20. +p.engines.engine00.smooth_gradient_decay = 1/50. +p.engines.engine00.floating_intensities = False +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 5e-7 +p.engines.engine00.position_refinement.max_shift = 1e-6 +p.engines.engine00.position_refinement.method = "GridSearch" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions in a predictible way (for MPI) +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((3e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_ml_serial.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_SDR.py b/templates/position_refinement/moonflower_posref_SDR.py new file mode 100644 index 000000000..47b5f5618 --- /dev/null +++ b/templates/position_refinement/moonflower_posref_SDR.py @@ -0,0 +1,103 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'SDR' +p.engines.engine00.probe_support = 1 +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 1e-7 +p.engines.engine00.position_refinement.max_shift = 1e-7 +p.engines.engine00.position_refinement.method = "GridSearch" +#p.engines.engine00.position_refinement.metric = "photon" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions in a predictible way (for MPI) +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((1e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_sdr.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/position_refinement/moonflower_posref_SDR_pycuda.py b/templates/position_refinement/moonflower_posref_SDR_pycuda.py new file mode 100644 index 000000000..2e71311af --- /dev/null +++ b/templates/position_refinement/moonflower_posref_SDR_pycuda.py @@ -0,0 +1,105 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +import numpy as np +from ptypy.core import Ptycho +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines(arch="cuda") + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=False) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'SDR_pycuda' +p.engines.engine00.probe_support = 1 +p.engines.engine00.numiter = 1000 +p.engines.engine00.numiter_contiguous = 10 +p.engines.engine00.position_refinement = u.Param() +p.engines.engine00.position_refinement.start = 50 +p.engines.engine00.position_refinement.stop = 950 +p.engines.engine00.position_refinement.interval = 10 +p.engines.engine00.position_refinement.nshifts = 32 +p.engines.engine00.position_refinement.amplitude = 1e-7 +p.engines.engine00.position_refinement.max_shift = 1e-7 +p.engines.engine00.position_refinement.method = "GridSearch" +#p.engines.engine00.position_refinement.metric = "photon" +p.engines.engine00.position_refinement.record = True + +# prepare and run +P = Ptycho(p, level=4) + +# Mess up the positions in a predictible way (for MPI) +a = 0. + +coords = [] +coords_start = [] +for pname, pod in P.pods.items(): + + # Save real position + coords.append(np.copy(pod.ob_view.coord)) + before = pod.ob_view.coord + psize = pod.pr_view.psize + perturbation = psize * ((1e-7 * np.array([np.sin(a), np.cos(a)])) // psize) + new_coord = before + perturbation # make sure integer number of pixels shift + pod.ob_view.coord = new_coord + coords_start.append(np.copy(pod.ob_view.coord)) + #pod.diff *= np.random.uniform(0.1,1) + a += 4. +coords = np.array(coords) +coords_start = np.array(coords_start) + +P.obj.reformat() + +# Run +P.run() +P.finalize() + +coords_new = [] +for pname, pod in P.pods.items(): + coords_new.append(np.copy(pod.ob_view.coord)) +coords_new = np.array(coords_new) + +import matplotlib.pyplot as plt +plt.figure(figsize=(10,10), dpi=60) +plt.title("RMSE = %.2f um" %(np.sqrt(np.sum((coords_new-coords)**2,axis=1)).mean()*1e6)) +plt.plot(coords[:,0], coords[:,1], marker='.', color='k', lw=0, label='original') +plt.plot(coords_start[:,0], coords_start[:,1], marker='x', color='r', lw=0, label='start') +plt.plot(coords_new[:,0], coords_new[:,1], marker='.', color='r', lw=0, label='end') +plt.legend() +plt.savefig("/".join([tmpdir, "ptypy", "posref_eval_sdr_pycuda.pdf"]), bbox_inches='tight') +plt.show() \ No newline at end of file diff --git a/templates/ptypy_i13_AuStar_farfield_9p0keV.py b/templates/ptypy_i13_AuStar_farfield.py similarity index 86% rename from templates/ptypy_i13_AuStar_farfield_9p0keV.py rename to templates/ptypy_i13_AuStar_farfield.py index cf2ad546f..731d5da53 100644 --- a/templates/ptypy_i13_AuStar_farfield_9p0keV.py +++ b/templates/ptypy_i13_AuStar_farfield.py @@ -1,18 +1,23 @@ - -import ptypy +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental farfield conditions in the hard X-ray regime. +""" from ptypy.core import Ptycho from ptypy import utils as u -import numpy as np + +import tempfile +tmpdir = tempfile.gettempdir() ### PTYCHO PARAMETERS p = u.Param() -p.verbose_level = 3 +p.verbose_level = "info" p.run = None p.data_type = "single" p.run = None p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home = "/".join([tmpdir, "ptypy"]) p.io.autoplot = u.Param() p.io.autoplot.layout ='nearfield' @@ -59,7 +64,7 @@ # Scan model and initial value parameters p.scans = u.Param() p.scans.scan00 = u.Param() -p.scans.scan00.name = 'Full' +p.scans.scan00.name = 'BlockFull' p.scans.scan00.coherence = u.Param() p.scans.scan00.coherence.num_probe_modes = 1 @@ -100,9 +105,9 @@ p.engines.engine00.overlap_max_iterations = 100 p.engines.engine00.fourier_relax_factor = 0.05 -#p.engines.engine01 = u.Param() -#p.engines.engine01.name = 'ML' -#p.engines.engine01.numiter = 50 +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML' +p.engines.engine01.numiter = 50 P = Ptycho(p,level=5) diff --git a/templates/ptypy_i13_AuStar_nearfield_9p7keV.py b/templates/ptypy_i13_AuStar_nearfield.py similarity index 86% rename from templates/ptypy_i13_AuStar_nearfield_9p7keV.py rename to templates/ptypy_i13_AuStar_nearfield.py index 65e000d8b..a07f89dce 100644 --- a/templates/ptypy_i13_AuStar_nearfield_9p7keV.py +++ b/templates/ptypy_i13_AuStar_nearfield.py @@ -1,19 +1,24 @@ - -import ptypy +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental nearfield conditions in the hard X-ray regime. +""" from ptypy.core import Ptycho from ptypy import utils as u -import numpy as np + +import tempfile +tmpdir = tempfile.gettempdir() ### PTYCHO PARAMETERS p = u.Param() -p.verbose_level = 3 +p.verbose_level = "info" p.run = None p.frames_per_block = 20 p.data_type = "single" p.run = None p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home = "/".join([tmpdir, "ptypy"]) p.io.autoplot = u.Param() p.io.autoplot.layout ='nearfield' @@ -60,7 +65,7 @@ # Scan model and initial value parameters p.scans = u.Param() p.scans.scan00 = u.Param() -p.scans.scan00.name = 'Full' +p.scans.scan00.name = 'BlockFull' p.scans.scan00.propagation = "nearfield" p.scans.scan00.coherence = u.Param() @@ -102,9 +107,9 @@ p.engines.engine00.overlap_max_iterations = 100 p.engines.engine00.fourier_relax_factor = 0.05 -#p.engines.engine01 = u.Param() -#p.engines.engine01.name = 'ML' -#p.engines.engine01.numiter = 50 +p.engines.engine01 = u.Param() +p.engines.engine01.name = 'ML' +p.engines.engine01.numiter = 50 P = Ptycho(p,level=5) diff --git a/templates/ptypy_id22ni_AuStar_focussed_17keV.py b/templates/ptypy_id22ni_AuStar_focused.py similarity index 86% rename from templates/ptypy_id22ni_AuStar_focussed_17keV.py rename to templates/ptypy_id22ni_AuStar_focused.py index c09071fd3..34baa3926 100644 --- a/templates/ptypy_id22ni_AuStar_focussed_17keV.py +++ b/templates/ptypy_id22ni_AuStar_focused.py @@ -1,21 +1,29 @@ -import numpy as np -import ptypy +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental farfield conditions and with a focused beam in the hard X-ray regime. +""" from ptypy.core import Ptycho from ptypy import utils as u + +import numpy as np +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() ### PTYCHO PARAMETERS -p.verbose_level = 3 +p.verbose_level = "info" p.data_type = "single" p.run = None p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home = "/".join([tmpdir, "ptypy"]) p.io.autoplot = u.Param() p.io.autoplot.layout='weak' -p.io.autosave = None -p.io.interaction = u.Param() +p.io.autosave = u.Param(active=False) +p.io.interaction = u.Param(active=True) # Simulation parameters sim = u.Param() @@ -63,7 +71,7 @@ # Scan model and initial value parameters p.scans = u.Param() p.scans.scan00 = u.Param() -p.scans.scan00.name = 'Full' +p.scans.scan00.name = 'BlockFull' p.scans.scan00.coherence = u.Param() p.scans.scan00.coherence.num_probe_modes = 4 @@ -106,5 +114,5 @@ p.engines.engine00.overlap_max_iterations = 100 p.engines.engine00.obj_smooth_std = 5 -u.verbose.set_level(3) +u.verbose.set_level("info") P = Ptycho(p,level=5) diff --git a/templates/ptypy_laser_logo_focussed_632nm.py b/templates/ptypy_laser_logo_focused.py similarity index 77% rename from templates/ptypy_laser_logo_focussed_632nm.py rename to templates/ptypy_laser_logo_focused.py index 937265217..e9946f3fc 100644 --- a/templates/ptypy_laser_logo_focussed_632nm.py +++ b/templates/ptypy_laser_logo_focused.py @@ -1,20 +1,27 @@ -import ptypy +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses a simulated Au Siemens star pattern under +experimental farfield conditions and with a focused optical laser beam. +""" from ptypy.core import Ptycho from ptypy import utils as u import ptypy.simulations as sim -import numpy as np +import pathlib +import numpy as np +import tempfile +tmpdir = tempfile.gettempdir() ### PTYCHO PARAMETERS p = u.Param() -p.verbose_level = 3 +p.verbose_level = "info" p.data_type = "single" p.run = None p.io = u.Param() -p.io.home = "/tmp/ptypy/" -p.io.autosave = None -p.io.autoplot = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) +p.io.autosave = u.Param(active=False) +p.io.autoplot = u.Param(active=True) p.io.autoplot.layout='minimal' # Simulation parameters @@ -43,9 +50,9 @@ sim.illumination.propagation.parallel = 0.03 sim.illumination.propagation.spot_size = None -ptypy_path = ptypy.__file__.strip('ptypy.__init__.py') +imgfile = "/".join([str(pathlib.Path(__file__).parent.resolve()), '../resources/ptypy_logo_1M.png']) sim.sample = u.Param() -sim.sample.model = -u.rgb2complex(u.imload('%s/resources/ptypy_logo_1M.png' % ptypy_path)[::-1,:,:-1]) +sim.sample.model = -u.rgb2complex(u.imload(imgfile)[::-1,:,:-1]) sim.sample.process = u.Param() sim.sample.process.offset = (0,0) sim.sample.process.zoom = 0.5 @@ -61,7 +68,7 @@ # Scan model and initial value parameters p.scans = u.Param() p.scans.ptypy = u.Param() -p.scans.ptypy.name = 'Full' +p.scans.ptypy.name = 'BlockFull' p.scans.ptypy.coherence = u.Param() p.scans.ptypy.coherence.num_probe_modes=1 @@ -86,5 +93,5 @@ p.engines.engine00.numiter = 40 p.engines.engine00.fourier_relax_factor = 0.05 -u.verbose.set_level(3) +u.verbose.set_level("info") P = Ptycho(p,level=5) diff --git a/templates/make_sample_ptyd.py b/templates/ptypy_make_sample_ptyd.py similarity index 93% rename from templates/make_sample_ptyd.py rename to templates/ptypy_make_sample_ptyd.py index 09153fe05..539ea7a38 100644 --- a/templates/make_sample_ptyd.py +++ b/templates/ptypy_make_sample_ptyd.py @@ -2,13 +2,11 @@ This script creates a sample *.ptyd data file using the built-in test Scan `ptypy.core.data.MoonFlowerScan` """ -import sys import time -import ptypy from ptypy import utils as u from ptypy.core.data import MoonFlowerScan # for verbose output -u.verbose.set_level(3) +u.verbose.set_level("info") # create data parameter branch data = u.Param() diff --git a/templates/minimal_load_and_run.py b/templates/ptypy_minimal_load_and_run.py similarity index 79% rename from templates/minimal_load_and_run.py rename to templates/ptypy_minimal_load_and_run.py index 3705d42e4..21ad514fb 100644 --- a/templates/minimal_load_and_run.py +++ b/templates/ptypy_minimal_load_and_run.py @@ -2,21 +2,23 @@ This script is a test for ptychographic reconstruction after an experiment has been carried out and the data is available in ptypy's data file format in the current directory as "sample.ptyd". Use together -with `make_sample_ptyd.py`. +with `ptypy_make_sample_ptyd.py`. """ -import ptypy from ptypy.core import Ptycho from ptypy import utils as u +import tempfile +tmpdir = tempfile.gettempdir() + p = u.Param() -p.verbose_level = 3 +p.verbose_level = "info" p.io = u.Param() -p.io.home = "/tmp/ptypy/" +p.io.home = "/".join([tmpdir, "ptypy"]) p.scans = u.Param() p.scans.MF = u.Param() p.scans.MF.data= u.Param() -p.scans.MF.name = 'Vanilla' +p.scans.MF.name = 'BlockVanilla' p.scans.MF.data.name = 'PtydScan' p.scans.MF.data.source = 'file' p.scans.MF.data.dfile = 'sample.ptyd' @@ -25,9 +27,8 @@ p.engines.engine00 = u.Param() p.engines.engine00.name = 'DM' p.engines.engine00.numiter = 80 -""" p.engines.engine01 = u.Param() p.engines.engine01.name = 'ML' p.engines.engine01.numiter = 20 -""" + P = Ptycho(p,level=5) diff --git a/templates/ptypy_minimal_prep_and_run.py b/templates/ptypy_minimal_prep_and_run.py new file mode 100644 index 000000000..ea46b6320 --- /dev/null +++ b/templates/ptypy_minimal_prep_and_run.py @@ -0,0 +1,54 @@ +""" +This script is a test for ptychographic reconstruction in the absence +of actual data. It uses the test Scan class +`ptypy.core.data.MoonFlowerScan` to provide "data". +""" +from ptypy.core import Ptycho +from ptypy import utils as u + +import tempfile +tmpdir = tempfile.gettempdir() + +p = u.Param() + +# for verbose output +p.verbose_level = "info" + +# set home path +p.io = u.Param() +p.io.home = "/".join([tmpdir, "ptypy"]) + +# saving intermediate results +p.io.autosave = u.Param(active=False) + +# opens plotting GUI if interaction set to active) +p.io.autoplot = u.Param(active=True) +p.io.interaction = u.Param(active=True) + +# max 200 frames (128x128px) of diffraction data +p.scans = u.Param() +p.scans.MF = u.Param() +# now you have to specify which ScanModel to use with scans.XX.name, +# just as you have to give 'name' for engines and PtyScan subclasses. +p.scans.MF.name = 'BlockVanilla' # or 'BlockFull' +p.scans.MF.data= u.Param() +p.scans.MF.data.name = 'MoonFlowerScan' +p.scans.MF.data.shape = 128 +p.scans.MF.data.num_frames = 200 +p.scans.MF.data.save = None + +# position distance in fraction of illumination frame +p.scans.MF.data.density = 0.2 +# total number of photon in empty beam +p.scans.MF.data.photons = 1e8 +# Gaussian FWHM of possible detector blurring +p.scans.MF.data.psf = 0. + +# attach a reconstrucion engine +p.engines = u.Param() +p.engines.engine00 = u.Param() +p.engines.engine00.name = 'DM' +p.engines.engine00.numiter = 80 + +# prepare and run +P = Ptycho(p,level=5) diff --git a/test/accelerate_tests/__init__.py b/test/accelerate_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/accelerate_tests/base_tests/__init__.py b/test/accelerate_tests/base_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/accelerate_tests/base_tests/address_manglers_test.py b/test/accelerate_tests/base_tests/address_manglers_test.py new file mode 100644 index 000000000..7e27c885a --- /dev/null +++ b/test/accelerate_tests/base_tests/address_manglers_test.py @@ -0,0 +1,96 @@ +import unittest +import sys +import numpy as np +from ptypy.accelerate.base.address_manglers import BaseMangler, RandomIntMangler + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class AddressManglersTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + + def tearDown(self): + np.set_printoptions() + + def prepare_addresses(self, max_bound=10, scan_pts=2, num_modes=3): + total_number_scan_positions = scan_pts ** 2 + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + max_bound # max bound is added in the DM_serial engine. + Y = Y.reshape((total_number_scan_positions)) + max_bound + + addr_original = np.zeros((total_number_scan_positions, num_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(num_modes): + for ob_mode in range(1): + addr_original[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + return addr_original + + def test_apply_bounding_box(self): + + scan_pts=2 + max_bound=10 + addr = self.prepare_addresses(scan_pts=scan_pts, max_bound=max_bound) + step_size = 3 + + mangler = BaseMangler(step_size, 50, 100, nshifts=1, max_bound=max_bound, ) + min_oby = 1 + max_oby = 10 + min_obx = 2 + max_obx = 9 + mangler.apply_bounding_box(addr[:,:,1,1], min_oby, max_oby) + mangler.apply_bounding_box(addr[:,:,1,2], min_obx, max_obx) + + np.testing.assert_array_less(addr[:,:,1,1], max_oby+1) + np.testing.assert_array_less(addr[:,:,1,2], max_obx+1) + np.testing.assert_array_less(min_oby-1, addr[:,:,1,1]) + np.testing.assert_array_less(min_obx-1, addr[:,:,1,2]) + + + def test_get_address(self): + # the other manglers are using the BaseMangler's get_address function + # so we set the deltas in a BaseMangler object and test get_address + + scan_pts=2 + addr_original = self.prepare_addresses(scan_pts=scan_pts) + total_number_scan_positions = scan_pts ** 2 + addr1 = np.copy(addr_original) + addr2 = np.copy(addr_original) + nshifts=1 + step_size=2 + mglr = BaseMangler(step_size, 50, 100, nshifts, max_bound=2) + # 2 shifts, with positive/negative shifting + mglr.delta = np.array([ + [1, 2], + [-4, -2] + ]) + mglr.get_address(0, addr_original, addr1, 10, 9) + mglr.get_address(1, addr_original, addr2, 10, 9) + + exp1 = np.copy(addr_original) + exp2 = np.copy(addr_original) + # element-wise here to prepare reference + for f in range(addr_original.shape[0]): + for m in range(addr_original.shape[1]): + exp1[f, m, 1, 1] = max(0, min(10, addr_original[f, m, 1, 1] + 1)) + exp1[f, m, 1, 2] = max(0, min(9, addr_original[f, m, 1, 2] + 2)) + exp2[f, m, 1, 1] = max(0, min(10, addr_original[f, m, 1, 1] - 4)) + exp2[f, m, 1, 2] = max(0, min(9, addr_original[f, m, 1, 2] - 2)) + + np.testing.assert_array_equal(addr1, exp1) + np.testing.assert_array_equal(addr2, exp2) diff --git a/test/accelerate_tests/base_tests/array_utils_test.py b/test/accelerate_tests/base_tests/array_utils_test.py new file mode 100644 index 000000000..06646b813 --- /dev/null +++ b/test/accelerate_tests/base_tests/array_utils_test.py @@ -0,0 +1,296 @@ +''' +Tests for the array_utils module +''' + +import unittest +import numpy as np +from ptypy.accelerate.base import FLOAT_TYPE, COMPLEX_TYPE +from ptypy.accelerate.base import array_utils as au + + +class ArrayUtilsTest(unittest.TestCase): + + def test_dot_resolution(self): + X, Y, Z = np.indices((3, 3, 1001), dtype=np.float32) + A = 10 ** Y + 1j * 10 ** X + out = au.dot(A, A) + np.testing.assert_array_equal(out, 60666606.0) + + def test_abs2_real_input(self): + single_dim = 50.0 + npts = single_dim ** 3 + array_to_be_absed = np.arange(npts) + absed = np.array([ix ** 2 for ix in array_to_be_absed]) + array_shape = (int(single_dim), int(single_dim), int(single_dim)) + array_to_be_absed.reshape(array_shape) + absed.reshape(array_shape) + out = au.abs2(array_to_be_absed) + np.testing.assert_array_equal(absed, out) + self.assertEqual(absed.dtype, np.float64) + + def test_abs2_complex_input(self): + single_dim = 50.0 + array_shape = (int(single_dim), int(single_dim), int(single_dim)) + npts = single_dim ** 3 + array_to_be_absed = np.arange(npts) + 1j * np.arange(npts) + absed = np.array([np.abs(ix ** 2) for ix in array_to_be_absed]) + absed.reshape(array_shape) + array_to_be_absed.reshape(array_shape) + out = au.abs2(array_to_be_absed) + np.testing.assert_array_equal(absed, out) + self.assertEqual(absed.dtype, np.float64) + + def test_sum_to_buffer(self): + + I = 4 + X = 2 + M = 4 + N = 4 + + in1 = np.empty((I, M, N), dtype=FLOAT_TYPE) + + # fill the input array + for idx in range(I): + in1[idx] = np.ones((M, N)) * (idx + 1.0) + + outshape = (X, M, N) + expected_out = np.empty(outshape) + + expected_out[0] = np.ones((M, N)) * 4.0 + expected_out[1] = np.ones((M, N)) * 6.0 + + in1_addr = np.empty((I, 3)) + + in1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (2, 0, 0), + (3, 0, 0)]) + + out1_addr = np.empty_like(in1_addr) + out1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (0, 0, 0), + (1, 0, 0)]) + + out = au.sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype=FLOAT_TYPE) + np.testing.assert_array_equal(out, expected_out) + + def test_sum_to_buffer_complex(self): + + I = 4 + X = 2 + M = 4 + N = 4 + + in1 = np.empty((I, M, N), dtype=COMPLEX_TYPE) + + # fill the input array + for idx in range(I): + in1[idx] = np.ones((M, N)) * (idx + 1.0) + 1j * np.ones((M, N)) * (idx + 1.0) + + outshape = (X, M, N) + expected_out = np.empty(outshape, dtype=COMPLEX_TYPE) + + expected_out[0] = np.ones((M, N)) * 4.0 + 1j * np.ones((M, N)) * 4.0 + expected_out[1] = np.ones((M, N)) * 6.0 + 1j * np.ones((M, N)) * 6.0 + + in1_addr = np.empty((I, 3)) + + in1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (2, 0, 0), + (3, 0, 0)]) + + out1_addr = np.empty_like(in1_addr) + out1_addr = np.array([(0, 0, 0), + (1, 0, 0), + (0, 0, 0), + (1, 0, 0)]) + + out = au.sum_to_buffer(in1, outshape, in1_addr, out1_addr, dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(out, expected_out) + + def test_norm2_1d_real(self): + a = np.array([1.0, 2.0], dtype=FLOAT_TYPE) + out = au.norm2(a) + np.testing.assert_array_equal(out, 5.0) + + def test_norm2_1d_complex(self): + a = np.array([1.0 + 1.0j, 2.0 + 2.0j], dtype=COMPLEX_TYPE) + out = au.norm2(a) + np.testing.assert_array_equal(out, 10.0) + + def test_norm2_2d_real(self): + a = np.array([[1.0, 2.0], + [3.0, 4.0]], dtype=FLOAT_TYPE) + out = au.norm2(a) + np.testing.assert_array_equal(out, 30.0) + + def test_norm2_2d_complex(self): + a = np.array([[1.0 + 1.0j, 2.0 + 2.0j], + [3.0 + 3.0j, 4.0 + 4.0j]], dtype=COMPLEX_TYPE) + out = au.norm2(a) + np.testing.assert_array_equal(out, 60.0) + + def test_norm2_3d_real(self): + a = np.array([[[1.0, 2.0], + [3.0, 4.0]], + [[5.0, 6.0], + [7.0, 8.0]]], dtype=FLOAT_TYPE) + out = au.norm2(a) + np.testing.assert_array_equal(out, 204.0) + + def test_norm2_3d_complex(self): + a = np.array([[[1.0 + 1.0j, 2.0 + 2.0j], + [3.0 + 3.0j, 4.0 + 4.0j]], + [[5.0 + 5.0j, 6.0 + 6.0j], + [7.0 + 7.0j, 8.0 + 8.0j]]], dtype=COMPLEX_TYPE) + out = au.norm2(a) + np.testing.assert_array_equal(out, 408.0) + + def test_complex_gaussian_filter_2d(self): + data = np.zeros((8, 8), dtype=COMPLEX_TYPE) + data[3:5, 3:5] = 2.0 + 2.0j + mfs = 3.0, 4.0 + out = au.complex_gaussian_filter(data, mfs) + expected_out = np.array([0.11033735 + 0.11033735j, 0.11888228 + 0.11888228j, 0.13116673 + 0.13116673j + , 0.13999543 + 0.13999543j, 0.13999543 + 0.13999543j, 0.13116673 + 0.13116673j + , 0.11888228 + 0.11888228j, 0.11033735 + 0.11033735j], dtype=COMPLEX_TYPE) + np.testing.assert_array_almost_equal(np.diagonal(out), expected_out) + + def test_complex_gaussian_filter_2d_batched(self): + batch_number = 2 + A = 5 + B = 5 + + data = np.zeros((batch_number, A, B), dtype=COMPLEX_TYPE) + data[:, 2:3, 2:3] = 2.0 + 2.0j + mfs = 3.0, 4.0 + out = au.complex_gaussian_filter(data, mfs) + + expected_out = np.array([[[0.07988770 + 0.0798877j, 0.07989411 + 0.07989411j, 0.07989471 + 0.07989471j, + 0.07989411 + 0.07989411j, 0.07988770 + 0.0798877j], + [0.08003781 + 0.08003781j, 0.08004424 + 0.08004424j, 0.08004485 + 0.08004485j, + 0.08004424 + 0.08004424j, 0.08003781 + 0.08003781j], + [0.08012911 + 0.08012911j, 0.08013555 + 0.08013555j, 0.08013615 + 0.08013615j, + 0.08013555 + 0.08013555j, 0.08012911 + 0.08012911j], + [0.08003781 + 0.08003781j, 0.08004424 + 0.08004424j, 0.08004485 + 0.08004485j, + 0.08004424 + 0.08004424j, 0.08003781 + 0.08003781j], + [0.07988770 + 0.0798877j, 0.07989411 + 0.07989411j, 0.07989471 + 0.07989471j, + 0.07989411 + 0.07989411j, 0.07988770 + 0.0798877j]], + + [[0.07988770 + 0.0798877j, 0.07989411 + 0.07989411j, 0.07989471 + 0.07989471j, + 0.07989411 + 0.07989411j, 0.07988770 + 0.0798877j], + [0.08003781 + 0.08003781j, 0.08004424 + 0.08004424j, 0.08004485 + 0.08004485j, + 0.08004424 + 0.08004424j, 0.08003781 + 0.08003781j], + [0.08012911 + 0.08012911j, 0.08013555 + 0.08013555j, 0.08013615 + 0.08013615j, + 0.08013555 + 0.08013555j, 0.08012911 + 0.08012911j], + [0.08003781 + 0.08003781j, 0.08004424 + 0.08004424j, 0.08004485 + 0.08004485j, + 0.08004424 + 0.08004424j, 0.08003781 + 0.08003781j], + [0.07988770 + 0.0798877j, 0.07989411 + 0.07989411j, 0.07989471 + 0.07989471j, + 0.07989411 + 0.07989411j, 0.07988770 + 0.0798877j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_almost_equal(out, expected_out) + + def test_mass_center_2d(self): + npts = 64 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X - Xoff) ** 2 + (Y - Yoff) ** 2 < rad ** 2] = probe_vals + + com = au.mass_center(np.abs(probe[0])) + expected_out = np.array([Yoff, Xoff]) + npts // 2 + np.testing.assert_array_almost_equal(com, expected_out, decimal=6) + + def test_mass_center_3d(self): + npts = 64 + probe = np.zeros((npts, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y, Z = np.meshgrid(x, x, x) + Xoff = 5.0 + Yoff = 2.0 + Zoff = 10.0 + probe[(X - Xoff) ** 2 + (Y - Yoff) ** 2 + (Z - Zoff) ** 2 < rad ** 2] = probe_vals + + com = au.mass_center(np.abs(probe)) + expected_out = np.array([Yoff, Xoff, Zoff]) + npts // 2 + np.testing.assert_array_almost_equal(com, expected_out, decimal=5) + + def test_interpolated_shift(self): + npts = 32 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X - Xoff) ** 2 + (Y - Yoff) ** 2 < rad ** 2] = probe_vals + offset = np.array([-Yoff, -Xoff]) + + not_shifted_probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + not_shifted_probe[0, (X) ** 2 + (Y) ** 2 < rad ** 2] = probe_vals + probe[0] = au.interpolated_shift(probe[0], offset) + np.testing.assert_array_almost_equal(probe, not_shifted_probe, decimal=8) + + def test_clip_magnitudes_to_range(self): + data = np.ones((5, 5), dtype=COMPLEX_TYPE) + data[2, 4] = 20.0 * np.exp(1j * np.pi / 2) + data[3, 1] = 0.2 * np.exp(1j * np.pi / 3) + + clip_min = 0.5 + clip_max = 2.0 + expected_out = np.ones_like(data) + expected_out[2, 4] = 2.0 * np.exp(1j * np.pi / 2) + expected_out[3, 1] = 0.5 * np.exp(1j * np.pi / 3) + au.clip_complex_magnitudes_to_range(data, clip_min, clip_max) + np.testing.assert_array_almost_equal(data, expected_out, decimal=7) # floating point precision I guess... + + def test_crop_pad_1(self): + # pad, integer, 2D + B = np.indices((4, 4), dtype=np.int32) + A = np.zeros((6, 6), dtype=B.dtype) + au.crop_pad_2d_simple(A, B.sum(0)) + exp_A = np.array([[0, 0, 0, 0, 0, 0], + [0, 0, 1, 2, 3, 0], + [0, 1, 2, 3, 4, 0], + [0, 2, 3, 4, 5, 0], + [0, 3, 4, 5, 6, 0], + [0, 0, 0, 0, 0, 0]]) + np.testing.assert_equal(A, exp_A) + + def test_crop_pad_2(self): + # crop, float, 3D + B = np.indices((4, 4), dtype=np.float32) + A = np.zeros((2, 2, 2), dtype=B.dtype) + au.crop_pad_2d_simple(A, B) + exp_A = np.array([[[1., 1.], + [2., 2.]], + [[1., 2.], + [1., 2.]]], dtype=np.float32) + np.testing.assert_array_almost_equal(A, exp_A) + + def test_crop_pad_3(self): + # crop/pad, complex, 3D + B = np.indices((4, 3), dtype=np.complex64) + B = np.indices((4, 3), dtype=np.complex64) + 1j * B[::-1, :, :] + A = np.zeros((2, 2, 5), dtype=B.dtype) + au.crop_pad_2d_simple(A, B) + exp_A = np.array([[[0. + 0.j, 1. + 0.j, 1. + 1.j, 1. + 2.j, 0. + 0.j], + [0. + 0.j, 2. + 0.j, 2. + 1.j, 2. + 2.j, 0. + 0.j]], + [[0. + 0.j, 0. + 1.j, 1. + 1.j, 2. + 1.j, 0. + 0.j], + [0. + 0.j, 0. + 2.j, 1. + 2.j, 2. + 2.j, 0. + 0.j]]], + dtype=np.complex64) + np.testing.assert_array_almost_equal(A, exp_A) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/base_tests/auxiliary_wave_kernel_test.py b/test/accelerate_tests/base_tests/auxiliary_wave_kernel_test.py new file mode 100644 index 000000000..93e753a51 --- /dev/null +++ b/test/accelerate_tests/base_tests/auxiliary_wave_kernel_test.py @@ -0,0 +1,429 @@ +''' + + +''' + +import unittest +import numpy as np +from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class AuxiliaryWaveKernelTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + + def tearDown(self): + np.set_printoptions() + + def prepare_arrays(self, scan_points = None): + B = 3 # frame size y + C = 3 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + if scan_points is None: + scan_pts = 2 # one dimensional scan point number + else: + scan_pts = scan_points + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + return addr, object_array, probe, exit_wave + + def test_build_aux_same_as_exit(self): + # setup + addr, object_array, probe, exit_wave = self.prepare_arrays() + auxiliary_wave = np.zeros_like(exit_wave) + + # test + AWK = AuxiliaryWaveKernel() + alpha_set = 1.0 + AWK.allocate() # doesn't actually do anything at the moment + AWK.build_aux(auxiliary_wave, addr, object_array, probe, exit_wave, alpha=alpha_set) + + # assert + expected_auxiliary_wave = np.array([[[-1. + 3.j, -1. + 3.j, -1. + 3.j], + [-1. + 3.j, -1. + 3.j, -1. + 3.j], + [-1. + 3.j, -1. + 3.j, -1. + 3.j]], + [[-2. + 14.j, -2. + 14.j, -2. + 14.j], + [-2. + 14.j, -2. + 14.j, -2. + 14.j], + [-2. + 14.j, -2. + 14.j, -2. + 14.j]], + [[-3. + 5.j, -3. + 5.j, -3. + 5.j], + [-3. + 5.j, -3. + 5.j, -3. + 5.j], + [-3. + 5.j, -3. + 5.j, -3. + 5.j]], + [[-4. + 28.j, -4. + 28.j, -4. + 28.j], + [-4. + 28.j, -4. + 28.j, -4. + 28.j], + [-4. + 28.j, -4. + 28.j, -4. + 28.j]], + [[-5. - 1.j, -5. - 1.j, -5. - 1.j], + [-5. - 1.j, -5. - 1.j, -5. - 1.j], + [-5. - 1.j, -5. - 1.j, -5. - 1.j]], + [[-6. + 10.j, -6. + 10.j, -6. + 10.j], + [-6. + 10.j, -6. + 10.j, -6. + 10.j], + [-6. + 10.j, -6. + 10.j, -6. + 10.j]], + [[-7. + 1.j, -7. + 1.j, -7. + 1.j], + [-7. + 1.j, -7. + 1.j, -7. + 1.j], + [-7. + 1.j, -7. + 1.j, -7. + 1.j]], + [[-8. + 24.j, -8. + 24.j, -8. + 24.j], + [-8. + 24.j, -8. + 24.j, -8. + 24.j], + [-8. + 24.j, -8. + 24.j, -8. + 24.j]], + [[-9. - 5.j, -9. - 5.j, -9. - 5.j], + [-9. - 5.j, -9. - 5.j, -9. - 5.j], + [-9. - 5.j, -9. - 5.j, -9. - 5.j]], + [[-10. + 6.j, -10. + 6.j, -10. + 6.j], + [-10. + 6.j, -10. + 6.j, -10. + 6.j], + [-10. + 6.j, -10. + 6.j, -10. + 6.j]], + [[-11. - 3.j, -11. - 3.j, -11. - 3.j], + [-11. - 3.j, -11. - 3.j, -11. - 3.j], + [-11. - 3.j, -11. - 3.j, -11. - 3.j]], + [[-12. + 20.j, -12. + 20.j, -12. + 20.j], + [-12. + 20.j, -12. + 20.j, -12. + 20.j], + [-12. + 20.j, -12. + 20.j, -12. + 20.j]], + [[-13. - 9.j, -13. - 9.j, -13. - 9.j], + [-13. - 9.j, -13. - 9.j, -13. - 9.j], + [-13. - 9.j, -13. - 9.j, -13. - 9.j]], + [[-14. + 2.j, -14. + 2.j, -14. + 2.j], + [-14. + 2.j, -14. + 2.j, -14. + 2.j], + [-14. + 2.j, -14. + 2.j, -14. + 2.j]], + [[-15. - 7.j, -15. - 7.j, -15. - 7.j], + [-15. - 7.j, -15. - 7.j, -15. - 7.j], + [-15. - 7.j, -15. - 7.j, -15. - 7.j]], + [[-16. + 16.j, -16. + 16.j, -16. + 16.j], + [-16. + 16.j, -16. + 16.j, -16. + 16.j], + [-16. + 16.j, -16. + 16.j, -16. + 16.j]]], dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(expected_auxiliary_wave, expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + def test_build_exit_aux_same_as_exit(self): + # setup + addr, object_array, probe, exit_wave = self.prepare_arrays() + auxiliary_wave = np.zeros_like(exit_wave) + + # test + AWK = AuxiliaryWaveKernel() + AWK.allocate() + AWK.build_exit(auxiliary_wave, addr, object_array, probe, exit_wave) + + # assert + expected_auxiliary_wave = np.array([[[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j], + [0. - 16.j, 0. - 16.j, 0. - 16.j]]], dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(auxiliary_wave, expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + # assert + expected_exit_wave = np.array([[[1. - 1.j, 1. - 1.j, 1. - 1.j], + [1. - 1.j, 1. - 1.j, 1. - 1.j], + [1. - 1.j, 1. - 1.j, 1. - 1.j]], + [[2. - 6.j, 2. - 6.j, 2. - 6.j], + [2. - 6.j, 2. - 6.j, 2. - 6.j], + [2. - 6.j, 2. - 6.j, 2. - 6.j]], + [[3. - 1.j, 3. - 1.j, 3. - 1.j], + [3. - 1.j, 3. - 1.j, 3. - 1.j], + [3. - 1.j, 3. - 1.j, 3. - 1.j]], + [[4. - 12.j, 4. - 12.j, 4. - 12.j], + [4. - 12.j, 4. - 12.j, 4. - 12.j], + [4. - 12.j, 4. - 12.j, 4. - 12.j]], + [[5. + 3.j, 5. + 3.j, 5. + 3.j], + [5. + 3.j, 5. + 3.j, 5. + 3.j], + [5. + 3.j, 5. + 3.j, 5. + 3.j]], + [[6. - 2.j, 6. - 2.j, 6. - 2.j], + [6. - 2.j, 6. - 2.j, 6. - 2.j], + [6. - 2.j, 6. - 2.j, 6. - 2.j]], + [[7. + 3.j, 7. + 3.j, 7. + 3.j], + [7. + 3.j, 7. + 3.j, 7. + 3.j], + [7. + 3.j, 7. + 3.j, 7. + 3.j]], + [[8. - 8.j, 8. - 8.j, 8. - 8.j], + [8. - 8.j, 8. - 8.j, 8. - 8.j], + [8. - 8.j, 8. - 8.j, 8. - 8.j]], + [[9. + 7.j, 9. + 7.j, 9. + 7.j], + [9. + 7.j, 9. + 7.j, 9. + 7.j], + [9. + 7.j, 9. + 7.j, 9. + 7.j]], + [[10. + 2.j, 10. + 2.j, 10. + 2.j], + [10. + 2.j, 10. + 2.j, 10. + 2.j], + [10. + 2.j, 10. + 2.j, 10. + 2.j]], + [[11. + 7.j, 11. + 7.j, 11. + 7.j], + [11. + 7.j, 11. + 7.j, 11. + 7.j], + [11. + 7.j, 11. + 7.j, 11. + 7.j]], + [[12. - 4.j, 12. - 4.j, 12. - 4.j], + [12. - 4.j, 12. - 4.j, 12. - 4.j], + [12. - 4.j, 12. - 4.j, 12. - 4.j]], + [[13. + 11.j, 13. + 11.j, 13. + 11.j], + [13. + 11.j, 13. + 11.j, 13. + 11.j], + [13. + 11.j, 13. + 11.j, 13. + 11.j]], + [[14. + 6.j, 14. + 6.j, 14. + 6.j], + [14. + 6.j, 14. + 6.j, 14. + 6.j], + [14. + 6.j, 14. + 6.j, 14. + 6.j]], + [[15. + 11.j, 15. + 11.j, 15. + 11.j], + [15. + 11.j, 15. + 11.j, 15. + 11.j], + [15. + 11.j, 15. + 11.j, 15. + 11.j]], + [[16. + 0.j, 16. + 0.j, 16. + 0.j], + [16. + 0.j, 16. + 0.j, 16. + 0.j], + [16. + 0.j, 16. + 0.j, 16. + 0.j]]], dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(exit_wave, expected_exit_wave, + err_msg="The exit_wave has not been updated as expected") + + def test_build_aux_no_ex(self): + # setup + addr, object_array, probe, exit_wave = self.prepare_arrays() + auxiliary_wave = np.zeros_like(exit_wave) + + # test + AWK = AuxiliaryWaveKernel() + AWK.allocate() + AWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, fac=1.0, add=False) + + # assert + expected_auxiliary_wave = np.array([[[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]]], dtype=np.complex64) + np.testing.assert_array_equal(auxiliary_wave, expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + # test + auxiliary_wave = exit_wave + AWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, fac=2.0, add=True) + + # assert + expected_auxiliary_wave = np.array([[[1. + 5.j, 1. + 5.j, 1. + 5.j], + [1. + 5.j, 1. + 5.j, 1. + 5.j], + [1. + 5.j, 1. + 5.j, 1. + 5.j]], + [[2. + 18.j, 2. + 18.j, 2. + 18.j], + [2. + 18.j, 2. + 18.j, 2. + 18.j], + [2. + 18.j, 2. + 18.j, 2. + 18.j]], + [[3. + 11.j, 3. + 11.j, 3. + 11.j], + [3. + 11.j, 3. + 11.j, 3. + 11.j], + [3. + 11.j, 3. + 11.j, 3. + 11.j]], + [[4. + 36.j, 4. + 36.j, 4. + 36.j], + [4. + 36.j, 4. + 36.j, 4. + 36.j], + [4. + 36.j, 4. + 36.j, 4. + 36.j]], + [[5. + 9.j, 5. + 9.j, 5. + 9.j], + [5. + 9.j, 5. + 9.j, 5. + 9.j], + [5. + 9.j, 5. + 9.j, 5. + 9.j]], + [[6. + 22.j, 6. + 22.j, 6. + 22.j], + [6. + 22.j, 6. + 22.j, 6. + 22.j], + [6. + 22.j, 6. + 22.j, 6. + 22.j]], + [[7. + 15.j, 7. + 15.j, 7. + 15.j], + [7. + 15.j, 7. + 15.j, 7. + 15.j], + [7. + 15.j, 7. + 15.j, 7. + 15.j]], + [[8. + 40.j, 8. + 40.j, 8. + 40.j], + [8. + 40.j, 8. + 40.j, 8. + 40.j], + [8. + 40.j, 8. + 40.j, 8. + 40.j]], + [[9. + 13.j, 9. + 13.j, 9. + 13.j], + [9. + 13.j, 9. + 13.j, 9. + 13.j], + [9. + 13.j, 9. + 13.j, 9. + 13.j]], + [[10. + 26.j, 10. + 26.j, 10. + 26.j], + [10. + 26.j, 10. + 26.j, 10. + 26.j], + [10. + 26.j, 10. + 26.j, 10. + 26.j]], + [[11. + 19.j, 11. + 19.j, 11. + 19.j], + [11. + 19.j, 11. + 19.j, 11. + 19.j], + [11. + 19.j, 11. + 19.j, 11. + 19.j]], + [[12. + 44.j, 12. + 44.j, 12. + 44.j], + [12. + 44.j, 12. + 44.j, 12. + 44.j], + [12. + 44.j, 12. + 44.j, 12. + 44.j]], + [[13. + 17.j, 13. + 17.j, 13. + 17.j], + [13. + 17.j, 13. + 17.j, 13. + 17.j], + [13. + 17.j, 13. + 17.j, 13. + 17.j]], + [[14. + 30.j, 14. + 30.j, 14. + 30.j], + [14. + 30.j, 14. + 30.j, 14. + 30.j], + [14. + 30.j, 14. + 30.j, 14. + 30.j]], + [[15. + 23.j, 15. + 23.j, 15. + 23.j], + [15. + 23.j, 15. + 23.j, 15. + 23.j], + [15. + 23.j, 15. + 23.j, 15. + 23.j]], + [[16. + 48.j, 16. + 48.j, 16. + 48.j], + [16. + 48.j, 16. + 48.j, 16. + 48.j], + [16. + 48.j, 16. + 48.j, 16. + 48.j]]], dtype=np.complex64) + np.testing.assert_array_equal(auxiliary_wave, expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + + def test_build_exit_alpha_tau(self): + + # setup + addr, object_array, probe, exit_wave = self.prepare_arrays(scan_points=1) + auxiliary_wave = np.zeros_like(exit_wave) + + # test + AWK = AuxiliaryWaveKernel() + AWK.allocate() + AWK.build_exit_alpha_tau(auxiliary_wave, addr, object_array, probe, exit_wave) + + # assert + expected_auxiliary_wave = np.array( + [[[0. -2.j, 0. -2.j, 0. -2.j], + [0. -2.j, 0. -2.j, 0. -2.j], + [0. -2.j, 0. -2.j, 0. -2.j]], + + [[0. -8.j, 0. -8.j, 0. -8.j], + [0. -8.j, 0. -8.j, 0. -8.j], + [0. -8.j, 0. -8.j, 0. -8.j]], + + [[0. -4.j, 0. -4.j, 0. -4.j], + [0. -4.j, 0. -4.j, 0. -4.j], + [0. -4.j, 0. -4.j, 0. -4.j]], + + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]]], dtype=np.complex64) + np.testing.assert_array_equal(auxiliary_wave, expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + # assert + expected_exit_wave = np.array( + [[[1. -1.j, 1. -1.j, 1. -1.j], + [1. -1.j, 1. -1.j, 1. -1.j], + [1. -1.j, 1. -1.j, 1. -1.j]], + + [[2. -6.j, 2. -6.j, 2. -6.j], + [2. -6.j, 2. -6.j, 2. -6.j], + [2. -6.j, 2. -6.j, 2. -6.j]], + + [[3. -1.j, 3. -1.j, 3. -1.j], + [3. -1.j, 3. -1.j, 3. -1.j], + [3. -1.j, 3. -1.j, 3. -1.j]], + + [[4.-12.j, 4.-12.j, 4.-12.j], + [4.-12.j, 4.-12.j, 4.-12.j], + [4.-12.j, 4.-12.j, 4.-12.j]]], dtype=np.complex64) + np.testing.assert_array_equal(exit_wave, expected_exit_wave, + err_msg="The exit_wave has not been updated as expected") + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/base_tests/engine_tests.py b/test/accelerate_tests/base_tests/engine_tests.py new file mode 100644 index 000000000..2b9379511 --- /dev/null +++ b/test/accelerate_tests/base_tests/engine_tests.py @@ -0,0 +1,171 @@ +""" +Test for the ML engine. + +This file is part of the PTYPY package. + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import unittest + +from test import utils as tu +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines("serial") +import tempfile +import shutil +import numpy as np + +class MLSerialTest(unittest.TestCase): + + def setUp(self): + self.outpath = tempfile.mkdtemp(suffix="ML_serial_test") + + def tearDown(self): + shutil.rmtree(self.outpath) + + def check_engine_output(self, output, plotting=False, debug=False): + P_ML, P_ML_serial = output + numiter = len(P_ML.runtime["iter_info"]) + LL_ML = np.array([P_ML.runtime["iter_info"][i]["error"][1] for i in range(numiter)]) + LL_ML_serial = np.array([P_ML_serial.runtime["iter_info"][i]["error"][1] for i in range(numiter)]) + crop = 42 + OBJ_ML_serial, OBJ_ML = P_ML_serial.obj.S["SMFG00"].data[0,crop:-crop,crop:-crop], P_ML.obj.S["SMFG00"].data[0,crop:-crop,crop:-crop] + PRB_ML_serial, PRB_ML = P_ML_serial.probe.S["SMFG00"].data[0], P_ML.probe.S["SMFG00"].data[0] + eng_ML = P_ML.engines["engine00"] + eng_ML_serial = P_ML_serial.engines["engine00"] + if debug: + import matplotlib.pyplot as plt + plt.figure("ML debug") + plt.imshow(np.abs(eng_ML.debug)) + plt.figure("ML serial debug") + plt.imshow(np.abs(eng_ML_serial.debug)) + plt.show() + + if plotting: + import matplotlib.pyplot as plt + plt.figure("Errors") + plt.plot(LL_ML, label="ML") + plt.plot(LL_ML_serial, label="ML_serial") + plt.legend() + plt.show() + plt.figure("Phase ML") + plt.imshow(np.angle(OBJ_ML)) + plt.figure("Ampltitude ML") + plt.imshow(np.abs(OBJ_ML)) + plt.figure("Phase ML serial") + plt.imshow(np.angle(OBJ_ML_serial)) + plt.figure("Amplitude ML serial") + plt.imshow(np.abs(OBJ_ML_serial)) + plt.figure("Phase difference") + plt.imshow(np.angle(OBJ_ML_serial) - np.angle(OBJ_ML), vmin=-0.1, vmax=0.1) + plt.colorbar() + plt.figure("Amplitude difference") + plt.imshow(np.abs(OBJ_ML_serial) - np.abs(OBJ_ML), vmin=-0.1, vmax=0.1) + plt.colorbar() + plt.show() + # np.testing.assert_allclose(eng_ML.debug, eng_ML_serial.debug, atol=1e-7, rtol=1e-7, + # err_msg="The debug arrays are not matching as expected") + RMSE_ob = (np.mean(np.abs(OBJ_ML_serial - OBJ_ML)**2)) + RMSE_pr = (np.mean(np.abs(PRB_ML_serial - PRB_ML)**2)) + # RMSE_LL = (np.mean(np.abs(LL_ML_serial - LL_ML)**2)) + np.testing.assert_allclose(RMSE_ob, 0.0, atol=1e-2, + err_msg="The object arrays are not matching as expected") + np.testing.assert_allclose(RMSE_pr, 0.0, atol=1e-2, + err_msg="The object arrays are not matching as expected") + # np.testing.assert_allclose(RMSE_LL, 0.0, atol=1e-7, + # err_msg="The log-likelihood errors are not matching as expected") + + + def test_ML_serial_base(self): + out = [] + for eng in ["ML", "ML_serial"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_serial_regularizer(self): + out = [] + for eng in ["ML", "ML_serial"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = True + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + + def test_ML_serial_preconditioner(self): + out = [] + for eng in ["ML", "ML_serial"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = True + engine_params.scale_probe_object = 1e-6 + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_serial_floating(self): + out = [] + for eng in ["ML", "ML_serial"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = True + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_serial_smoothing_regularizer(self): + out = [] + for eng in ["ML", "ML_serial"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.smooth_gradient = 20 + engine_params.smooth_gradient_decay = 1/10. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_serial_all(self): + out = [] + for eng in ["ML", "ML_serial"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = True + engine_params.reg_del2_amplitude = 1. + engine_params.smooth_gradient = 20 + engine_params.smooth_gradient_decay = 1/10. + engine_params.scale_precond = True + engine_params.scale_probe_object = 1e-6 + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + +if __name__ == "__main__": + unittest.main() diff --git a/test/accelerate_tests/base_tests/fourier_update_kernel_test.py b/test/accelerate_tests/base_tests/fourier_update_kernel_test.py new file mode 100644 index 000000000..fb057408f --- /dev/null +++ b/test/accelerate_tests/base_tests/fourier_update_kernel_test.py @@ -0,0 +1,570 @@ +''' + + +''' + +import unittest +import numpy as np +import ptypy.utils as u +from ptypy.accelerate.base.kernels import FourierUpdateKernel, AuxiliaryWaveKernel + + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class FourierUpdateKernelTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + + def tearDown(self): + np.set_printoptions() + + def test_init(self): + attrs = ["denom", + "fshape", + "nmodes", + "npy"] + + fake_aux = np.zeros((10, 20, 30)) # not used except to initialise + fake_nmodes = 5# not used except to initialise + FUK = FourierUpdateKernel(fake_aux, nmodes=fake_nmodes) + for attr in attrs: + self.assertTrue(hasattr(FUK, attr), msg="FourierUpdateKernel does not have attribute: %s" % attr) + + np.testing.assert_equal(FUK.kernels, + ['fourier_error', 'error_reduce', 'fmag_all_update'], + err_msg='FourierUpdateKernel does not have the correct functions registered.') + + def test_allocate(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + FUK.allocate() + + expected_fdev_shape = (f.shape[0] // total_number_modes, f.shape[1], f.shape[2]) + expected_fdev_type = FLOAT_TYPE + + expected_ferr_shape = (f.shape[0] // total_number_modes, f.shape[1], f.shape[2]) + expected_ferr_type = FLOAT_TYPE + + np.testing.assert_equal(FUK.npy.fdev.shape, expected_fdev_shape) + np.testing.assert_equal(FUK.npy.fdev.dtype, expected_fdev_type) + np.testing.assert_equal(FUK.npy.ferr.shape, expected_ferr_shape) + np.testing.assert_equal(FUK.npy.ferr.dtype, expected_ferr_type) + + def test_fourier_error(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE)# the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3)) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + + mask_sum = mask.sum(-1).sum(-1) + + err_fmag = np.zeros(N, dtype=FLOAT_TYPE) + pbound_set = 0.9 + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + FUK.allocate() + FUK.fourier_error(f, addr, fmag, mask, mask_sum) + + + expected_fdev = np.array([[[7.7459664, 6.7459664, 5.7459664, 4.7459664, 3.7459664], + [2.7459664, 1.7459664, 0.74596643, -0.25403357, -1.2540336], + [-2.2540336, -3.2540336, -4.2540336, -5.2540336, -6.2540336], + [-7.2540336, -8.254034, -9.254034, -10.254034, -11.254034], + [-12.254034, -13.254034, -14.254034, -15.254034, -16.254034]], + + [[-6.3452415, -7.3452415, -8.345242, -9.345242, -10.345242], + [-11.345242, -12.345242, -13.345242, -14.345242, -15.345242], + [-16.345242, -17.345242, -18.345242, -19.345242, -20.345242], + [-21.345242, -22.345242, -23.345242, -24.345242, -25.345242], + [-26.345242, -27.345242, -28.345242, -29.345242, -30.345242]], + + [[-20.13363, -21.13363, -22.13363, -23.13363, -24.13363], + [-25.13363, -26.13363, -27.13363, -28.13363, -29.13363], + [-30.13363, -31.13363, -32.13363, -33.13363, -34.13363], + [-35.13363, -36.13363, -37.13363, -38.13363, -39.13363], + [-40.13363, -41.13363, -42.13363, -43.13363, -44.13363]], + + [[-33.866074, -34.866074, -35.866074, -36.866074, -37.866074], + [-38.866074, -39.866074, -40.866074, -41.866074, -42.866074], + [-43.866074, -44.866074, -45.866074, -46.866074, -47.866074], + [-48.866074, -49.866074, -50.866074, -51.866074, -52.866074], + [-53.866074, -54.866074, -55.866074, -56.866074, -57.866074]]], + dtype=FLOAT_TYPE) + np.testing.assert_array_equal(FUK.npy.fdev, expected_fdev, + err_msg="fdev does not give the expected error " + "for the fourier_update_kernel.fourier_error emthods") + + expected_ferr = np.array([[[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [7.54033208e-01, 3.04839879e-01, 5.56465909e-02, 6.45330548e-03, 1.57260016e-01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [5.26210022e+00, 6.81290817e+00, 8.56371498e+00, 1.05145216e+01, 1.26653280e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[1.61048353e+00, 2.15810299e+00, 2.78572226e+00, 3.49334168e+00, 4.28096104e+00], + [5.14858055e+00, 6.09619951e+00, 7.12381887e+00, 8.23143768e+00, 9.41905785e+00], + [1.06866770e+01, 1.20342960e+01, 1.34619150e+01, 1.49695349e+01, 1.65571537e+01], + [1.82247734e+01, 1.99723930e+01, 2.18000126e+01, 2.37076321e+01, 2.56952515e+01], + [2.77628708e+01, 2.99104881e+01, 3.21381073e+01, 3.44457283e+01, 3.68333473e+01]], + + [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [6.31699409e+01, 6.82966690e+01, 7.36233978e+01, 7.91501160e+01, 8.48768463e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.23437180e+02, 1.30563919e+02, 1.37890640e+02, 1.45417374e+02, 1.53144089e+02], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[4.58764343e+01, 4.86257210e+01, 5.14550095e+01, 5.43642960e+01, 5.73535805e+01], + [6.04228668e+01, 6.35721550e+01, 6.68014374e+01, 7.01107254e+01, 7.35000076e+01], + [7.69692993e+01, 8.05185852e+01, 8.41478729e+01, 8.78571548e+01, 9.16464386e+01], + [9.55157242e+01, 9.94650116e+01, 1.03494293e+02, 1.07603584e+02, 1.11792870e+02], + [1.16062157e+02, 1.20411446e+02, 1.24840721e+02, 1.29350006e+02, 1.33939301e+02]]], + dtype=FLOAT_TYPE) + np.testing.assert_array_equal(FUK.npy.ferr, expected_ferr, + err_msg="ferr does not give the expected error " + "for the fourier_update_kernel.fourier_error emthods") + + def test_error_reduce(self): + # array from the previous test + ferr = np.array([[[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [7.54033208e-01, 3.04839879e-01, 5.56465909e-02, 6.45330548e-03, 1.57260016e-01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [5.26210022e+00, 6.81290817e+00, 8.56371498e+00, 1.05145216e+01, 1.26653280e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[1.61048353e+00, 2.15810299e+00, 2.78572226e+00, 3.49334168e+00, 4.28096104e+00], + [5.14858055e+00, 6.09619951e+00, 7.12381887e+00, 8.23143768e+00, 9.41905785e+00], + [1.06866770e+01, 1.20342960e+01, 1.34619150e+01, 1.49695349e+01, 1.65571537e+01], + [1.82247734e+01, 1.99723930e+01, 2.18000126e+01, 2.37076321e+01, 2.56952515e+01], + [2.77628708e+01, 2.99104881e+01, 3.21381073e+01, 3.44457283e+01, 3.68333473e+01]], + + [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [6.31699409e+01, 6.82966690e+01, 7.36233978e+01, 7.91501160e+01, 8.48768463e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.23437180e+02, 1.30563919e+02, 1.37890640e+02, 1.45417374e+02, 1.53144089e+02], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[4.58764343e+01, 4.86257210e+01, 5.14550095e+01, 5.43642960e+01, 5.73535805e+01], + [6.04228668e+01, 6.35721550e+01, 6.68014374e+01, 7.01107254e+01, 7.35000076e+01], + [7.69692993e+01, 8.05185852e+01, 8.41478729e+01, 8.78571548e+01, 9.16464386e+01], + [9.55157242e+01, 9.94650116e+01, 1.03494293e+02, 1.07603584e+02, 1.11792870e+02], + [1.16062157e+02, 1.20411446e+02, 1.24840721e+02, 1.29350006e+02, 1.33939301e+02]]], + dtype=FLOAT_TYPE) + + + # print(repr(ferr)) + #print(ferr.shape) + #print(repr(ferr)) + auxiliary_shape = (4, 5, 5) + fake_aux = np.zeros(auxiliary_shape, dtype=COMPLEX_TYPE) + scan_pts = 2 # one dimensional scan point number + N = scan_pts ** 2 + + addr = np.zeros((N, 1, 5, 3)) + + FUK = FourierUpdateKernel(fake_aux, nmodes=1) + FUK.allocate() + err_fmag = np.zeros(N, dtype=FLOAT_TYPE) + FUK.error_reduce(addr, err_fmag) + + + + expected_ferr = np.array([[[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [7.54033208e-01, 3.04839879e-01, 5.56465909e-02, 6.45330548e-03, 1.57260016e-01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [5.26210022e+00, 6.81290817e+00, 8.56371498e+00, 1.05145216e+01, 1.26653280e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[1.61048353e+00, 2.15810299e+00, 2.78572226e+00, 3.49334168e+00, 4.28096104e+00], + [5.14858055e+00, 6.09619951e+00, 7.12381887e+00, 8.23143768e+00, 9.41905785e+00], + [1.06866770e+01, 1.20342960e+01, 1.34619150e+01, 1.49695349e+01, 1.65571537e+01], + [1.82247734e+01, 1.99723930e+01, 2.18000126e+01, 2.37076321e+01, 2.56952515e+01], + [2.77628708e+01, 2.99104881e+01, 3.21381073e+01, 3.44457283e+01, 3.68333473e+01]], + + [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [6.31699409e+01, 6.82966690e+01, 7.36233978e+01, 7.91501160e+01, 8.48768463e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.23437180e+02, 1.30563919e+02, 1.37890640e+02, 1.45417374e+02, 1.53144089e+02], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[4.58764343e+01, 4.86257210e+01, 5.14550095e+01, 5.43642960e+01, 5.73535805e+01], + [6.04228668e+01, 6.35721550e+01, 6.68014374e+01, 7.01107254e+01, 7.35000076e+01], + [7.69692993e+01, 8.05185852e+01, 8.41478729e+01, 8.78571548e+01, 9.16464386e+01], + [9.55157242e+01, 9.94650116e+01, 1.03494293e+02, 1.07603584e+02, 1.11792870e+02], + [1.16062157e+02, 1.20411446e+02, 1.24840721e+02, 1.29350006e+02, 1.33939301e+02]]], + dtype=FLOAT_TYPE) + + np.testing.assert_array_equal(expected_ferr, ferr, err_msg="The fourier_update_kernel.error_reduce" + "is not behaving as expected.") + + def test_fmag_update(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3)) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + + # print("address book is:") + # print(repr(addr)) + + ''' + test + ''' + + err_fmag = np.zeros(N, dtype=FLOAT_TYPE) + pbound_set = 0.9 + mask_sum = mask.sum(-1).sum(-1) + + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + FUK.allocate() + FUK.fourier_error(f, addr, fmag, mask, mask_sum) + FUK.error_reduce(addr, err_fmag) + FUK.fmag_all_update(f, addr, fmag, mask, err_fmag, pbound=pbound_set) + # print("f before:") + # print(repr(f)) + # print(fm.shape) + # print(f.shape) + # print("f after:") + # print(repr(f)) + # self.assertTrue(False) + expected_f = np.array([[[ 1. +1.j , 1. +1.j , 1. +1.j , 1. +1.j , 1. +1.j ], + [ 0.6955777 +0.6955777j , 0.8064393 +0.8064393j , 0.91730094 +0.91730094j, 1.0281626 +1.0281626j , 1.1390243 +1.1390243j ], + [ 1. +1.j , 1. +1.j , 1. +1.j , 1. +1.j , 1. +1.j ], + [ 1.804194 +1.804194j , 1.9150558 +1.9150558j , 2.0259173 +2.0259173j , 2.136779 +2.136779j , 2.2476408 +2.2476408j ], + [ 1. +1.j , 1. +1.j , 1. +1.j , 1. +1.j , 1. +1.j ]], + + [[ 2. +2.j , 2. +2.j , 2. +2.j , 2. +2.j , 2. +2.j ], + [ 1.3911554 +1.3911554j , 1.6128786 +1.6128786j , 1.8346019 +1.8346019j , 2.0563252 +2.0563252j , 2.2780485 +2.2780485j ], + [ 2. +2.j , 2. +2.j , 2. +2.j , 2. +2.j , 2. +2.j ], + [ 3.608388 +3.608388j , 3.8301115 +3.8301115j , 4.0518346 +4.0518346j , 4.273558 +4.273558j , 4.4952817 +4.4952817j ], + [ 2. +2.j , 2. +2.j , 2. +2.j , 2. +2.j , 2. +2.j ]], + + [[ 3. +3.j , 3. +3.j , 3. +3.j , 3. +3.j , 3. +3.j ], + [ 2.086733 +2.086733j , 2.4193177 +2.4193177j , 2.7519028 +2.7519028j , 3.084488 +3.084488j , 3.4170728 +3.4170728j ], + [ 3. +3.j , 3. +3.j , 3. +3.j , 3. +3.j , 3. +3.j ], + [ 5.412582 +5.412582j , 5.7451673 +5.7451673j , 6.077752 +6.077752j , 6.4103374 +6.4103374j , 6.742923 +6.742923j ], + [ 3. +3.j , 3. +3.j , 3. +3.j , 3. +3.j , 3. +3.j ]], + + [[ 4. +4.j , 4. +4.j , 4. +4.j , 4. +4.j , 4. +4.j ], + [ 2.7823107 +2.7823107j , 3.2257571 +3.2257571j , 3.6692038 +3.6692038j , 4.1126504 +4.1126504j , 4.556097 +4.556097j ], + [ 4. +4.j , 4. +4.j , 4. +4.j , 4. +4.j , 4. +4.j ], + [ 7.216776 +7.216776j , 7.660223 +7.660223j , 8.103669 +8.103669j , 8.547116 +8.547116j , 8.990563 +8.990563j ], + [ 4. +4.j , 4. +4.j , 4. +4.j , 4. +4.j , 4. +4.j ]], + + [[ 6.618852 +6.618852j , 6.87398 +6.87398j , 7.129109 +7.129109j , 7.3842373 +7.3842373j , 7.6393657 +7.6393657j ], + [ 7.894494 +7.894494j , 8.149623 +8.149623j , 8.404751 +8.404751j , 8.659879 +8.659879j , 8.915008 +8.915008j ], + [ 9.1701355 +9.1701355j , 9.425264 +9.425264j , 9.680393 +9.680393j , 9.935521 +9.935521j , 10.190649 +10.190649j ], + [10.445778 +10.445778j , 10.700907 +10.700907j , 10.956035 +10.956035j , 11.211163 +11.211163j , 11.466292 +11.466292j ], + [11.72142 +11.72142j , 11.976548 +11.976548j , 12.231676 +12.231676j , 12.486806 +12.486806j , 12.741934 +12.741934j ]], + + [[ 7.942622 +7.942622j , 8.248776 +8.248776j , 8.554931 +8.554931j , 8.861084 +8.861084j , 9.167239 +9.167239j ], + [ 9.4733925 +9.4733925j , 9.779547 +9.779547j , 10.085701 +10.085701j , 10.391855 +10.391855j , 10.6980095 +10.6980095j ], + [11.004163 +11.004163j , 11.310318 +11.310318j , 11.616471 +11.616471j , 11.922626 +11.922626j , 12.228779 +12.228779j ], + [12.534934 +12.534934j , 12.841087 +12.841087j , 13.147242 +13.147242j , 13.453396 +13.453396j , 13.75955 +13.75955j ], + [14.065704 +14.065704j , 14.371859 +14.371859j , 14.678012 +14.678012j , 14.984167 +14.984167j , 15.290321 +15.290321j ]], + + [[ 9.266393 +9.266393j , 9.623572 +9.623572j , 9.980753 +9.980753j , 10.337932 +10.337932j , 10.695112 +10.695112j ], + [11.052292 +11.052292j , 11.4094715 +11.4094715j , 11.766651 +11.766651j , 12.123831 +12.123831j , 12.48101 +12.48101j ], + [12.83819 +12.83819j , 13.195371 +13.195371j , 13.552549 +13.552549j , 13.90973 +13.90973j , 14.266909 +14.266909j ], + [14.62409 +14.62409j , 14.981269 +14.981269j , 15.338449 +15.338449j , 15.695628 +15.695628j , 16.052809 +16.052809j ], + [16.409988 +16.409988j , 16.767168 +16.767168j , 17.124348 +17.124348j , 17.48153 +17.48153j , 17.838707 +17.838707j ]], + + [[10.590163 +10.590163j , 10.998368 +10.998368j , 11.406574 +11.406574j , 11.814779 +11.814779j , 12.222985 +12.222985j ], + [12.63119 +12.63119j , 13.039396 +13.039396j , 13.447601 +13.447601j , 13.855806 +13.855806j , 14.264012 +14.264012j ], + [14.672217 +14.672217j , 15.080423 +15.080423j , 15.488628 +15.488628j , 15.896834 +15.896834j , 16.305038 +16.305038j ], + [16.713245 +16.713245j , 17.12145 +17.12145j , 17.529655 +17.529655j , 17.93786 +17.93786j , 18.346067 +18.346067j ], + [18.754272 +18.754272j , 19.162477 +19.162477j , 19.570683 +19.570683j , 19.97889 +19.97889j , 20.387094 +20.387094j ]], + + [[ 9. +9.j , 9. +9.j , 9. +9.j , 9. +9.j , 9. +9.j ], + [16.35309 +16.35309j , 16.64565 +16.64565j , 16.938211 +16.938211j , 17.23077 +17.23077j , 17.52333 +17.52333j ], + [ 9. +9.j , 9. +9.j , 9. +9.j , 9. +9.j , 9. +9.j ], + [19.278687 +19.278687j , 19.571249 +19.571249j , 19.86381 +19.86381j , 20.156368 +20.156368j , 20.448927 +20.448927j ], + [ 9. +9.j , 9. +9.j , 9. +9.j , 9. +9.j , 9. +9.j ]], + + [[10. +10.j , 10. +10.j , 10. +10.j , 10. +10.j , 10. +10.j ], + [18.170101 +18.170101j , 18.495167 +18.495167j , 18.820234 +18.820234j , 19.1453 +19.1453j , 19.470367 +19.470367j ], + [10. +10.j , 10. +10.j , 10. +10.j , 10. +10.j , 10. +10.j ], + [21.420763 +21.420763j , 21.745832 +21.745832j , 22.0709 +22.0709j , 22.395964 +22.395964j , 22.721031 +22.721031j ], + [10. +10.j , 10. +10.j , 10. +10.j , 10. +10.j , 10. +10.j ]], + + [[11. +11.j , 11. +11.j , 11. +11.j , 11. +11.j , 11. +11.j ], + [19.98711 +19.98711j , 20.344685 +20.344685j , 20.702257 +20.702257j , 21.05983 +21.05983j , 21.417404 +21.417404j ], + [11. +11.j , 11. +11.j , 11. +11.j , 11. +11.j , 11. +11.j ], + [23.56284 +23.56284j , 23.920416 +23.920416j , 24.277988 +24.277988j , 24.63556 +24.63556j , 24.993134 +24.993134j ], + [11. +11.j , 11. +11.j , 11. +11.j , 11. +11.j , 11. +11.j ]], + + [[12. +12.j , 12. +12.j , 12. +12.j , 12. +12.j , 12. +12.j ], + [21.804121 +21.804121j , 22.1942 +22.1942j , 22.584282 +22.584282j , 22.974361 +22.974361j , 23.36444 +23.36444j ], + [12. +12.j , 12. +12.j , 12. +12.j , 12. +12.j , 12. +12.j ], + [25.704914 +25.704914j , 26.094997 +26.094997j , 26.485079 +26.485079j , 26.875156 +26.875156j , 27.265236 +27.265236j ], + [12. +12.j , 12. +12.j , 12. +12.j , 12. +12.j , 12. +12.j ]], + + [[23.484367 +23.484367j , 23.793953 +23.793953j , 24.103535 +24.103535j , 24.41312 +24.41312j , 24.7227 +24.7227j ], + [25.032284 +25.032284j , 25.341867 +25.341867j , 25.651451 +25.651451j , 25.961035 +25.961035j , 26.270618 +26.270618j ], + [26.5802 +26.5802j , 26.889784 +26.889784j , 27.199366 +27.199366j , 27.508951 +27.508951j , 27.818535 +27.818535j ], + [28.128117 +28.128117j , 28.437698 +28.437698j , 28.747284 +28.747284j , 29.056868 +29.056868j , 29.36645 +29.36645j ], + [29.676033 +29.676033j , 29.985619 +29.985619j , 30.2952 +30.2952j , 30.604782 +30.604782j , 30.914366 +30.914366j ]], + + [[25.290857 +25.290857j , 25.624256 +25.624256j , 25.957653 +25.957653j , 26.291052 +26.291052j , 26.624447 +26.624447j ], + [26.957846 +26.957846j , 27.291243 +27.291243j , 27.62464 +27.62464j , 27.958038 +27.958038j , 28.291435 +28.291435j ], + [28.62483 +28.62483j , 28.95823 +28.95823j , 29.291626 +29.291626j , 29.625023 +29.625023j , 29.958424 +29.958424j ], + [30.291819 +30.291819j , 30.625214 +30.625214j , 30.958612 +30.958612j , 31.292011 +31.292011j , 31.625406 +31.625406j ], + [31.958805 +31.958805j , 32.292206 +32.292206j , 32.6256 +32.6256j , 32.958996 +32.958996j , 33.292393 +33.292393j ]], + + [[27.097347 +27.097347j , 27.454561 +27.454561j , 27.811771 +27.811771j , 28.168985 +28.168985j , 28.526192 +28.526192j ], + [28.883406 +28.883406j , 29.240616 +29.240616j , 29.597828 +29.597828j , 29.95504 +29.95504j , 30.312252 +30.312252j ], + [30.669462 +30.669462j , 31.026674 +31.026674j , 31.383884 +31.383884j , 31.741096 +31.741096j , 32.09831 +32.09831j ], + [32.45552 +32.45552j , 32.81273 +32.81273j , 33.16994 +33.16994j , 33.527153 +33.527153j , 33.884365 +33.884365j ], + [34.241577 +34.241577j , 34.59879 +34.59879j , 34.956 +34.956j , 35.31321 +35.31321j , 35.67042 +35.67042j ]], + + [[28.903837 +28.903837j , 29.284864 +29.284864j , 29.66589 +29.66589j , 30.046917 +30.046917j , 30.427938 +30.427938j ], + [30.808966 +30.808966j , 31.189991 +31.189991j , 31.571016 +31.571016j , 31.952044 +31.952044j , 32.33307 +32.33307j ], + [32.714092 +32.714092j , 33.09512 +33.09512j , 33.476143 +33.476143j , 33.85717 +33.85717j , 34.238197 +34.238197j ], + [34.61922 +34.61922j , 35.000244 +35.000244j , 35.38127 +35.38127j , 35.7623 +35.7623j , 36.143322 +36.143322j ], + [36.52435 +36.52435j , 36.905376 +36.905376j , 37.2864 +37.2864j , 37.667423 +37.667423j , 38.04845 +38.04845j ]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(f, expected_f, err_msg="the f array from the fmag_all_update kernesl isnot behaving as expected.") + + # TODO: This test needs to be redesigne to NOT use components from the archive test + @unittest.skip('This test needs to be redone') + def test_log_likelihood(self): + nmodes = 1 + PtychoInstance = tu.get_ptycho_instance('log_likelihood_test', nmodes) # noqa: F821 + ptypy_error_metric = self.get_ptypy_loglikelihood(PtychoInstance) + LLerr_expected = np.array([LL for LL in ptypy_error_metric.values()]).astype(np.float32) + + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') # noqa: F821 + addr = vectorised_scan['meta']['addr'].reshape((len(ptypy_error_metric)//nmodes, nmodes, 5, 3)) + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + mask = vectorised_scan['mask'] + exit_wave = vectorised_scan['exit wave'] + mag = np.sqrt(vectorised_scan['diffraction']) + + aux = np.zeros_like(exit_wave) + AWK = AuxiliaryWaveKernel() + AWK.allocate() + AWK.build_aux_no_ex(aux, addr, obj, probe, fac=1.0, add=False) + + scan = list(PtychoInstance.model.scans.values())[0] + geo = scan.geometries[0] + aux[:] = geo.propagator.fw(aux) + + FUK = FourierUpdateKernel(aux, nmodes=1) + FUK.allocate() + LLerr = np.zeros_like(LLerr_expected, dtype=np.float32) + FUK.log_likelihood(aux, addr, mag, mask, LLerr) + + np.testing.assert_allclose(LLerr, LLerr_expected, rtol=1e-6, err_msg="LLerr does not give the expected error " + "for the fourier_update_kernel.log_likelihood method") + + def get_ptypy_loglikelihood(self, a_ptycho_instance): + error_dct = {} + for dname, diff_view in a_ptycho_instance.diff.views.items(): + I = diff_view.data + fmask = diff_view.pod.mask + LL = np.zeros_like(diff_view.data) + for name, pod in diff_view.pods.items(): + LL += u.abs2(pod.fw(pod.probe * pod.object)) + + error_dct[dname] = (np.sum(fmask * (LL - I) ** 2 / (I + 1.)) + / np.prod(LL.shape)) + return error_dct + + + def test_exit_error(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + aux = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + aux[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3)) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + err_sum = np.zeros(N, dtype=FLOAT_TYPE) + FUK = FourierUpdateKernel(aux, nmodes=total_number_modes) + FUK.allocate() + FUK.exit_error(aux, addr) + + expected_ferr = np.array([[[ 2.3999996, 2.3999996, 2.3999996, 2.3999996, 2.3999996], + [ 2.3999996, 2.3999996, 2.3999996, 2.3999996, 2.3999996], + [ 2.3999996, 2.3999996, 2.3999996, 2.3999996, 2.3999996], + [ 2.3999996, 2.3999996, 2.3999996, 2.3999996, 2.3999996], + [ 2.3999996, 2.3999996, 2.3999996, 2.3999996, 2.3999996]], + + [[13.92, 13.92, 13.92, 13.92, 13.92], + [13.92, 13.92, 13.92, 13.92, 13.92], + [13.92, 13.92, 13.92, 13.92, 13.92], + [13.92, 13.92, 13.92, 13.92, 13.92], + [13.92, 13.92, 13.92, 13.92, 13.92]], + + [[35.68, 35.68, 35.68, 35.68, 35.68 ], + [35.68, 35.68, 35.68, 35.68, 35.68 ], + [35.68, 35.68, 35.68, 35.68, 35.68 ], + [35.68, 35.68, 35.68, 35.68, 35.68 ], + [35.68, 35.68, 35.68, 35.68, 35.68 ]], + + [[67.68, 67.68, 67.68, 67.68, 67.68 ], + [67.68, 67.68, 67.68, 67.68, 67.68 ], + [67.68, 67.68, 67.68, 67.68, 67.68 ], + [67.68, 67.68, 67.68, 67.68, 67.68 ], + [67.68, 67.68, 67.68, 67.68, 67.68 ]]], dtype=FLOAT_TYPE) + + np.testing.assert_array_equal(FUK.npy.ferr, expected_ferr, + err_msg="ferr does not give the expected error " + "for the fourier_update_kernel.fourier_error emthods") + + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/base_tests/gradient_descent_kernel_test.py b/test/accelerate_tests/base_tests/gradient_descent_kernel_test.py new file mode 100644 index 000000000..453edc2ff --- /dev/null +++ b/test/accelerate_tests/base_tests/gradient_descent_kernel_test.py @@ -0,0 +1,279 @@ +''' + + +''' + +import unittest +import numpy as np +from ptypy.accelerate.base.kernels import GradientDescentKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class GradientDescentKernelTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + + def tearDown(self): + np.set_printoptions() + + def test_init(self): + attrs = ["denom", + "fshape", + "bshape", + "nmodes", + "npy"] + + fake_aux = np.zeros((10, 20, 30)) # not used except to initialise + fake_nmodes = 5# not used except to initialise + GDK = GradientDescentKernel(fake_aux, nmodes=fake_nmodes) + for attr in attrs: + self.assertTrue(hasattr(GDK, attr), msg="FourierUpdateKernel does not have attribute: %s" % attr) + + def prepare_arrays(self): + nmodes = 2 + N_buf = 4 + N = 3 + A = 3 + i_sh = (N, A, A) + e_sh = (N*nmodes, A, A) + f_sh = (N_buf, A, A) + a_sh = (N_buf * nmodes, A, A) + w = np.ones(i_sh, dtype=FLOAT_TYPE) + for idx, sl in enumerate(w): + sl[idx % A, idx % A] = 0.0 + X,Y,Z = np.indices(a_sh, dtype=COMPLEX_TYPE) + b_f = X + 1j * Y + b_a = Y + 1j * Z + b_b = Z + 1j * X + err_sum = np.zeros((N,), dtype=FLOAT_TYPE) + addr = np.zeros((N,nmodes,5,3), dtype=INT_TYPE) + I = np.empty(i_sh, dtype=FLOAT_TYPE) + I[:] = np.round(np.abs(b_f[:N])**2 % 20) + for pos_idx in range(N): + for mode_idx in range(nmodes): + exit_idx = pos_idx * nmodes + mode_idx + addr[pos_idx, mode_idx] = np.array([[mode_idx, 0, 0], + [0, 0, 0], + [exit_idx, 0, 0], + [pos_idx, 0, 0], + [pos_idx, 0, 0]], dtype=INT_TYPE) + return b_f, b_a, b_b, I, w, err_sum, addr + + def test_allocate(self): + ''' + setup + ''' + pass + + def test_make_model(self): + b_f, b_a, b_b, I, w, err_sum, addr = self.prepare_arrays() + + GDK=GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_model(b_f, addr) + #print('Im',repr(GDK.npy.Imodel)) + exp_Imodel = np.array([[[ 1., 1., 1.], + [ 3., 3., 3.], + [ 9., 9., 9.]], + + [[13., 13., 13.], + [15., 15., 15.], + [21., 21., 21.]], + + [[41., 41., 41.], + [43., 43., 43.], + [49., 49., 49.]], + + [[85., 85., 85.], + [87., 87., 87.], + [93., 93., 93.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal(exp_Imodel, GDK.npy.Imodel, + err_msg="`Imodel` buffer has not been updated as expected") + + def test_floating_intensity(self): + b_f, b_a, b_b, I, w, err_sum, addr = self.prepare_arrays() + fic = np.ones((I.shape[0],), dtype=I.dtype) + GDK=GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.npy.Imodel[:I.shape[0]] = I * (1+np.arange(I.shape[0]))[::-1].reshape((I.shape[0],1,1)) + GDK.floating_intensity(addr, w, I, fic) + #print('Imodel',repr(GDK.npy.Imodel)) + #print('fic',repr(1./fic)) + exp_Imodel = np.array([[[0., 0., 0.], + [1., 1., 1.], + [4., 4., 4.]], + + [[1., 1., 1.], + [2., 2., 2.], + [5., 5., 5.]], + + [[4., 4., 4.], + [5., 5., 5.], + [8., 8., 8.]], + + [[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]], dtype=np.float32) + exp_fic=1./np.array([3., 2., 1.], dtype=np.float32) + np.testing.assert_array_almost_equal(exp_Imodel, GDK.npy.Imodel, + err_msg="`Imodel` buffer has not been updated as expected") + np.testing.assert_array_almost_equal(exp_fic, fic, + err_msg="floating intensity coeff (fic) has not been updated as expected") + + + def test_make_a012(self): + b_f, b_a, b_b, I, w, err_sum, addr = self.prepare_arrays() + fic = np.ones((I.shape[0],), dtype=I.dtype) + GDK=GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_a012(b_f, b_a, b_b, addr, I, fic) + #print('Imodel',repr(GDK.npy.Imodel)) + #print('LLerr',repr(GDK.npy.LLerr)) + #print('LLden',repr(GDK.npy.LLden)) + exp_A0 = np.array([[[ 1., 1., 1.], + [ 2., 2., 2.], + [ 5., 5., 5.]], + + [[12., 12., 12.], + [13., 13., 13.], + [16., 16., 16.]], + + [[37., 37., 37.], + [38., 38., 38.], + [41., 41., 41.]], + + [[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal(exp_A0, GDK.npy.Imodel, + err_msg="`Imodel` buffer (=A0) has not been updated as expected") + exp_A1 = np.array([[[ 0., 0., 0.], + [ 2., 6., 10.], + [ 4., 12., 20.]], + + [[ 0., 0., 0.], + [10., 14., 18.], + [20., 28., 36.]], + + [[ 0., 0., 0.], + [18., 22., 26.], + [36., 44., 52.]], + + [[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal(exp_A1, GDK.npy.LLerr, + err_msg="`LLerr` buffer (=A1) has not been updated as expected") + exp_A2 = np.array([[[ 0., 4., 12.], + [ 4., 8., 16.], + [12., 16., 24.]], + + [[ 0., 12., 28.], + [12., 24., 40.], + [28., 40., 56.]], + + [[ 0., 20., 44.], + [20., 40., 64.], + [44., 64., 88.]], + + [[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal(exp_A2, GDK.npy.LLden, + err_msg="`LLden` buffer (=A2) has not been updated as expected") + + def test_fill_b(self): + b_f, b_a, b_b, I, w, err_sum, addr = self.prepare_arrays() + fic = np.ones((I.shape[0],), dtype=I.dtype) + Brenorm = 0.35 + B = np.zeros((3,), dtype=FLOAT_TYPE) + GDK=GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_a012(b_f, b_a, b_b, addr, I, fic) + GDK.fill_b(addr, Brenorm, w, B) + #print('B',repr(B)) + exp_B = np.array([ 4699.8, 5398.4, 13398.], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal(exp_B, B, + err_msg="`B` has not been updated as expected") + + + def test_error_reduce(self): + b_f, b_a, b_b, I, w, err_sum, addr = self.prepare_arrays() + GDK=GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.npy.LLerr = np.indices(GDK.npy.LLerr.shape, dtype=FLOAT_TYPE)[0] + GDK.error_reduce(addr, err_sum) + #print('Err',repr(err_sum)) + exp_err = np.array([ 0., 9., 18.], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal(exp_err, err_sum, + err_msg="`err_sum` has not been updated as expected") + return + + def test_main(self): + b_f, b_a, b_b, I, w, err_sum, addr = self.prepare_arrays() + GDK=GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.main(b_f, addr, w, I) + #print('B_F',repr(b_f)) + #print('LL',repr(GDK.npy.LLerr)) + exp_b_f = np.array([[[ 0. +0.j, 0. +0.j, 0. +0.j], + [ -0. -1.j, -0. -1.j, -0. -1.j], + [ -0. -8.j, -0. -8.j, -0. -8.j]], + + [[ 0. +0.j, 0. +0.j, 0. +0.j], + [ -1. -1.j, -1. -1.j, -1. -1.j], + [ -4. -8.j, -4. -8.j, -4. -8.j]], + + [[ -2. +0.j, -2. +0.j, -2. +0.j], + [ -4. -2.j, -0. +0.j, -4. -2.j], + [-10.-10.j, -10.-10.j, -10.-10.j]], + + [[ -3. +0.j, -3. +0.j, -3. +0.j], + [ -6. -2.j, -0. +0.j, -6. -2.j], + [-15.-10.j, -15.-10.j, -15.-10.j]], + + [[-16. +0.j, -16. +0.j, -16. +0.j], + [-20. -5.j, -20. -5.j, -20. -5.j], + [-32.-16.j, -32.-16.j, -0. +0.j]], + + [[-20. +0.j, -20. +0.j, -20. +0.j], + [-25. -5.j, -25. -5.j, -25. -5.j], + [-40.-16.j, -40.-16.j, -0. +0.j]], + + [[ 6. +0.j, 6. +0.j, 6. +0.j], + [ 6. +1.j, 6. +1.j, 6. +1.j], + [ 6. +2.j, 6. +2.j, 6. +2.j]], + + [[ 7. +0.j, 7. +0.j, 7. +0.j], + [ 7. +1.j, 7. +1.j, 7. +1.j], + [ 7. +2.j, 7. +2.j, 7. +2.j]]], dtype=COMPLEX_TYPE) + np.testing.assert_array_almost_equal(exp_b_f, b_f, + err_msg="Auxiliary has not been updated as expected") + exp_LL =np.array([[[ 0., 0., 0.], + [ 1., 1., 1.], + [16., 16., 16.]], + + [[ 1., 1., 1.], + [ 4., 0., 4.], + [25., 25., 25.]], + + [[16., 16., 16.], + [25., 25., 25.], + [64., 64., 0.]], + + [[ 0., 0., 0.], + [ 0., 0., 0.], + [ 0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal(exp_LL, GDK.npy.LLerr, + err_msg="LogLikelihood error has not been updated as expected") + return + + + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/base_tests/import_test.py b/test/accelerate_tests/base_tests/import_test.py new file mode 100644 index 000000000..755a80866 --- /dev/null +++ b/test/accelerate_tests/base_tests/import_test.py @@ -0,0 +1,10 @@ +""" +Import test +""" +import unittest + +class AutoLoaderTest(unittest.TestCase): + + def test_load_engines_serial(self): + import ptypy + ptypy.load_gpu_engines("serial") diff --git a/test/accelerate_tests/base_tests/po_update_kernel_test.py b/test/accelerate_tests/base_tests/po_update_kernel_test.py new file mode 100644 index 000000000..953c26b54 --- /dev/null +++ b/test/accelerate_tests/base_tests/po_update_kernel_test.py @@ -0,0 +1,512 @@ +''' + + +''' + +import unittest +import numpy as np +from ptypy.accelerate.base.kernels import PoUpdateKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class PoUpdateKernelTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + + def tearDown(self): + np.set_printoptions() + + def test_init(self): + POUK = PoUpdateKernel() + + np.testing.assert_equal(POUK.kernels, + ['pr_update', 'ob_update'], + err_msg='PoUpdateKernel does not have the correct functions registered.') + + def prepare_arrays(self): + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + object_array_denominator = np.empty_like(object_array, dtype=FLOAT_TYPE) + for idx in range(G): + object_array_denominator[idx] = np.ones((H, I)) * (5 * idx + 2) # + 1j * np.ones((H, I)) * (5 * idx + 2) + + probe_denominator = np.empty_like(probe, dtype=FLOAT_TYPE) + for idx in range(D): + probe_denominator[idx] = np.ones((E, F)) * (5 * idx + 2) # + 1j * np.ones((E, F)) * (5 * idx + 2) + + return addr, object_array, object_array_denominator, probe, exit_wave, probe_denominator + + def test_ob_update(self): + # setup + addr, object_array, object_array_denominator, probe, exit_wave, probe_denominator = self.prepare_arrays() + + # test + POUK = PoUpdateKernel() + POUK.allocate() # doesn't do anything but is the call signature + POUK.ob_update(addr, object_array, object_array_denominator, probe, exit_wave) + + # assert + expected_object_array = np.array([[[15. + 1.j, 53. + 1.j, 53. + 1.j, 53. + 1.j, 53. + 1.j, 39. + 1.j, 1. + 1.j], + [77. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 125. + 1.j, + 1. + 1.j], + [77. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 125. + 1.j, + 1. + 1.j], + [77. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 125. + 1.j, + 1. + 1.j], + [77. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 201. + 1.j, 125. + 1.j, + 1. + 1.j], + [63. + 1.j, 149. + 1.j, 149. + 1.j, 149. + 1.j, 149. + 1.j, 87. + 1.j, + 1. + 1.j], + [1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j]], + [[24. + 4.j, 68. + 4.j, 68. + 4.j, 68. + 4.j, 68. + 4.j, 48. + 4.j, 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, + 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, + 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, + 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, + 4. + 4.j], + [72. + 4.j, 164. + 4.j, 164. + 4.j, 164. + 4.j, 164. + 4.j, 96. + 4.j, + 4. + 4.j], + [4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j]]], + dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(object_array, expected_object_array, + err_msg="The object array has not been updated as expected") + + # assert + expected_object_array_denominator = np.array([[[12., 22., 22., 22., 22., 12., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [12., 22., 22., 22., 22., 12., 2.], + [2., 2., 2., 2., 2., 2., 2.]], + + [[17., 27., 27., 27., 27., 17., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [17., 27., 27., 27., 27., 17., 7.], + [7., 7., 7., 7., 7., 7., 7.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_equal(object_array_denominator, expected_object_array_denominator, + err_msg="The object array denominatorhas not been updated as expected") + + def test_pr_update(self): + # setup + addr, object_array, object_array_denominator, probe, exit_wave, probe_denominator = self.prepare_arrays() + + # test + POUK = PoUpdateKernel() + POUK.allocate() # this doesn't do anything, but is the call pattern. + POUK.pr_update(addr, probe, probe_denominator, object_array, exit_wave) + + # assert + expected_probe = np.array([[[313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j], + [313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j], + [313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j], + [313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j], + [313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j, 313. + 1.j]], + + [[394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j], + [394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j], + [394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j], + [394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j], + [394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j, 394. + 2.j]]], + dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(probe, expected_probe, + err_msg="The probe has not been updated as expected") + + # assert + expected_probe_denominator = np.array([[[138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.]], + + [[143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_equal(probe_denominator, expected_probe_denominator, + err_msg="The probe denominatorhas not been updated as expected") + + def test_pr_update_ML(self): + # setup + addr, object_array, object_array_denominator, probe, exit_wave, probe_denominator = self.prepare_arrays() + + # test + POUK = PoUpdateKernel() + POUK.allocate() # this doesn't do anything, but is the call pattern. + POUK.pr_update_ML(addr, probe, object_array, exit_wave) + + # assert + expected_probe = np.array([[[625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j]], + + [[786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j]]], + dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(probe, expected_probe, + err_msg="The probe has not been updated as expected") + + def test_ob_update_ML(self): + # setup + addr, object_array, object_array_denominator, probe, exit_wave, probe_denominator = self.prepare_arrays() + + # test + POUK = PoUpdateKernel() + POUK.allocate() # this doesn't do anything, but is the call pattern. + POUK.ob_update_ML(addr, object_array, probe, exit_wave) + + # assert + expected_object_array = np.array( + [[[29. + 1.j, 105. + 1.j, 105. + 1.j, 105. + 1.j, 105. + 1.j, 77. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [125. + 1.j, 297. + 1.j, 297. + 1.j, 297. + 1.j, 297. + 1.j, 173. + 1.j, 1. + 1.j], + [1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j]], + + [[44. + 4.j, 132. + 4.j, 132. + 4.j, 132. + 4.j, 132. + 4.j, 92. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [140. + 4.j, 324. + 4.j, 324. + 4.j, 324. + 4.j, 324. + 4.j, 188. + 4.j, 4. + 4.j], + [4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j]]], + dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(object_array, expected_object_array, + err_msg="The object array has not been updated as expected") + + + def test_pr_update_local(self): + # setup + B = 5 # frame size y + C = 5 # frame size x + + D = 1 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 1 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + auxiliary_wave = exit_wave.copy() * 1.5 + + object_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + # test + POUK = PoUpdateKernel() + POUK.allocate() # this doesn't do anything, but is the call pattern. + POUK.ob_norm_local(addr, object_array, object_norm) + POUK.pr_update_local(addr, probe, object_array, exit_wave, auxiliary_wave, object_norm, object_norm.max()) + + # assert + expected_probe = np.array([[[0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j], + [0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j], + [0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j], + [0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j], + [0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j, 0.5+1.j]]], dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(probe, expected_probe, + err_msg="The probe has not been updated as expected") + + def test_ob_update_local(self): + # setup + B = 5 # frame size y + C = 5 # frame size x + + D = 1 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 1 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + auxiliary_wave = exit_wave.copy() * 2 + + probe_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + # test + POUK = PoUpdateKernel() + POUK.allocate() # this doesn't do anything, but is the call pattern. + POUK.pr_norm_local(addr, probe, probe_norm) + POUK.ob_update_local(addr, object_array, probe, exit_wave, auxiliary_wave, probe_norm) + + # assert + expected_object_array = np.array([[[0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 1.+1.j, 1.+1.j], + [0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 1.+1.j, 1.+1.j], + [0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 1.+1.j, 1.+1.j], + [0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 1.+1.j, 1.+1.j], + [0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 0.+1.j, 1.+1.j, 1.+1.j], + [1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j], + [1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j]]], dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(object_array, expected_object_array, + err_msg="The object array has not been updated as expected") + + def test_pr_norm_local(self): + # setup + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + # test + POUK = PoUpdateKernel() + POUK.allocate() # this doesn't do anything, but is the call pattern. + POUK.pr_norm_local(addr, probe, probe_norm) + + # assert + expected_probe_norm = np.array([[[10., 10., 10., 10., 10.], + [10., 10., 10., 10., 10.], + [10., 10., 10., 10., 10.], + [10., 10., 10., 10., 10.], + [10., 10., 10., 10., 10.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_equal(probe_norm, expected_probe_norm, + err_msg="The probe norm has not been updated as expected") + + + def test_ob_norm_local(self): + # setup + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + object_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + # test + POUK = PoUpdateKernel() + POUK.allocate() # this doesn't do anything, but is the call pattern. + POUK.ob_norm_local(addr, object_array, object_norm) + + # assert + expected_object_norm = np.array([[[34., 34., 34., 34., 34.], + [34., 34., 34., 34., 34.], + [34., 34., 34., 34., 34.], + [34., 34., 34., 34., 34.], + [34., 34., 34., 34., 34.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_equal(object_norm, expected_object_norm, + err_msg="The object norm has not been updated as expected") + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/base_tests/position_correction_kernel_test.py b/test/accelerate_tests/base_tests/position_correction_kernel_test.py new file mode 100644 index 000000000..50544c43e --- /dev/null +++ b/test/accelerate_tests/base_tests/position_correction_kernel_test.py @@ -0,0 +1,326 @@ +''' + + +''' + +import unittest +import numpy as np +from ptypy.accelerate.base.kernels import PositionCorrectionKernel +from ptypy import utils as u +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class PositionCorrectionKernelTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + self.params = u.Param() + self.params.nshifts = 4 + self.params.method = "Annealing" + self.params.amplitude = 2e-9 + self.params.start = 0 + self.params.stop = 10 + self.params.max_shift = 2e-9 + self.params.amplitude_decay = True + self.resolution = [1e-9,1e-9] + + def tearDown(self): + np.set_printoptions() + + def test_build_aux(self): + ''' + setup + ''' + B = 3 # frame size y + C = 3 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 2) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 2) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + + ''' + test + ''' + auxiliary_wave = np.zeros((A, B, C), dtype=COMPLEX_TYPE) + + PCK = PositionCorrectionKernel(auxiliary_wave, total_number_modes, self.params, self.resolution) + PCK.allocate() # doesn't actually do anything at the moment + PCK.build_aux(auxiliary_wave, addr, object_array, probe) + + expected_auxiliary_wave = np.array([[[-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j]], + + [[-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j]], + + [[-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j]], + + [[-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j]], + + [[-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j]], + + [[-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j]], + + [[-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j]], + + [[-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j]], + + [[-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j]], + + [[-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j]], + + [[-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j]], + + [[-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j]], + + [[-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j], + [-3. +4.j, -3. +4.j, -3. +4.j]], + + [[-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j], + [-6.+13.j, -6.+13.j, -6.+13.j]], + + [[-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j], + [-4. +7.j, -4. +7.j, -4. +7.j]], + + [[-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j], + [-7.+22.j, -7.+22.j, -7.+22.j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(expected_auxiliary_wave, expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + def test_fourier_error(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + auxiliary_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + auxiliary_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE)# the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3)) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + + mask_sum = mask.sum(-1).sum(-1) + + + PCK = PositionCorrectionKernel(auxiliary_wave, total_number_modes, self.params, self.resolution) + PCK.allocate() + PCK.fourier_error(auxiliary_wave, addr, fmag, mask, mask_sum) + + + expected_ferr = np.array([[[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [7.54033208e-01, 3.04839879e-01, 5.56465909e-02, 6.45330548e-03, 1.57260016e-01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [5.26210022e+00, 6.81290817e+00, 8.56371498e+00, 1.05145216e+01, 1.26653280e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[1.61048353e+00, 2.15810299e+00, 2.78572226e+00, 3.49334168e+00, 4.28096104e+00], + [5.14858055e+00, 6.09619951e+00, 7.12381887e+00, 8.23143768e+00, 9.41905785e+00], + [1.06866770e+01, 1.20342960e+01, 1.34619150e+01, 1.49695349e+01, 1.65571537e+01], + [1.82247734e+01, 1.99723930e+01, 2.18000126e+01, 2.37076321e+01, 2.56952515e+01], + [2.77628708e+01, 2.99104881e+01, 3.21381073e+01, 3.44457283e+01, 3.68333473e+01]], + + [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [6.31699409e+01, 6.82966690e+01, 7.36233978e+01, 7.91501160e+01, 8.48768463e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.23437180e+02, 1.30563919e+02, 1.37890640e+02, 1.45417374e+02, 1.53144089e+02], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[4.58764343e+01, 4.86257210e+01, 5.14550095e+01, 5.43642960e+01, 5.73535805e+01], + [6.04228668e+01, 6.35721550e+01, 6.68014374e+01, 7.01107254e+01, 7.35000076e+01], + [7.69692993e+01, 8.05185852e+01, 8.41478729e+01, 8.78571548e+01, 9.16464386e+01], + [9.55157242e+01, 9.94650116e+01, 1.03494293e+02, 1.07603584e+02, 1.11792870e+02], + [1.16062157e+02, 1.20411446e+02, 1.24840721e+02, 1.29350006e+02, 1.33939301e+02]]], + dtype=FLOAT_TYPE) + np.testing.assert_array_equal(PCK.npy.ferr, expected_ferr, + err_msg="ferr does not give the expected error " + "for the fourier_update_kernel.fourier_error emthods") + + def test_error_reduce(self): + # array from the previous test + ferr = np.array([[[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [7.54033208e-01, 3.04839879e-01, 5.56465909e-02, 6.45330548e-03, 1.57260016e-01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [5.26210022e+00, 6.81290817e+00, 8.56371498e+00, 1.05145216e+01, 1.26653280e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[1.61048353e+00, 2.15810299e+00, 2.78572226e+00, 3.49334168e+00, 4.28096104e+00], + [5.14858055e+00, 6.09619951e+00, 7.12381887e+00, 8.23143768e+00, 9.41905785e+00], + [1.06866770e+01, 1.20342960e+01, 1.34619150e+01, 1.49695349e+01, 1.65571537e+01], + [1.82247734e+01, 1.99723930e+01, 2.18000126e+01, 2.37076321e+01, 2.56952515e+01], + [2.77628708e+01, 2.99104881e+01, 3.21381073e+01, 3.44457283e+01, 3.68333473e+01]], + + [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [6.31699409e+01, 6.82966690e+01, 7.36233978e+01, 7.91501160e+01, 8.48768463e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.23437180e+02, 1.30563919e+02, 1.37890640e+02, 1.45417374e+02, 1.53144089e+02], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[4.58764343e+01, 4.86257210e+01, 5.14550095e+01, 5.43642960e+01, 5.73535805e+01], + [6.04228668e+01, 6.35721550e+01, 6.68014374e+01, 7.01107254e+01, 7.35000076e+01], + [7.69692993e+01, 8.05185852e+01, 8.41478729e+01, 8.78571548e+01, 9.16464386e+01], + [9.55157242e+01, 9.94650116e+01, 1.03494293e+02, 1.07603584e+02, 1.11792870e+02], + [1.16062157e+02, 1.20411446e+02, 1.24840721e+02, 1.29350006e+02, 1.33939301e+02]]], + dtype=FLOAT_TYPE) + + + # print(repr(ferr)) + # print(ferr.shape) + # print(repr(ferr)) + auxiliary_shape = (4, 5, 5) + fake_aux = np.zeros(auxiliary_shape, dtype=COMPLEX_TYPE) + scan_pts = 2 # one dimensional scan point number + N = scan_pts ** 2 + + addr = np.zeros((N, 1, 5, 3)) + + PCK = PositionCorrectionKernel(fake_aux, 1, self.params, self.resolution) + PCK.allocate() + err_fmag = np.zeros(N, dtype=FLOAT_TYPE) + PCK.error_reduce(addr, err_fmag) + + + + expected_ferr = np.array([[[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [7.54033208e-01, 3.04839879e-01, 5.56465909e-02, 6.45330548e-03, 1.57260016e-01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [5.26210022e+00, 6.81290817e+00, 8.56371498e+00, 1.05145216e+01, 1.26653280e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[1.61048353e+00, 2.15810299e+00, 2.78572226e+00, 3.49334168e+00, 4.28096104e+00], + [5.14858055e+00, 6.09619951e+00, 7.12381887e+00, 8.23143768e+00, 9.41905785e+00], + [1.06866770e+01, 1.20342960e+01, 1.34619150e+01, 1.49695349e+01, 1.65571537e+01], + [1.82247734e+01, 1.99723930e+01, 2.18000126e+01, 2.37076321e+01, 2.56952515e+01], + [2.77628708e+01, 2.99104881e+01, 3.21381073e+01, 3.44457283e+01, 3.68333473e+01]], + + [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [6.31699409e+01, 6.82966690e+01, 7.36233978e+01, 7.91501160e+01, 8.48768463e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.23437180e+02, 1.30563919e+02, 1.37890640e+02, 1.45417374e+02, 1.53144089e+02], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[4.58764343e+01, 4.86257210e+01, 5.14550095e+01, 5.43642960e+01, 5.73535805e+01], + [6.04228668e+01, 6.35721550e+01, 6.68014374e+01, 7.01107254e+01, 7.35000076e+01], + [7.69692993e+01, 8.05185852e+01, 8.41478729e+01, 8.78571548e+01, 9.16464386e+01], + [9.55157242e+01, 9.94650116e+01, 1.03494293e+02, 1.07603584e+02, 1.11792870e+02], + [1.16062157e+02, 1.20411446e+02, 1.24840721e+02, 1.29350006e+02, 1.33939301e+02]]], + dtype=FLOAT_TYPE) + + np.testing.assert_array_equal(expected_ferr, ferr, err_msg="The fourier_update_kernel.error_reduce" + "is not behaving as expected.") + + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/cuda_pycuda_tests/__init__.py b/test/accelerate_tests/cuda_pycuda_tests/__init__.py new file mode 100644 index 000000000..04582430f --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/__init__.py @@ -0,0 +1,43 @@ +import unittest +import numpy as np + +# shall we run the performance tests? +perfrun = False + +def have_pycuda(): + try: + import pycuda.driver + return True + except: + return False + +if have_pycuda(): + import pycuda.driver as cuda + from pycuda import gpuarray + from pycuda.tools import make_default_context + from ptypy.accelerate import cuda_pycuda + + # make sure this is called once + cuda.init() + +@unittest.skipIf(not have_pycuda(), "no PyCUDA or GPU drivers available") +class PyCudaTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + self.ctx = make_default_context() + self.stream = cuda.Stream() + # enable assertions in CUDA kernels for testing + if not 'perf' in self._testMethodName: + self.opts_old = cuda_pycuda.debug_options.copy() + if '-DNDEBUG' in cuda_pycuda.debug_options: + cuda_pycuda.debug_options.remove('-DNDEBUG') + + def tearDown(self): + np.set_printoptions() + self.ctx.pop() + self.ctx.detach() + if not 'perf' in self._testMethodName: + cuda_pycuda.debug_options = self.opts_old + diff --git a/test/accelerate_tests/cuda_pycuda_tests/address_manglers_test.py b/test/accelerate_tests/cuda_pycuda_tests/address_manglers_test.py new file mode 100644 index 000000000..2704dcf97 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/address_manglers_test.py @@ -0,0 +1,77 @@ +import unittest +import numpy as np +from . import perfrun, PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.base import address_manglers as am + from ptypy.accelerate.cuda_pycuda import address_manglers as gam + + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class AddressManglersTest(PyCudaTest): + + def prepare_addresses(self, max_bound=10, scan_pts=2, num_modes=3): + total_number_scan_positions = scan_pts ** 2 + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + max_bound # max bound is added in the DM_serial engine. + Y = Y.reshape((total_number_scan_positions)) + max_bound + + addr_original = np.zeros((total_number_scan_positions, num_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(num_modes): + for ob_mode in range(1): + addr_original[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + return addr_original + + def test_get_address_REGRESSION(self): + # the other manglers are using the BaseMangler's get_address function + # so we set the deltas in a BaseMangler object and test get_address + + scan_pts=2 + addr_original = self.prepare_addresses(scan_pts=scan_pts) + addr_original_dev = gpuarray.to_gpu(addr_original) + nshifts=1 + step_size=2 + mglr = gam.BaseMangler(step_size, 50, 100, nshifts, max_bound=2) + # 2 shifts, with positive/negative shifting + mglr.delta = np.array([ + [1, 2], + [-4, -2] + ], dtype=np.int32) + mglr._setup_delta_gpu() + + addr1 = addr_original_dev.copy() + mglr.get_address(0, addr_original_dev, addr1, 10, 9) + + addr2 = addr_original_dev.copy() + mglr.get_address(1, addr_original_dev, addr2, 10, 9) + + exp1 = np.copy(addr_original) + exp2 = np.copy(addr_original) + # element-wise here to prepare reference + for f in range(addr_original.shape[0]): + for m in range(addr_original.shape[1]): + exp1[f, m, 1, 1] = max(0, min(10, addr_original[f, m, 1, 1] + 1)) + exp1[f, m, 1, 2] = max(0, min(9, addr_original[f, m, 1, 2] + 2)) + exp2[f, m, 1, 1] = max(0, min(10, addr_original[f, m, 1, 1] - 4)) + exp2[f, m, 1, 2] = max(0, min(9, addr_original[f, m, 1, 2] - 2)) + + np.testing.assert_array_equal(addr2.get(), exp2) + np.testing.assert_array_equal(addr1.get(), exp1) + diff --git a/test/accelerate_tests/cuda_pycuda_tests/array_utils_test.py b/test/accelerate_tests/cuda_pycuda_tests/array_utils_test.py new file mode 100644 index 000000000..912b68bfe --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/array_utils_test.py @@ -0,0 +1,540 @@ +''' + + +''' + +import unittest +import numpy as np +from . import perfrun, PyCudaTest, have_pycuda +from ptypy.accelerate.base import array_utils as au + +if have_pycuda(): + from pycuda import gpuarray + import ptypy.accelerate.cuda_pycuda.array_utils as gau + +class ArrayUtilsTest(PyCudaTest): + + def test_dot_float_float(self): + ## Arrange + X,Y,Z = np.indices((3,3,1001), dtype=np.float32) + A = 10 ** Y + A_dev = gpuarray.to_gpu(A) + + ## Act + AU = gau.ArrayUtilsKernel(acc_dtype=np.float32) + out_dev = AU.dot(A_dev, A_dev) + out = out_dev.get() + + ## Assert + np.testing.assert_allclose(out, 30333303.0, rtol=1e-7) + + def test_dot_float_double(self): + ## Arrange + X,Y,Z = np.indices((3,3,1001), dtype=np.float32) + A = 10 ** Y + A_dev = gpuarray.to_gpu(A) + + ## Act + AU = gau.ArrayUtilsKernel(acc_dtype=np.float64) + out_dev = AU.dot(A_dev, A_dev) + out = out_dev.get() + + ## Assert + np.testing.assert_equal(out, 30333303.0) + + def test_dot_complex_float(self): + ## Arrange + X,Y,Z = np.indices((3,3,1001), dtype=np.float32) + A = 10 ** Y + 1j * 10 ** X + A_dev = gpuarray.to_gpu(A) + + ## Act + AU = gau.ArrayUtilsKernel(acc_dtype=np.float32) + out_dev = AU.dot(A_dev, A_dev) + out = out_dev.get() + + ## Assert + np.testing.assert_allclose(out, 60666606.0, rtol=1e-7) + + def test_dot_complex_double(self): + ## Arrange + X,Y,Z = np.indices((3,3,1001), dtype=np.float32) + A = 10 ** Y + 1j * 10 ** X + A_dev = gpuarray.to_gpu(A) + + ## Act + AU = gau.ArrayUtilsKernel(acc_dtype=np.float64) + out_dev = AU.dot(A_dev, A_dev) + out = out_dev.get() + + ## Assert + np.testing.assert_array_equal(out, 60666606.0) + + @unittest.skipIf(not perfrun, "Performance test") + def test_dot_performance(self): + ## Arrange + X,Y,Z = np.indices((3,3,1021301), dtype=np.float32) + A = 10 ** Y + 1j * 10 ** X + A_dev = gpuarray.to_gpu(A) + + ## Act + AU = gau.ArrayUtilsKernel(acc_dtype=np.float64) + out_dev = AU.dot(A_dev, A_dev) + + def test_transpose_2D(self): + ## Arrange + inp,_ = np.indices((5,3), dtype=np.int32) + inp_dev = gpuarray.to_gpu(inp) + out_dev = gpuarray.empty((3,5), dtype=np.int32) + + ## Act + AU = gau.TransposeKernel() + AU.transpose(inp_dev, out_dev) + + ## Assert + out_exp = np.transpose(inp, (1, 0)) + out = out_dev.get() + np.testing.assert_array_equal(out, out_exp) + + def test_transpose_2D_large(self): + ## Arrange + inp,_ = np.indices((137,61), dtype=np.int32) + inp_dev = gpuarray.to_gpu(inp) + out_dev = gpuarray.empty((61,137), dtype=np.int32) + + ## Act + AU = gau.TransposeKernel() + AU.transpose(inp_dev, out_dev) + + ## Assert + out_exp = np.transpose(inp, (1, 0)) + out = out_dev.get() + np.testing.assert_array_equal(out, out_exp) + + def test_transpose_4D(self): + ## Arrange + inp = np.random.randint(0, 10000, (250, 3, 5, 3), dtype=np.int32) # like addr + inp_dev = gpuarray.to_gpu(inp) + out_dev = gpuarray.empty((5, 3, 250, 3), dtype=np.int32) + + ## Act + AU = gau.TransposeKernel() + AU.transpose(inp_dev.reshape(750, 15), out_dev.reshape(15, 750)) + + ## Assert + out_exp = np.transpose(inp, (2, 3, 0, 1)) + out = out_dev.get() + np.testing.assert_array_equal(out, out_exp) + + def test_complex_gaussian_filter_1d_no_blurring_UNITY(self): + # Arrange + data = np.zeros((11,), dtype=np.complex64) + data[5] = 1.0 +1.0j + mfs = [0] + data_dev = gpuarray.to_gpu(data) + tmp_dev = gpuarray.empty((11,), dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs, tmp=tmp_dev) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + self.assertTrue(np.testing.assert_allclose(out_exp, out, rtol=1e-5) is None) + + def test_complex_gaussian_filter_1d_little_blurring_UNITY(self): + # Arrange + data = np.zeros((11,), dtype=np.complex64) + data[5] = 1.0 +1.0j + mfs = [0.2] + data_dev = gpuarray.to_gpu(data) + tmp_dev = gpuarray.empty((11,), dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs, tmp=tmp_dev) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + np.testing.assert_allclose(out_exp, out, rtol=1e-5) + + + def test_complex_gaussian_filter_1d_more_blurring_UNITY(self): + # Arrange + data = np.zeros((11,), dtype=np.complex64) + data[5] = 1.0 +1.0j + mfs = [2.0] + data_dev = gpuarray.to_gpu(data) + tmp_dev = gpuarray.empty((11,), dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs, tmp=tmp_dev) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + np.testing.assert_allclose(out_exp, out, rtol=1e-5) + + def test_complex_gaussian_filter_2d_no_blurring_UNITY(self): + # Arrange + data = np.zeros((11, 11), dtype=np.complex64) + data[5, 5] = 1.0+1.0j + mfs = 0.0,0.0 + data_dev = gpuarray.to_gpu(data) + tmp_dev = gpuarray.empty((11,11), dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs, tmp=tmp_dev) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + np.testing.assert_allclose(out_exp, out, rtol=1e-5) + + def test_complex_gaussian_filter_2d_little_blurring_UNITY(self): + # Arrange + data = np.zeros((11, 11), dtype=np.complex64) + data[5, 5] = 1.0+1.0j + mfs = 0.2,0.2 + data_dev = gpuarray.to_gpu(data) + tmp_dev = gpuarray.empty((11,11),dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs, tmp=tmp_dev) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + np.testing.assert_allclose(out_exp, out, rtol=1e-5) + + def test_complex_gaussian_filter_2d_more_blurring_UNITY(self): + # Arrange + data = np.zeros((8, 8), dtype=np.complex64) + data[3:5, 3:5] = 2.0+2.0j + mfs = 3.0,4.0 + data_dev = gpuarray.to_gpu(data) + #tmp_dev = gpuarray.empty((8,8), dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + np.testing.assert_allclose(out_exp, out, rtol=1e-4) + + def test_complex_gaussian_filter_2d_nonsquare_UNITY(self): + # Arrange + data = np.zeros((32, 16), dtype=np.complex64) + data[3:4, 11:12] = 2.0+2.0j + data[3:5, 3:5] = 2.0+2.0j + data[20:25,3:5] = 2.0+2.0j + mfs = 1.0,1.0 + data_dev = gpuarray.to_gpu(data) + tmp_dev = gpuarray.empty(data_dev.shape, dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs, tmp=tmp_dev) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + + np.testing.assert_allclose(out_exp, out, rtol=1e-4) + + def test_complex_gaussian_filter_2d_batched(self): + # Arrange + batch_number = 2 + A = 5 + B = 5 + data = np.zeros((batch_number, A, B), dtype=np.complex64) + data[:, 2:3, 2:3] = 2.0+2.0j + mfs = 3.0,4.0 + data_dev = gpuarray.to_gpu(data) + tmp_dev = gpuarray.empty((batch_number,A,B), dtype=np.complex64) + + # Act + GS = gau.GaussianSmoothingKernel() + GS.convolution(data_dev, mfs, tmp=tmp_dev) + + # Assert + out_exp = au.complex_gaussian_filter(data, mfs) + out = data_dev.get() + np.testing.assert_allclose(out_exp, out, rtol=1e-4) + + + def test_crop_pad_simple_1_UNITY(self): + # pad, integer, 2D + B = np.indices((4, 4), dtype=np.int32).sum(0) + A = np.zeros((6, 6), dtype=B.dtype) + B_dev = gpuarray.to_gpu(B) + A_dev = gpuarray.to_gpu(A) + + # Act + au.crop_pad_2d_simple(A, B) + k = gau.CropPadKernel(queue=self.stream) + k.crop_pad_2d_simple(A_dev, B_dev) + + # Assert + np.testing.assert_allclose(A, A_dev.get(), rtol=1e-6, atol=1e-6) + + def test_crop_pad_simple_2_UNITY(self): + # crop, float, 3D + B = np.indices((4, 4), dtype=np.float32) + A = np.zeros((2, 2, 2), dtype=B.dtype) + B_dev = gpuarray.to_gpu(B) + A_dev = gpuarray.to_gpu(A) + + # Act + au.crop_pad_2d_simple(A, B) + k = gau.CropPadKernel(queue=self.stream) + k.crop_pad_2d_simple(A_dev, B_dev) + + + # Assert + np.testing.assert_allclose(A, A_dev.get(), rtol=1e-6, atol=1e-6) + + def test_crop_pad_simple_3_UNITY(self): + # crop/pad, complex, 3D + B = np.indices((4, 3), dtype=np.complex64) + B = np.indices((4, 3), dtype=np.complex64) + 1j * B[::-1, :, :] + A = np.zeros((2, 2, 5), dtype=B.dtype) + B_dev = gpuarray.to_gpu(B) + A_dev = gpuarray.to_gpu(A) + + # Act + au.crop_pad_2d_simple(A, B) + k = gau.CropPadKernel(queue=self.stream) + k.crop_pad_2d_simple(A_dev, B_dev) + + # Assert + np.testing.assert_allclose(A, A_dev.get(), rtol=1e-6, atol=1e-6) + + def test_crop_pad_simple_difflike_UNITY(self): + np.random.seed(1983) + # crop/pad, 4D + D = np.random.randint(0, 3000, (100,256,256)).astype(np.float32) + A = np.zeros((100,260,260), dtype=D.dtype) + B = np.zeros((100,250,250), dtype=D.dtype) + B_dev = gpuarray.to_gpu(B) + A_dev = gpuarray.to_gpu(A) + D_dev = gpuarray.to_gpu(D) + + # Act + au.crop_pad_2d_simple(A, D) + au.crop_pad_2d_simple(B, D) + k = gau.CropPadKernel(queue=self.stream) + k.crop_pad_2d_simple(A_dev, D_dev) + k.crop_pad_2d_simple(B_dev, D_dev) + + # Assert + np.testing.assert_allclose(A, A_dev.get(), rtol=1e-6, atol=1e-6) + np.testing.assert_allclose(B, B_dev.get(), rtol=1e-6, atol=1e-6) + + def test_crop_pad_simple_oblike_UNITY(self): + np.random.seed(1983) + # crop/pad, 4D + B = np.random.rand(2,1230,1434).astype(np.complex64) \ + +2j * np.pi * np.random.randn(2,1230,1434).astype(np.complex64) + A = np.ones((2,1000,1500), dtype=B.dtype) + B_dev = gpuarray.to_gpu(B) + A_dev = gpuarray.to_gpu(A) + + # Act + au.crop_pad_2d_simple(A, B) + k = gau.CropPadKernel(queue=self.stream) + k.crop_pad_2d_simple(A_dev, B_dev) + + # Assert + np.testing.assert_allclose(A, A_dev.get(), rtol=1e-6, atol=1e-6) + + def test_max_abs2_complex_UNITY(self): + np.random.seed(1983) + X = (np.random.randint(-1000, 1000, (3,100,200)).astype(np.float32) + \ + 1j * np.random.randint(-1000, 1000, (3,100,200)).astype(np.float32)).astype(np.complex64) + out = np.zeros((1,), dtype=np.float32) + X_dev = gpuarray.to_gpu(X) + out_dev = gpuarray.to_gpu(out) + + out = au.max_abs2(X) + + MAK = gau.MaxAbs2Kernel(queue=self.stream) + MAK.max_abs2(X_dev, out_dev) + + np.testing.assert_allclose(out_dev.get(), out, rtol=1e-6, atol=1e-6, + err_msg="The object norm array has not been updated as expected") + + def test_max_abs2_float_UNITY(self): + np.random.seed(1983) + X = np.random.randint(-1000, 1000, (3,100,200)).astype(np.float32) + + out = np.zeros((1,), dtype=np.float32) + X_dev = gpuarray.to_gpu(X) + out_dev = gpuarray.to_gpu(out) + + out = au.max_abs2(X) + + MAK = gau.MaxAbs2Kernel(queue=self.stream) + MAK.max_abs2(X_dev, out_dev) + + np.testing.assert_allclose(out_dev.get(), out, rtol=1e-6, atol=1e-6, + err_msg="The object norm array has not been updated as expected") + + + def test_clip_magnitudes_to_range_UNITY(self): + np.random.seed(1987) + A = np.random.random((2,10,10)) + B = A[0] + 1j* A[1] + B = B.astype(np.complex64) + B_gpu = gpuarray.to_gpu(B) + + au.clip_complex_magnitudes_to_range(B, 0.2,0.8) + CMK = gau.ClipMagnitudesKernel() + CMK.clip_magnitudes_to_range(B_gpu, 0.2, 0.8) + + np.testing.assert_allclose(B_gpu.get(), B, rtol=1e-6, atol=1e-6, + err_msg="The magnitudes of the array have not been clipped as expected") + + + def test_mass_center_2d_UNITY(self): + np.random.seed(1987) + A = np.random.random((128, 128)).astype(np.float32) + A_gpu = gpuarray.to_gpu(A) + + out = au.mass_center(A) + + MCK = gau.MassCenterKernel() + mc_d = MCK.mass_center(A_gpu) + mc = mc_d.get() + + np.testing.assert_allclose(out, mc, rtol=1e-6, atol=1e-6, + err_msg="The centre of mass of the array has not been calculated as expected") + + + def test_mass_center_3d_UNITY(self): + np.random.seed(1987) + A = np.random.random((128, 128, 128)).astype(np.float32) + A_gpu = gpuarray.to_gpu(A) + + out = au.mass_center(A) + + MCK = gau.MassCenterKernel() + mc_d = MCK.mass_center(A_gpu) + mc = mc_d.get() + + np.testing.assert_allclose(out, mc, rtol=1e-6, atol=1e-6, + err_msg="The centre of mass of the array has not been calculated as expected") + + def test_abs2sum_complex_float_UNITY(self): + np.random.seed(1987) + A = np.random.random((3, 321, 123)).astype(np.float32) + B = A + A**2 * 1j + B_gpu = gpuarray.to_gpu(B) + + out = au.abs2(B).sum(0) + + A2SK = gau.Abs2SumKernel(dtype=B_gpu.dtype) + a2s_d = A2SK.abs2sum(B_gpu) + a2s = a2s_d.get() + + np.testing.assert_allclose(out, a2s, rtol=1e-6, atol=1e-6, + err_msg="The sum of absolute values along the first dimension has not been calculated as expected") + + def test_abs2sum_complex_double_UNITY(self): + np.random.seed(1987) + A = np.random.random((3, 321, 123)).astype(np.float64) + B = A + A**2 * 1j + B_gpu = gpuarray.to_gpu(B) + + out = au.abs2(B).sum(0) + + A2SK = gau.Abs2SumKernel(dtype=B_gpu.dtype) + a2s_d = A2SK.abs2sum(B_gpu) + a2s = a2s_d.get() + + np.testing.assert_allclose(out, a2s, rtol=1e-6, atol=1e-6, + err_msg="The sum of absolute values along the first dimension has not been calculated as expected") + + def test_interpolate_shift_2D_UNITY(self): + np.random.seed(1987) + A = np.random.random((259, 252)).astype(np.float32) + A = A + A**2 * 1j + A_gpu = gpuarray.to_gpu(A) + + cen_old = np.array([100.123, 5.678]).astype(np.float32) + cen_new = np.array([128.5, 127.5]).astype(np.float32) + shift = cen_new - cen_old + + out = au.interpolated_shift(A, shift, do_linear=True) + + ISK = gau.InterpolatedShiftKernel() + isk_d = ISK.interpolate_shift(A_gpu, shift) + isk = isk_d.get() + + np.testing.assert_allclose(out, isk, rtol=1e-6, atol=1e-6, + err_msg="The shifting of array has not been calculated as expected") + + def test_interpolate_shift_3D_UNITY(self): + np.random.seed(1987) + A = np.random.random((3, 200, 300)).astype(np.float32) + A = A + A**2 * 1j + A_gpu = gpuarray.to_gpu(A) + + cen_old = np.array([0., 180.123, 5.678]).astype(np.float32) + cen_new = np.array([0., 128.5, 127.5]).astype(np.float32) + shift = cen_new - cen_old + + out = au.interpolated_shift(A, shift, do_linear=True) + + ISK = gau.InterpolatedShiftKernel() + isk_d = ISK.interpolate_shift(A_gpu, shift[1:]) + isk = isk_d.get() + + np.testing.assert_allclose(out, isk, rtol=1e-6, atol=1e-6, + err_msg="The shifting of array has not been calculated as expected") + + def test_interpolate_shift_integer_UNITY(self): + np.random.seed(1987) + A = np.random.random((3, 200, 300)).astype(np.float32) + A = A + A**2 * 1j + A_gpu = gpuarray.to_gpu(A) + + cen_old = np.array([0, 180, 5]).astype(np.float32) + cen_new = np.array([0, 128, 127]).astype(np.float32) + shift = cen_new - cen_old + + out = au.interpolated_shift(A, shift, do_linear=True) + + ISK = gau.InterpolatedShiftKernel() + isk_d = ISK.interpolate_shift(A_gpu, shift[1:]) + isk = isk_d.get() + + np.testing.assert_allclose(out, isk, rtol=1e-6, atol=1e-6, + err_msg="The shifting of array has not been calculated as expected") + + def test_interpolate_shift_no_shift_UNITY(self): + np.random.seed(1987) + A = np.random.random((3, 200, 300)).astype(np.float32) + A = A + A**2 * 1j + A_gpu = gpuarray.to_gpu(A) + + cen_old = np.array([0, 0, 0]).astype(np.float32) + cen_new = np.array([0, 0, 0]).astype(np.float32) + shift = cen_new - cen_old + + out = au.interpolated_shift(A, shift, do_linear=True) + + ISK = gau.InterpolatedShiftKernel() + isk_d = ISK.interpolate_shift(A_gpu, shift[1:]) + isk = isk_d.get() + + np.testing.assert_allclose(out, isk, rtol=1e-6, atol=1e-6, + err_msg="The shifting of array has not been calculated as expected") + diff --git a/test/accelerate_tests/cuda_pycuda_tests/auxiliary_wave_kernel_test.py b/test/accelerate_tests/cuda_pycuda_tests/auxiliary_wave_kernel_test.py new file mode 100644 index 000000000..4765aca3b --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/auxiliary_wave_kernel_test.py @@ -0,0 +1,666 @@ +''' + + +''' + +import unittest +import numpy as np +from . import perfrun, PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import AuxiliaryWaveKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class AuxiliaryWaveKernelTest(PyCudaTest): + + def prepare_arrays(self, performance=False, scan_points=None): + if not performance: + B = 3 # frame size y + C = 3 # frame size x + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + if scan_points is None: + scan_pts = 2 # one dimensional scan point number + else: + scan_pts = scan_points + else: + B = 128 + C = 128 + D = 2 + E = B + F = C + npts_greater_than = 1215 + G = 4 + if scan_points is None: + scan_pts = 14 + else: + scan_pts = scan_points + + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + if performance: + print('addr={}, obj={}, pr={}, ex={}'.format(addr.shape, object_array.shape, probe.shape, exit_wave.shape)) + # assert False + + return addr, object_array, probe, exit_wave + + def copy_to_gpu(self, addr, object_array, probe, exit_wave): + return (gpuarray.to_gpu(addr), + gpuarray.to_gpu(object_array), + gpuarray.to_gpu(probe), + gpuarray.to_gpu(exit_wave)) + + def test_init(self): + # should we really test for private attributes? + # Only the public interface should be checked - what clients rely on + attrs = ["_ob_shape", + "_ob_id"] + + AWK = AuxiliaryWaveKernel(self.stream) + for attr in attrs: + self.assertTrue(hasattr(AWK, attr), msg="AuxiliaryWaveKernel does not have attribute: %s" % attr) + + np.testing.assert_equal(AWK.kernels, + ['build_aux', 'build_exit'], + err_msg='AuxiliaryWaveKernel does not have the correct functions registered.') + + def test_build_aux_same_as_exit_REGRESSION(self): + ## Arrange + cpudata = self.prepare_arrays() + addr, object_array, probe, exit_wave = self.copy_to_gpu(*cpudata) + auxiliary_wave = gpuarray.zeros_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + alpha_set = FLOAT_TYPE(1.0) + + AWK.build_aux(auxiliary_wave, addr, object_array, probe, exit_wave, alpha=alpha_set) + + expected_auxiliary_wave = np.array([[[-1. + 3.j, -1. + 3.j, -1. + 3.j], + [-1. + 3.j, -1. + 3.j, -1. + 3.j], + [-1. + 3.j, -1. + 3.j, -1. + 3.j]], + [[-2.+14.j, -2.+14.j, -2.+14.j], + [-2.+14.j, -2.+14.j, -2.+14.j], + [-2.+14.j, -2.+14.j, -2.+14.j]], + [[-3. + 5.j, -3. + 5.j, -3. + 5.j], + [-3. + 5.j, -3. + 5.j, -3. + 5.j], + [-3. + 5.j, -3. + 5.j, -3. + 5.j]], + [[-4.+28.j, -4.+28.j, -4.+28.j], + [-4.+28.j, -4.+28.j, -4.+28.j], + [-4.+28.j, -4.+28.j, -4.+28.j]], + [[-5. - 1.j, -5. - 1.j, -5. - 1.j], + [-5. - 1.j, -5. - 1.j, -5. - 1.j], + [-5. - 1.j, -5. - 1.j, -5. - 1.j]], + [[-6.+10.j, -6.+10.j, -6.+10.j], + [-6.+10.j, -6.+10.j, -6.+10.j], + [-6.+10.j, -6.+10.j, -6.+10.j]], + [[-7. + 1.j, -7. + 1.j, -7. + 1.j], + [-7. + 1.j, -7. + 1.j, -7. + 1.j], + [-7. + 1.j, -7. + 1.j, -7. + 1.j]], + [[-8.+24.j, -8.+24.j, -8.+24.j], + [-8.+24.j, -8.+24.j, -8.+24.j], + [-8.+24.j, -8.+24.j, -8.+24.j]], + [[-9. - 5.j, -9. - 5.j, -9. - 5.j], + [-9. - 5.j, -9. - 5.j, -9. - 5.j], + [-9. - 5.j, -9. - 5.j, -9. - 5.j]], + [[-10. + 6.j, -10. + 6.j, -10. + 6.j], + [-10. + 6.j, -10. + 6.j, -10. + 6.j], + [-10. + 6.j, -10. + 6.j, -10. + 6.j]], + [[-11. - 3.j, -11. - 3.j, -11. - 3.j], + [-11. - 3.j, -11. - 3.j, -11. - 3.j], + [-11. - 3.j, -11. - 3.j, -11. - 3.j]], + [[-12.+20.j, -12.+20.j, -12.+20.j], + [-12.+20.j, -12.+20.j, -12.+20.j], + [-12.+20.j, -12.+20.j, -12.+20.j]], + [[-13. - 9.j, -13. - 9.j, -13. - 9.j], + [-13. - 9.j, -13. - 9.j, -13. - 9.j], + [-13. - 9.j, -13. - 9.j, -13. - 9.j]], + [[-14. + 2.j, -14. + 2.j, -14. + 2.j], + [-14. + 2.j, -14. + 2.j, -14. + 2.j], + [-14. + 2.j, -14. + 2.j, -14. + 2.j]], + [[-15. - 7.j, -15. - 7.j, -15. - 7.j], + [-15. - 7.j, -15. - 7.j, -15. - 7.j], + [-15. - 7.j, -15. - 7.j, -15. - 7.j]], + [[-16.+16.j, -16.+16.j, -16.+16.j], + [-16.+16.j, -16.+16.j, -16.+16.j], + [-16.+16.j, -16.+16.j, -16.+16.j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(expected_auxiliary_wave, auxiliary_wave.get(), + err_msg="The auxiliary_wave has not been updated as expected") + + + def test_build_aux_same_as_exit_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = np.zeros_like(exit_wave) + auxiliary_wave_dev = gpuarray.zeros_like(exit_wave_dev) + + ## Act + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel(self.stream) + alpha_set = FLOAT_TYPE(.75) + + AWK.build_aux(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, exit_wave_dev, alpha=alpha_set) + nAWK.build_aux(auxiliary_wave, addr, object_array, probe, exit_wave, alpha=alpha_set) + + ## Assert + np.testing.assert_array_equal(auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + def test_build_aux2_same_as_exit_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = np.zeros_like(exit_wave) + auxiliary_wave_dev = gpuarray.zeros_like(exit_wave_dev) + + ## Act + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel(self.stream) + alpha_set = FLOAT_TYPE(.75) + + AWK.build_aux2(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, exit_wave_dev, alpha=alpha_set) + nAWK.build_aux(auxiliary_wave, addr, object_array, probe, exit_wave, alpha=alpha_set) + + ## Assert + np.testing.assert_array_equal(auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + def test_build_exit_aux_same_as_exit_REGRESSION(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave_dev = gpuarray.zeros_like(exit_wave_dev) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + alpha_set = 1.0 + AWK.build_exit(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, exit_wave_dev) + + ## Assert + expected_auxiliary_wave = np.array([[[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(expected_auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The auxiliary_wave has not been updated as expected") + + expected_exit_wave = np.array([[[1. - 1.j, 1. - 1.j, 1. - 1.j], + [1. - 1.j, 1. - 1.j, 1. - 1.j], + [1. - 1.j, 1. - 1.j, 1. - 1.j]], + [[2. - 6.j, 2. - 6.j, 2. - 6.j], + [2. - 6.j, 2. - 6.j, 2. - 6.j], + [2. - 6.j, 2. - 6.j, 2. - 6.j]], + [[3. - 1.j, 3. - 1.j, 3. - 1.j], + [3. - 1.j, 3. - 1.j, 3. - 1.j], + [3. - 1.j, 3. - 1.j, 3. - 1.j]], + [[4. - 12.j, 4. - 12.j, 4. - 12.j], + [4. - 12.j, 4. - 12.j, 4. - 12.j], + [4. - 12.j, 4. - 12.j, 4. - 12.j]], + [[5. + 3.j, 5. + 3.j, 5. + 3.j], + [5. + 3.j, 5. + 3.j, 5. + 3.j], + [5. + 3.j, 5. + 3.j, 5. + 3.j]], + [[6. - 2.j, 6. - 2.j, 6. - 2.j], + [6. - 2.j, 6. - 2.j, 6. - 2.j], + [6. - 2.j, 6. - 2.j, 6. - 2.j]], + [[7. + 3.j, 7. + 3.j, 7. + 3.j], + [7. + 3.j, 7. + 3.j, 7. + 3.j], + [7. + 3.j, 7. + 3.j, 7. + 3.j]], + [[8. - 8.j, 8. - 8.j, 8. - 8.j], + [8. - 8.j, 8. - 8.j, 8. - 8.j], + [8. - 8.j, 8. - 8.j, 8. - 8.j]], + [[9. + 7.j, 9. + 7.j, 9. + 7.j], + [9. + 7.j, 9. + 7.j, 9. + 7.j], + [9. + 7.j, 9. + 7.j, 9. + 7.j]], + [[10. + 2.j, 10. + 2.j, 10. + 2.j], + [10. + 2.j, 10. + 2.j, 10. + 2.j], + [10. + 2.j, 10. + 2.j, 10. + 2.j]], + [[11. + 7.j, 11. + 7.j, 11. + 7.j], + [11. + 7.j, 11. + 7.j, 11. + 7.j], + [11. + 7.j, 11. + 7.j, 11. + 7.j]], + [[12. - 4.j, 12. - 4.j, 12. - 4.j], + [12. - 4.j, 12. - 4.j, 12. - 4.j], + [12. - 4.j, 12. - 4.j, 12. - 4.j]], + [[13. + 11.j, 13. + 11.j, 13. + 11.j], + [13. + 11.j, 13. + 11.j, 13. + 11.j], + [13. + 11.j, 13. + 11.j, 13. + 11.j]], + [[14. + 6.j, 14. + 6.j, 14. + 6.j], + [14. + 6.j, 14. + 6.j, 14. + 6.j], + [14. + 6.j, 14. + 6.j, 14. + 6.j]], + [[15. + 11.j, 15. + 11.j, 15. + 11.j], + [15. + 11.j, 15. + 11.j, 15. + 11.j], + [15. + 11.j, 15. + 11.j, 15. + 11.j]], + [[16. + 0.j, 16. + 0.j, 16. + 0.j], + [16. + 0.j, 16. + 0.j, 16. + 0.j], + [16. + 0.j, 16. + 0.j, 16. + 0.j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(expected_exit_wave, exit_wave_dev.get(), + err_msg="The exit_wave has not been updated as expected") + + def test_build_exit_aux_same_as_exit_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = np.zeros_like(exit_wave) + auxiliary_wave_dev = gpuarray.zeros_like(exit_wave_dev) + + ## Act + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel(self.stream) + + AWK.build_exit(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, exit_wave_dev) + nAWK.build_exit(auxiliary_wave, addr, object_array, probe, exit_wave) + + ## Assert + np.testing.assert_array_equal(auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + np.testing.assert_array_equal(exit_wave, exit_wave_dev.get(), + err_msg="The gpu exit_wave does not look the same as the numpy version") + + def test_build_aux_no_ex_noadd_REGRESSION(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr, object_array, probe, exit_wave = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = gpuarray.zeros_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, + fac=1.0, add=False) + + ## Assert + expected_auxiliary_wave = np.array([[[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]]], dtype=np.complex64) + np.testing.assert_array_equal(auxiliary_wave.get(), expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + def test_build_aux_no_ex_noadd_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave_dev = gpuarray.zeros_like(exit_wave_dev) + auxiliary_wave = np.zeros_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_aux_no_ex(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, + fac=1.0, add=False) + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + nAWK.allocate() + nAWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, fac=1.0, add=False) + + ## Assert + np.testing.assert_array_equal(auxiliary_wave_dev.get(), auxiliary_wave, + err_msg="The auxiliary_wave does not match numpy") + + def test_build_aux2_no_ex_noadd_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave_dev = gpuarray.zeros_like(exit_wave_dev) + auxiliary_wave = np.zeros_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_aux2_no_ex(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, + fac=1.0, add=False) + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + nAWK.allocate() + nAWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, fac=1.0, add=False) + + ## Assert + np.testing.assert_array_equal(auxiliary_wave_dev.get(), auxiliary_wave, + err_msg="The auxiliary_wave does not match numpy") + + + def test_build_aux_no_ex_add_REGRESSION(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr, object_array, probe, exit_wave = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = gpuarray.ones_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + fac = 2.0 + AWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, fac=fac, add=True) + + ## Assert + expected_auxiliary_wave = np.array([[[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]], + [[0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j], + [0. + 2.j, 0. + 2.j, 0. + 2.j]], + [[0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j], + [0. + 8.j, 0. + 8.j, 0. + 8.j]], + [[0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j], + [0. + 4.j, 0. + 4.j, 0. + 4.j]], + [[0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j], + [0. + 16.j, 0. + 16.j, 0. + 16.j]]], dtype=np.complex64) + expected_auxiliary_wave = fac*expected_auxiliary_wave + 1 + np.testing.assert_array_equal(auxiliary_wave.get(), expected_auxiliary_wave, + err_msg="The auxiliary_wave has not been updated as expected") + + def test_build_aux_no_ex_add_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave_dev = gpuarray.ones_like(exit_wave_dev) + auxiliary_wave = np.ones_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_aux_no_ex(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, + fac=2.0, add=True) + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + nAWK.allocate() + nAWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, fac=2.0, add=True) + + ## Assert + np.testing.assert_array_equal(auxiliary_wave_dev.get(), auxiliary_wave, + err_msg="The auxiliary_wave does not match numpy") + + def test_build_aux2_no_ex_add_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays() + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave_dev = gpuarray.ones_like(exit_wave_dev) + auxiliary_wave = np.ones_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_aux2_no_ex(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, + fac=2.0, add=True) + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + nAWK.allocate() + nAWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, fac=2.0, add=True) + + ## Assert + np.testing.assert_array_equal(auxiliary_wave_dev.get(), auxiliary_wave, + err_msg="The auxiliary_wave does not match numpy") + + + @unittest.skipIf(not perfrun, "performance test") + def test_build_aux_no_ex_performance(self): + addr, object_array, probe, exit_wave = self.prepare_arrays(performance=True) + addr, object_array, probe, exit_wave = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = gpuarray.zeros_like(exit_wave) + + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_aux_no_ex(auxiliary_wave, addr, object_array, probe, + fac=1.0, add=False) + + + def test_build_exit_alpha_tau_REGRESSION(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays(scan_points=1) + addr, object_array, probe, exit_wave = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = gpuarray.zeros_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_exit_alpha_tau(auxiliary_wave, addr, object_array, probe, exit_wave) + + # Assert + expected_auxiliary_wave = np.array( + [[[0. -2.j, 0. -2.j, 0. -2.j], + [0. -2.j, 0. -2.j, 0. -2.j], + [0. -2.j, 0. -2.j, 0. -2.j]], + + [[0. -8.j, 0. -8.j, 0. -8.j], + [0. -8.j, 0. -8.j, 0. -8.j], + [0. -8.j, 0. -8.j, 0. -8.j]], + + [[0. -4.j, 0. -4.j, 0. -4.j], + [0. -4.j, 0. -4.j, 0. -4.j], + [0. -4.j, 0. -4.j, 0. -4.j]], + + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]]], dtype=np.complex64) + np.testing.assert_allclose(auxiliary_wave.get(), expected_auxiliary_wave, rtol=1e-6, atol=1e-6, + err_msg="The auxiliary_wave has not been updated as expected") + + expected_exit_wave = np.array( + [[[1. -1.j, 1. -1.j, 1. -1.j], + [1. -1.j, 1. -1.j, 1. -1.j], + [1. -1.j, 1. -1.j, 1. -1.j]], + + [[2. -6.j, 2. -6.j, 2. -6.j], + [2. -6.j, 2. -6.j, 2. -6.j], + [2. -6.j, 2. -6.j, 2. -6.j]], + + [[3. -1.j, 3. -1.j, 3. -1.j], + [3. -1.j, 3. -1.j, 3. -1.j], + [3. -1.j, 3. -1.j, 3. -1.j]], + + [[4.-12.j, 4.-12.j, 4.-12.j], + [4.-12.j, 4.-12.j, 4.-12.j], + [4.-12.j, 4.-12.j, 4.-12.j]]], dtype=np.complex64) + np.testing.assert_allclose(exit_wave.get(), expected_exit_wave, rtol=1e-6, atol=1e-6, + err_msg="The exit_wave has not been updated as expected") + + def test_build_exit_alpha_tau_UNITY(self): + ## Arrange + addr, object_array, probe, exit_wave = self.prepare_arrays(scan_points=1) + addr_dev, object_array_dev, probe_dev, exit_wave_dev = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave_dev = gpuarray.ones_like(exit_wave_dev) + auxiliary_wave = np.ones_like(exit_wave) + + ## Act + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_exit_alpha_tau(auxiliary_wave_dev, addr_dev, object_array_dev, probe_dev, exit_wave_dev, alpha=0.8, tau=0.6) + from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + nAWK.allocate() + nAWK.build_exit_alpha_tau(auxiliary_wave, addr, object_array, probe, exit_wave, alpha=0.8, tau=0.6) + + ## Assert + np.testing.assert_allclose(auxiliary_wave_dev.get(), auxiliary_wave, rtol=1e-6, atol=1e-6, + err_msg="The auxiliary_wave does not match numpy") + ## Assert + np.testing.assert_allclose(exit_wave_dev.get(), exit_wave, rtol=1e-6, atol=1e-6, + err_msg="The exit_wave does not match numpy") + + @unittest.skipIf(not perfrun, "performance test") + def test_build_exit_alpha_tau_performance(self): + addr, object_array, probe, exit_wave = self.prepare_arrays(performance=True, scan_points=1) + addr, object_array, probe, exit_wave = self.copy_to_gpu(addr, object_array, probe, exit_wave) + auxiliary_wave = gpuarray.zeros_like(exit_wave) + + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_exit_alpha_tau(auxiliary_wave, addr, object_array, probe, exit_wave, alpha=0.8, tau=0.6) + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/cuda_pycuda_tests/derivatives_kernel_test.py b/test/accelerate_tests/cuda_pycuda_tests/derivatives_kernel_test.py new file mode 100644 index 000000000..1c4fe3b26 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/derivatives_kernel_test.py @@ -0,0 +1,334 @@ +''' + + +''' + +import unittest +import numpy as np +from . import perfrun, PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.array_utils import DerivativesKernel +from ptypy.utils.math_utils import delxf, delxb + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class DerivativesKernelTest(PyCudaTest): + + def test_delxf_1dim(self): + inp = np.array([0, 1, 2, 4, 8, 0, 6], dtype=np.float32) + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev) + + outp[:] = outp_dev.get() + + exp = np.array([1, 1, 2, 4, -8, 6, 0], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_1dim_inplace(self): + inp = np.array([0, 1, 2, 4, 8, 0, 6], dtype=np.float32) + inp_dev = gpuarray.to_gpu(inp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=inp_dev) + + outp = inp_dev.get() + + exp = np.array([1, 1, 2, 4, -8, 6, 0], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_2dim1(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=0) + + outp[:] = outp_dev.get() + + + exp = np.array([ + [1, -6, -1], + [0, 0, 0] + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_2dim2(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=1) + + outp[:] = outp_dev.get() + + exp = np.array([ + [2, 4, 0], + [-5, 9, 0] + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxb_1dim(self): + inp = np.array([0, 1, 2, 4, 8, 0, 6], dtype=np.float32) + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxb(inp_dev, out=outp_dev) + + outp[:] = outp_dev.get() + + exp = np.array([0, 1, 1, 2, 4, -8, 6], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxb_2dim1(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxb(inp_dev, out=outp_dev, axis=0) + + outp[:] = outp_dev.get() + + + exp = np.array([ + [0, 0, 0], + [1, -6, -1], + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxb_2dim2(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxb(inp_dev, out=outp_dev, axis=1) + + outp[:] = outp_dev.get() + + + exp = np.array([ + [0, 2, 4], + [0, -5, 9] + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_2dim2complex(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ],dtype=np.float32) + 1j * np.array([ + [0, 4, 12], + [2, -8, 10] + ],dtype=np.float32) + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=1) + + outp[:] = outp_dev.get() + + exp = np.array([ + [2, 4, 0], + [-5, 9, 0] + ], dtype=np.float32) + 1j * np.array([ + [4, 8, 0], + [-10, 18, 0] + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_3dim2(self): + inp = np.array([ + [ + [1, 2, 4,], + [7, 11, 16,], + ], + [ + [22, 29, 37,], + [46, 56, 67] + ] + ], dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=1) + + outp[:] = outp_dev.get() + + exp = np.array([ + [ + [6, 9, 12,], + [0, 0, 0,], + ], + [ + [24, 27, 30,], + [0, 0, 0], + ] + ], dtype=np.float32) + + np.testing.assert_array_equal(outp, exp) + + def test_delxf_3dim1_unity(self): + inp = np.ascontiguousarray(np.random.randn(33, 283, 142), dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=0) + outp[:] = outp_dev.get() + + exp = delxf(inp, axis=0) + np.testing.assert_array_almost_equal(outp, exp) + + def test_delxf_3dim2_unity1(self): + inp = np.array([ + [ [1], [2], [4]], + [ [8], [16], [32]] + ], dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=1) + outp[:] = outp_dev.get() + + exp = delxf(inp, axis=1) + + np.testing.assert_array_almost_equal(np.squeeze(outp), np.squeeze(exp)) + + def test_delxf_3dim2_unity2(self): + inp = np.array([ + [ [1, 2], [4, 7], [11,16] ], + [ [22,29], [37,46], [56,67]] + ], dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=1) + outp[:] = outp_dev.get() + + exp = delxf(inp, axis=1) + + np.testing.assert_array_almost_equal(np.squeeze(outp), np.squeeze(exp)) + + def test_delxf_3dim2_unity(self): + inp = np.ascontiguousarray(np.random.randn(33, 283, 142), dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=1) + outp[:] = outp_dev.get() + + exp = delxf(inp, axis=1) + np.testing.assert_array_almost_equal(outp, exp) + + def test_delxf_3dim3_unity(self): + inp = np.ascontiguousarray(np.random.randn(33, 283, 142), dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=2) + outp[:] = outp_dev.get() + + exp = delxf(inp, axis=2) + np.testing.assert_array_almost_equal(outp, exp) + + def test_delxb_3dim3_unity(self): + inp = np.ascontiguousarray(np.random.randn(33, 283, 142), dtype=np.float32) + + inp_dev = gpuarray.to_gpu(inp) + outp = np.zeros_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxb(inp_dev, out=outp_dev, axis=2) + outp[:] = outp_dev.get() + + exp = delxb(inp, axis=2) + np.testing.assert_array_almost_equal(outp, exp) + + @unittest.skipIf(not perfrun, "performance test") + def test_perf_3d_0(self): + shape = [500, 1024, 1024] + inp = np.ones(shape, dtype=np.complex64) + inp_dev = gpuarray.to_gpu(inp) + outp = np.ones_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=0) + outp[:] = outp_dev.get() + np.testing.assert_array_equal(outp, 0) + + @unittest.skipIf(not perfrun, "performance test") + def test_perf_3d_1(self): + shape = [500, 1024, 1024] + inp = np.ones(shape, dtype=np.complex64) + inp_dev = gpuarray.to_gpu(inp) + outp = np.ones_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=1) + outp[:] = outp_dev.get() + np.testing.assert_array_equal(outp, 0) + + @unittest.skipIf(not perfrun, "performance test") + def test_perf_3d_2(self): + shape = [500, 1024, 1024] + inp = np.ones(shape, dtype=np.complex64) + inp_dev = gpuarray.to_gpu(inp) + outp = np.ones_like(inp) + outp_dev = gpuarray.to_gpu(outp) + + DK = DerivativesKernel(inp.dtype, queue=self.stream) + DK.delxf(inp_dev, out=outp_dev, axis=2) + outp[:] = outp_dev.get() + np.testing.assert_array_equal(outp, 0) \ No newline at end of file diff --git a/test/accelerate_tests/cuda_pycuda_tests/engine_tests.py b/test/accelerate_tests/cuda_pycuda_tests/engine_tests.py new file mode 100644 index 000000000..71542d4f9 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/engine_tests.py @@ -0,0 +1,172 @@ +""" +Test for the ML engine. + +This file is part of the PTYPY package. + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" +import unittest + +from test import utils as tu +from ptypy import utils as u +import ptypy +ptypy.load_gpu_engines("cuda") +import tempfile +import shutil +import numpy as np + +class MLPycudaTest(unittest.TestCase): + + def setUp(self): + self.outpath = tempfile.mkdtemp(suffix="ML_pycuda_test") + + def tearDown(self): + shutil.rmtree(self.outpath) + + def check_engine_output(self, output, plotting=False, debug=False, scan="MF"): + key = "S%sG00" %scan + P_ML_serial, P_ML_pycuda = output + numiter = len(P_ML_serial.runtime["iter_info"]) + LL_ML_serial = np.array([P_ML_serial.runtime["iter_info"][i]["error"][1] for i in range(numiter)]) + LL_ML_pycuda = np.array([P_ML_pycuda.runtime["iter_info"][i]["error"][1] for i in range(numiter)]) + crop = 42 + OBJ_ML_serial, OBJ_ML_pycuda = P_ML_serial.obj.S[key].data[0,crop:-crop,crop:-crop], P_ML_pycuda.obj.S[key].data[0,crop:-crop,crop:-crop] + PRB_ML_serial, PRB_ML_pycuda = P_ML_serial.probe.S[key].data[0], P_ML_pycuda.probe.S[key].data[0] + MED_ML_serial = np.median(np.angle(OBJ_ML_serial)) + MED_ML_pycuda = np.median(np.angle(OBJ_ML_pycuda)) + eng_ML_serial = P_ML_serial.engines["engine00"] + eng_ML_pycuda = P_ML_pycuda.engines["engine00"] + if debug: + import matplotlib.pyplot as plt + plt.figure("ML serial debug") + plt.imshow(np.abs(eng_ML_serial.debug)) + plt.figure("ML pycuda debug") + plt.imshow(np.abs(eng_ML_pycuda.debug)) + plt.show() + + if plotting: + import matplotlib.pyplot as plt + plt.figure("Errors") + plt.plot(LL_ML_serial, label="ML_serial") + plt.plot(LL_ML_pycuda, label="ML_pycuda") + plt.legend() + plt.show() + plt.figure("Phase ML serial") + plt.imshow(np.angle(OBJ_ML_serial*np.exp(-1j*MED_ML_serial))) + plt.figure("Ampltitude ML serial") + plt.imshow(np.abs(OBJ_ML_serial)) + plt.figure("Phase ML pycuda") + plt.imshow(np.angle(OBJ_ML_pycuda*np.exp(-1j*MED_ML_pycuda))) + plt.figure("Amplitude ML pycuda") + plt.imshow(np.abs(OBJ_ML_pycuda)) + plt.figure("Phase difference") + plt.imshow(np.angle(OBJ_ML_pycuda) - np.angle(OBJ_ML_serial), vmin=-0.1, vmax=0.1) + plt.colorbar() + plt.figure("Amplitude difference") + plt.imshow(np.abs(OBJ_ML_pycuda) - np.abs(OBJ_ML_serial), vmin=-0.1, vmax=0.1) + plt.colorbar() + plt.show() + # np.testing.assert_allclose(eng_ML_serial.debug, eng_ML_pycuda.debug, atol=1e-7, rtol=1e-7, + # err_msg="The debug arrays are not matching as expected") + RMSE_ob = (np.mean(np.abs(OBJ_ML_pycuda - OBJ_ML_serial)**2)) + RMSE_pr = (np.mean(np.abs(PRB_ML_pycuda - PRB_ML_serial)**2)) + # RMSE_LL = (np.mean(np.abs(LL_ML_serial - LL_ML)**2)) + np.testing.assert_allclose(RMSE_ob, 0.0, atol=1e-2, + err_msg="The object arrays are not matching as expected") + np.testing.assert_allclose(RMSE_pr, 0.0, atol=1e-2, + err_msg="The object arrays are not matching as expected") + # np.testing.assert_allclose(RMSE_LL, 0.0, atol=1e-7, + # err_msg="The log-likelihood errors are not matching as expected") + + def test_ML_pycuda_base(self): + out = [] + for eng in ["ML_serial", "ML_pycuda"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_pycuda_regularizer(self): + out = [] + for eng in ["ML_serial", "ML_pycuda"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = True + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_pycuda_preconditioner(self): + out = [] + for eng in ["ML_serial", "ML_pycuda"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = True + engine_params.scale_probe_object = 1e-6 + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_pycuda_floating(self): + out = [] + for eng in ["ML_serial", "ML_pycuda"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = True + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_pycuda_smoothing_regularizer(self): + out = [] + for eng in ["ML_serial", "ML_pycuda"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 200 + engine_params.floating_intensities = False + engine_params.reg_del2 = False + engine_params.reg_del2_amplitude = 1. + engine_params.smooth_gradient = 20 + engine_params.smooth_gradient_decay = 1/10. + engine_params.scale_precond = False + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="critical")) + self.check_engine_output(out, plotting=False, debug=False) + + def test_ML_pycuda_all(self): + out = [] + for eng in ["ML_serial", "ML_pycuda"]: + engine_params = u.Param() + engine_params.name = eng + engine_params.numiter = 100 + engine_params.floating_intensities = False + engine_params.reg_del2 = True + engine_params.reg_del2_amplitude = 1. + engine_params.smooth_gradient = 20 + engine_params.smooth_gradient_decay = 1/10. + engine_params.scale_precond = True + engine_params.scale_probe_object = 1e-6 + out.append(tu.EngineTestRunner(engine_params, output_path=self.outpath, init_correct_probe=True, + scanmodel="BlockFull", autosave=False, verbose_level="info")) + self.check_engine_output(out, plotting=False, debug=False) + +if __name__ == "__main__": + unittest.main() diff --git a/test/accelerate_tests/cuda_pycuda_tests/engine_utils_test.py b/test/accelerate_tests/cuda_pycuda_tests/engine_utils_test.py new file mode 100644 index 000000000..5299146b2 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/engine_utils_test.py @@ -0,0 +1,54 @@ +''' + + +''' + +import unittest +import numpy as np +from . import perfrun, PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.engines.ML_pycuda import Regul_del2_pycuda + from ptypy.engines.ML import Regul_del2 + from pycuda.tools import make_default_context + from pycuda.driver import mem_alloc + + +class EngineUtilsTest(PyCudaTest): + + def test_regul_del2_grad_unity(self): + ## Arrange + A = (np.random.randn(40,40) + +1j*np.random.randn(40,40)).astype(np.complex64) + A_dev = gpuarray.to_gpu(A) + + ## Act + Reg = Regul_del2(0.1) + Reg_dev = Regul_del2_pycuda(0.1, allocator=mem_alloc) + grad_dev = Reg_dev.grad(A_dev).get() + grad = Reg.grad(A) + #grad_dev = grad + ## Assert + np.testing.assert_allclose(grad_dev, grad, rtol=1e-7) + np.testing.assert_allclose(Reg_dev.LL, Reg.LL, rtol=1e-7) + + + def test_regul_del2_coeff_unity(self): + ## Arrange + A = (np.random.randn(40,40) + +1j*np.random.randn(40,40)).astype(np.complex64) + B = (np.random.randn(40,40) + +1j*np.random.randn(40,40)).astype(np.complex64) + A_dev = gpuarray.to_gpu(A) + B_dev = gpuarray.to_gpu(B) + + ## Act + Reg = Regul_del2(0.1) + Reg_dev = Regul_del2_pycuda(0.1, allocator=mem_alloc) + d = Reg_dev.poly_line_coeffs(A_dev, B_dev) + c = Reg.poly_line_coeffs(A, B) + #grad_dev = grad + #d = c + ## Assert + np.testing.assert_allclose(c, d, rtol=1e-6) diff --git a/test/accelerate_tests/cuda_pycuda_tests/fft_scaling_test.py b/test/accelerate_tests/cuda_pycuda_tests/fft_scaling_test.py new file mode 100644 index 000000000..8449adae0 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/fft_scaling_test.py @@ -0,0 +1,265 @@ +''' + + +''' + +import unittest +import numpy as np +from . import PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.fft import FFT as ReiknaFFT + from ptypy.accelerate.cuda_pycuda.cufft import FFT_cuda, FFT_skcuda + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +def get_forward_cuFFT(f, stream, + pre_fft, post_fft, inplace, + symmetric, external=True): + if external: + return FFT_cuda(f, stream, pre_fft=pre_fft, post_fft=post_fft, inplace=inplace, + symmetric=symmetric, forward=True).ft + else: + return FFT_skcuda(f, stream, pre_fft=pre_fft, post_fft=post_fft, inplace=inplace, + symmetric=symmetric, forward=True).ft + +def get_reverse_cuFFT(f, stream, + pre_fft, post_fft, inplace, + symmetric, external=True): + if external: + return FFT_cuda(f, stream, pre_fft=pre_fft, post_fft=post_fft, inplace=inplace, + symmetric=symmetric, forward=False).ift + else: + return FFT_skcuda(f, stream, pre_fft=pre_fft, post_fft=post_fft, inplace=inplace, + symmetric=symmetric, forward=False).ift + +def get_forward_Reikna(f, stream, + pre_fft, post_fft, inplace, + symmetric, external=True): + return ReiknaFFT(f, stream, + pre_fft=pre_fft, post_fft=post_fft, inplace=inplace, symmetric=symmetric).ft + +def get_reverse_Reikna(f, stream, + pre_fft, post_fft, inplace, + symmetric, external=True): + return ReiknaFFT(f, stream, + pre_fft=pre_fft, post_fft=post_fft, inplace=inplace, symmetric=symmetric).ift + + + +class FftScalingTest(PyCudaTest): + + def get_input(self): + rows = cols = 32 + batches = 1 + f = np.ones(shape=(batches, rows, cols), dtype=COMPLEX_TYPE) + return f + + #### Trivial foward transform tests #### + + def fwd_test(self, symmetric, factory, preffact=None, postfact=None, external=True): + f = self.get_input() + f_d = gpuarray.to_gpu(f) + if preffact is not None: + pref = preffact * np.ones(shape=f.shape[-2:], dtype=np.complex64) + pref_d = gpuarray.to_gpu(pref) + else: + preffact=1.0 + pref_d = None + if postfact is not None: + post = postfact * np.ones(shape=f.shape[-2:], dtype=np.complex64) + post_d = gpuarray.to_gpu(post) + else: + postfact=1.0 + post_d = None + ft = factory(f, self.stream, + pre_fft=pref_d, post_fft=post_d, inplace=True, + symmetric=symmetric, external=external) + ft(f_d, f_d) + f_back = f_d.get() + elements = f.shape[-2] * f.shape[-1] + scale = 1.0 if not symmetric else 1.0 / np.sqrt(elements) + expected = elements * scale * preffact * postfact + self.assertAlmostEqual(f_back[0,0,0], expected) + np.testing.assert_array_almost_equal(f_back.flat[1:], 0) + + def test_fwd_noscale_reikna(self): + self.fwd_test(False, get_forward_Reikna) + + def test_fwd_noscale_cufft(self): + self.fwd_test(False, get_forward_cuFFT) + + def test_fwd_noscale_cufft_skcuda(self): + self.fwd_test(False, get_forward_cuFFT, external=False) + + def test_fwd_scale_reikna(self): + self.fwd_test(True, get_forward_Reikna) + + def test_fwd_scale_cufft(self): + self.fwd_test(True, get_forward_cuFFT) + + def test_fwd_scale_cufft_skcuda(self): + self.fwd_test(True, get_forward_cuFFT, external=False) + + def test_prefilt_fwd_noscale_reikna(self): + self.fwd_test(False, get_forward_Reikna, preffact=2.0) + + def test_prefilt_fwd_noscale_cufft(self): + self.fwd_test(False, get_forward_cuFFT, preffact=2.0) + + def test_prefilt_fwd_noscale_cufft_skcuda(self): + self.fwd_test(False, get_forward_cuFFT, preffact=2.0, external=False) + + def test_prefilt_fwd_scale_reikna(self): + self.fwd_test(True, get_forward_Reikna, preffact=2.0) + + def test_prefilt_fwd_scale_cufft(self): + self.fwd_test(True, get_forward_cuFFT, preffact=2.0) + + def test_prefilt_fwd_scale_cufft_skcuda(self): + self.fwd_test(True, get_forward_cuFFT, preffact=2.0, external=False) + + def test_postfilt_fwd_noscale_reikna(self): + self.fwd_test(False, get_forward_Reikna, postfact=2.0) + + def test_postfilt_fwd_noscale_cufft(self): + self.fwd_test(False, get_forward_cuFFT, postfact=2.0) + + def test_postfilt_fwd_noscale_cufft_skcuda(self): + self.fwd_test(False, get_forward_cuFFT, postfact=2.0, external=False) + + def test_postfilt_fwd_scale_reikna(self): + self.fwd_test(True, get_forward_Reikna, postfact=2.0) + + def test_postfilt_fwd_scale_cufft(self): + self.fwd_test(True, get_forward_cuFFT, postfact=2.0) + + def test_postfilt_fwd_scale_cufft_skcuda(self): + self.fwd_test(True, get_forward_cuFFT, postfact=2.0, external=False) + + def test_prepostfilt_fwd_noscale_reikna(self): + self.fwd_test(False, get_forward_Reikna, postfact=2.0, preffact=1.5) + + def test_prepostfilt_fwd_noscale_cufft(self): + self.fwd_test(False, get_forward_cuFFT, postfact=2.0, preffact=1.5) + + def test_prepostfilt_fwd_noscale_cufft_skcuda(self): + self.fwd_test(False, get_forward_cuFFT, postfact=2.0, preffact=1.5, external=False) + + def test_prepostfilt_fwd_scale_reikna(self): + self.fwd_test(True, get_forward_Reikna, postfact=2.0, preffact=1.5) + + def test_prepostfilt_fwd_scale_cufft(self): + self.fwd_test(True, get_forward_cuFFT, postfact=2.0, preffact=1.5) + + def test_prepostfilt_fwd_scale_cufft_skcuda(self): + self.fwd_test(True, get_forward_cuFFT, postfact=2.0, preffact=1.5, external=False) + + + ############# Trivial inverse transform tests ######### + + def rev_test(self, symmetric, factory, preffact=None, postfact=None, external=True): + f = self.get_input() + f_d = gpuarray.to_gpu(f) + if preffact is not None: + pref = preffact * np.ones(shape=f.shape[-2:], dtype=np.complex64) + pref_d = gpuarray.to_gpu(pref) + else: + preffact=1.0 + pref_d = None + if postfact is not None: + post = postfact * np.ones(shape=f.shape[-2:], dtype=np.complex64) + post_d = gpuarray.to_gpu(post) + else: + postfact=1.0 + post_d = None + ift = factory(f, self.stream, + pre_fft=pref_d, post_fft=post_d, inplace=True, symmetric=symmetric, + external=external) + ift(f_d, f_d) + f_back = f_d.get() + elements = f.shape[-2] * f.shape[-1] + scale = 1.0 if not symmetric else np.sqrt(elements) + expected = scale * preffact * postfact + self.assertAlmostEqual(f_back[0,0,0], expected) + np.testing.assert_array_almost_equal(f_back.flat[1:], 0) + + + def test_rev_noscale_reikna(self): + self.rev_test(False, get_reverse_Reikna) + + def test_rev_noscale_cufft(self): + self.rev_test(False, get_reverse_cuFFT) + + def test_rev_noscale_cufft_skcuda(self): + self.rev_test(False, get_reverse_cuFFT, external=False) + + def test_rev_scale_reikna(self): + self.rev_test(True, get_reverse_Reikna) + + def test_rev_scale_cufft(self): + self.rev_test(True, get_reverse_cuFFT) + + def test_rev_scale_cufft_skcuda(self): + self.rev_test(True, get_reverse_cuFFT, external=False) + + def test_prefilt_rev_noscale_reikna(self): + self.rev_test(False, get_reverse_Reikna, preffact=1.5) + + def test_prefilt_rev_noscale_cufft(self): + self.rev_test(False, get_reverse_cuFFT, preffact=1.5) + + def test_prefilt_rev_noscale_cufft_skcuda(self): + self.rev_test(False, get_reverse_cuFFT, preffact=1.5, external=False) + + def test_prefilt_rev_scale_reikna(self): + self.rev_test(True, get_reverse_Reikna, preffact=1.5) + + def test_prefilt_rev_scale_cufft(self): + self.rev_test(True, get_reverse_cuFFT, preffact=1.5) + + def test_prefilt_rev_scale_cufft_skcuda(self): + self.rev_test(True, get_reverse_cuFFT, preffact=1.5, external=False) + + def test_postfilt_rev_noscale_reikna(self): + self.rev_test(False, get_reverse_Reikna, postfact=1.5) + + def test_postfilt_rev_noscale_cufft(self): + self.rev_test(False, get_reverse_cuFFT, postfact=1.5) + + def test_postfilt_rev_noscale_cufft_skcuda(self): + self.rev_test(False, get_reverse_cuFFT, postfact=1.5, external=False) + + def test_postfilt_rev_scale_reikna(self): + self.rev_test(True, get_reverse_Reikna, postfact=1.5) + + def test_postfilt_rev_scale_cufft(self): + self.rev_test(True, get_reverse_cuFFT, postfact=1.5) + + def test_postfilt_rev_scale_cufft_skcuda(self): + self.rev_test(True, get_reverse_cuFFT, postfact=1.5, external=False) + + def test_prepostfilt_rev_noscale_reikna(self): + self.rev_test(False, get_reverse_Reikna, postfact=1.5, preffact=2.0) + + def test_prepostfilt_rev_noscale_cufft(self): + self.rev_test(False, get_reverse_cuFFT, postfact=1.5, preffact=2.0) + + def test_prepostfilt_rev_noscale_cufft_skcuda(self): + self.rev_test(False, get_reverse_cuFFT, postfact=1.5, preffact=2.0, external=False) + + def test_prepostfilt_rev_scale_reikna(self): + self.rev_test(True, get_reverse_Reikna, postfact=1.5, preffact=2.0) + + def test_prepostfilt_rev_scale_cufft(self): + self.rev_test(True, get_reverse_cuFFT, postfact=1.5, preffact=2.0) + + def test_prepostfilt_rev_scale_cufft_skcuda(self): + self.rev_test(True, get_reverse_cuFFT, postfact=1.5, preffact=2.0, external=False) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/cuda_pycuda_tests/fft_setstream_test.py b/test/accelerate_tests/cuda_pycuda_tests/fft_setstream_test.py new file mode 100644 index 000000000..1220702b7 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/fft_setstream_test.py @@ -0,0 +1,98 @@ +import unittest +import numpy as np +from . import PyCudaTest, have_pycuda +import time + +if have_pycuda(): + import pycuda.driver as cuda + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.fft import FFT as ReiknaFFT + from ptypy.accelerate.cuda_pycuda.cufft import FFT_cuda as cuFFT + from ptypy.accelerate.cuda_pycuda.cufft import FFT_skcuda as SkcudaCuFFT + + COMPLEX_TYPE = np.complex64 + FLOAT_TYPE = np.float32 + INT_TYPE = np.int32 + +class FftSetStreamTest(PyCudaTest): + + def helper(self, FFT): + f = np.ones(shape=(200, 128, 128), dtype=COMPLEX_TYPE) + t1 = time.time() + FW = FFT(f, self.stream, pre_fft=None, post_fft=None, inplace=True, + symmetric=True) + self.stream.synchronize() + t2 = time.time() + dur1 = t2 - t1 + f_dev = gpuarray.to_gpu(f) + + # measure with events to make sure that something actually + # happened in the right stream + ev1 = cuda.Event() + ev2 = cuda.Event() + rt1 = time.time() + ev1.record(self.stream) + FW.ft(f_dev, f_dev) + ev2.record(self.stream) + ev1.synchronize() + ev2.synchronize() + self.stream.synchronize() + rt2 = time.time() + cput = rt2-rt1 + gput = ev1.time_till(ev2)*1e-3 + rel = 1-gput/cput + + print('Origial: CPU={}, GPU={}, reldiff={}'.format(cput, gput, rel)) + + self.assertEqual(self.stream, FW.queue) + self.assertLess(rel, 0.3) # max 30% diff + + stream2 = cuda.Stream() + + measure = False # measure time to set the stream + if measure: + avg = 100 + else: + avg = 1 + t1 = time.time() + for i in range(avg): + FW.queue = stream2 + stream2.synchronize() + t2 = time.time() + dur2 = (t2 - t1)/avg + + + ev1 = cuda.Event() + ev2 = cuda.Event() + rt1 = time.time() + ev1.record(stream2) + FW.ft(f_dev, f_dev) + ev2.record(stream2) + ev1.synchronize() + ev2.synchronize() + stream2.synchronize() + self.stream.synchronize() + rt2 = time.time() + cput = rt2-rt1 + gput = ev1.time_till(ev2)*1e-3 + rel = 1 - gput/cput + + print('New: CPU={}, GPU={}, reldiff={}'.format(cput, gput, rel)) + + self.assertEqual(stream2, FW.queue) + self.assertLess(rel, 0.3) # max 30% diff + + if measure: + print('initial: {}, set_stream: {}'.format(dur1, dur2)) + assert False + + + + def test_set_stream_a_reikna(self): + self.helper(ReiknaFFT) + + def test_set_stream_b_cufft(self): + self.helper(cuFFT) + + def test_set_stream_c_skcuda_cufft(self): + self.helper(SkcudaCuFFT) diff --git a/test/accelerate_tests/cuda_pycuda_tests/fft_tests/__init__.py b/test/accelerate_tests/cuda_pycuda_tests/fft_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/accelerate_tests/cuda_pycuda_tests/fft_tests/cufft_init_test.py b/test/accelerate_tests/cuda_pycuda_tests/fft_tests/cufft_init_test.py new file mode 100644 index 000000000..c1894cc31 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/fft_tests/cufft_init_test.py @@ -0,0 +1,28 @@ + +import unittest +from test.accelerate_tests.cuda_pycuda_tests import PyCudaTest, have_pycuda + +if have_pycuda(): + from filtered_cufft import FilteredFFT + +class CuFFTInitTest(PyCudaTest): + + def test_import_fft(self): + ft = FilteredFFT(2, 32, 32, False, True, 0, 0, 0) + + + def test_import_fft_different_shape(self): + ft = FilteredFFT(2, 128, 128, False, True, 0, 0, 0) + + + @unittest.expectedFailure + def test_import_fft_not_square(self): + ft = FilteredFFT(2, 32, 64, False, True, 0, 0, 0) + + @unittest.expectedFailure + def test_import_fft_not_pow2(self): + ft = FilteredFFT(2, 40, 40, False, True, 0, 0, 0) + + +if __name__=="__main__": + unittest.main() diff --git a/test/accelerate_tests/cuda_pycuda_tests/fft_tests/fft_accuracy_test.py b/test/accelerate_tests/cuda_pycuda_tests/fft_tests/fft_accuracy_test.py new file mode 100644 index 000000000..7c30c3221 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/fft_tests/fft_accuracy_test.py @@ -0,0 +1,48 @@ +''' +''' + +import unittest +import numpy as np +import scipy.fft as fft +from test.accelerate_tests.cuda_pycuda_tests import PyCudaTest, have_pycuda + + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.fft import FFT as ReiknaFFT + from ptypy.accelerate.cuda_pycuda.cufft import FFT_cuda as cuFFT + +class FftAccurracyTest(PyCudaTest): + + def gen_input(self): + rows = cols = 32 + batches = 1 + f = np.random.randn(batches, rows, cols) + 1j * np.random.randn(batches,rows, cols) + f = np.ascontiguousarray(f.astype(np.complex64)) + return f + + def test_random_cufft_fwd(self): + f = self.gen_input() + cuft = cuFFT(f, self.stream, inplace=True, pre_fft=None, post_fft=None, symmetric=None, forward=True).ft + reikft = ReiknaFFT(f, self.stream, inplace=True, pre_fft=None, post_fft=None, symmetric=False).ft + for i in range(10): + f = self.gen_input() + y = fft.fft2(f) + + x_d = gpuarray.to_gpu(f) + cuft(x_d, x_d) + y_cufft = x_d.get().reshape(y.shape) + + x_d = gpuarray.to_gpu(f) + reikft(x_d, x_d) + y_reikna = x_d.get().reshape(y.shape) + + # cufft_diff = np.max(np.abs(y_cufft - y)) + # reikna_diff = np.max(np.abs(y_reikna-y)) + # cufft_rdiff = np.max(np.abs(y_cufft - y) / np.abs(y)) + # reikna_rdiff = np.max(np.abs(y_reikna - y) / np.abs(y)) + # print('{}: {}\t{}\t{}\t{}'.format(i, cufft_diff, reikna_diff, cufft_rdiff, reikna_rdiff)) + + # Note: check if this tolerance and test case is ok + np.testing.assert_allclose(y, y_cufft, atol=1e-6, rtol=5e-5, err_msg='cuFFT error at index {}'.format(i)) + np.testing.assert_allclose(y, y_reikna, atol=1e-6, rtol=5e-5, err_msg='reikna FFT error at index {}'.format(i)) diff --git a/test/accelerate_tests/cuda_pycuda_tests/fourier_update_kernel_test.py b/test/accelerate_tests/cuda_pycuda_tests/fourier_update_kernel_test.py new file mode 100644 index 000000000..3d7cb5fa6 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/fourier_update_kernel_test.py @@ -0,0 +1,685 @@ +''' + + +''' + +import unittest +import numpy as np +from . import PyCudaTest, have_pycuda + + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import FourierUpdateKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class FourierUpdateKernelTest(PyCudaTest): + + + def test_fmag_all_update_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE)# the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + # print("address book is:") + # print(repr(addr)) + + ''' + test + ''' + mask_sum = mask.sum(-1).sum(-1) + + err_fmag = np.zeros(N, dtype=FLOAT_TYPE) + from ptypy.accelerate.base.kernels import FourierUpdateKernel as npFourierUpdateKernel + pbound_set = 0.9 + nFUK = npFourierUpdateKernel(f, nmodes=total_number_modes) + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + + nFUK.allocate() + FUK.allocate() + + nFUK.fourier_error(f, addr, fmag, mask, mask_sum) + nFUK.error_reduce(addr, err_fmag) + # print(np.sqrt(pbound_set/err_fmag)) + f_d = gpuarray.to_gpu(f) + fmag_d = gpuarray.to_gpu(fmag) + mask_d = gpuarray.to_gpu(mask) + err_fmag_d = gpuarray.to_gpu(err_fmag) + addr_d = gpuarray.to_gpu(addr) + + # now set the state for both. + + FUK.gpu.fdev = gpuarray.to_gpu(nFUK.npy.fdev) + FUK.gpu.ferr = gpuarray.to_gpu(nFUK.npy.ferr) + + FUK.fmag_all_update(f_d, addr_d, fmag_d, mask_d, err_fmag_d, pbound=pbound_set) + + + nFUK.fmag_all_update(f, addr, fmag, mask, err_fmag, pbound=pbound_set) + expected_f = f + measured_f = f_d.get() + np.testing.assert_allclose(expected_f, measured_f, rtol=1e-6, err_msg="Numpy f " + "is \n%s, \nbut gpu f is \n %s, \n mask is:\n %s \n" % (repr(expected_f), + repr(measured_f), + repr(mask))) + + def test_fmag_update_nopbound_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE)# the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + # print("address book is:") + # print(repr(addr)) + + ''' + test + ''' + mask_sum = mask.sum(-1).sum(-1) + + err_fmag = np.zeros(N, dtype=FLOAT_TYPE) + from ptypy.accelerate.base.kernels import FourierUpdateKernel as npFourierUpdateKernel + nFUK = npFourierUpdateKernel(f, nmodes=total_number_modes) + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + + nFUK.allocate() + FUK.allocate() + + nFUK.fourier_error(f, addr, fmag, mask, mask_sum) + nFUK.error_reduce(addr, err_fmag) + # print(np.sqrt(pbound_set/err_fmag)) + f_d = gpuarray.to_gpu(f) + fmag_d = gpuarray.to_gpu(fmag) + mask_d = gpuarray.to_gpu(mask) + addr_d = gpuarray.to_gpu(addr) + + # now set the state for both. + + FUK.gpu.fdev = gpuarray.to_gpu(nFUK.npy.fdev) + FUK.gpu.ferr = gpuarray.to_gpu(nFUK.npy.ferr) + + FUK.fmag_update_nopbound(f_d, addr_d, fmag_d, mask_d) + nFUK.fmag_update_nopbound(f, addr, fmag, mask) + + expected_f = f + measured_f = f_d.get() + np.testing.assert_allclose(measured_f, expected_f, rtol=1e-6, err_msg="Numpy f " + "is \n%s, \nbut gpu f is \n %s, \n mask is:\n %s \n" % (repr(expected_f), + repr(measured_f), + repr(mask))) + + + def test_fourier_error_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number of object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), + dtype=FLOAT_TYPE) # the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + mask_sum = mask.sum(-1).sum(-1) + + from ptypy.accelerate.base.kernels import FourierUpdateKernel as npFourierUpdateKernel + f_d = gpuarray.to_gpu(f) + fmag_d = gpuarray.to_gpu(fmag) + mask_d = gpuarray.to_gpu(mask) + addr_d = gpuarray.to_gpu(addr) + mask_sum_d = gpuarray.to_gpu(mask_sum) + + nFUK = npFourierUpdateKernel(f, nmodes=total_number_modes) + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + + nFUK.allocate() + FUK.allocate() + + nFUK.fourier_error(f, addr, fmag, mask, mask_sum) + FUK.fourier_error(f_d, addr_d, fmag_d, mask_d, mask_sum_d) + + expected_fdev = nFUK.npy.fdev + measured_fdev = FUK.gpu.fdev.get() + np.testing.assert_allclose(expected_fdev, measured_fdev, rtol=1e-6, err_msg="Numpy fdev " + "is \n%s, \nbut gpu fdev is \n %s, \n " % ( + repr(expected_fdev), + repr(measured_fdev))) + + expected_ferr = nFUK.npy.ferr + measured_ferr = FUK.gpu.ferr.get() + + np.testing.assert_array_equal(expected_ferr, measured_ferr, err_msg="Numpy ferr" + "is \n%s, \nbut gpu ferr is \n %s, \n " % ( + repr(expected_ferr), + repr(measured_ferr))) + def test_fourier_deviation_UNITY(self): + ''' + setup - using the fourier_error as reference, so we need mask, etc. + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number of object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), + dtype=FLOAT_TYPE) # the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + mask_sum = mask.sum(-1).sum(-1) + + from ptypy.accelerate.base.kernels import FourierUpdateKernel as npFourierUpdateKernel + f_d = gpuarray.to_gpu(f) + fmag_d = gpuarray.to_gpu(fmag) + addr_d = gpuarray.to_gpu(addr) + + nFUK = npFourierUpdateKernel(f, nmodes=total_number_modes) + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + + nFUK.allocate() + FUK.allocate() + + nFUK.fourier_deviation(f, addr, fmag) + FUK.fourier_deviation(f_d, addr_d, fmag_d) + + expected_fdev = nFUK.npy.fdev + measured_fdev = FUK.gpu.fdev.get() + np.testing.assert_allclose(measured_fdev, expected_fdev, rtol=1e-6, err_msg="Numpy fdev " + "is \n%s, \nbut gpu fdev is \n %s, \n " % ( + repr(expected_fdev), + repr(measured_fdev))) + + + + def test_error_reduce_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number og object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape).item()).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), + dtype=FLOAT_TYPE) # the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + # print("address book is:") + # print(repr(addr)) + + ''' + test + ''' + err_fmag = np.zeros(N, dtype=FLOAT_TYPE) + mask_sum = mask.sum(-1).sum(-1) + + from ptypy.accelerate.base.kernels import FourierUpdateKernel as npFourierUpdateKernel + f_d = gpuarray.to_gpu(f) + fmag_d = gpuarray.to_gpu(fmag) + mask_d = gpuarray.to_gpu(mask) + addr_d = gpuarray.to_gpu(addr) + err_fmag_d = gpuarray.to_gpu(err_fmag) + mask_sum_d = gpuarray.to_gpu(mask_sum) + pbound_set = 0.9 + nFUK = npFourierUpdateKernel(f, nmodes=total_number_modes) + FUK = FourierUpdateKernel(f, nmodes=total_number_modes, queue_thread=self.stream) + + nFUK.allocate() + FUK.allocate() + + nFUK.fourier_error(f, addr, fmag, mask, mask_sum) + nFUK.error_reduce(addr, err_fmag) + + + FUK.fourier_error(f_d, addr_d, fmag_d, mask_d, mask_sum_d) + FUK.error_reduce(addr_d, err_fmag_d) + + expected_err_fmag = err_fmag + measured_err_fmag = err_fmag_d.get() + + np.testing.assert_allclose(expected_err_fmag, measured_err_fmag, rtol=1.15207385e-07, + err_msg="Numpy err_fmag" + "is \n%s, \nbut gpu err_fmag is \n %s, \n " % ( + repr(expected_err_fmag), + repr(measured_err_fmag))) + + def test_error_reduce(self): + # array from the previous test + ferr = np.array([[[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [7.54033208e-01, 3.04839879e-01, 5.56465909e-02, 6.45330548e-03, 1.57260016e-01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [5.26210022e+00, 6.81290817e+00, 8.56371498e+00, 1.05145216e+01, 1.26653280e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[1.61048353e+00, 2.15810299e+00, 2.78572226e+00, 3.49334168e+00, 4.28096104e+00], + [5.14858055e+00, 6.09619951e+00, 7.12381887e+00, 8.23143768e+00, 9.41905785e+00], + [1.06866770e+01, 1.20342960e+01, 1.34619150e+01, 1.49695349e+01, 1.65571537e+01], + [1.82247734e+01, 1.99723930e+01, 2.18000126e+01, 2.37076321e+01, 2.56952515e+01], + [2.77628708e+01, 2.99104881e+01, 3.21381073e+01, 3.44457283e+01, 3.68333473e+01]], + + [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [6.31699409e+01, 6.82966690e+01, 7.36233978e+01, 7.91501160e+01, 8.48768463e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.23437180e+02, 1.30563919e+02, 1.37890640e+02, 1.45417374e+02, 1.53144089e+02], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]], + + [[4.58764343e+01, 4.86257210e+01, 5.14550095e+01, 5.43642960e+01, 5.73535805e+01], + [6.04228668e+01, 6.35721550e+01, 6.68014374e+01, 7.01107254e+01, 7.35000076e+01], + [7.69692993e+01, 8.05185852e+01, 8.41478729e+01, 8.78571548e+01, 9.16464386e+01], + [9.55157242e+01, 9.94650116e+01, 1.03494293e+02, 1.07603584e+02, 1.11792870e+02], + [1.16062157e+02, 1.20411446e+02, 1.24840721e+02, 1.29350006e+02, 1.33939301e+02]]], + dtype=FLOAT_TYPE) + # print(ferr.shape) + scan_pts = 2 # one dimensional scan point number + N = scan_pts ** 2 + + addr = np.zeros((N, 1, 5, 3)) + aux = np.zeros((4, 5, 5)) + FUK = FourierUpdateKernel(aux, nmodes=1) + err_mag = np.zeros(N, dtype=FLOAT_TYPE) + err_mag_d = gpuarray.to_gpu(err_mag) + FUK.gpu.ferr = gpuarray.to_gpu(ferr) + addr_d = gpuarray.to_gpu(addr) + + FUK.error_reduce(addr_d, err_mag_d) + + # print(repr(ferr)) + measured_err_mag = err_mag_d.get() + + # print(repr(measured_err_mag)) + + expected_err_mag = np.array([45.096806, 388.54788, 1059.5702, 2155.6968], dtype=FLOAT_TYPE) + + np.testing.assert_array_equal(expected_err_mag, measured_err_mag, err_msg="The fourier_update_kernel.error_reduce" + "is not behaving as expected.") + + + def log_likelihood_UNITY_tester(self, use_version2=False): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number of object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask = np.empty(shape=(N, B, C), + dtype=FLOAT_TYPE) # the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + mask_sum = mask.sum(-1).sum(-1) + LLerr = np.zeros_like(mask_sum, dtype=np.float32) + f_d = gpuarray.to_gpu(f) + fmag_d = gpuarray.to_gpu(fmag) + mask_d = gpuarray.to_gpu(mask) + addr_d = gpuarray.to_gpu(addr) + LLerr_d = gpuarray.to_gpu(LLerr) + + from ptypy.accelerate.base.kernels import FourierUpdateKernel as npFourierUpdateKernel + nFUK = npFourierUpdateKernel(f, nmodes=total_number_modes) + nFUK.allocate() + nFUK.log_likelihood(f, addr, fmag, mask, LLerr) + + FUK = FourierUpdateKernel(f, nmodes=total_number_modes) + FUK.allocate() + if use_version2: + FUK.log_likelihood2(f_d, addr_d, fmag_d, mask_d, LLerr_d) + else: + FUK.log_likelihood(f_d, addr_d, fmag_d, mask_d, LLerr_d) + + expected_err_phot = LLerr + measured_err_phot = LLerr_d.get() + + np.testing.assert_allclose(expected_err_phot, measured_err_phot, err_msg="Numpy log-likelihood error " + "is \n%s, \nbut gpu log-likelihood error is \n%s, \n " % ( + repr(expected_err_phot), + repr(measured_err_phot)), rtol=1e-5) + def test_log_likelihood_UNITY(self): + self.log_likelihood_UNITY_tester(False) + + def test_log_likelihood2_UNITY(self): + self.log_likelihood_UNITY_tester(True) + + def test_exit_error_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number of object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + aux = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + aux[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + from ptypy.accelerate.base.kernels import FourierUpdateKernel as npFourierUpdateKernel + aux_d = gpuarray.to_gpu(aux) + addr_d = gpuarray.to_gpu(addr) + + nFUK = npFourierUpdateKernel(aux, nmodes=total_number_modes) + FUK = FourierUpdateKernel(aux, nmodes=total_number_modes) + + nFUK.allocate() + FUK.allocate() + + nFUK.exit_error(aux, addr, ) + FUK.exit_error(aux_d, addr_d) + + expected_ferr = nFUK.npy.ferr + measured_ferr = FUK.gpu.ferr.get() + + np.testing.assert_allclose(expected_ferr, measured_ferr, err_msg="Numpy ferr" + "is \n%s, \nbut gpu ferr is \n %s, \n " % ( + repr(expected_ferr), + repr(measured_ferr)), rtol=1e-7) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/cuda_pycuda_tests/gpudata_test.py b/test/accelerate_tests/cuda_pycuda_tests/gpudata_test.py new file mode 100644 index 000000000..d3b4c2fe7 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/gpudata_test.py @@ -0,0 +1,258 @@ +''' +''' + +import unittest +import numpy as np +from . import PyCudaTest, have_pycuda + +if have_pycuda(): + import pycuda.driver as cuda + from pycuda.compiler import SourceModule + from ptypy.accelerate.cuda_pycuda.mem_utils import GpuData, GpuDataManager + +class GpuDataTest(PyCudaTest): + + def test_to_gpu_new(self): + # arrange + cpu = 2. * np.ones((5,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + + # act + gpu = gdata.to_gpu(cpu, '1', self.stream) + self.stream.synchronize() + + # assert + np.testing.assert_array_equal(cpu, gpu.get()) + + def test_to_gpu_sameid(self): + # arrange + cpu = 2. * np.ones((5,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + + # act + gpu1 = gdata.to_gpu(cpu, '1', self.stream) + cpu *= 2. + gpu2 = gdata.to_gpu(cpu, '1', self.stream) + self.stream.synchronize() + + # assert + np.testing.assert_array_equal(gpu1.get(), gpu2.get()) + + def test_to_gpu_new_syncback(self): + # arrange + cpu = 2. * np.ones((5,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=True) + + # act + gpu1 = gdata.to_gpu(cpu, '1', self.stream) + gpu1.fill(np.float32(3.), self.stream) + cpu2 = 2. * cpu + gpu2 = gdata.to_gpu(cpu2, '2', self.stream) + self.stream.synchronize() + + # assert + np.testing.assert_array_equal(cpu, 3.) + np.testing.assert_array_equal(gpu2.get(), cpu2) + + def test_to_gpu_new_nosyncback(self): + # arrange + cpu = 2. * np.ones((5,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + + # act + gpu1 = gdata.to_gpu(cpu, '1', self.stream) + gpu1.fill(np.float32(3.), self.stream) + cpu2 = 2. * cpu + gpu2 = gdata.to_gpu(cpu2, '2', self.stream) + self.stream.synchronize() + + # assert + np.testing.assert_array_equal(cpu, 2.) + np.testing.assert_array_equal(gpu2.get(), cpu2) + + def test_from_gpu(self): + # arrange + cpu = 2. * np.ones((5,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + + # act + gpu1 = gdata.to_gpu(cpu, '1', self.stream) + gpu1.fill(np.float32(3.), self.stream) + gdata.from_gpu(self.stream) + self.stream.synchronize() + + def test_data_variable_size(self): + # arrange + cpu = np.ones((2,5), dtype=np.float32) + cpu2 = 2. * np.ones((1,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + + # act + gpu = gdata.to_gpu(cpu, '1', self.stream) + gpu2 = gdata.to_gpu(cpu2, '2', self.stream) + self.stream.synchronize() + + # assert + np.testing.assert_array_equal(gpu2.get(), cpu2) + self.assertEqual(cpu2.nbytes, gpu2.nbytes) + np.testing.assert_array_equal(gpu.get(), np.array([ + [2, 2, 2, 2, 2], + [1, 1, 1, 1, 1] + ], dtype=np.float32)) + + def test_data_variable_size_raise(self): + # arrange + cpu = np.ones((1,5), dtype=np.float32) + cpu2 = np.ones((2,4), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + + # act/assert + with self.assertRaises(Exception): + gdata.to_gpu(cpu2, '1', self.stream) + + def test_data_resize_raise(self): + # arrange + cpu = np.ones((5,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + gpu = gdata.to_gpu(cpu, '1', self.stream) + cpu2 = np.ones((10,5), dtype=np.float32) + + # act + gdata.resize(cpu2.nbytes) + gpu2 = gdata.to_gpu(cpu2, '1', self.stream) + + # assert + self.assertEqual(gdata.gpuId, '1') + self.assertEqual(gdata.nbytes, cpu2.nbytes) + self.assertEqual(gpu2.size, cpu2.size) + self.assertGreaterEqual(gdata.nbytes_buffer, cpu2.nbytes) + + def test_data_resize_shrink(self): + # arrange + cpu = np.ones((5,5), dtype=np.float32) + gdata = GpuData(cpu.nbytes, syncback=False) + gpu = gdata.to_gpu(cpu, '1', self.stream) + cpu2 = np.ones((4,6), dtype=np.float32) + + # act + gdata.resize(cpu2.nbytes) + gpu2 = gdata.to_gpu(cpu2, '1', self.stream) + + # assert + self.assertEqual(gdata.gpuId, '1') + self.assertEqual(gdata.nbytes, cpu2.nbytes) + self.assertEqual(gpu2.size, cpu2.size) + self.assertGreaterEqual(gdata.nbytes_buffer, cpu2.nbytes) + + def test_datamanager_memory(self): + # arrange / act + gdm = GpuDataManager(128, 4) + gdm.reset(124, 3) + + # assert + self.assertEqual(gdm.memory, 3*128) + self.assertEqual(gdm.nbytes, 124) + + def test_datamanager_free(self): + # arrange + gdm = GpuDataManager(128, 2) + + # act + gdm.free() + + # assert + self.assertEqual(gdm.memory, 0) + + def test_datamanager_newids(self): + # arrange + cpu1 = 2. * np.ones((5,5), dtype=np.float32) + cpu2 = 2. * cpu1 # 4 + cpu3 = 2. * cpu2 # 8 + cpu4 = 2. * cpu3 # 16 + gdm = GpuDataManager(cpu1.nbytes, 4, syncback=False) + + # act + gpu1 = gdm.to_gpu(cpu1, '1', self.stream)[1] + gpu2 = gdm.to_gpu(cpu2, '2', self.stream)[1] + gpu11 = gdm.to_gpu(-1.*cpu1, '1', self.stream)[1] + gpu21 = gdm.to_gpu(-1.*cpu4, '2', self.stream)[1] + gpu3 = gdm.to_gpu(cpu3, '3', self.stream)[1] + gpu31 = gdm.to_gpu(-1.*cpu1, '3', self.stream)[1] + gpu4 = gdm.to_gpu(cpu4, '4', self.stream)[1] + gpu41 = gdm.to_gpu(-1.*cpu1, '4', self.stream)[1] + self.stream.synchronize() + + # assert + np.testing.assert_array_equal(cpu1, gpu1.get()) + np.testing.assert_array_equal(cpu1, gpu11.get()) + np.testing.assert_array_equal(cpu1, 2.) + np.testing.assert_array_equal(cpu2, gpu2.get()) + np.testing.assert_array_equal(cpu2, gpu21.get()) + np.testing.assert_array_equal(cpu2, 4.) + np.testing.assert_array_equal(cpu3, gpu3.get()) + np.testing.assert_array_equal(cpu3, gpu31.get()) + np.testing.assert_array_equal(cpu3, 8.) + np.testing.assert_array_equal(cpu4, gpu4.get()) + np.testing.assert_array_equal(cpu4, gpu41.get()) + np.testing.assert_array_equal(cpu4, 16.) + + def test_datamanager_syncback(self): + # arrange + cpu1 = 2. * np.ones((5,5), dtype=np.float32) + cpu2 = 2. * cpu1 # 4 + cpu3 = 2. * cpu2 # 8 + cpu4 = 2. * cpu3 # 16 + gdm = GpuDataManager(cpu1.nbytes, 2, syncback=True) + + # act + gpu1 = gdm.to_gpu(cpu1, '1', self.stream)[1] + gpu2 = gdm.to_gpu(cpu2, '2', self.stream)[1] + gpu1.fill(np.float32(3.), self.stream) + gpu2.fill(np.float32(5.), self.stream) + gpu3 = gdm.to_gpu(cpu3, '3', self.stream)[1] + gpu3.fill(np.float32(7.), self.stream) + gpu4 = gdm.to_gpu(cpu4, '4', self.stream)[1] + gpu4.fill(np.float32(9.), self.stream) + gdm.syncback = False + gpu5 = gdm.to_gpu(cpu4*.2, '5', self.stream)[1] + gpu6 = gdm.to_gpu(cpu4*.4, '6', self.stream)[1] + self.stream.synchronize() + + # assert + np.testing.assert_array_equal(cpu1, 3.) + np.testing.assert_array_equal(cpu2, 5.) + np.testing.assert_array_equal(cpu3, 8.) + np.testing.assert_array_equal(cpu4, 16.) + + def test_data_synctransfer(self): + # arrange + sh = (1024, 1024, 1) # 4MB + cpu1 = cuda.pagelocked_zeros(sh, np.float32, order="C", mem_flags=0) + cpu2 = cuda.pagelocked_zeros(sh, np.float32, order="C", mem_flags=0) + cpu1[:] = 1. + cpu2[:] = 2. + gdata = GpuData(cpu1.nbytes, syncback=True) + # long-running kernel + knl = """ + extern "C" __global__ void tfill(float* d, int sz, float dval) { + for (int i = 0; i < sz; ++i) + d[i] = dval; + } + """ + mod = SourceModule(knl, no_extern_c=True) + tfill = mod.get_function('tfill') + + # act + s2 = cuda.Stream() + gpu1 = gdata.to_gpu(cpu1, '1', self.stream) + tfill(gpu1, np.int32(gpu1.size), np.float32(2.), grid=(1,1,1), block=(1,1,1), stream=self.stream) + gdata.record_done(self.stream) # it will fail without this + gpu2 = gdata.to_gpu(cpu2, '2', s2) + tfill(gpu1, np.int32(gpu2.size), np.float32(4.), grid=(1,1,1), block=(1,1,1), stream=s2) + gdata.from_gpu(s2) + self.stream.synchronize() + s2.synchronize() + + # assert + np.testing.assert_array_equal(cpu1, 2.) + np.testing.assert_array_equal(cpu2, 4.) diff --git a/test/accelerate_tests/cuda_pycuda_tests/gradient_descent_kernel_test.py b/test/accelerate_tests/cuda_pycuda_tests/gradient_descent_kernel_test.py new file mode 100644 index 000000000..6caed13f2 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/gradient_descent_kernel_test.py @@ -0,0 +1,327 @@ +''' + + +''' + +import unittest +import numpy as np +from . import perfrun, PyCudaTest, have_pycuda + + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import GradientDescentKernel + + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class GradientDescentKernelTest(PyCudaTest): + + def prepare_arrays(self, performance=False): + if not performance: + nmodes = 2 + N_buf = 4 + N = 3 + A = 3 + else: + nmodes = 4 + N_buf = 100 + N = 80 + A = 512 + i_sh = (N, A, A) + e_sh = (N*nmodes, A, A) + f_sh = (N_buf, A, A) + a_sh = (N_buf * nmodes, A, A) + w = np.ones(i_sh, dtype=FLOAT_TYPE) + for idx, sl in enumerate(w): + sl[idx % A, idx % A] = 0.0 + X, Y, Z = np.indices(a_sh, dtype=COMPLEX_TYPE) + b_f = X + 1j * Y + b_a = Y + 1j * Z + b_b = Z + 1j * X + err_sum = np.zeros((N,), dtype=FLOAT_TYPE) + fic = np.ones((N,), dtype=FLOAT_TYPE) + addr = np.zeros((N, nmodes, 5, 3), dtype=INT_TYPE) + I = np.empty(i_sh, dtype=FLOAT_TYPE) + I[:] = np.round(np.abs(b_f[:N])**2 % 20) + for pos_idx in range(N): + for mode_idx in range(nmodes): + exit_idx = pos_idx * nmodes + mode_idx + addr[pos_idx, mode_idx] = np.array([[mode_idx, 0, 0], + [0, 0, 0], + [exit_idx, 0, 0], + [pos_idx, 0, 0], + [pos_idx, 0, 0]], dtype=INT_TYPE) + return (gpuarray.to_gpu(b_f), + gpuarray.to_gpu(b_a), + gpuarray.to_gpu(b_b), + gpuarray.to_gpu(I), + gpuarray.to_gpu(w), + gpuarray.to_gpu(err_sum), + gpuarray.to_gpu(addr), + gpuarray.to_gpu(fic)) + + def test_allocate(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays() + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + + def test_make_model(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays() + + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_model(b_f, addr) + + exp_Imodel = np.array([[[1., 1., 1.], + [3., 3., 3.], + [9., 9., 9.]], + + [[13., 13., 13.], + [15., 15., 15.], + [21., 21., 21.]], + + [[41., 41., 41.], + [43., 43., 43.], + [49., 49., 49.]], + + [[85., 85., 85.], + [87., 87., 87.], + [93., 93., 93.]]], dtype=FLOAT_TYPE) + + np.testing.assert_array_almost_equal( + exp_Imodel, GDK.gpu.Imodel.get(), + err_msg="`Imodel` buffer has not been updated as expected") + + @unittest.skipIf(not perfrun, "performance test") + def test_make_model_performance(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays(performance=True) + + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_model(b_f, addr) + + def test_floating_intensity(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays() + GDK=GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.gpu.Imodel[0] = I[0] * 3. + GDK.gpu.Imodel[1] = I[1] * 2. + GDK.gpu.Imodel[2] = I[2] + GDK.floating_intensity(addr, w, I, fic) + #print('Imodel',repr(GDK.gpu.Imodel)) + #print('fic',repr(1./fic)) + exp_Imodel = np.array([[[0., 0., 0.], + [1., 1., 1.], + [4., 4., 4.]], + + [[1., 1., 1.], + [2., 2., 2.], + [5., 5., 5.]], + + [[4., 4., 4.], + [5., 5., 5.], + [8., 8., 8.]], + + [[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]], dtype=np.float32) + exp_fic=1./np.array([3., 2., 1.], dtype=np.float32) + np.testing.assert_array_almost_equal(exp_Imodel, GDK.gpu.Imodel.get(), + err_msg="`Imodel` buffer has not been updated as expected") + np.testing.assert_array_almost_equal(exp_fic, fic.get(), + err_msg="floating intensity coeff (fic) has not been updated as expected") + + def test_make_a012(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays() + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_a012(b_f, b_a, b_b, addr, I, fic) + + exp_A0 = np.array([[[1., 1., 1.], + [2., 2., 2.], + [5., 5., 5.]], + + [[12., 12., 12.], + [13., 13., 13.], + [16., 16., 16.]], + + [[37., 37., 37.], + [38., 38., 38.], + [41., 41., 41.]], + + [[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal( + exp_A0, GDK.gpu.Imodel.get(), + err_msg="`Imodel` buffer (=A0) has not been updated as expected") + + exp_A1 = np.array([[[0., 0., 0.], + [2., 6., 10.], + [4., 12., 20.]], + + [[0., 0., 0.], + [10., 14., 18.], + [20., 28., 36.]], + + [[0., 0., 0.], + [18., 22., 26.], + [36., 44., 52.]], + + [[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal( + exp_A1, GDK.gpu.LLerr.get(), + err_msg="`LLerr` buffer (=A1) has not been updated as expected") + + exp_A2 = np.array([[[0., 4., 12.], + [4., 8., 16.], + [12., 16., 24.]], + + [[0., 12., 28.], + [12., 24., 40.], + [28., 40., 56.]], + + [[0., 20., 44.], + [20., 40., 64.], + [44., 64., 88.]], + + [[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal( + exp_A2, GDK.gpu.LLden.get(), + err_msg="`LLden` buffer (=A2) has not been updated as expected") + + @unittest.skipIf(not perfrun, "performance test") + def test_make_a012_performance(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays(performance=True) + + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_a012(b_f, b_a, b_b, addr, I, fic) + + def test_fill_b(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays() + Brenorm = 0.35 + B = np.zeros((3,), dtype=FLOAT_TYPE) + B_dev = gpuarray.to_gpu(B) + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_a012(b_f, b_a, b_b, addr, I, fic) + GDK.fill_b(addr, Brenorm, w, B_dev) + B[:] = B_dev.get() + + exp_B = np.array([ 4699.8, 5398.4, 13398.], dtype=FLOAT_TYPE) + np.testing.assert_allclose( + B, exp_B, + rtol=1e-7, + err_msg="`B` has not been updated as expected") + + @unittest.skipIf(not perfrun, "performance test") + def test_fill_b_perf(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays(performance=True) + Brenorm = 0.35 + B = np.zeros((3,), dtype=FLOAT_TYPE) + B_dev = gpuarray.to_gpu(B) + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.make_a012(b_f, b_a, b_b, addr, I, fic) + GDK.fill_b(addr, Brenorm, w, B_dev) + + def test_error_reduce(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays() + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.npy.LLerr = np.indices(GDK.gpu.LLerr.shape, dtype=FLOAT_TYPE)[0] + GDK.gpu.LLerr = gpuarray.to_gpu(GDK.npy.LLerr) + GDK.error_reduce(addr, err_sum) + + exp_err = np.array([0., 9., 18.], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal( + exp_err, err_sum.get(), + err_msg="`err_sum` has not been updated as expected") + + @unittest.skipIf(not perfrun, "performance test") + def test_error_reduce_perf(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays(performance=True) + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.npy.LLerr = np.indices(GDK.gpu.LLerr.shape, dtype=FLOAT_TYPE)[0] + GDK.gpu.LLerr = gpuarray.to_gpu(GDK.npy.LLerr) + GDK.error_reduce(addr, err_sum) + + def test_main(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays() + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.main(b_f, addr, w, I) + + exp_b_f = np.array([[[0. + 0.j, 0. + 0.j, 0. + 0.j], + [-0. - 1.j, -0. - 1.j, -0. - 1.j], + [-0. - 8.j, -0. - 8.j, -0. - 8.j]], + + [[0. + 0.j, 0. + 0.j, 0. + 0.j], + [-1. - 1.j, -1. - 1.j, -1. - 1.j], + [-4. - 8.j, -4. - 8.j, -4. - 8.j]], + + [[-2. + 0.j, -2. + 0.j, -2. + 0.j], + [-4. - 2.j, -0. + 0.j, -4. - 2.j], + [-10.-10.j, -10.-10.j, -10.-10.j]], + + [[-3. + 0.j, -3. + 0.j, -3. + 0.j], + [-6. - 2.j, -0. + 0.j, -6. - 2.j], + [-15.-10.j, -15.-10.j, -15.-10.j]], + + [[-16. + 0.j, -16. + 0.j, -16. + 0.j], + [-20. - 5.j, -20. - 5.j, -20. - 5.j], + [-32.-16.j, -32.-16.j, -0. + 0.j]], + + [[-20. + 0.j, -20. + 0.j, -20. + 0.j], + [-25. - 5.j, -25. - 5.j, -25. - 5.j], + [-40.-16.j, -40.-16.j, -0. + 0.j]], + + [[6. + 0.j, 6. + 0.j, 6. + 0.j], + [6. + 1.j, 6. + 1.j, 6. + 1.j], + [6. + 2.j, 6. + 2.j, 6. + 2.j]], + + [[7. + 0.j, 7. + 0.j, 7. + 0.j], + [7. + 1.j, 7. + 1.j, 7. + 1.j], + [7. + 2.j, 7. + 2.j, 7. + 2.j]]], dtype=COMPLEX_TYPE) + np.testing.assert_array_almost_equal( + exp_b_f, b_f.get(), + err_msg="Auxiliary has not been updated as expected") + + exp_LL = np.array([[[0., 0., 0.], + [1., 1., 1.], + [16., 16., 16.]], + + [[1., 1., 1.], + [4., 0., 4.], + [25., 25., 25.]], + + [[16., 16., 16.], + [25., 25., 25.], + [64., 64., 0.]], + + [[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]], dtype=FLOAT_TYPE) + np.testing.assert_array_almost_equal( + exp_LL, GDK.gpu.LLerr.get(), + err_msg="LogLikelihood error has not been updated as expected") + + @unittest.skipIf(not perfrun, "performance test") + def test_main_perf(self): + b_f, b_a, b_b, I, w, err_sum, addr, fic = self.prepare_arrays(performance=True) + GDK = GradientDescentKernel(b_f, addr.shape[1]) + GDK.allocate() + GDK.main(b_f, addr, w, I) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/accelerate_tests/cuda_pycuda_tests/import_test.py b/test/accelerate_tests/cuda_pycuda_tests/import_test.py new file mode 100644 index 000000000..8e445acf3 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/import_test.py @@ -0,0 +1,10 @@ +""" +Import test +""" +import unittest + +class AutoLoaderTest(unittest.TestCase): + + def test_load_engines_cuda(self): + import ptypy + ptypy.load_gpu_engines("cuda") diff --git a/test/accelerate_tests/cuda_pycuda_tests/multi_gpu_test.py b/test/accelerate_tests/cuda_pycuda_tests/multi_gpu_test.py new file mode 100644 index 000000000..64cc5110d --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/multi_gpu_test.py @@ -0,0 +1,84 @@ +''' +''' + +import unittest +from mpi4py.MPI import Get_version +import numpy as np +from . import PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + import pycuda.driver as cuda + from ptypy.accelerate.cuda_pycuda import multi_gpu as mgpu + from ptypy.utils import parallel + +from pkg_resources import parse_version + +class GpuDataTest(PyCudaTest): + """ + This is a test class for MPI - to really check if it all works, it needs + to be run as: + + mpirun -np 2 pytest multi_gpu_test.py + + For CUDA-aware MPI testing, currently the environment variable + + OMPI_MCA_opal_cuda_support=true + + needs to be set, mpi4py version 3.1.0+ used, a pycuda build from master, + and a cuda-aware MPI version. + """ + + def setUp(self): + if parallel.rank_local < cuda.Device.count(): + self.device = cuda.Device(parallel.rank_local) + self.ctx = self.device.make_context() + self.ctx.push() + else: + self.ctx = None + + def tearDown(self): + if self.ctx is not None: + self.ctx.pop() + self.ctx.detach() + + @unittest.skipIf(parallel.rank != 0, "Only in MPI rank 0") + def test_version(self): + v1 = parse_version("3.1.0") + v2 = parse_version(parse_version("3.1.0a").base_version) + + self.assertGreaterEqual(v2, v1) + + def test_compute_mode(self): + attr = cuda.Context.get_device().get_attributes() + self.assertIn(cuda.device_attribute.COMPUTE_MODE, attr) + mode = attr[cuda.device_attribute.COMPUTE_MODE] + self.assertIn(mode, + [cuda.compute_mode.DEFAULT, cuda.compute_mode.PROHIBITED, cuda.compute_mode.EXCLUSIVE_PROCESS] + ) + + def multigpu_tester(self, com): + if self.ctx is None: + return + + data = np.ones((2, 1), dtype=np.float32) + data_dev = gpuarray.to_gpu(data) + sz = parallel.size + com.allReduceSum(data_dev) + + out = data_dev.get() + np.testing.assert_allclose(out, sz * data, rtol=1e-6) + + def test_multigpu_auto(self): + self.multigpu_tester(mgpu.get_multi_gpu_communicator()) + + def test_multigpu_mpi(self): + self.multigpu_tester(mgpu.MultiGpuCommunicatorMpi()) + + @unittest.skipIf(not mgpu.have_cuda_mpi, "Cuda-aware MPI not available") + def test_multigpu_cudampi(self): + self.multigpu_tester(mgpu.MultiGpuCommunicatorCudaMpi()) + + @unittest.skipIf(not mgpu.have_nccl, "NCCL not available") + def test_multigpu_nccl(self): + self.multigpu_tester(mgpu.MultiGpuCommunicatorNccl()) \ No newline at end of file diff --git a/test/accelerate_tests/cuda_pycuda_tests/po_update_kernel_test.py b/test/accelerate_tests/cuda_pycuda_tests/po_update_kernel_test.py new file mode 100644 index 000000000..27c6abb56 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/po_update_kernel_test.py @@ -0,0 +1,943 @@ +''' + + +''' + +import unittest +import numpy as np +from . import PyCudaTest, have_pycuda +from ptypy.accelerate.base.array_utils import max_abs2 + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import PoUpdateKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class PoUpdateKernelTest(PyCudaTest): + + def prepare_arrays(self, scan_points=None): + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + if scan_points is None: + scan_pts = 2 # one dimensional scan point number + else: + scan_pts = scan_points + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + object_array_denominator = np.empty_like(object_array, dtype=FLOAT_TYPE) + for idx in range(G): + object_array_denominator[idx] = np.ones((H, I)) * (5 * idx + 2) + + probe_denominator = np.empty_like(probe, dtype=FLOAT_TYPE) + for idx in range(D): + probe_denominator[idx] = np.ones((E, F)) * (5 * idx + 2) + + return (gpuarray.to_gpu(addr), + gpuarray.to_gpu(object_array), + gpuarray.to_gpu(object_array_denominator), + gpuarray.to_gpu(probe), + gpuarray.to_gpu(exit_wave), + gpuarray.to_gpu(probe_denominator)) + + + def test_init(self): + POUK = PoUpdateKernel() + np.testing.assert_equal(POUK.kernels, ['pr_update', 'ob_update'], + err_msg='PoUpdateKernel does not have the correct functions registered.') + + def ob_update_REGRESSION_tester(self, atomics=True): + + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + object_array_denominator = np.empty_like(object_array, dtype=FLOAT_TYPE) + for idx in range(G): + object_array_denominator[idx] = np.ones((H, I)) * (5 * idx + 2) + + + POUK = PoUpdateKernel() + from ptypy.accelerate.base.kernels import PoUpdateKernel as npPoUpdateKernel + nPOUK = npPoUpdateKernel() + # print("object array denom before:") + # print(object_array_denominator) + object_array_dev = gpuarray.to_gpu(object_array) + object_array_denominator_dev = gpuarray.to_gpu(object_array_denominator) + probe_dev = gpuarray.to_gpu(probe) + exit_wave_dev = gpuarray.to_gpu(exit_wave) + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr, (2, 3, 0, 1))) + addr_dev = gpuarray.to_gpu(addr2) + else: + addr_dev = gpuarray.to_gpu(addr) + + print(object_array_denominator) + POUK.ob_update(addr_dev, object_array_dev, object_array_denominator_dev, probe_dev, exit_wave_dev, atomics=atomics) + print("\n\n cuda version") + print(object_array_denominator_dev.get()) + nPOUK.ob_update(addr, object_array, object_array_denominator, probe, exit_wave) + print("\n\n numpy version") + print(object_array_denominator) + + + + expected_object_array = np.array([[[15.+1.j, 53.+1.j, 53.+1.j, 53.+1.j, 53.+1.j, 39.+1.j, 1.+1.j], + [77.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 125.+1.j, 1.+1.j], + [77.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 125.+1.j, 1.+1.j], + [77.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 125.+1.j, 1.+1.j], + [77.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 201.+1.j, 125.+1.j, 1.+1.j], + [63.+1.j, 149.+1.j, 149.+1.j, 149.+1.j, 149.+1.j, 87.+1.j, 1.+1.j], + [1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j, 1.+1.j]], + [[24. + 4.j, 68. + 4.j, 68. + 4.j, 68. + 4.j, 68. + 4.j, 48. + 4.j, 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, 4. + 4.j], + [92. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 228. + 4.j, 140. + 4.j, 4. + 4.j], + [72. + 4.j, 164. + 4.j, 164. + 4.j, 164. + 4.j, 164. + 4.j, 96. + 4.j, 4. + 4.j], + [4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j]]], + dtype=COMPLEX_TYPE) + + + np.testing.assert_array_equal(object_array, expected_object_array, + err_msg="The object array has not been updated as expected") + + expected_object_array_denominator = np.array([[[12., 22., 22., 22., 22., 12., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [22., 42., 42., 42., 42., 22., 2.], + [12., 22., 22., 22., 22., 12., 2.], + [ 2., 2., 2., 2., 2., 2., 2.]], + + [[17., 27., 27., 27., 27., 17., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [27., 47., 47., 47., 47., 27., 7.], + [17., 27., 27., 27., 27., 17., 7.], + [ 7., 7., 7., 7., 7., 7., 7.]]], + dtype=FLOAT_TYPE) + + + np.testing.assert_array_equal(object_array_denominator_dev.get(), expected_object_array_denominator, + err_msg="The object array denominatorhas not been updated as expected") + + + def test_ob_update_atomics_REGRESSION(self): + self.ob_update_REGRESSION_tester(atomics=True) + + def test_ob_update_tiled_REGRESSION(self): + self.ob_update_REGRESSION_tester(atomics=False) + + def ob_update_UNITY_tester(self, atomics=True): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + object_array_denominator = np.empty_like(object_array, dtype=FLOAT_TYPE) + for idx in range(G): + object_array_denominator[idx] = np.ones((H, I)) * (5 * idx + 2) + + + POUK = PoUpdateKernel() + + from ptypy.accelerate.base.kernels import PoUpdateKernel as npPoUpdateKernel + nPOUK = npPoUpdateKernel() + + object_array_dev = gpuarray.to_gpu(object_array) + object_array_denominator_dev = gpuarray.to_gpu(object_array_denominator) + probe_dev = gpuarray.to_gpu(probe) + exit_wave_dev = gpuarray.to_gpu(exit_wave) + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr, (2, 3, 0, 1))) + addr_dev = gpuarray.to_gpu(addr2) + else: + addr_dev = gpuarray.to_gpu(addr) + + # print(object_array_denominator) + POUK.ob_update(addr_dev, object_array_dev, object_array_denominator_dev, probe_dev, exit_wave_dev, atomics=atomics) + # print("\n\n cuda version") + # print(repr(object_array_dev.get())) + # print(repr(object_array_denominator_dev.get())) + nPOUK.ob_update(addr, object_array, object_array_denominator, probe, exit_wave) + # print("\n\n numpy version") + # print(repr(object_array_denominator)) + # print(repr(object_array)) + + + np.testing.assert_array_equal(object_array, object_array_dev.get(), + err_msg="The object array has not been updated as expected") + + + np.testing.assert_array_equal(object_array_denominator, object_array_denominator_dev.get(), + err_msg="The object array denominatorhas not been updated as expected") + + + def test_ob_update_atomics_UNITY(self): + self.ob_update_UNITY_tester(atomics=True) + + def test_ob_update_tiled_UNITY(self): + self.ob_update_UNITY_tester(atomics=False) + + def pr_update_REGRESSION_tester(self, atomics=True): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + probe_denominator = np.empty_like(probe, dtype=FLOAT_TYPE) + for idx in range(D): + probe_denominator[idx] = np.ones((E, F)) * (5 * idx + 2) + + POUK = PoUpdateKernel() + + # print("probe array before:") + # print(repr(probe)) + # print("probe denominator array before:") + # print(repr(probe_denominator)) + + object_array_dev = gpuarray.to_gpu(object_array) + probe_denominator_dev = gpuarray.to_gpu(probe_denominator) + probe_dev = gpuarray.to_gpu(probe) + exit_wave_dev = gpuarray.to_gpu(exit_wave) + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr, (2, 3, 0, 1))) + addr_dev = gpuarray.to_gpu(addr2) + else: + addr_dev = gpuarray.to_gpu(addr) + + + POUK.pr_update(addr_dev, probe_dev, probe_denominator_dev, object_array_dev, exit_wave_dev, atomics=atomics) + + # print("probe array after:") + # print(repr(probe)) + # print("probe denominator array after:") + # print(repr(probe_denominator)) + expected_probe = np.array([[[313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j], + [313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j], + [313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j], + [313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j], + [313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j, 313.+1.j]], + + [[394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j], + [394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j], + [394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j], + [394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j], + [394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j, 394.+2.j]]], + dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(probe_dev.get(), expected_probe, + err_msg="The probe has not been updated as expected") + + expected_probe_denominator = np.array([[[138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.], + [138., 138., 138., 138., 138.]], + + [[143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.], + [143., 143., 143., 143., 143.]]], + dtype=FLOAT_TYPE) + + np.testing.assert_array_equal(probe_denominator_dev.get(), expected_probe_denominator, + err_msg="The probe denominatorhas not been updated as expected") + + + def test_pr_update_atomics_REGRESSION(self): + self.pr_update_REGRESSION_tester(atomics=True) + + def test_pr_update_tiled_REGRESSION(self): + self.pr_update_REGRESSION_tester(atomics=False) + + def pr_update_UNITY_tester(self, atomics=True): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): # + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + probe_denominator = np.empty_like(probe, dtype=FLOAT_TYPE) + for idx in range(D): + probe_denominator[idx] = np.ones((E, F)) * (5 * idx + 2) + + POUK = PoUpdateKernel() + from ptypy.accelerate.base.kernels import PoUpdateKernel as npPoUpdateKernel + nPOUK = npPoUpdateKernel() + + # print("probe array before:") + # print(repr(probe)) + # print("probe denominator array before:") + # print(repr(probe_denominator)) + + object_array_dev = gpuarray.to_gpu(object_array) + probe_denominator_dev = gpuarray.to_gpu(probe_denominator) + probe_dev = gpuarray.to_gpu(probe) + exit_wave_dev = gpuarray.to_gpu(exit_wave) + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr, (2, 3, 0, 1))) + addr_dev = gpuarray.to_gpu(addr2) + else: + addr_dev = gpuarray.to_gpu(addr) + + + POUK.pr_update(addr_dev, probe_dev, probe_denominator_dev, object_array_dev, exit_wave_dev, atomics=atomics) + nPOUK.pr_update(addr, probe, probe_denominator, object_array, exit_wave) + + # print("probe array after:") + # print(repr(probe)) + # print("probe denominator array after:") + # print(repr(probe_denominator)) + + np.testing.assert_array_equal(probe, probe_dev.get(), + err_msg="The probe has not been updated as expected") + + np.testing.assert_array_equal(probe_denominator, probe_denominator_dev.get(), + err_msg="The probe denominatorhas not been updated as expected") + + + def test_pr_update_atomics_UNITY(self): + self.pr_update_UNITY_tester(atomics=True) + + def test_pr_update_tiled_UNITY(self): + self.pr_update_UNITY_tester(atomics=False) + + + def pr_update_ML_tester(self, atomics=False): + ''' + setup + ''' + addr, object_array, object_array_denominator, probe, exit_wave, probe_denominator = self.prepare_arrays() + ''' + test + ''' + POUK = PoUpdateKernel() + + POUK.allocate() # this doesn't do anything, but is the call pattern. + + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr.get(), (2, 3, 0, 1))) + addr = gpuarray.to_gpu(addr2) + + POUK.pr_update_ML(addr, probe, object_array, exit_wave, atomics=atomics) + + expected_probe = np.array([[[625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j], + [625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j, 625. + 1.j]], + + [[786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j], + [786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j, 786. + 2.j]]], + dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(probe.get(), expected_probe, + err_msg="The probe has not been updated as expected") + + def test_pr_update_ML_atomics_REGRESSION(self): + self.pr_update_ML_tester(True) + + def test_pr_update_ML_tiled_REGRESSION(self): + self.pr_update_ML_tester(False) + + def ob_update_ML_tester(self, atomics=True): + ''' + setup + ''' + addr, object_array, object_array_denominator, probe, exit_wave, probe_denominator = self.prepare_arrays() + ''' + test + ''' + POUK = PoUpdateKernel() + + POUK.allocate() # this doesn't do anything, but is the call pattern. + + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr.get(), (2, 3, 0, 1))) + addr = gpuarray.to_gpu(addr2) + + POUK.ob_update_ML(addr, object_array, probe, exit_wave, atomics=atomics) + + expected_object_array = np.array( + [[[29. + 1.j, 105. + 1.j, 105. + 1.j, 105. + 1.j, 105. + 1.j, 77. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [153. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 401. + 1.j, 249. + 1.j, 1. + 1.j], + [125. + 1.j, 297. + 1.j, 297. + 1.j, 297. + 1.j, 297. + 1.j, 173. + 1.j, 1. + 1.j], + [1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j, 1. + 1.j]], + + [[44. + 4.j, 132. + 4.j, 132. + 4.j, 132. + 4.j, 132. + 4.j, 92. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [180. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 452. + 4.j, 276. + 4.j, 4. + 4.j], + [140. + 4.j, 324. + 4.j, 324. + 4.j, 324. + 4.j, 324. + 4.j, 188. + 4.j, 4. + 4.j], + [4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j, 4. + 4.j]]], + dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(object_array.get(), expected_object_array, + err_msg="The object array has not been updated as expected") + + def test_ob_update_ML_atomics_REGRESSION(self): + self.ob_update_ML_tester(True) + + def test_ob_update_ML_tiled_REGRESSION(self): + self.ob_update_ML_tester(False) + + def test_ob_update_local_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + auxiliary_wave = exit_wave.copy() * 2 + + probe_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + from ptypy.accelerate.base.kernels import PoUpdateKernel as npPoUpdateKernel + nPOUK = npPoUpdateKernel() + POUK = PoUpdateKernel(queue_thread=self.stream) + + object_array_dev = gpuarray.to_gpu(object_array) + probe_dev = gpuarray.to_gpu(probe) + exit_wave_dev = gpuarray.to_gpu(exit_wave) + auxiliary_wave_dev = gpuarray.to_gpu(auxiliary_wave) + probe_norm_dev = gpuarray.to_gpu(probe_norm) + addr_dev = gpuarray.to_gpu(addr) + + POUK.pr_norm_local(addr_dev, probe_dev, probe_norm_dev) + POUK.ob_update_local(addr_dev, object_array_dev, probe_dev, exit_wave_dev, auxiliary_wave_dev, probe_norm_dev, a=0.5, b=0.5) + nPOUK.pr_norm_local(addr, probe, probe_norm) + nPOUK.ob_update_local(addr, object_array, probe, exit_wave, auxiliary_wave, probe_norm, a=0.5, b=0.5) + + np.testing.assert_allclose(object_array_dev.get(), object_array, rtol=1e-6, atol=1e-6, + err_msg="The object array has not been updated as expected") + + def test_pr_update_local_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + auxiliary_wave = exit_wave.copy() * 1.5 + + object_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + from ptypy.accelerate.base.kernels import PoUpdateKernel as npPoUpdateKernel + nPOUK = npPoUpdateKernel() + POUK = PoUpdateKernel() + + object_array_dev = gpuarray.to_gpu(object_array) + probe_dev = gpuarray.to_gpu(probe) + exit_wave_dev = gpuarray.to_gpu(exit_wave) + auxiliary_wave_dev = gpuarray.to_gpu(auxiliary_wave) + object_norm_dev = gpuarray.to_gpu(object_norm) + addr_dev = gpuarray.to_gpu(addr) + + POUK.ob_norm_local(addr_dev, object_array_dev, object_norm_dev) + POUK.pr_update_local(addr_dev, probe_dev, object_array_dev,exit_wave_dev, auxiliary_wave_dev, object_norm_dev, gpuarray.max(object_norm_dev, stream=self.stream), a=0.5, b=0.5) + nPOUK.ob_norm_local(addr, object_array, object_norm) + nPOUK.pr_update_local(addr, probe, object_array, exit_wave, auxiliary_wave, object_norm, object_norm.max(), a=0.5, b=0.5) + + np.testing.assert_allclose(probe_dev.get(), probe, rtol=1e-6, atol=1e-6, + err_msg="The probe has not been updated as expected") + + def test_ob_norm_local_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + object_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + from ptypy.accelerate.base.kernels import PoUpdateKernel as npPoUpdateKernel + nPOUK = npPoUpdateKernel() + POUK = PoUpdateKernel(queue_thread=self.stream) + + object_array_dev = gpuarray.to_gpu(object_array) + object_norm_dev = gpuarray.to_gpu(object_norm) + addr_dev = gpuarray.to_gpu(addr) + + POUK.ob_norm_local(addr_dev, object_array_dev, object_norm_dev) + nPOUK.ob_norm_local(addr, object_array, object_norm) + + np.testing.assert_allclose(object_norm_dev.get(), object_norm, rtol=1e-6, atol=1e-6, + err_msg="The object norm has not been updated as expected") + + def test_pr_norm_local_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 1 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_norm = np.empty(shape=(1,B,C), dtype=FLOAT_TYPE) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + from ptypy.accelerate.base.kernels import PoUpdateKernel as npPoUpdateKernel + nPOUK = npPoUpdateKernel() + POUK = PoUpdateKernel() + + probe_dev = gpuarray.to_gpu(probe) + probe_norm_dev = gpuarray.to_gpu(probe_norm) + addr_dev = gpuarray.to_gpu(addr) + + POUK.pr_norm_local(addr_dev, probe_dev, probe_norm_dev) + nPOUK.pr_norm_local(addr, probe, probe_norm) + + np.testing.assert_allclose(probe_norm_dev.get(), probe_norm, rtol=1e-6, atol=1e-6, + err_msg="The probe norm has not been updated as expected") + + +if __name__ == '__main__': + unittest.main() diff --git a/test/accelerate_tests/cuda_pycuda_tests/position_correction_kernel_test.py b/test/accelerate_tests/cuda_pycuda_tests/position_correction_kernel_test.py new file mode 100644 index 000000000..8cbb89c96 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/position_correction_kernel_test.py @@ -0,0 +1,149 @@ +''' + + +''' + +import unittest +import numpy as np +from . import PyCudaTest, have_pycuda +from ptypy import utils as u + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import PositionCorrectionKernel + from ptypy.accelerate.base.kernels import PositionCorrectionKernel as abPositionCorrectionKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + + +class PositionCorrectionKernelTest(PyCudaTest): + + def setUp(self): + PyCudaTest.setUp(self) + self.params = u.Param() + self.params.nshifts = 4 + self.params.method = "Annealing" + self.params.amplitude = 2e-9 + self.params.start = 0 + self.params.stop = 10 + self.params.max_shift = 2e-9 + self.params.amplitude_decay = True + self.resolution = [1e-9,1e-9] + + def update_addr_and_error_state_UNITY_helper(self, size, modes): + ## Arrange + addr = np.ones((size, modes, 5, 3), dtype=np.int32) + mangled_addr = 2 * addr + err_state = np.zeros((size,), dtype=np.float32) + err_state[5:] = 2. + err_sum = np.ones((size, ), dtype=np.float32) + addr_gpu = gpuarray.to_gpu(addr) + mangled_addr_gpu = gpuarray.to_gpu(mangled_addr) + err_state_gpu = gpuarray.to_gpu(err_state) + err_sum_gpu = gpuarray.to_gpu(err_sum) + aux = np.ones((1,1,1), dtype=np.complex64) + + ## Act + PCK = PositionCorrectionKernel(aux, modes, self.params, self.resolution, queue_thread=self.stream) + PCK.update_addr_and_error_state(addr_gpu, err_state_gpu, mangled_addr_gpu, err_sum_gpu) + abPCK = abPositionCorrectionKernel(aux, modes, self.params, self.resolution) + abPCK.update_addr_and_error_state(addr, err_state, mangled_addr, err_sum) + + ## Assert + np.testing.assert_array_equal(addr_gpu.get(), addr) + np.testing.assert_array_equal(err_state_gpu.get(), err_state) + + def test_update_addr_and_error_state_UNITY_small_onemode(self): + self.update_addr_and_error_state_UNITY_helper(4, 1) + + def test_update_addr_and_error_state_UNITY_large_onemode(self): + self.update_addr_and_error_state_UNITY_helper(323, 1) + + def test_update_addr_and_error_state_UNITY_small_multimode(self): + self.update_addr_and_error_state_UNITY_helper(4, 3) + + def test_update_addr_and_error_state_UNITY_large_multimode(self): + self.update_addr_and_error_state_UNITY_helper(323, 3) + + def log_likelihood_ml_UNITY(self): + ''' + setup + ''' + B = 5 # frame size y + C = 5 # frame size x + + D = 2 # number of probe modes + G = 2 # number of object modes + + E = B # probe size y + F = C # probe size x + + scan_pts = 2 # one dimensional scan point number + + N = scan_pts ** 2 + total_number_modes = G * D + A = N * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + f = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + f[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + fmag = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured magnitudes NxAxB + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + I = fmag**2 + + mask = np.empty(shape=(N, B, C), + dtype=FLOAT_TYPE) # the masks for the measured magnitudes either 1xAxB or NxAxB + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + w = mask /(I+1.) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((N,)) + Y = Y.reshape((N,)) + + addr = np.zeros((N, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [position_idx, 0, 0], + [position_idx, 0, 0]]) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + mask_sum = mask.sum(-1).sum(-1) + LLerr = np.zeros_like(mask_sum, dtype=np.float32) + f_d = gpuarray.to_gpu(f) + w_d = gpuarray.to_gpu(w) + I_d = gpuarray.to_gpu(I) + addr_d = gpuarray.to_gpu(addr) + LLerr_d = gpuarray.to_gpu(LLerr) + + ## Act + PCK = PositionCorrectionKernel(f, total_number_modes, self.params, self.resolution, queue_thread=self.stream) + abPCK = abPositionCorrectionKernel(f, total_number_modes, self.params, self.resolution) + abPCK.log_likelihood_ml(f, addr, I, w, LLerr) + PCK.log_likelihood_ml(f_d, addr_d, I_d, w_d, LLerr_d) + + expected_err_phot = LLerr + measured_err_phot = LLerr_d.get() + + np.testing.assert_allclose(expected_err_phot, measured_err_phot, err_msg="Numpy log-likelihood error " + "is \n%s, \nbut gpu log-likelihood error is \n%s, \n " % ( + repr(expected_err_phot), + repr(measured_err_phot)), rtol=1e-5) diff --git a/test/accelerate_tests/cuda_pycuda_tests/propagation_kernel_test.py b/test/accelerate_tests/cuda_pycuda_tests/propagation_kernel_test.py new file mode 100644 index 000000000..93fbad431 --- /dev/null +++ b/test/accelerate_tests/cuda_pycuda_tests/propagation_kernel_test.py @@ -0,0 +1,158 @@ +''' + +''' + +import unittest +import numpy as np +import ptypy.utils as u +from . import PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import PropagationKernel + +from ptypy.core import geometry +from ptypy.core import Base as theBase + +# subclass for dictionary access +Base = type('Base',(theBase,),{}) + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class PropagationKernelTest(PyCudaTest): + + def set_up_farfield(self,shape, resolution=None): + P = Base() + P.CType = COMPLEX_TYPE + P.Ftype = FLOAT_TYPE + g = u.Param() + g.energy = None # u.keV2m(1.0)/6.32e-7 + g.lam = 5.32e-7 + g.distance = 15e-2 + g.psize = 24e-6 + g.shape = shape + g.propagation = "farfield" + if resolution is not None: + g.resolution = resolution + G = geometry.Geo(owner=P, pars=g) + return G + + def set_up_nearfield(self, shape): + P = Base() + P.CType = COMPLEX_TYPE + P.Ftype = FLOAT_TYPE + g = u.Param() + g.energy = None # u.keV2m(1.0)/6.32e-7 + g.lam = 1e-10 + g.distance = 1.0 + g.psize = 100e-9 + g.shape = shape + g.propagation = "nearfield" + G = geometry.Geo(owner=P, pars=g) + return G + + def test_farfield_propagator_forward_UNITY(self): + # setup + SH = (2,16,16) + aux = np.zeros((SH), dtype=COMPLEX_TYPE) + aux[:,5:11,5:11] = 1. + 2j + aux_d = gpuarray.to_gpu(aux) + geo = self.set_up_farfield(SH[1:]) + + # test + aux = geo.propagator.fw(aux) + PropK = PropagationKernel(aux_d, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.fw(aux_d, aux_d) + + np.testing.assert_allclose(aux_d.get(), aux, atol=1e-06, rtol=5e-5, + err_msg="Numpy aux is \n%s, \nbut gpu aux is \n %s, \n " % (repr(aux), repr(aux_d.get()))) + + def test_farfield_propagator_backward_UNITY(self): + # setup + SH = (2,16,16) + aux = np.zeros((SH), dtype=COMPLEX_TYPE) + aux[:,5:11,5:11] = 1. + 2j + aux_d = gpuarray.to_gpu(aux) + geo = self.set_up_farfield(SH[1:]) + + # test + aux = geo.propagator.bw(aux) + PropK = PropagationKernel(aux_d, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.bw(aux_d, aux_d) + + np.testing.assert_allclose(aux_d.get(), aux, atol=1e-06, rtol=5e-5, + err_msg="Numpy aux is \n%s, \nbut gpu aux is \n %s, \n " % (repr(aux), repr(aux_d.get()))) + + def test_farfield_propagator_forward_crop_pad_UNITY(self): + # setup + SH = (2,16,16) + aux = np.zeros((SH), dtype=COMPLEX_TYPE) + aux[:,5:11,5:11] = 1. + 2j + aux_d = gpuarray.to_gpu(aux) + geo = self.set_up_farfield(SH[1:]) + geo = self.set_up_farfield(SH[1:], resolution=0.5*geo.resolution) + + # test + aux = geo.propagator.fw(aux) + PropK = PropagationKernel(aux_d, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.fw(aux_d, aux_d) + + np.testing.assert_allclose(aux_d.get(), aux, atol=1e-06, rtol=5e-5, + err_msg="Numpy aux is \n%s, \nbut gpu aux is \n %s, \n " % (repr(aux), repr(aux_d.get()))) + + def test_farfield_propagator_backward_crop_pad_UNITY(self): + # setup + SH = (2,16,16) + aux = np.zeros((SH), dtype=COMPLEX_TYPE) + aux[:,5:11,5:11] = 1. + 2j + aux_d = gpuarray.to_gpu(aux) + geo = self.set_up_farfield(SH[1:]) + geo = self.set_up_farfield(SH[1:], resolution=0.5*geo.resolution) + + # test + aux = geo.propagator.bw(aux) + PropK = PropagationKernel(aux_d, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.bw(aux_d, aux_d) + + np.testing.assert_allclose(aux_d.get(), aux, atol=1e-06, rtol=5e-5, + err_msg="Numpy aux is \n%s, \nbut gpu aux is \n %s, \n " % (repr(aux), repr(aux_d.get()))) + + def test_nearfield_propagator_forward_UNITY(self): + # setup + SH = (2,16,16) + aux = np.zeros((SH), dtype=COMPLEX_TYPE) + aux[:,5:11,5:11] = 1. + 2j + aux_d = gpuarray.to_gpu(aux) + geo = self.set_up_nearfield(SH[1:]) + + # test + aux = geo.propagator.fw(aux) + PropK = PropagationKernel(aux_d, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.fw(aux_d, aux_d) + + np.testing.assert_allclose(aux_d.get(), aux, atol=1e-06, rtol=5e-5, + err_msg="Numpy aux is \n%s, \nbut gpu aux is \n %s, \n " % (repr(aux), repr(aux_d.get()))) + + def test_nearfield_propagator_backward_UNITY(self): + # setup + SH = (2,16,16) + aux = np.zeros((SH), dtype=COMPLEX_TYPE) + aux[:,5:11,5:11] = 1. + 2j + aux_d = gpuarray.to_gpu(aux) + geo = self.set_up_nearfield(SH[1:]) + + # test + aux = geo.propagator.bw(aux) + PropK = PropagationKernel(aux_d, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.bw(aux_d, aux_d) + + np.testing.assert_allclose(aux_d.get(), aux, atol=1e-06, rtol=5e-5, + err_msg="Numpy aux is \n%s, \nbut gpu aux is \n %s, \n " % (repr(aux), repr(aux_d.get()))) \ No newline at end of file diff --git a/test/accelerate_tests/ocl_pyopencl_tests/__init__.py b/test/accelerate_tests/ocl_pyopencl_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/accelerate_tests/ocl_pyopencl_tests/ocl_kernels_test.py b/test/accelerate_tests/ocl_pyopencl_tests/ocl_kernels_test.py new file mode 100644 index 000000000..8dbeeeb3b --- /dev/null +++ b/test/accelerate_tests/ocl_pyopencl_tests/ocl_kernels_test.py @@ -0,0 +1,780 @@ +''' + + +''' + +import unittest +import numpy as np + +try: + import pyopencl as pocl + from pyopencl import array as cla + from ptypy.accelerate.ocl.ocl_kernels import AuxiliaryWaveKernel, FourierUpdateKernel + # from ptypy.accelerate.ocl.npy_kernels_for_block import AuxiliaryWaveKernel + from ptypy.accelerate.ocl import get_ocl_queue + have_ocl = True +except ImportError: + have_ocl = False + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +@unittest.skipIf(not have_ocl, "no PyOpenCL or GPU drivers available") +class AuxiliaryWaveKernelTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + self.queue = get_ocl_queue() + + def tearDown(self): + np.set_printoptions() + del self.queue + + def test_init(self): + attrs = [] + + AWK = AuxiliaryWaveKernel(self.queue) + self.queue.finish() + for attr in attrs: + self.assertTrue(hasattr(AWK, attr), msg="AuxiliaryWaveKernel does not have attribute: %s" % attr) + + np.testing.assert_equal(AWK.kernels, + ['build_aux', 'build_exit'], + err_msg='AuxiliaryWaveKernel does not have the correct functions registered.') + + def _configure(self): + B = 4 # frame size y + C = 4 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + pr_npy = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + pr_npy[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + ob_npy = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + ob_npy[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + ex_npy = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + ex_npy[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(Y.flat, X.flat): + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + exit_idx += 1 + mode_idx += 1 + position_idx += 1 + + return pr_npy, ob_npy, ex_npy, addr + + def test_build_aux_unity(self): + ''' + test + ''' + pr_npy, ob_npy, ex_npy, addr = self._configure() + aux_npy = np.zeros_like(ex_npy) + from ptypy.accelerate.ocl.npy_kernels_for_block import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel(self.queue) + AWK.ocl_wg_size = None # (1, 3, 3) #None + alpha = 1.0 + + ob_dev = cla.to_device(self.queue, ob_npy) + pr_dev = cla.to_device(self.queue, pr_npy) + addr_dev = cla.to_device(self.queue, addr) + aux_dev = cla.to_device(self.queue, aux_npy) + ex_dev = cla.to_device(self.queue, ex_npy) + self.queue.finish() + AWK.build_aux(aux_dev, addr_dev, ob_dev, pr_dev, ex_dev, alpha) + nAWK.build_aux(aux_npy, addr, ob_npy, pr_npy, ex_npy, alpha) + d = aux_dev.get() + np.testing.assert_array_equal(aux_npy, aux_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + def test_build_aux_capped_unity(self): + ''' + test + ''' + pr_npy, ob_npy, ex_npy, addr = self._configure() + aux_npy = np.zeros_like(ex_npy) + # now use only a part of the stacks + sh = addr.shape + addr = addr[:sh[0] // 2, ...] + ex_npy = ex_npy[:sh[0] // 2 * sh[1], ...] + from ptypy.accelerate.ocl.npy_kernels_for_block import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel(self.queue) + AWK.ocl_wg_size = None # (1, 3, 3) #None + alpha = 1.0 + + ob_dev = cla.to_device(self.queue, ob_npy) + pr_dev = cla.to_device(self.queue, pr_npy) + addr_dev = cla.to_device(self.queue, addr) + aux_dev = cla.to_device(self.queue, aux_npy) + ex_dev = cla.to_device(self.queue, ex_npy) + self.queue.finish() + AWK.build_aux(aux_dev, addr_dev, ob_dev, pr_dev, ex_dev, alpha) + nAWK.build_aux(aux_npy, addr, ob_npy, pr_npy, ex_npy, alpha) + d = aux_dev.get() + np.testing.assert_array_equal(aux_npy, aux_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + def test_build_exit_unity(self): + ''' + test + ''' + pr_npy, ob_npy, ex_npy, addr = self._configure() + aux_npy = np.zeros_like(ex_npy) + from ptypy.accelerate.ocl.npy_kernels_for_block import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel(self.queue) + AWK.ocl_wg_size = None # (1, 3, 3) #None + + ob_dev = cla.to_device(self.queue, ob_npy) + pr_dev = cla.to_device(self.queue, pr_npy) + addr_dev = cla.to_device(self.queue, addr) + aux_dev = cla.to_device(self.queue, aux_npy) + ex_dev = cla.to_device(self.queue, ex_npy) + self.queue.finish() + AWK.build_exit(aux_dev, addr_dev, ob_dev, pr_dev, ex_dev) + nAWK.build_exit(aux_npy, addr, ob_npy, pr_npy, ex_npy) + np.testing.assert_array_equal(aux_npy, aux_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + def test_build_exit_capped_unity(self): + ''' + test + ''' + pr_npy, ob_npy, ex_npy, addr = self._configure() + aux_npy = np.zeros_like(ex_npy) + # now use only a part of the stacks + sh = addr.shape + addr = addr[:sh[0] // 2, ...] + ex_npy = ex_npy[:sh[0] // 2 * sh[1], ...] + from ptypy.accelerate.ocl.npy_kernels_for_block import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel(self.queue) + AWK.ocl_wg_size = None # (1, 3, 3) #None + + ob_dev = cla.to_device(self.queue, ob_npy) + pr_dev = cla.to_device(self.queue, pr_npy) + addr_dev = cla.to_device(self.queue, addr) + aux_dev = cla.to_device(self.queue, aux_npy) + ex_dev = cla.to_device(self.queue, ex_npy) + self.queue.finish() + AWK.build_exit(aux_dev, addr_dev, ob_dev, pr_dev, ex_dev) + nAWK.build_exit(aux_npy, addr, ob_npy, pr_npy, ex_npy) + np.testing.assert_array_equal(aux_npy, aux_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + +@unittest.skipIf(not have_ocl, "no PyOpenCL or GPU drivers available") +class FourierUpdateKernelTest(unittest.TestCase): + + def setUp(self): + import sys + np.set_printoptions(threshold=sys.maxsize, linewidth=np.inf) + self.queue = get_ocl_queue() + + def tearDown(self): + np.set_printoptions() + del self.queue + + def test_init(self): + attrs = [] + aux = np.zeros((16, 3, 3), dtype=COMPLEX_TYPE) + nmodes = 4 + FUK = FourierUpdateKernel(aux, nmodes, self.queue) + self.queue.finish() + for attr in attrs: + self.assertTrue(hasattr(FUK, attr), msg="AuxiliaryWaveKernel does not have attribute: %s" % attr) + + def test_all_capped_unity(self): + ''' + test + ''' + nmodes = 2 + # pr_npy, ob_npy, ex_npy, addr_npy = self._configure() + addr_npy = np.zeros((4, nmodes, 5, 3), dtype=INT_TYPE) + shape = (4, 3, 3) + L, M, N = shape + fshape = shape + shape = (nmodes * L, M, N) + + X, Y, Z = np.indices(shape) + aux_npy = (1j*Z+X+Z).astype(COMPLEX_TYPE) * 200 + mag_npy = np.indices(fshape).sum(0).astype(FLOAT_TYPE) * 200 ** 2 * nmodes + ma_npy = (mag_npy > 10).astype(FLOAT_TYPE) + err_fourier_npy = np.zeros((L,), dtype=FLOAT_TYPE) + mask_sum_npy = ma_npy.sum(-1).sum(-1) + + from ptypy.accelerate.ocl.npy_kernels_for_block import FourierUpdateKernel as nFourierUpdateKernel + nFUK = nFourierUpdateKernel(aux_npy, nmodes) + FUK = FourierUpdateKernel(aux_npy, nmodes, self.queue) + FUK.ocl_wg_size = None # (1, 3, 3) #None + + FUK.allocate() + nFUK.allocate() + self.queue.finish() + + # now use only a part of the stacks + sh = addr_npy.shape + mag_npy = mag_npy[:sh[0] // 2, ...] + ma_npy = ma_npy[:sh[0] // 2, ...] + addr_npy = addr_npy[:sh[0] // 2, ...] + mask_sum_npy = mask_sum_npy[:sh[0] // 2, ...] + err_fourier_npy = err_fourier_npy[:sh[0] // 2, ...] + + # copy + mag_dev = cla.to_device(self.queue, mag_npy) + ma_dev = cla.to_device(self.queue, ma_npy) + aux_dev = cla.to_device(self.queue, aux_npy) + addr_dev = cla.to_device(self.queue, addr_npy) + mask_sum_dev = cla.to_device(self.queue, mask_sum_npy) + err_fourier_dev = cla.to_device(self.queue, err_fourier_npy) + + self.queue.finish() + FUK.fourier_error(aux_dev, addr_dev, mag_dev, ma_dev, mask_sum_dev) + FUK.error_reduce(addr_dev, err_fourier_dev) + FUK.fmag_all_update(aux_dev, addr_dev, mag_dev, ma_dev, err_fourier_dev, pbound=0.5) + nFUK.fourier_error(aux_npy, addr_npy, mag_npy, ma_npy, mask_sum_npy) + nFUK.error_reduce(addr_npy, err_fourier_npy) + nFUK.fmag_all_update(aux_npy, addr_npy, mag_npy, ma_npy, err_fourier_npy, pbound=0.5) + np.testing.assert_array_almost_equal_nulp(nFUK.npy.fdev, FUK.npy.fdev.get()) + # err_msg="The gpu fdev differs more than single precision allows.") + np.testing.assert_array_almost_equal_nulp(nFUK.npy.ferr, FUK.npy.ferr.get()) + # err_msg="The gpu ferr differs more than single precision allows.") + np.testing.assert_array_almost_equal_nulp(aux_npy, aux_dev.get(), 80) #, + # err_msg="The gpu auxiliary_wave differs more than single precision allows.") + + """ + def test_build_aux_same_as_exit_REGRESSION(self): + ''' + setup + ''' + B = 3 # frame size y + C = 3 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + ex_npy = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + ex_npy[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + auxiliary_wave = np.zeros_like(ex_npy) + + AWK = AuxiliaryWaveKernel() + alpha_set = 1.0 + AWK.configure(object_array, addr, alpha=alpha_set) + + object_array_dev = gpuarray.to_gpu(object_array) + probe_dev = gpuarray.to_gpu(probe) + addr_dev = gpuarray.to_gpu(addr) + auxiliary_wave_dev = gpuarray.to_gpu(auxiliary_wave) + ex_npy_dev = gpuarray.to_gpu(ex_npy) + + AWK.build_aux(auxiliary_wave_dev, object_array_dev, probe_dev, ex_npy_dev, addr_dev) + + + expected_auxiliary_wave = np.array([[[-1. + 3.j, -1. + 3.j, -1. + 3.j], + [-1. + 3.j, -1. + 3.j, -1. + 3.j], + [-1. + 3.j, -1. + 3.j, -1. + 3.j]], + [[-2.+14.j, -2.+14.j, -2.+14.j], + [-2.+14.j, -2.+14.j, -2.+14.j], + [-2.+14.j, -2.+14.j, -2.+14.j]], + [[-3. + 5.j, -3. + 5.j, -3. + 5.j], + [-3. + 5.j, -3. + 5.j, -3. + 5.j], + [-3. + 5.j, -3. + 5.j, -3. + 5.j]], + [[-4.+28.j, -4.+28.j, -4.+28.j], + [-4.+28.j, -4.+28.j, -4.+28.j], + [-4.+28.j, -4.+28.j, -4.+28.j]], + [[-5. - 1.j, -5. - 1.j, -5. - 1.j], + [-5. - 1.j, -5. - 1.j, -5. - 1.j], + [-5. - 1.j, -5. - 1.j, -5. - 1.j]], + [[-6.+10.j, -6.+10.j, -6.+10.j], + [-6.+10.j, -6.+10.j, -6.+10.j], + [-6.+10.j, -6.+10.j, -6.+10.j]], + [[-7. + 1.j, -7. + 1.j, -7. + 1.j], + [-7. + 1.j, -7. + 1.j, -7. + 1.j], + [-7. + 1.j, -7. + 1.j, -7. + 1.j]], + [[-8.+24.j, -8.+24.j, -8.+24.j], + [-8.+24.j, -8.+24.j, -8.+24.j], + [-8.+24.j, -8.+24.j, -8.+24.j]], + [[-9. - 5.j, -9. - 5.j, -9. - 5.j], + [-9. - 5.j, -9. - 5.j, -9. - 5.j], + [-9. - 5.j, -9. - 5.j, -9. - 5.j]], + [[-10. + 6.j, -10. + 6.j, -10. + 6.j], + [-10. + 6.j, -10. + 6.j, -10. + 6.j], + [-10. + 6.j, -10. + 6.j, -10. + 6.j]], + [[-11. - 3.j, -11. - 3.j, -11. - 3.j], + [-11. - 3.j, -11. - 3.j, -11. - 3.j], + [-11. - 3.j, -11. - 3.j, -11. - 3.j]], + [[-12.+20.j, -12.+20.j, -12.+20.j], + [-12.+20.j, -12.+20.j, -12.+20.j], + [-12.+20.j, -12.+20.j, -12.+20.j]], + [[-13. - 9.j, -13. - 9.j, -13. - 9.j], + [-13. - 9.j, -13. - 9.j, -13. - 9.j], + [-13. - 9.j, -13. - 9.j, -13. - 9.j]], + [[-14. + 2.j, -14. + 2.j, -14. + 2.j], + [-14. + 2.j, -14. + 2.j, -14. + 2.j], + [-14. + 2.j, -14. + 2.j, -14. + 2.j]], + [[-15. - 7.j, -15. - 7.j, -15. - 7.j], + [-15. - 7.j, -15. - 7.j, -15. - 7.j], + [-15. - 7.j, -15. - 7.j, -15. - 7.j]], + [[-16.+16.j, -16.+16.j, -16.+16.j], + [-16.+16.j, -16.+16.j, -16.+16.j], + [-16.+16.j, -16.+16.j, -16.+16.j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(expected_auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The auxiliary_wave has not been updated as expected") + + object_array_dev.gpudata.free() + auxiliary_wave_dev.gpudata.free() + probe_dev.gpudata.free() + ex_npy_dev.gpudata.free() + addr_dev.gpudata.free() + + def test_build_aux_same_as_exit_UNITY(self): + ''' + setup + ''' + B = 3 # frame size y + C = 3 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + ex_npy = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + ex_npy[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + auxiliary_wave = np.zeros_like(ex_npy) + from ptypy.accelerate.base.auxiliary_wave_kernel import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + AWK = AuxiliaryWaveKernel() + alpha_set = 1.0 + AWK.configure(object_array, addr, alpha=alpha_set) + nAWK.configure(object_array, addr, alpha=alpha_set) + + object_array_dev = gpuarray.to_gpu(object_array) + probe_dev = gpuarray.to_gpu(probe) + addr_dev = gpuarray.to_gpu(addr) + auxiliary_wave_dev = gpuarray.to_gpu(auxiliary_wave) + ex_npy_dev = gpuarray.to_gpu(ex_npy) + + AWK.build_aux(auxiliary_wave_dev, object_array_dev, probe_dev, ex_npy_dev, addr_dev) + nAWK.build_aux(auxiliary_wave, object_array, probe, ex_npy, addr) + + + np.testing.assert_array_equal(auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + object_array_dev.gpudata.free() + auxiliary_wave_dev.gpudata.free() + probe_dev.gpudata.free() + ex_npy_dev.gpudata.free() + addr_dev.gpudata.free() + + def test_build_exit_aux_same_as_exit_REGRESSION(self): + ''' + setup + ''' + B = 3 # frame size y + C = 3 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + ex_npy = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + ex_npy[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + auxiliary_wave = np.zeros_like(ex_npy) + + object_array_dev = gpuarray.to_gpu(object_array) + probe_dev = gpuarray.to_gpu(probe) + addr_dev = gpuarray.to_gpu(addr) + auxiliary_wave_dev = gpuarray.to_gpu(auxiliary_wave) + ex_npy_dev = gpuarray.to_gpu(ex_npy) + AWK = AuxiliaryWaveKernel() + + alpha_set = 1.0 + AWK.configure(object_array, addr, alpha=alpha_set) + + AWK.build_exit(auxiliary_wave_dev, object_array_dev, probe_dev, ex_npy_dev, addr_dev) + # + # print("auxiliary_wave after") + # print(repr(auxiliary_wave_dev.get())) + # + # print("ex_npy after") + # print(repr(ex_npy)) + + expected_auxiliary_wave = np.array([[[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]], + [[0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j], + [0. - 2.j, 0. - 2.j, 0. - 2.j]], + [[0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j], + [0. - 8.j, 0. - 8.j, 0. - 8.j]], + [[0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j], + [0. - 4.j, 0. - 4.j, 0. - 4.j]], + [[0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j], + [0.-16.j, 0.-16.j, 0.-16.j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(expected_auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The auxiliary_wave has not been updated as expected") + + expected_ex_npy = np.array([[[1. - 1.j, 1. - 1.j, 1. - 1.j], + [1. - 1.j, 1. - 1.j, 1. - 1.j], + [1. - 1.j, 1. - 1.j, 1. - 1.j]], + [[2. - 6.j, 2. - 6.j, 2. - 6.j], + [2. - 6.j, 2. - 6.j, 2. - 6.j], + [2. - 6.j, 2. - 6.j, 2. - 6.j]], + [[3. - 1.j, 3. - 1.j, 3. - 1.j], + [3. - 1.j, 3. - 1.j, 3. - 1.j], + [3. - 1.j, 3. - 1.j, 3. - 1.j]], + [[4. - 12.j, 4. - 12.j, 4. - 12.j], + [4. - 12.j, 4. - 12.j, 4. - 12.j], + [4. - 12.j, 4. - 12.j, 4. - 12.j]], + [[5. + 3.j, 5. + 3.j, 5. + 3.j], + [5. + 3.j, 5. + 3.j, 5. + 3.j], + [5. + 3.j, 5. + 3.j, 5. + 3.j]], + [[6. - 2.j, 6. - 2.j, 6. - 2.j], + [6. - 2.j, 6. - 2.j, 6. - 2.j], + [6. - 2.j, 6. - 2.j, 6. - 2.j]], + [[7. + 3.j, 7. + 3.j, 7. + 3.j], + [7. + 3.j, 7. + 3.j, 7. + 3.j], + [7. + 3.j, 7. + 3.j, 7. + 3.j]], + [[8. - 8.j, 8. - 8.j, 8. - 8.j], + [8. - 8.j, 8. - 8.j, 8. - 8.j], + [8. - 8.j, 8. - 8.j, 8. - 8.j]], + [[9. + 7.j, 9. + 7.j, 9. + 7.j], + [9. + 7.j, 9. + 7.j, 9. + 7.j], + [9. + 7.j, 9. + 7.j, 9. + 7.j]], + [[10. + 2.j, 10. + 2.j, 10. + 2.j], + [10. + 2.j, 10. + 2.j, 10. + 2.j], + [10. + 2.j, 10. + 2.j, 10. + 2.j]], + [[11. + 7.j, 11. + 7.j, 11. + 7.j], + [11. + 7.j, 11. + 7.j, 11. + 7.j], + [11. + 7.j, 11. + 7.j, 11. + 7.j]], + [[12. - 4.j, 12. - 4.j, 12. - 4.j], + [12. - 4.j, 12. - 4.j, 12. - 4.j], + [12. - 4.j, 12. - 4.j, 12. - 4.j]], + [[13. + 11.j, 13. + 11.j, 13. + 11.j], + [13. + 11.j, 13. + 11.j, 13. + 11.j], + [13. + 11.j, 13. + 11.j, 13. + 11.j]], + [[14. + 6.j, 14. + 6.j, 14. + 6.j], + [14. + 6.j, 14. + 6.j, 14. + 6.j], + [14. + 6.j, 14. + 6.j, 14. + 6.j]], + [[15. + 11.j, 15. + 11.j, 15. + 11.j], + [15. + 11.j, 15. + 11.j, 15. + 11.j], + [15. + 11.j, 15. + 11.j, 15. + 11.j]], + [[16. + 0.j, 16. + 0.j, 16. + 0.j], + [16. + 0.j, 16. + 0.j, 16. + 0.j], + [16. + 0.j, 16. + 0.j, 16. + 0.j]]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(expected_ex_npy, ex_npy_dev.get(), + err_msg="The ex_npy has not been updated as expected") + + object_array_dev.gpudata.free() + auxiliary_wave_dev.gpudata.free() + probe_dev.gpudata.free() + ex_npy_dev.gpudata.free() + addr_dev.gpudata.free() + + def test_build_exit_aux_same_as_exit_UNITY(self): + ''' + setup + ''' + B = 3 # frame size y + C = 3 # frame size x + + D = 2 # number of probe modes + E = B # probe size y + F = C # probe size x + + npts_greater_than = 2 # how many points bigger than the probe the object is. + G = 2 # number of object modes + H = B + npts_greater_than # object size y + I = C + npts_greater_than # object size x + + scan_pts = 2 # one dimensional scan point number + + total_number_scan_positions = scan_pts ** 2 + total_number_modes = G * D + A = total_number_scan_positions * total_number_modes # this is a 16 point scan pattern (4x4 grid) over all the modes + + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + + object_array = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + object_array[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + + ex_npy = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + ex_npy[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((total_number_scan_positions)) + Y = Y.reshape((total_number_scan_positions)) + + addr = np.zeros((total_number_scan_positions, total_number_modes, 5, 3), dtype=INT_TYPE) + + exit_idx = 0 + position_idx = 0 + for xpos, ypos in zip(X, Y):# + mode_idx = 0 + for pr_mode in range(D): + for ob_mode in range(G): + addr[position_idx, mode_idx] = np.array([[pr_mode, 0, 0], + [ob_mode, ypos, xpos], + [exit_idx, 0, 0], + [0, 0, 0], + [0, 0, 0]], dtype=INT_TYPE) + mode_idx += 1 + exit_idx += 1 + position_idx += 1 + + ''' + test + ''' + auxiliary_wave = np.zeros_like(ex_npy) + + object_array_dev = gpuarray.to_gpu(object_array) + probe_dev = gpuarray.to_gpu(probe) + addr_dev = gpuarray.to_gpu(addr) + auxiliary_wave_dev = gpuarray.to_gpu(auxiliary_wave) + ex_npy_dev = gpuarray.to_gpu(ex_npy) + + from ptypy.accelerate.base.auxiliary_wave_kernel import AuxiliaryWaveKernel as npAuxiliaryWaveKernel + nAWK = npAuxiliaryWaveKernel() + + AWK = AuxiliaryWaveKernel() + + alpha_set = 1.0 + + AWK.configure(object_array, addr, alpha=alpha_set) + nAWK.configure(object_array, addr, alpha=alpha_set) + + AWK.build_exit(auxiliary_wave_dev, object_array_dev, probe_dev, ex_npy_dev, addr_dev) + nAWK.build_exit(auxiliary_wave, object_array, probe, ex_npy, addr) + + np.testing.assert_array_equal(auxiliary_wave, auxiliary_wave_dev.get(), + err_msg="The gpu auxiliary_wave does not look the same as the numpy version") + + np.testing.assert_array_equal(ex_npy, ex_npy_dev.get(), + err_msg="The gpu ex_npy does not look the same as the numpy version") + + object_array_dev.gpudata.free() + auxiliary_wave_dev.gpudata.free() + probe_dev.gpudata.free() + ex_npy_dev.gpudata.free() + addr_dev.gpudata.free() + """ + + +if __name__ == '__main__': + unittest.main() diff --git a/test/archive_tests/__init__.py b/test/archive_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/archive_tests/array_based_tests/__init__.py b/test/archive_tests/array_based_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/archive_tests/array_based_tests/constraints_regression_test.py b/test/archive_tests/array_based_tests/constraints_regression_test.py new file mode 100644 index 000000000..dcaa5e3a4 --- /dev/null +++ b/test/archive_tests/array_based_tests/constraints_regression_test.py @@ -0,0 +1,977 @@ +''' +The tests for the constraints +''' + + +import unittest +import numpy as np +from copy import deepcopy +from ptypy.accelerate.array_based import constraints as con, FLOAT_TYPE, COMPLEX_TYPE + +class ConstraintsRegressionTest(unittest.TestCase): + ''' + a module to holds the constraints + ''' + + def test_renormalise_fourier_magnitudes_pbound_none(self): + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + pbound = None # the power bound + + fmag = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE)# the measured magnitudes NxAxB + mask = np.empty(shape=(N, A, B), dtype=np.int32)# the masks for the measured magnitudes either 1xAxB or NxAxB + err_fmag = np.empty(shape=(N, ), dtype=FLOAT_TYPE)# deviation from the diffraction pattern for each af + f = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # the current iterant + af = np.empty(shape=(M, A, B), dtype=FLOAT_TYPE)# the absolute magnitudes of f + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32)# the address book + + ## now lets fill them with some values, these are junk and not supposed to be indicative of real values. + + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + err_fmag[:] = np.ones((N,)) # this shouldn't be used a pbound is None + + f_fill = np.array([ix + 1j*(ix**2) for ix in range(np.prod(f.shape))]).reshape((M, A, B)) + f[:] = f_fill + + af[:] = np.sqrt((f*f.conj()).real) + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + ma = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + expected_out = np.array([[[0.0 + 0.0j, 1.0 + 1.0j, 2.0 + 4.0j, 3.0 + 9.0j], + [0.97014252 + 3.88057009j, 0.98058065 + 4.90290327e+00j, 0.98639394 + 5.91836367e+00j, 0.98994949 + 6.92964646e+00j]], + [[0.99227787 + 7.93822300j, 0.99388373 + 8.94495358j, 0.99503719 + 9.95037188j, 0.99589322 + 10.9548254j], + [0.99654579 + 1.19585495e+01j, 0.99705446 + 1.29617079e+01j, 0.99745872 + 1.39644221e+01j, 0.99778514 + 1.49667770e+01j]], + [[16.00000000 + 2.56000000e+02j, 17.00000000 + 2.89000000e+02j, 18.00000000 + 3.24000000e+02j,19.00000000 + 3.61000000e+02j], + [0.99875232 + 1.99750464e+01j, 0.99886812 + 2.09762305e+01j, 0.99896851 + 2.19773073e+01j, 0.99905617 + 2.29782920e+01j]], + [[24.00000000 + 5.76000000e+02j, 25.00000000 + 6.25000000e+02j, 26.00000000 + 6.76000000e+02j, 27.00000000 + 7.29000000e+02j], + [6.79099767 + 1.90147935e+02j, 5.68736780 + 1.64933666e+02j, 4.93196972 + 1.47959092e+02j, 4.38406204 + 1.35905923e+02j]], + [[3.96911150 + 1.27011568e+02j, 3.64424035 + 1.20259932e+02j, 3.38312644 + 1.15026299e+02j, 3.16875114 + 1.10906290e+02j], + [2.98963737 + 1.07626945e+02j, 2.83777037 + 1.04997504e+02j, 2.70738796 + 1.02880743e+02j, 2.59424135 + 1.01175413e+02j]], + [[40.00000000 + 1.60000000e+03j, 41.00000000 + 1.68100000e+03j, 42.00000000 + 1.76400000e+03j, 43.00000000 + 1.84900000e+03j], + [2.19725511 + 9.66792247e+01j, 2.14043168 + 9.63194257e+01j, 2.08875234 + 9.60826078e+01j, 2.04154957 + 9.59528299e+01j]]], dtype=COMPLEX_TYPE) + + + out = con.renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + np.testing.assert_allclose(out, expected_out, rtol=1e-6) + + + def test_renormalise_fourier_magnitudes_pbound_not_none(self): + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + pbound = 5.0 # the power bound + + fmag = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE)# the measured magnitudes NxAxB + mask = np.empty(shape=(N, A, B), dtype=np.int32)# the masks for the measured magnitudes either 1xAxB or NxAxB + err_fmag = np.empty(shape=(N, ), dtype=FLOAT_TYPE)# deviation from the diffraction pattern for each af + f = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # the current iterant + af = np.empty(shape=(M, A, B), dtype=FLOAT_TYPE)# the absolute magnitudes of f + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32)# the address book + + ## now lets fill them with some values, these are junk and not supposed to be indicative of real values. + + fmag_fill = np.arange(np.prod(fmag.shape)).reshape(fmag.shape).astype(fmag.dtype) + fmag[:] = fmag_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + err_fmag_fill = np.ones((N,))*(pbound+0.1) # should be greater than the pbound + err_fmag_fill[N//2] = 4.0 # this one should be less than the pbound and not update + err_fmag[:] = err_fmag_fill # this shouldn't be used a pbound is None + + f_fill = np.array([ix + 1j*(ix**2) for ix in range(np.prod(f.shape))]).reshape((M, A, B)) + f[:] = f_fill + + af[:] = np.sqrt((f*f.conj()).real) + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + ma = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + expected_out = np.array([[[0.0 +0.0j, 1.0 +1.0j, 2.0 +4.0j, 3.0 +9.0j], + [3.97014832 + 1.58805933e+01j, 4.96039867 +2.48019943e+01j, 5.95060349 +3.57036209e+01j, 6.94078636 +4.85855026e+01j]], + [[0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], + [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j]], + [[16.0 + 2.56e+02j, 17.0 + 2.89e+02j, 18.0 + 3.24e+02j, 19.0 + 3.61e+02j], + [19.81278992 + 3.96255798e+02j, 20.80293846 +4.36861725e+02j, 21.79308891 + 4.79447937e+02j, 22.78323746 + 5.24014465e+02j]], + [[ 24.0 + 5.76e+02j, 25.0 +6.25e+02j, 26.0 +6.76e+02j, 27.0 + 7.29e+02j], + [27.79103851 +7.78149109e+02j, 28.77031326 +8.34339111e+02j, 29.75301552 + 8.92590515e+02j, 30.73776817 + 9.52870789e+02j]], + [[0.0 + 0.0j, 0.00 + 0.00e+00j, 0.00 +0.00e+00j, 0.00 + 0.00e+00j], + [0.00 +0.0j, 0.00 + 0.00j, 0.00 + 0.00j, 0.00 + 0.00j]], + [[40.00 +1.60e+03j, 41.00 + 1.681e+03j, 42.0 +1.764e+03j, 43.0 + 1.849e+03j], + [43.58813858 + 1.91787805e+03j, 44.57772446 + 2.00599768e+03j, 45.56736755 +2.09609888e+03j, 46.55705261 + 2.18818140e+03j]]], dtype=COMPLEX_TYPE) + + + out = con.renormalise_fourier_magnitudes(f, af, fmag, mask, err_fmag, addr_info, pbound) + np.testing.assert_allclose(out, expected_out) + + def test_get_difference_pbound_is_none(self): + alpha = 1.0 # feedback constant + pbound = 5.0 # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + + backpropagated_solution = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE)# The current iterant backpropagated + probe_object = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE)# the probe multiplied by the object + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE)# deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32)# the address book + + # now fill it with stuff + + backpropagated_solution_fill = np.array([ix + 1j*(ix**2) for ix in range(np.prod(backpropagated_solution.shape))]).reshape((M, A, B)) + backpropagated_solution[:] = backpropagated_solution_fill + + probe_object_fill = np.array([ix + 1j*ix for ix in range(10, 10+np.prod(backpropagated_solution.shape), 1)]).reshape((M, A, B)) + probe_object[:] = probe_object_fill + + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array([ix**2 + 1j*ix for ix in range(20, 20+np.prod(backpropagated_solution.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + expected_out = np.array([[[-390.-10.j, -430.-10.j, -472.-10.j, -516.-10.j], + [-562.-10.j, -610.-10.j, -660.-10.j, -712.-10.j]], + [[-766.-10.j, -822.-10.j, -880.-10.j, -940.-10.j], + [-1002.-10.j, -1066.-10.j, -1132.-10.j, -1200.-10.j]], + [[-1270.-10.j, -1342.-10.j, -1416.-10.j, -1492.-10.j], + [-1570.-10.j, -1650.-10.j, -1732.-10.j, -1816.-10.j]], + [[-1902.-10.j, -1990.-10.j, -2080.-10.j, -2172.-10.j], + [-2266.-10.j, -2362.-10.j, -2460.-10.j, -2560.-10.j]], + [[-2662.-10.j, -2766.-10.j, -2872.-10.j, -2980.-10.j], + [-3090.-10.j, -3202.-10.j, -3316.-10.j, -3432.-10.j]], + [[-3550.-10.j, -3670.-10.j, -3792.-10.j, -3916.-10.j], + [-4042.-10.j, -4170.-10.j, -4300.-10.j, -4432.-10.j]]], dtype=COMPLEX_TYPE) + + out = con.get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, probe_object) + np.testing.assert_allclose(expected_out, out) + + def test_get_difference_pbound_is_not_none(self): + alpha = 1.0 # feedback constant + pbound = 5.0 # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 3 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + + backpropagated_solution = np.empty(shape=(M, A, B), + dtype=COMPLEX_TYPE) # The current iterant backpropagated + probe_object = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # the probe multiplied by the object + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + + # now fill it with stuff + + backpropagated_solution_fill = np.array( + [ix + 1j * (ix ** 2) for ix in range(np.prod(backpropagated_solution.shape))]).reshape((M, A, B)) + backpropagated_solution[:] = backpropagated_solution_fill + + probe_object_fill = np.array( + [ix + 1j * ix for ix in range(10, 10 + np.prod(backpropagated_solution.shape), 1)]).reshape((M, A, B)) + probe_object[:] = probe_object_fill + + err_fmag_fill = np.ones((N,))*(pbound+0.1) # should be higher than pbound + err_fmag_fill[N // 2] = 4.0# except for this one!! + err_fmag[:] = err_fmag_fill + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(backpropagated_solution.shape), 1)]).reshape( + (M, A, B)) + exit_wave[:] = exit_wave_fill + + pa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + oa = np.zeros((M, 3), dtype=np.int32) # not going to be used here + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + expected_out = np.array([[[-10.0 -10.0j, -10.0 -10.0j, -10.0 -8.0j, -10.0 -4.0j], + [-10.0 + 2.0j, -10.0 +10.0j, -10.0 +20.0j, -10. +32.0j]], + [[-766.0 -10.0j, -822.0 -10.00j, -880.0 -10.0j, -940.0 -10.0j], + [-1002.0 -10.0j, -1066.0 -10.0j, -1132.0 -10.0j, -1200.0 -10.0j]], + [[-10.0 +230.0j, -10.0 +262.0j, -10.0 +296.0j, -10.0 +332.0j], + [-10.0 +370.0j, -10.0 +410.0j, -10.0 +452.0j, -10.0 +496.0j]], + [[-10.0 +542.0j, -10.0 +590.0j, -10.0 +640.0j, -10.0 +692.0j], + [-10.0 +746.0j, -10.0 +802.0j, -10.0 +860.0j, -10.0 +920.0j]], + [[-2662.0 -10.0j, -2766.0 -10.0j, -2872.0 -10.0j,-2980.0 -10.0j], + [-3090.0 -10.0j, -3202.0 -10.0j, -3316.0 -10.0j, -3432.0 -10.0j]], + [[-10.0 +1550.0j, -10.0 +1630.0j, -10.0 +1712.0j, -10.0 +1796.0j], + [-10.0 +1882.0j, -10.0 +1970.0j, -10.0 +2060.0j,-10.0 +2152.0j]]]) + + out = con.get_difference(addr_info, alpha, backpropagated_solution, err_fmag, exit_wave, pbound, + probe_object) + + np.testing.assert_allclose(expected_out, out) + + + def test_difference_map_fourier_constraint_pbound_is_none_with_realspace_error_and_LL_error(self): + + alpha = 1.0 # feedback constant + pbound = None # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE)# deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32)# the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE)# the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), dtype=np.int32)# the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j*(ix**2) for ix in range(np.prod(obj.shape))]).reshape((num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j*ix for ix in range(10, 10+np.prod(probe.shape), 1)]).reshape((num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j)# this would actually vary + postfilter.fill(20.0 + 3.0j)# this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array([ix**2 + 1j*ix for ix in range(20, 20+np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx>0: + pa[::idx,0]=idx # multimodal could work like this, but this is not a concrete thing. + + + X, Y = np.meshgrid(range(npts_greater_than), range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx>0: + oa[::idx,0]=idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)]*num_probe_modes*num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + expected_out = np.array([[6.07364329e+12, 8.87756439e+13, 9.12644403e+12, 1.39851186e+14], + [6.38221460e+23, 8.94021509e+24, 3.44468266e+23, 6.03329184e+24], + [3.88072739e+18, 2.67132771e+19, 9.15414239e+18,4.41441340e+19]], dtype=FLOAT_TYPE) + + expected_ew = np.array([[[-4.24456960e+08 +3.33502272e+08j, -5.54233664e+08 +4.81765952e+08j, -7.26160640e+08 +6.74398016e+08j, -9.44673984e+08 +9.15834816e+08j], + [-4.24455232e+08 +3.33500608e+08j, -5.54232768e+08 +4.81764832e+08j, -7.26159680e+08 +6.74396992e+08j, -9.44673792e+08 +9.15834752e+08j]], + [[-1.60761126e+09 +1.53736205e+09j, -1.97845542e+09 +1.92965094e+09j, -2.40919706e+09 +2.38405555e+09j, -2.90427264e+09 +2.90501248e+09j], + [-1.60760742e+09 +1.53735821e+09j, -1.97845261e+09 +1.92964813e+09j, -2.40919450e+09 +2.38405299e+09j, -2.90427085e+09 +2.90501120e+09j]], + [[-8.77013248e+08 +4.54775968e+08j, -1.05411565e+09 +6.40012672e+08j, -1.27632653e+09 +8.72576064e+08j, -1.54808115e+09 +1.15690163e+09j], + [-8.77010560e+08 +4.54773344e+08j, -1.05411462e+09 +6.40011584e+08j, -1.27632589e+09 +8.72575488e+08j, -1.54808205e+09 +1.15690304e+09j]], + [[-2.33229235e+09 +1.83610880e+09j, -2.75933594e+09 +2.27424461e+09j, -3.24923520e+09 +2.77745434e+09j, -3.80642586e+09 +3.35017344e+09j], + [-2.33228800e+09 +1.83610432e+09j, -2.75933338e+09 +2.27424154e+09j, -3.24923290e+09 +2.77745203e+09j, -3.80642509e+09 +3.35017293e+09j]], + [[-1.32365261e+09 +3.21670784e+08j, -1.47709235e+09 +4.69934400e+08j, -1.67268250e+09 +6.62566528e+08j, -1.91485875e+09 +9.04003328e+08j], + [-1.32365056e+09 +3.21669120e+08j, -1.47709133e+09 +4.69933312e+08j, -1.67268122e+09 +6.62565568e+08j, -1.91485837e+09 +9.04003328e+08j]], + [[-2.69611136e+09 +1.52553024e+09j, -3.09061837e+09 +1.91781939e+09j, -3.54502349e+09 +2.37222400e+09j, -4.06376115e+09 +2.89318067e+09j], + [-2.69610726e+09 +1.52552653e+09j, -3.09061555e+09 +1.91781658e+09j, -3.54502042e+09 +2.37222144e+09j, -4.06375987e+09 +2.89317965e+09j]], + [[-2.15481728e+09 +4.42944416e+08j, -2.35558246e+09 +6.28181120e+08j, -2.60145664e+09 +8.60744448e+08j, -2.89687424e+09 +1.14507034e+09j], + [-2.15481421e+09 +4.42941824e+08j, -2.35558118e+09 +6.28180096e+08j, -2.60145562e+09 +8.60743936e+08j, -2.89687475e+09 +1.14507149e+09j]], + [[-3.79940096e+09 +1.82427738e+09j, -4.25010765e+09 +2.26241280e+09j, -4.76367002e+09 +2.76562253e+09j, -5.34452326e+09 +3.33834163e+09j], + [-3.79939610e+09 +1.82427277e+09j, -4.25010458e+09 +2.26240998e+09j, -4.76366746e+09 +2.76562022e+09j, -5.34452275e+09 +3.33834138e+09j]]], dtype= COMPLEX_TYPE) + + out = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, postfilter, pbound=pbound, alpha=alpha, LL_error=True, do_realspace_error=True) + np.testing.assert_allclose(out, + expected_out, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(exit_wave, + expected_ew, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + def test_difference_map_fourier_constraint_pbound_is_none_no_error(self): + + alpha = 1.0 # feedback constant + pbound = None # the power bound + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx > 0: + pa[::idx, 0] = idx # multimodal could work like this, but this is not a concrete thing. + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx > 0: + oa[::idx, + 0] = idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + expected_out = np.array([[6.07364329e+12, 8.87756439e+13, 9.12644403e+12, 1.39851186e+14], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0]], + dtype=FLOAT_TYPE) + + expected_ew = np.array([[[-4.24456960e+08 + 3.33502272e+08j, -5.54233664e+08 + 4.81765952e+08j, + -7.26160640e+08 + 6.74398016e+08j, -9.44673984e+08 + 9.15834816e+08j], + [-4.24455232e+08 + 3.33500608e+08j, -5.54232768e+08 + 4.81764832e+08j, + -7.26159680e+08 + 6.74396992e+08j, -9.44673792e+08 + 9.15834752e+08j]], + [[-1.60761126e+09 + 1.53736205e+09j, -1.97845542e+09 + 1.92965094e+09j, + -2.40919706e+09 + 2.38405555e+09j, -2.90427264e+09 + 2.90501248e+09j], + [-1.60760742e+09 + 1.53735821e+09j, -1.97845261e+09 + 1.92964813e+09j, + -2.40919450e+09 + 2.38405299e+09j, -2.90427085e+09 + 2.90501120e+09j]], + [[-8.77013248e+08 + 4.54775968e+08j, -1.05411565e+09 + 6.40012672e+08j, + -1.27632653e+09 + 8.72576064e+08j, -1.54808115e+09 + 1.15690163e+09j], + [-8.77010560e+08 + 4.54773344e+08j, -1.05411462e+09 + 6.40011584e+08j, + -1.27632589e+09 + 8.72575488e+08j, -1.54808205e+09 + 1.15690304e+09j]], + [[-2.33229235e+09 + 1.83610880e+09j, -2.75933594e+09 + 2.27424461e+09j, + -3.24923520e+09 + 2.77745434e+09j, -3.80642586e+09 + 3.35017344e+09j], + [-2.33228800e+09 + 1.83610432e+09j, -2.75933338e+09 + 2.27424154e+09j, + -3.24923290e+09 + 2.77745203e+09j, -3.80642509e+09 + 3.35017293e+09j]], + [[-1.32365261e+09 + 3.21670784e+08j, -1.47709235e+09 + 4.69934400e+08j, + -1.67268250e+09 + 6.62566528e+08j, -1.91485875e+09 + 9.04003328e+08j], + [-1.32365056e+09 + 3.21669120e+08j, -1.47709133e+09 + 4.69933312e+08j, + -1.67268122e+09 + 6.62565568e+08j, -1.91485837e+09 + 9.04003328e+08j]], + [[-2.69611136e+09 + 1.52553024e+09j, -3.09061837e+09 + 1.91781939e+09j, + -3.54502349e+09 + 2.37222400e+09j, -4.06376115e+09 + 2.89318067e+09j], + [-2.69610726e+09 + 1.52552653e+09j, -3.09061555e+09 + 1.91781658e+09j, + -3.54502042e+09 + 2.37222144e+09j, -4.06375987e+09 + 2.89317965e+09j]], + [[-2.15481728e+09 + 4.42944416e+08j, -2.35558246e+09 + 6.28181120e+08j, + -2.60145664e+09 + 8.60744448e+08j, -2.89687424e+09 + 1.14507034e+09j], + [-2.15481421e+09 + 4.42941824e+08j, -2.35558118e+09 + 6.28180096e+08j, + -2.60145562e+09 + 8.60743936e+08j, -2.89687475e+09 + 1.14507149e+09j]], + [[-3.79940096e+09 + 1.82427738e+09j, -4.25010765e+09 + 2.26241280e+09j, + -4.76367002e+09 + 2.76562253e+09j, -5.34452326e+09 + 3.33834163e+09j], + [-3.79939610e+09 + 1.82427277e+09j, -4.25010458e+09 + 2.26240998e+09j, + -4.76366746e+09 + 2.76562022e+09j, -5.34452275e+09 + 3.33834138e+09j]]], + dtype=COMPLEX_TYPE) + + out = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=False, + do_realspace_error=False) + np.testing.assert_allclose(out, + expected_out, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(exit_wave, + expected_ew, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + + def test_difference_map_fourier_constraint_pbound_is_not_none_with_realspace_and_LL_error(self): + ''' + mixture of high and low p bound values respect to the fourier error + ''' + #expected_fourier_error = np.array([6.07364329e+12, 8.87756439e+13, 9.12644403e+12, 1.39851186e+14]) + pbound = 8.86e13 # this should now mean some of the arrays update differently through the logic + alpha = 1.0 # feedback constant + num_object_modes = 1 # for example + num_probe_modes = 2 # for example + + N = 4 # number of measured points + M = N * num_object_modes * num_probe_modes # exit wave length + A = 2 # for example + B = 4 # for example + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + C = A + npts_greater_than + D = B + npts_greater_than + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(M, A, B), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(M, 5, 3), dtype=np.int32) # the address book + Idata = np.empty(shape=(N, A, B), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, A, B), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(num_probe_modes, A, B), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(num_object_modes, C, D), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(A, B), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + Idata_fill = np.arange(np.prod(Idata.shape)).reshape(Idata.shape).astype(Idata.dtype) + Idata[:] = Idata_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (num_object_modes, C, D)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (num_probe_modes, A, B)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((M, A, B)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((M, 3), dtype=np.int32) + for idx in range(num_probe_modes): + if idx > 0: + pa[::idx, 0] = idx # multimodal could work like this, but this is not a concrete thing. + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((M, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + for idx in range(num_object_modes): + if idx > 0: + oa[::idx,0] = idx # multimodal could work like this, but this is not a concrete thing (less likely for object) + ea = np.array([np.array([ix, 0, 0]) for ix in range(M)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * num_probe_modes * num_object_modes) + ma = np.zeros((M, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + + expected_out = np.array([[ 0.06855128, 1.00198244, 0.10300727, 1.57845582], + [6.38221460e+23, 8.94021509e+24, 3.44468266e+23, 6.03329184e+24], + [1.89878600e+07, 3.28113995e+19, 4.72013640e+07, 4.89360300e+19]], dtype=FLOAT_TYPE) + + expected_ew = np.array([[[0.00000000e+00 + 0.00000000e+00j, 0.00000000e+00 + 3.80000000e+01j, -4.00000000e+01 + 1.20000000e+02j, - 1.26000000e+02 + 2.52000000e+02j], + [-6.60000000e+02 + 9.24000000e+02j, - 9.66000000e+02 + 1.28800000e+03j, -1.34400000e+03 + 1.72800000e+03j, - 1.80000000e+03 + 2.25000000e+03j]], + [[-6.90095424e+08 + 5.49665856e+08j, - 9.02111168e+08 + 7.77216384e+08j, -1.16220429e+09 + 1.05506266e+09j, - 1.47481139e+09 + 1.38764147e+09j], + [-2.52512358e+09 + 2.52505446e+09j, - 3.05479731e+09 + 3.08208256e+09j, -3.65618765e+09 + 3.71304602e+09j, - 4.33373184e+09 + 4.42238157e+09j]], + [[0.00000000e+00 + 3.60000000e+01j, - 3.80000000e+01 + 1.14000000e+02j, -1.20000000e+02 + 2.40000000e+02j, - 2.52000000e+02 + 4.20000000e+02j], + [-9.24000000e+02 + 1.23200000e+03j, - 1.28800000e+03 + 1.65600000e+03j, -1.72800000e+03 + 2.16000000e+03j, - 2.25000000e+03 + 2.75000000e+03j]], + [[-1.49062272e+09 + 9.55004032e+08j, - 1.78523661e+09 + 1.25600128e+09j, -2.13328819e+09 + 1.61265498e+09j, - 2.53921434e+09 + 2.02940147e+09j], + [-3.17395840e+09 + 2.71720909e+09j, - 3.73343309e+09 + 3.29248486e+09j, -4.36517990e+09 + 3.94225101e+09j, - 5.07363635e+09 + 4.67094528e+09j]], + [[0.00000000e+00 + 0.00000000e+00j, 0.00000000e+00 + 3.80000000e+01j, -4.00000000e+01 + 1.20000000e+02j, - 1.26000000e+02 + 2.52000000e+02j], + [-6.60000000e+02 + 9.24000000e+02j, - 9.66000000e+02 + 1.28800000e+03j, -1.34400000e+03 + 1.72800000e+03j, - 1.80000000e+03 + 2.25000000e+03j]], + [[-1.73131635e+09 + 5.37834304e+08j, - 1.96699507e+09 + 7.65384832e+08j, -2.25075123e+09 + 1.04323117e+09j, - 2.58702131e+09 + 1.37581005e+09j], + [-3.66090240e+09 + 2.51322291e+09j, - 4.21423872e+09 + 3.07025101e+09j, -4.83929190e+09 + 3.70121421e+09j, - 5.54049946e+09 + 4.41055027e+09j]], + [[0.00000000e+00 + 3.60000000e+01j, - 3.80000000e+01 + 1.14000000e+02j, -1.20000000e+02 + 2.40000000e+02j, - 2.52000000e+02 + 4.20000000e+02j], + [-9.24000000e+02 + 1.23200000e+03j, - 1.28800000e+03 + 1.65600000e+03j, -1.72800000e+03 + 2.16000000e+03j, - 2.25000000e+03 + 2.75000000e+03j]], + [[-2.92006195e+09 + 9.43172416e+08j, - 3.23833907e+09 + 1.24416986e+09j, -3.61005363e+09 + 1.60082368e+09j, - 4.03964314e+09 + 2.01757018e+09j], + [-4.67873485e+09 + 2.70537754e+09j, - 5.26187366e+09 + 3.28065306e+09j, -5.91728384e+09 + 3.93041971e+09j, - 6.64940288e+09 + 4.65911398e+09j]]] + ,dtype=COMPLEX_TYPE) + + out = con.difference_map_fourier_constraint(mask, Idata, obj, probe, exit_wave, addr_info, prefilter, + postfilter, pbound=pbound, alpha=alpha, LL_error=True, + do_realspace_error=True) + + np.testing.assert_allclose(out, + expected_out, + err_msg="The returned errors are not consistent.") + + np.testing.assert_allclose(exit_wave, + expected_ew, + err_msg="The expected in-place update of the exit wave didn't work properly.") + + + def test_difference_map_iterator_with_probe_update(self): + ''' + This test, assumes the logic below this function works fine, and just does some iterations of difference map on + some spoof data to check that the combination works. + ''' + num_iter = 2 + + pbound = 8.86e13 # this should now mean some of the arrays update differently through the logic + alpha = 1.0 # feedback constant + + # diffraction frame size + B = 2 # for example + C = 4 # for example + + # probe dimensions + D = 2 # for example + E = B + F = C + + scan_pts = 2 # a 2x2 grid + N = scan_pts**2 # the number of measurement points in a scan + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + + # object dimensions + G = 1 # for example + H = B + npts_greater_than + I = C + npts_greater_than + + A = scan_pts ** 2 * G * D # number of exit waves + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(A, 5, 3), dtype=np.int32) # the address book + diffraction = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, B, C), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + diffraction_fill = np.arange(np.prod(diffraction.shape)).reshape(diffraction.shape).astype(diffraction.dtype) + diffraction[:] = diffraction_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (G, H, I)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (D, B, C)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((A, B, C)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + # mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((A, 3), dtype=np.int32) + pa[:N,0] = 0 + pa[N:, 0] = 1 + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((A, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + + + ea = np.array([np.array([ix, 0, 0]) for ix in range(A)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * D * G) + ma = np.zeros((A, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + + for idx in range(D): + cfact_probe[idx] = np.ones((B, C)) * 5 * (idx + 1) + + expected_probe = np.array([[[-361.18814087-1000.74768066j, -148.70419312 +206.73210144j, + -91.97548676 -9.23460865j, 16.05268097 -41.11487198j], + [-152.72857666 +109.68946838j, -62.70831680 -58.41201782j, + 37.14255524 -17.69169426j, -4.25983000 -6.36473894j]], + + [[-749.64007568-2050.26147461j, -401.52606201 +451.15054321j, + -218.26188660 -36.93523788j, 41.09194946 -91.68986511j], + [-350.60354614 +226.57118225j, -109.65759277 -124.06006622j, + 68.98976898 -24.72795677j, -5.64832449 -11.25561905j]]], dtype=COMPLEX_TYPE) + + expected_obj = np.array([[[ -0.00000000e+00 -0.00000000e+00j, -1.27605135e-02 -1.07031791e-02j, + 1.80982396e-01 -1.77402645e-01j, -2.30214819e-01 -1.09420657e+00j, + -5.07897234e+00 -9.75304246e-01j, 5.00000000e+00 +2.50000000e+01j], + [ -1.48292825e-01 -4.61030722e-01j, -5.86787999e-01 +3.41154838e+00j, + -1.08777246e+01 -8.06874752e+00j, -3.86462593e+01 +1.36726942e+01j, + 6.36259308e+01 +8.40691147e+01j, 1.10000000e+01 +1.21000000e+02j], + [ 1.10468502e+01 -5.40999889e+00j, -2.75495262e+01 -7.16127729e+00j, + -3.61000290e+01 +9.38624268e+01j, 2.70963776e+02 -2.03708401e+01j, + 2.82062347e+02 +2.01701343e+03j, 1.70000000e+01 +2.89000000e+02j], + [ 1.80000000e+01 +3.24000000e+02j, 1.90000000e+01 +3.61000000e+02j, + 2.00000000e+01 +4.00000000e+02j, 2.10000000e+01 +4.41000000e+02j, + 2.20000000e+01 +4.84000000e+02j, 2.30000000e+01 +5.29000000e+02j]]], dtype=COMPLEX_TYPE) + + expected_errors = np.array([[[ 1.30852982e-01+0.j, 7.86592126e-01+0.j, 2.91434258e-01+0.j, + 1.26918125e+00+0.j], + [ 2.88552762e+24+0.j, 6.12232725e+25+0.j, 6.29929433e+23+0.j, + 4.21546840e+25+0.j], + [ 1.75457960e+07+0.j, 7.23184240e+07+0.j, 4.43651240e+07+0.j, + 3.27585548e+19+0.j]], + + [[ 1.28861861e-02+0.j, 1.27825022e-01+0.j, 2.06416398e-02+0.j, + 1.36703640e+11+0.j], + [ 2.88552762e+24+0.j, 6.12232725e+25+0.j, 6.29929433e+23+0.j, + 4.21546840e+25+0.j], + [ 0.00000000e+00+0.j, 0.00000000e+00+0.j, 0.00000000e+00+0.j, + 3.27586977e+19+0.j]]], dtype=COMPLEX_TYPE) + + + + + errors = con.difference_map_iterator(diffraction=diffraction, + obj=obj, + object_weights=obj_weights, + cfact_object=cfact_object, + mask=mask, + probe=probe, + cfact_probe=cfact_probe, + probe_support=None, + probe_weights=probe_weights, + exit_wave=exit_wave, + addr=addr_info, + pre_fft=prefilter, + post_fft=postfilter, + pbound=pbound, + overlap_max_iterations=10, + update_object_first=False, + obj_smooth_std=None, + overlap_converge_factor=1.4e-3, + probe_center_tol=None, + probe_update_start=1, + alpha=alpha, + clip_object=None, + LL_error=True, + num_iterations=num_iter) + + + + np.testing.assert_array_equal(expected_probe, + probe, + err_msg="The probe has not behaved as expected.") + + np.testing.assert_array_equal(expected_obj, + obj, + err_msg="The object has not behaved as expected.") + + np.testing.assert_array_equal(expected_errors, + errors, + err_msg="The errors have not behaved as expected.") + + def test_difference_map_iterator_with_no_probe_update_and_object_update(self): + ''' + This test, assumes the logic below this function works fine, and just does some iterations of difference map on + some spoof data to check that the combination works. + ''' + num_iter = 1 + + pbound = 8.86e13 # this should now mean some of the arrays update differently through the logic + alpha = 1.0 # feedback constant + + # diffraction frame size + B = 2 # for example + C = 4 # for example + + # probe dimensions + D = 2 # for example + E = B + F = C + + scan_pts = 2 # a 2x2 grid + N = scan_pts**2 # the number of measurement points in a scan + npts_greater_than = int(np.sqrt(N)) # object is bigger than the probe by this amount + + # object dimensions + G = 1 # for example + H = B + npts_greater_than + I = C + npts_greater_than + + A = scan_pts ** 2 * G * D # number of exit waves + + err_fmag = np.empty(shape=(N,), dtype=FLOAT_TYPE) # deviation from the diffraction pattern for each af + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) # exit wave + addr_info = np.empty(shape=(A, 5, 3), dtype=np.int32) # the address book + diffraction = np.empty(shape=(N, B, C), dtype=FLOAT_TYPE) # the measured intensities NxAxB + mask = np.empty(shape=(N, B, C), + dtype=np.int32) # the masks for the measured magnitudes either 1xAxB or NxAxB + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) # the probe function + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) # the object function + prefilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + postfilter = np.empty(shape=(B, C), dtype=COMPLEX_TYPE) + + # now fill it with stuff. Data won't ever look like this except in type and usage! + diffraction_fill = np.arange(np.prod(diffraction.shape)).reshape(diffraction.shape).astype(diffraction.dtype) + diffraction[:] = diffraction_fill + + obj_fill = np.array([ix + 1j * (ix ** 2) for ix in range(np.prod(obj.shape))]).reshape( + (G, H, I)) + obj[:] = obj_fill + + probe_fill = np.array([ix + 1j * ix for ix in range(10, 10 + np.prod(probe.shape), 1)]).reshape( + (D, B, C)) + probe[:] = probe_fill + + prefilter.fill(30.0 + 2.0j) # this would actually vary + postfilter.fill(20.0 + 3.0j) # this too + err_fmag_fill = np.ones((N,)) + err_fmag[:] = err_fmag_fill # this shouldn't be used as pbound is None + + exit_wave_fill = np.array( + [ix ** 2 + 1j * ix for ix in range(20, 20 + np.prod(exit_wave.shape), 1)]).reshape((A, B, C)) + exit_wave[:] = exit_wave_fill + + mask_fill = np.ones_like(mask) + # mask_fill[::2, ::2] = 0 # checkerboard for testing + mask[:] = mask_fill + + pa = np.zeros((A, 3), dtype=np.int32) + pa[:N,0] = 0 + pa[N:, 0] = 1 + + X, Y = np.meshgrid(range(npts_greater_than), + range(npts_greater_than)) # assume square scan grid. Again, not always true. + oa = np.zeros((A, 3), dtype=np.int32) + oa[:N, 1] = X.ravel() + oa[N:, 1] = X.ravel() + oa[:N, 2] = Y.ravel() + oa[N:, 2] = Y.ravel() + + + ea = np.array([np.array([ix, 0, 0]) for ix in range(A)]) + da = np.array([np.array([ix, 0, 0]) for ix in range(N)] * D * G) + ma = np.zeros((A, 3), dtype=np.int32) + + addr_info[:, 0, :] = pa + addr_info[:, 1, :] = oa + addr_info[:, 2, :] = ea + addr_info[:, 3, :] = da + addr_info[:, 4, :] = ma + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + + for idx in range(D): + cfact_probe[idx] = np.ones((B, C)) * 5 * (idx + 1) + + expected_probe = deepcopy(probe) + + expected_obj = np.array([[[ -0.00000000e+00 -0.00000000e+00j, + 1.00000000e+00 +1.00000000e+00j, + 2.00000000e+00 +4.00000000e+00j, + 3.00000000e+00 +9.00000000e+00j, + 4.00000000e+00 +1.60000000e+01j, + 5.00000000e+00 +2.50000000e+01j], + [ 6.00000000e+00 +3.60000000e+01j, + -7.95981900e+06 +1.43803590e+07j, + -7.64522750e+06 +1.61365070e+07j, + -7.34606000e+06 +1.82074500e+07j, + -1.48699840e+07 +4.33746560e+07j, + 1.10000000e+01 +1.21000000e+02j], + [ 1.20000000e+01 +1.44000000e+02j, + -1.60912900e+07 +7.23797920e+07j, + -1.52878190e+07 +8.04868400e+07j, + -1.45362140e+07 +8.92972240e+07j, + -2.90441140e+07 +2.07500928e+08j, + 1.70000000e+01 +2.89000000e+02j], + [ 1.80000000e+01 +3.24000000e+02j, + 1.90000000e+01 +3.61000000e+02j, + 2.00000000e+01 +4.00000000e+02j, + 2.10000000e+01 +4.41000000e+02j, + 2.20000000e+01 +4.84000000e+02j, + 2.30000000e+01 +5.29000000e+02j]]], dtype=COMPLEX_TYPE) + + expected_errors = np.array([[[ 1.30852982e-01+0.j, 7.86592126e-01+0.j, 2.91434258e-01+0.j, + 1.26918125e+00+0.j], + [ 2.88552762e+24+0.j, 6.12232725e+25+0.j, 6.29929433e+23+0.j, + 4.21546840e+25+0.j], + [ 1.75457960e+07+0.j, 7.23184240e+07+0.j, 4.43651240e+07+0.j, + 3.27585548e+19+0.j]]], dtype=COMPLEX_TYPE) + + + errors = con.difference_map_iterator(diffraction=diffraction, + obj=obj, + object_weights=obj_weights, + cfact_object=cfact_object, + mask=mask, + probe=probe, + cfact_probe=cfact_probe, + probe_support=None, + probe_weights=probe_weights, + exit_wave=exit_wave, + addr=addr_info, + pre_fft=prefilter, + post_fft=postfilter, + pbound=pbound, + overlap_max_iterations=10, + update_object_first=True, + obj_smooth_std=None, + overlap_converge_factor=1.4e-3, + probe_center_tol=None, + probe_update_start=2, + alpha=alpha, + clip_object=None, + LL_error=True, + num_iterations=num_iter) + + np.testing.assert_array_equal(expected_probe, + probe, + err_msg="The probe has not behaved as expected.") + + np.testing.assert_array_equal(expected_obj, + obj, + err_msg="The object has not behaved as expected.") + + np.testing.assert_array_equal(expected_errors, + errors, + err_msg="The error has not behaved as expected.") + + + +if __name__ == '__main__': + unittest.main() + + + diff --git a/test/archive_tests/array_based_tests/constraints_unity_test.py b/test/archive_tests/array_based_tests/constraints_unity_test.py new file mode 100644 index 000000000..9c9588768 --- /dev/null +++ b/test/archive_tests/array_based_tests/constraints_unity_test.py @@ -0,0 +1,221 @@ +''' +The tests for the constraints +''' + + +import unittest +import numpy as np +from test.archive_tests.array_based_tests import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from collections import OrderedDict +from ptypy.engines.utils import basic_fourier_update +from ptypy.accelerate.array_based.constraints import difference_map_fourier_constraint +from ptypy.accelerate import array_based as ab + +@unittest.skip("Skip these until I have had chance to investigate the tolerances.") +class ConstraintsUnityTest(unittest.TestCase): + + def test_difference_map_fourier_constraint_pbound_none_UNITY(self): + ab.FLOAT_TYPE =np.float64 + ab.COMPLEX_TYPE = np.complex128 + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + PodPtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + + ptypy_ewf, ptypy_error= self.ptypy_difference_map_fourier_constraint(PodPtychoInstance) + errors = difference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=None, + alpha=1.0, + LL_error=True) + rtol=1e-7 + for idx, key in enumerate(ptypy_ewf.keys()): + np.testing.assert_allclose(ptypy_ewf[key], + vectorised_scan['exit wave'][idx], + err_msg="The array-based and pod-based exit waves are not consistent", + rtol =rtol) + + ptypy_fmag = [] + ptypy_phot = [] + ptypy_exit = [] + + for idx, key in enumerate(ptypy_error.keys()): + err_fmag, err_phot, err_exit = ptypy_error[key] + ptypy_fmag.append(err_fmag) + ptypy_phot.append(err_phot) + ptypy_exit.append(err_exit) + + ptypy_fmag = np.array(ptypy_fmag) + ptypy_phot = np.array(ptypy_phot) + + ptypy_exit = np.array(ptypy_exit) + + npy_fmag = errors[0, :] + npy_phot = errors[1, :] + npy_exit = errors[2, :] + ab.FLOAT_TYPE =np.float32 + ab.COMPLEX_TYPE = np.complex64 + + np.testing.assert_array_equal(npy_fmag, + ptypy_fmag, + err_msg="The array-based and pod-based fmag errors are not consistent", + rtol=rtol) + + np.testing.assert_array_equal(npy_phot, + ptypy_phot, + err_msg="The array-based and pod-based phot errors are not consistent", + rtol=rtol) + # there is a slight difference in numpy in the way the mean is calculated here. It 1e-13 and a diagnostic so almost equal is fine + np.testing.assert_allclose(npy_exit, + ptypy_exit, + err_msg="The array-based and pod-based exit errors are not consistent", + rtol=rtol) + + def test_difference_map_fourier_constraint_pbound_less_than_fourier_error_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + PodPtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + pbound = 0.597053604126 + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + + ptypy_ewf, ptypy_error= self.ptypy_difference_map_fourier_constraint(PodPtychoInstance, pbound=pbound) + errors = difference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=pbound, + alpha=1.0, + LL_error=True) + + for idx, key in enumerate(ptypy_ewf.keys()): + np.testing.assert_array_equal(ptypy_ewf[key], + vectorised_scan['exit wave'][idx], + err_msg="The array-based and pod-based exit waves are not consistent") + + ptypy_fmag = [] + ptypy_phot = [] + ptypy_exit = [] + + for idx, key in enumerate(ptypy_error.keys()): + err_fmag, err_phot, err_exit = ptypy_error[key] + ptypy_fmag.append(err_fmag) + ptypy_phot.append(err_phot) + ptypy_exit.append(err_exit) + + ptypy_fmag = np.array(ptypy_fmag) + ptypy_phot = np.array(ptypy_phot) + ptypy_exit = np.array(ptypy_exit) + + npy_fmag = errors[0, :] + npy_phot = errors[1, :] + npy_exit = errors[2, :] + + np.testing.assert_array_equal(npy_fmag, + ptypy_fmag, + err_msg="The array-based and pod-based fmag errors are not consistent") + + np.testing.assert_array_equal(npy_phot, + ptypy_phot, + err_msg="The array-based and pod-based phot errors are not consistent") + # there is a slight difference in numpy in the way the mean is calculated here. It 1e-13 and a diagnostic so almost equal is fine + np.testing.assert_allclose(npy_exit, + ptypy_exit, + err_msg="The array-based and pod-based exit errors are not consistent") + + def test_difference_map_fourier_constraint_pbound_greater_than_fourier_error_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + PodPtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + pbound = 200.0 + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + + first_view_id = vectorised_scan['meta']['view_IDs'][0] + master_pod = PtychoInstance.diff.V[first_view_id].pod + propagator = master_pod.geometry.propagator + + ptypy_ewf, ptypy_error = self.ptypy_difference_map_fourier_constraint(PodPtychoInstance, pbound=pbound) + errors = difference_map_fourier_constraint(vectorised_scan['mask'], + vectorised_scan['diffraction'], + vectorised_scan['obj'], + vectorised_scan['probe'], + vectorised_scan['exit wave'], + vectorised_scan['meta']['addr'], + prefilter=propagator.pre_fft, + postfilter=propagator.post_fft, + pbound=pbound, + alpha=1.0, + LL_error=True) + + for idx, key in enumerate(ptypy_ewf.keys()): + np.testing.assert_array_equal(ptypy_ewf[key], + vectorised_scan['exit wave'][idx], + err_msg="The array-based and pod-based exit waves are not consistent") + + ptypy_fmag = [] + ptypy_phot = [] + ptypy_exit = [] + + for idx, key in enumerate(ptypy_error.keys()): + err_fmag, err_phot, err_exit = ptypy_error[key] + ptypy_fmag.append(err_fmag) + ptypy_phot.append(err_phot) + ptypy_exit.append(err_exit) + + ptypy_fmag = np.array(ptypy_fmag) + ptypy_phot = np.array(ptypy_phot) + ptypy_exit = np.array(ptypy_exit) + + npy_fmag = errors[0, :] + npy_phot = errors[1, :] + npy_exit = errors[2, :] + + np.testing.assert_array_equal(npy_fmag, + ptypy_fmag, + err_msg="The array-based and pod-based fmag errors are not consistent") + + np.testing.assert_array_equal(npy_phot, + ptypy_phot, + err_msg="The array-based and pod-based phot errors are not consistent") + # there is a slight difference in numpy in the way the mean is calculated here. It 1e-13 and a diagnostic so almost equal is fine + np.testing.assert_allclose(npy_exit, + ptypy_exit, + err_msg="The array-based and pod-based exit errors are not consistent") + + def ptypy_difference_map_fourier_constraint(self, a_ptycho_instance, pbound=None): + error_dct = OrderedDict() + exit_wave = OrderedDict() + for dname, diff_view in a_ptycho_instance.diff.views.items(): + di_view = a_ptycho_instance.diff.V[dname] + error_dct[dname] = basic_fourier_update(di_view, + pbound=pbound, + alpha=1.0) + for name, pod in di_view.pods.items(): + exit_wave[name] = pod.exit + return exit_wave, error_dct + + +if __name__ == '__main__': + unittest.main() + + + diff --git a/test/archive_tests/array_based_tests/data_utils_test.py b/test/archive_tests/array_based_tests/data_utils_test.py new file mode 100644 index 000000000..2062c0d2c --- /dev/null +++ b/test/archive_tests/array_based_tests/data_utils_test.py @@ -0,0 +1,54 @@ +''' +Created on 4 Jan 2018 + +@author: clb02321 +''' +import unittest +from test.archive_tests.array_based_tests import utils as tu +import numpy as np + +from ptypy.accelerate.array_based import data_utils as du + + + +class DataUtilsTest(unittest.TestCase): + ''' + tests the conversion between pods and numpy arrays + ''' + + def test_pod_to_numpy(self): + ''' + tests if the vectorisation process works + ''' + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + du.pod_to_arrays(PtychoInstance, 'S0000', scan_model='Full') + + def test_numpy_pod_consistency(self): + ''' + vectorises the Ptycho instance. + ''' + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr = vectorised_scan['meta']['addr'] + view_IDS = vectorised_scan['meta']['view_IDs'] + + # check the probe references match up + vectorised_scan['probe'] *= np.random.rand(*vectorised_scan['probe'].shape) + + pa, oa, ea, da, ma = zip(*addr) + + for idx, vID in enumerate(view_IDS): + np.testing.assert_array_equal(vectorised_scan['probe'][pa[idx][0]], PtychoInstance.pr.V[vID].data) + + + + vectorised_scan['exit wave'] *= np.random.rand(*vectorised_scan['exit wave'].shape) + + for idx, vID in enumerate(view_IDS): + np.testing.assert_array_equal(vectorised_scan['exit wave'][ea[idx][0]], PtychoInstance.ex.V[vID].data) + + + + +if __name__ == "__main__": + unittest.main() diff --git a/test/archive_tests/array_based_tests/error_metric_test_regression_test.py b/test/archive_tests/array_based_tests/error_metric_test_regression_test.py new file mode 100644 index 000000000..a35408c50 --- /dev/null +++ b/test/archive_tests/array_based_tests/error_metric_test_regression_test.py @@ -0,0 +1,103 @@ +''' +A test for the module of the relevant error metrics +''' + +import unittest +import numpy as np +from test.archive_tests.array_based_tests import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based import COMPLEX_TYPE, FLOAT_TYPE +import ptypy.utils as u +from collections import OrderedDict +from archive.array_based.error_metrics import log_likelihood, far_field_error, realspace_error +from ptypy.accelerate.array_based.object_probe_interaction import scan_and_multiply + + +class ErrorMetricRegressionTest(unittest.TestCase): + + def test_loglikelihood_regression(self): + ''' + Test that it runs + ''' + # should be able to completely remove this + PtychoInstance = tu.get_ptycho_instance('log_likelihood_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.diff.V[first_view_id].pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + mask = vectorised_scan['mask'] + exit_wave = vectorised_scan['exit wave'] + diffraction = vectorised_scan['diffraction'] + + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + log_likelihood(probe_object, mask, diffraction, propagator.pre_fft, propagator.post_fft, addr_info) + + + def test_far_field_error_regression(self): + PtychoInstance = tu.get_ptycho_instance('log_likelihood_test') + af, fmag, mask = self.get_current_and_measured_solution(PtychoInstance) + far_field_error(af, fmag, mask) + + + def test_realspace_error_regression_a(self): + # the case when there is only one mode + I = 5 + M = 20 + N = 30 + out_length = I + ea_first_column = range(I) + da_first_column = range(I) + + difference = np.empty(shape=(I, M, N), dtype=COMPLEX_TYPE) + for idx in range(I): + difference[idx] = np.ones((M, N)) *idx + 1j * np.ones((M, N)) *idx + + error = realspace_error(difference, ea_first_column, da_first_column, out_length) + + expected_error = np.array([ 0.0, 2.0, 8.0, 18.0, 32.0], dtype=FLOAT_TYPE) + np.testing.assert_array_equal(error, expected_error) + + def test_realspace_error_regression_b(self): + # multiple modes + I = 10 + M = 20 + N = 30 + out_length = 5 + ea_first_column = range(I) + da_first_column = list(range(int(I/2))) + list(range(int(I/2))) + + difference = np.empty(shape=(I, M, N), dtype=COMPLEX_TYPE) + for idx in range(I): + difference[idx] = np.ones((M, N)) * idx + 1j * np.ones((M, N)) * idx + + error = realspace_error(difference, ea_first_column, da_first_column, out_length) + + expected_error = np.array([50., 74., 106., 146., 194.], dtype=FLOAT_TYPE) + np.testing.assert_array_equal(error, expected_error) + + + def get_current_and_measured_solution(self, a_ptycho_instance): + alpha = 1.0 + fmag = [] + af = [] + mask = [] + for dname, diff_view in a_ptycho_instance.diff.views.items(): + fmag.append(np.sqrt(np.abs(diff_view.data))) + af2 = np.zeros_like(diff_view.data) + f = OrderedDict() + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + f[name] = pod.fw((1 + alpha) * pod.probe * pod.object + - alpha * pod.exit) + af2 += u.abs2(f[name]) + mask.append(diff_view.pod.mask) + af.append(np.sqrt(af2)) + return np.array(af), np.array(fmag), np.array(mask) + + + +if __name__ == '__main__': + unittest.main() diff --git a/test/archive_tests/array_based_tests/error_metric_unity_test.py b/test/archive_tests/array_based_tests/error_metric_unity_test.py new file mode 100644 index 000000000..d2db3d160 --- /dev/null +++ b/test/archive_tests/array_based_tests/error_metric_unity_test.py @@ -0,0 +1,106 @@ +''' +A test for the module of the relevant error metrics + SHOULD THIS EXIST? Ptypy has no in built functions for these, so I could refactor so that it does, or just not both testing. +''' + +import unittest +import numpy as np +from test.archive_tests.array_based_tests import utils as tu +from ptypy.accelerate.array_based import data_utils as du +import ptypy.utils as u +from collections import OrderedDict +from archive.array_based.error_metrics import log_likelihood, far_field_error +from ptypy.accelerate.array_based.object_probe_interaction import scan_and_multiply + + +class ErrorMetricUnityTest(unittest.TestCase): + + def test_loglikelihood_numpy_UNITY(self): + ''' + Check that it gives the same result as the ptypy original + + ''' + error_metric = {} + PodPtychoInstance = tu.get_ptycho_instance('log_likelihood_test') + ptypy_error_metric =self.get_ptypy_loglikelihood(PodPtychoInstance) + PtychoInstance = tu.get_ptycho_instance('log_likelihood_test') + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + first_view_id = vectorised_scan['meta']['view_IDs'][0] + propagator = PtychoInstance.diff.V[first_view_id].pod.geometry.propagator + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + mask = vectorised_scan['mask'] + exit_wave = vectorised_scan['exit wave'] + diffraction = vectorised_scan['diffraction'] + + probe_object = scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + + vals = log_likelihood(probe_object, mask, diffraction, propagator.pre_fft, propagator.post_fft, addr_info) + k = 0 + for name, view in PtychoInstance.diff.V.items(): + error_metric[name] = vals[k] + k += 1 + + + for name, view in PodPtychoInstance.diff.V.items(): + ptypy_error = ptypy_error_metric[name] + numpy_error = error_metric[name] + np.testing.assert_array_equal(ptypy_error, numpy_error) + + def test_far_field_error_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('log_likelihood_test') + PodPtychoInstance = tu.get_ptycho_instance('log_likelihood_test') + af, fmag, mask = self.get_current_and_measured_solution(PtychoInstance) + fmag_npy = far_field_error(af, fmag, mask) + fmag_ptypy = self.get_ptypy_far_field_error(PodPtychoInstance) + np.testing.assert_array_equal(fmag_ptypy, fmag_npy) + + @unittest.skip("I wonder if its possible to put this in.") + def test_real_space_error_UNITY(self): + pass + + + def get_current_and_measured_solution(self, a_ptycho_instance): + alpha = 1.0 + fmag = [] + af = [] + mask = [] + for dname, diff_view in a_ptycho_instance.diff.views.items(): + fmag.append(np.sqrt(np.abs(diff_view.data))) + af2 = np.zeros_like(diff_view.data) + f = OrderedDict() + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + f[name] = pod.fw((1 + alpha) * pod.probe * pod.object + - alpha * pod.exit) + af2 += u.abs2(f[name]) + mask.append(diff_view.pod.mask) + af.append(np.sqrt(af2)) + return np.array(af), np.array(fmag), np.array(mask) + + def get_ptypy_far_field_error(self, a_ptycho_instance): + + err_fmag = [] + af, fmag, mask = self.get_current_and_measured_solution(a_ptycho_instance) + for i in range(af.shape[0]): + fdev = af[i] - fmag[i] + err_fmag.append(np.sum(mask[i] * fdev ** 2) / mask[i].sum()) + return np.array(err_fmag) + + def get_ptypy_loglikelihood(self, a_ptycho_instance): + error_dct = {} + for dname, diff_view in a_ptycho_instance.diff.views.items(): + I = diff_view.data + fmask = diff_view.pod.mask + LL = np.zeros_like(diff_view.data) + for name, pod in diff_view.pods.items(): + LL += u.abs2(pod.fw(pod.probe * pod.object)) + + error_dct[dname] = (np.sum(fmask * (LL - I) ** 2 / (I + 1.)) + / np.prod(LL.shape)) + return error_dct + +if __name__ == '__main__': + unittest.main() diff --git a/test/archive_tests/array_based_tests/farfield_propagator_regression_test.py b/test/archive_tests/array_based_tests/farfield_propagator_regression_test.py new file mode 100644 index 000000000..5bd6fb660 --- /dev/null +++ b/test/archive_tests/array_based_tests/farfield_propagator_regression_test.py @@ -0,0 +1,92 @@ +''' +Test for the propagation in numpy +SHOULD REFACTOR HERE to be less dependent on the main framework. We just want to test the propagator works with 3x3 data. +''' + +import unittest +import numpy as np +from test.archive_tests.array_based_tests import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based import object_probe_interaction as opi +from archive.array_based import propagation as prop +from copy import deepcopy as copy +TOLERANCE=4 + +class FarfieldPropagatorRegressionTest(unittest.TestCase): + def setUp(self): + self.PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + self.GeoPtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + self.vectorised_scan = du.pod_to_arrays(self.PtychoInstance, 'S0000') + self.pod_vectorised_scan = du.pod_to_arrays(self.GeoPtychoInstance, 'S0000') + self.first_view_id = self.pod_vectorised_scan['meta']['view_IDs'][0] + + def test_fourier_transform_farfield_nofilter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + prop.farfield_propagator(vec_ew) + + def test_fourier_transform_farfield_with_prefilter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + prop.farfield_propagator(vec_ew, prefilter=propagator.pre_fft) + + + def test_fourier_transform_farfield_with_postfilter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + prop.farfield_propagator(vec_ew, prefilter=None, postfilter=propagator.post_fft) + + def test_fourier_transform_farfield_with_pre_and_post_filter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + prop.farfield_propagator(vec_ew, prefilter=propagator.pre_fft, postfilter=propagator.post_fft) + + def test_inverse_fourier_transform_farfield_nofilter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + prop.farfield_propagator(vec_ew, direction='backward') + + def test_inverse_fourier_transform_farfield_with_prefilter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + prop.farfield_propagator(vec_ew, prefilter=propagator.pre_ifft, direction='backward') + + def test_inverse_fourier_transform_farfield_with_postfilter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + prop.farfield_propagator(vec_ew, prefilter=None, postfilter=propagator.post_ifft, direction='backward') + + def test_inverse_fourier_transform_farfield_with_pre_and_post_filter(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + prop.farfield_propagator(vec_ew, prefilter=propagator.pre_ifft, postfilter=propagator.post_ifft, direction='backward') + + def get_exit_wave(self, a_vectorised_scan): + ''' + a pretested method + :param a_vectorised_scan: A scan that has been vectorised. + :return: the exit wave + ''' + vec_addr_info = a_vectorised_scan['meta']['addr'] + vec_probe = a_vectorised_scan['probe'] + vec_obj = a_vectorised_scan['obj'] + vec_ew = a_vectorised_scan['exit wave'] + return opi.scan_and_multiply(vec_probe, + vec_obj, + vec_ew.shape, + vec_addr_info) + + + def diffraction_transform_with_geo(self, propagator, ew, direction='forward'): + result_array_geo = np.zeros_like(ew) + meta = self.pod_vectorised_scan['meta'] # probably want to extract these at a later date, but just to get stuff going... + view_dlayer = 0 # what is this? + addr_info = meta['addr'][:,view_dlayer] # addresses, object references + for _pa, _oa, ea, _da, _ma in addr_info: + if direction=='forward': + result_array_geo[ea[0]] = propagator.fw(ew[ea[0]]) + else: + result_array_geo[ea[0]] = propagator.bw(ew[ea[0]]) + return result_array_geo + + +if __name__ == "__main__": + unittest.main() diff --git a/test/archive_tests/array_based_tests/farfield_propagator_unity_test.py b/test/archive_tests/array_based_tests/farfield_propagator_unity_test.py new file mode 100644 index 000000000..f14d24bb9 --- /dev/null +++ b/test/archive_tests/array_based_tests/farfield_propagator_unity_test.py @@ -0,0 +1,166 @@ +''' +Test for the propagation in numpy +SHOULD REFACTOR HERE to be less dependent on the main framework. We just want to test the propagator works with 3x3 data. +''' + +import unittest +import numpy as np +from test.archive_tests.array_based_tests import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based import object_probe_interaction as opi +from archive.array_based import propagation as prop +from copy import deepcopy as copy +TOLERANCE=4 + + +class FarfieldPropagatorUnityTest(unittest.TestCase): + def setUp(self): + self.PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + self.GeoPtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + self.vectorised_scan = du.pod_to_arrays(self.PtychoInstance, 'S0000') + self.pod_vectorised_scan = du.pod_to_arrays(self.GeoPtychoInstance, 'S0000') + self.first_view_id = self.pod_vectorised_scan['meta']['view_IDs'][0] + + def test_fourier_transform_farfield_nofilter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + pre_fft = 1.0 + post_fft = 1.0 + + geo_propagator.pre_fft = pre_fft + geo_propagator.post_fft = post_fft + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=None, postfilter=None) + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew) + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo, decimal=TOLERANCE) + + def test_fourier_transform_farfield_with_prefilter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + post_fft = 1.0 + + geo_propagator.post_fft = post_fft + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=propagator.pre_fft, postfilter=None) + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew) + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo, decimal=TOLERANCE) + + def test_fourier_transform_farfield_with_postfilter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + pre_fft = 1.0 + + geo_propagator.pre_fft = pre_fft + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=None, postfilter=propagator.post_fft) + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew) + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo, decimal=TOLERANCE) + + def test_fourier_transform_farfield_with_pre_and_post_filter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=propagator.pre_fft, postfilter=propagator.post_fft) + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew) + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo) + + def test_inverse_fourier_transform_farfield_nofilter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + pre_ifft = 1.0 + post_ifft = 1.0 + + geo_propagator.pre_ifft = pre_ifft + geo_propagator.post_ifft = post_ifft + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=None, postfilter=None, direction='backward') + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew, direction='backward') + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo, decimal=TOLERANCE) + + def test_inverse_fourier_transform_farfield_with_prefilter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + post_ifft = 1.0 + + geo_propagator.post_ifft = post_ifft + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=propagator.pre_ifft, postfilter=None, direction='backward') + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew, direction='backward') + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo, decimal=TOLERANCE) + + def test_inverse_fourier_transform_farfield_with_postfilter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + pre_ifft = 1.0 + + geo_propagator.pre_ifft = pre_ifft + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=None, postfilter=propagator.post_ifft, direction='backward') + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew, direction='backward') + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo, decimal=TOLERANCE) + + def test_inverse_fourier_transform_farfield_with_pre_and_post_filter_UNITY(self): + vec_ew = self.get_exit_wave(self.vectorised_scan) + pod_ew = self.get_exit_wave(self.pod_vectorised_scan) + geo_propagator = copy(self.GeoPtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + propagator = copy(self.PtychoInstance.di.V[self.first_view_id].pod.geometry.propagator) + + + result_array_npy = prop.farfield_propagator(vec_ew, prefilter=propagator.pre_ifft, postfilter=propagator.post_ifft, direction='backward') + result_array_geo = self.diffraction_transform_with_geo(geo_propagator, pod_ew, direction='backward') + np.testing.assert_array_almost_equal(result_array_npy, result_array_geo, decimal=TOLERANCE) + + + def get_exit_wave(self, a_vectorised_scan): + ''' + a pretested method + :param a_vectorised_scan: A scan that has been vectorised. + :return: the exit wave + ''' + vec_addr_info = a_vectorised_scan['meta']['addr'] + vec_probe = a_vectorised_scan['probe'] + vec_obj = a_vectorised_scan['obj'] + vec_ew = a_vectorised_scan['exit wave'] + return opi.scan_and_multiply(vec_probe, + vec_obj, + vec_ew.shape, + vec_addr_info) + + + def diffraction_transform_with_geo(self, propagator, ew, direction='forward'): + result_array_geo = np.zeros_like(ew) + meta = self.pod_vectorised_scan['meta'] # probably want to extract these at a later date, but just to get stuff going... + addr_info = meta['addr'] # addresses, object references + for _pa, _oa, ea, _da, _ma in addr_info: + if direction=='forward': + result_array_geo[ea[0]] = propagator.fw(ew[ea[0]]) + else: + result_array_geo[ea[0]] = propagator.bw(ew[ea[0]]) + return result_array_geo + + +# + +if __name__ == "__main__": + unittest.main() diff --git a/test/archive_tests/array_based_tests/object_probe_interaction_regression_test.py b/test/archive_tests/array_based_tests/object_probe_interaction_regression_test.py new file mode 100644 index 000000000..06b607bea --- /dev/null +++ b/test/archive_tests/array_based_tests/object_probe_interaction_regression_test.py @@ -0,0 +1,1303 @@ +''' +tests for the object-probe interactions, including the specific DM, ePIE etc updates + +''' + +import unittest +import numpy as np +from test.archive_tests.array_based_tests import utils as tu +from copy import deepcopy +from ptypy.accelerate.array_based import COMPLEX_TYPE, FLOAT_TYPE +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based import object_probe_interaction as opi + + + +class ObjectProbeInteractionRegressionTest(unittest.TestCase): + def test_scan_and_multiply(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + blank = np.ones_like(probe) + po = opi.scan_and_multiply(blank, obj, exit_wave.shape, addr_info) + + for idx, p in enumerate(iter(PtychoInstance.pods.values())): + np.testing.assert_array_equal(po[idx], p.object) + + def test_exit_wave_calculation(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + + po = opi.scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + for idx, p in enumerate(iter(PtychoInstance.pods.values())): + np.testing.assert_array_equal(po[idx], p.object * p.probe) + + def test_difference_map_realspace_constraint(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + probe_and_object = opi.scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + + opi.difference_map_realspace_constraint(probe_and_object, + exit_wave, + alpha=1.0) + + def test_extract_array_from_exit_wave_regression_case_a(self): + # two cases for this a) the array to be updated is bigger than the extracted array (which is the same size as the exit wave) + # b) the other way round + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + opi.extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, array_to_be_updated, + update_addr, cfact, weights) + + expected = np.array([[-9.50000000 + 0.5j, 0.20000000 + 0.2j], + [-9.50000000 + 0.5j, 4.80952406 + 0.04761905j], + [-9.50000000 + 0.5j, 4.80952406 + 0.04761905j], + [-9.50000000 + 0.5j, 4.80952406 + 0.04761905j], + [-9.50000000 + 0.5j, 4.80952406 + 0.04761905j], + [0.10000000 + 0.1j, 4.80952406 + 0.04761905j], + [0.10000000 + 0.1j, 0.20000000 + 0.2j]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(np.diagonal(array_to_be_updated), + expected, + err_msg="The array has not been extracted properly from the exit wave.") + + def test_extract_array_from_exit_wave_regression_case_b(self): + # two cases for this a) the array to be updated is bigger than the extracted array (which is the same size as the exit wave) + # b) the other way round + # + npts_greater_than = 2 + B = 5 + C = 5 + + D = 2 + E = C + npts_greater_than + F = C + npts_greater_than + + G = 2 + H = B + I = C + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + extract_addr[index::scan_pts ** 2, 1] = X + extract_addr[index::scan_pts ** 2, 2] = Y + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + update_addr[:, 1] = np.zeros((A,)) + update_addr[:, 2] = np.zeros((A,)) + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + opi.extract_array_from_exit_wave(exit_wave, exit_addr, array_to_be_extracted, extract_addr, + array_to_be_updated, + update_addr, cfact, weights) + + expected = np.array([[11.83333397 - 0.16666667j, 4.80952406 + 0.04761905j], + [11.83333397 - 0.16666667j, 4.80952406 + 0.04761905j], + [11.83333397 - 0.16666667j, 4.80952406 + 0.04761905j], + [11.83333397 - 0.16666667j, 4.80952406 + 0.04761905j], + [11.83333397 - 0.16666667j, 4.80952406 + 0.04761905j]], + dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(expected, np.diagonal(array_to_be_updated)) + + def test_difference_map_update_probe_regression_with_support(self): + ''' + This tests difference_map_update_probe, which wraps extract_array_from_exit_wave + ''' + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(update_addr, extract_addr, exit_addr, dummy_addr, dummy_addr)) + probe_support = np.ones_like(array_to_be_updated) * 100.0 + #(ob, probe_weights, probe, exit_wave, addr_info, cfact_probe, probe_support = None) + opi.difference_map_update_probe(array_to_be_extracted, weights, array_to_be_updated, exit_wave, addr_info, cfact, probe_support=probe_support) + expected_output = np.array([[-500.00000000 + 500.j, 400.00000000 + 400.j], + [-500.00000000 + 500.j, 571.42858887 + 95.23809814j], + [-500.00000000 + 500.j, 571.42858887 + 95.23809814j], + [-500.00000000 + 500.j, 571.42858887 + 95.23809814j], + [-500.00000000 + 500.j, 571.42858887 + 95.23809814j], + [100.00000000 + 100.j, 571.42858887 + 95.23809814j], + [100.00000000 + 100.j, 400.00000000 + 400.j]], dtype=COMPLEX_TYPE) + np.testing.assert_array_equal(np.diagonal(array_to_be_updated), expected_output) + + def test_difference_map_update_probe_regression_without_support(self): + ''' + This tests difference_map_update_probe, which wraps extract_array_from_exit_wave + ''' + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(update_addr, extract_addr, exit_addr, dummy_addr, dummy_addr)) + #(ob, probe_weights, probe, exit_wave, addr_info, cfact_probe, probe_support = None) + opi.difference_map_update_probe(array_to_be_extracted, weights, array_to_be_updated, exit_wave, addr_info, cfact, probe_support=None) + + expected_output = np.array([[-5.00000000+5.j, 4.00000000+4.j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [ 1.00000000+1.j, 5.71428585+0.95238096j], + [ 1.00000000+1.j, 4.00000000+4.j]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(np.diagonal(array_to_be_updated), expected_output) + + + def test_difference_map_update_object_with_no_smooth_or_clip_regression(self): + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(extract_addr, update_addr , exit_addr, dummy_addr, dummy_addr)) + + opi.difference_map_update_object(array_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, cfact, ob_smooth_std=None, clip_object=None) + expected_output = np.array([[-5.00000000 + 5.j, 4.00000000 + 4.j], + [-5.00000000 + 5.j, 5.71428585 + 0.95238096j], + [-5.00000000 + 5.j, 5.71428585 + 0.95238096j], + [-5.00000000 + 5.j, 5.71428585 + 0.95238096j], + [-5.00000000 + 5.j, 5.71428585 + 0.95238096j], + [1.00000000 + 1.j, 5.71428585 + 0.95238096j], + [1.00000000 + 1.j, 4.00000000 + 4.j]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(np.diagonal(array_to_be_updated), expected_output) + + + def test_difference_map_update_object_with_smooth_but_no_clip_regression(self): + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(extract_addr, update_addr , exit_addr, dummy_addr, dummy_addr)) + obj_smooth_std = 2 # integer + opi.difference_map_update_object(array_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, cfact, ob_smooth_std=obj_smooth_std, clip_object=None) + expected_output = np.array([[-5.00000000+5.j, 4.00000000+4.j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [-5.00000000+5.j, 5.71428585+0.95238096j], + [ 1.00000000+1.j, 5.71428585+0.95238096j], + [ 1.00000000+1.j, 4.00000000+4.j]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(np.diagonal(array_to_be_updated), expected_output) + + @unittest.skip("Not used at the moment.") + def test_difference_map_update_object_with_no_smooth_but_clipping_regression(self): + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + array_to_be_extracted = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + array_to_be_extracted[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + extract_addr = np.empty(shape=(A, 3), dtype=int) + extract_addr[:, 0] = np.array(range(D)).repeat(A / D) + extract_addr[:, 1] = np.zeros((A,)) + extract_addr[:, 2] = np.zeros((A,)) + + array_to_be_updated = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + array_to_be_updated[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + update_addr = np.empty(shape=(A, 3), dtype=int) + update_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + update_addr[index::scan_pts ** 2, 1] = X + update_addr[index::scan_pts ** 2, 2] = Y + + weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + weights[:] = np.linspace(-1, 1, G) + cfact = np.empty_like(array_to_be_updated) + for idx in range(G): + cfact[idx] = np.ones((H, I)) * 10 * (idx + 1) + + dummy_addr = np.zeros_like(extract_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(extract_addr, update_addr , exit_addr, dummy_addr, dummy_addr)) + clip = (0.8, 1.0) + opi.difference_map_update_object(array_to_be_updated, weights, array_to_be_extracted, exit_wave, addr_info, cfact, ob_smooth_std=None, clip_object=clip) + expected_output = np.array([[-0.70710677+0.70710677j, 0.70710677+0.70710683j], + [-0.70710677+0.70710677j, 0.98639393+0.16439897j], + [-0.70710677+0.70710677j, 0.98639393+0.16439897j], + [-0.70710677+0.70710677j, 0.98639393+0.16439897j], + [-0.70710677+0.70710677j, 0.98639393+0.16439897j], + [ 0.70710677+0.70710683j, 0.98639393+0.16439897j], + [ 0.70710677+0.70710683j, 0.70710677+0.70710683j]], dtype=COMPLEX_TYPE) + + np.testing.assert_array_equal(np.diagonal(array_to_be_updated), expected_output) + + def test_center_probe_no_change_regression(self): + npts = 64 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X-Xoff)**2 + (Y-Yoff)**2 < rad**2] = probe_vals + center_tolerance = 10.0 + original_probe = np.copy(probe) + opi.center_probe(probe, center_tolerance) + + np.testing.assert_array_equal(probe, original_probe) + + def test_center_probe_with_change_regression(self): + npts = 64 + probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + rad = 10.0 + probe_vals = 2 + 3j + x = np.array(range(npts)) - npts // 2 + X, Y = np.meshgrid(x, x) + Xoff = 5.0 + Yoff = 2.0 + probe[0, (X-Xoff)**2 + (Y-Yoff)**2 < rad**2] = probe_vals + center_tolerance = 1.0 + + not_shifted_probe = np.zeros((1, npts, npts), dtype=COMPLEX_TYPE) + not_shifted_probe[0, (X)**2 + (Y)**2 < rad**2] = probe_vals + opi.center_probe(probe, center_tolerance) + np.testing.assert_array_almost_equal(probe, not_shifted_probe, decimal=8) # interpolation obviously won't make this exact! + + def test_difference_map_overlap_update_test_order_of_updates_a(self): + ''' + This tests the order in which the object and probe are updated + ''' + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = True + do_update_probe = False + # this should mean that the object gets updated but the probe does not change + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr , exit_addr, dummy_addr, dummy_addr)) + + original_probe = deepcopy(probe) + expected_object=np.array([[[-5.00000000+5.j,-5.00000000+5.j,-5.00000000+5.j, + -5.00000000+5.j,-5.00000000+5.j,1.00000000+1.j,1.00000000+1.j], + [10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,1.00000000+1.j,1.00000000+1.j], + [10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,1.00000000+1.j,1.00000000+1.j], + [10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,1.00000000+1.j,1.00000000+1.j], + [10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,10.33333397-1.66666675j, + 10.33333397-1.66666675j,1.00000000+1.j,1.00000000+1.j], + [-21.00000000+5.j,-21.00000000+5.j,-21.00000000+5.j, + -21.00000000+5.j,-21.00000000+5.j,1.00000000+1.j, + 1.00000000+1.j], + [1.00000000+1.j,1.00000000+1.j,1.00000000+1.j, + 1.00000000+1.j,1.00000000+1.j,1.00000000+1.j, + 1.00000000+1.j]], + + [[4.00000000+4.j,4.76923084+1.53846157j, + 4.76923084+1.53846157j,4.76923084+1.53846157j, + 4.76923084+1.53846157j,4.76923084+1.53846157j,4.00000000+4.j], + [4.00000000+4.j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j,4.00000000+4.j], + [4.00000000+4.j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j,4.00000000+4.j], + [4.00000000+4.j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j,4.00000000+4.j], + [4.00000000+4.j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j, + 5.71428585+0.95238096j,5.71428585+0.95238096j,4.00000000+4.j], + [4.00000000+4.j,6.00000000+1.53846157j, + 6.00000000+1.53846157j,6.00000000+1.53846157j, + 6.00000000+1.53846157j,6.00000000+1.53846157j,4.00000000+4.j], + [4.00000000+4.j,4.00000000+4.j,4.00000000+4.j, + 4.00000000+4.j,4.00000000+4.j,4.00000000+4.j, + 4.00000000+4.j]]], dtype=COMPLEX_TYPE) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_array_equal(original_probe, + probe, + err_msg="The probe has been updated when it shouldn't have been.") + np.testing.assert_array_equal(expected_object, + obj, + err_msg="The object has not been updated correctly.") + + + def test_difference_map_overlap_update_test_order_of_updates_b(self): + ''' + This tests the order in which the object and probe are updated + ''' + + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = False + do_update_probe = True + # This should mean that the probe is updated, but not the object since max_iterations=1 + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr , exit_addr, dummy_addr, dummy_addr)) + + original_obj = deepcopy(obj) + + expected_probe = np.array([[[ 6.09090948-0.45454547j, 6.09090948-0.45454547j, 6.09090948-0.45454547j, + 6.09090948-0.45454547j, 6.09090948-0.45454547j], + [ 6.09090948-0.45454547j, 6.09090948-0.45454547j, 6.09090948-0.45454547j, + 6.09090948-0.45454547j, 6.09090948-0.45454547j], + [ 6.09090948-0.45454547j, 6.09090948-0.45454547j, 6.09090948-0.45454547j, + 6.09090948-0.45454547j, 6.09090948-0.45454547j], + [ 6.09090948-0.45454547j, 6.09090948-0.45454547j, 6.09090948-0.45454547j, + 6.09090948-0.45454547j, 6.09090948-0.45454547j], + [ 6.09090948-0.45454547j, 6.09090948-0.45454547j, 6.09090948-0.45454547j, + 6.09090948-0.45454547j, 6.09090948-0.45454547j]], + [[ 3.08270693+0.07518797j, 3.08270693+0.07518797j, 3.08270693+0.07518797j, + 3.08270693+0.07518797j, 3.08270693+0.07518797j], + [ 3.08270693+0.07518797j, 3.08270693+0.07518797j, 3.08270693+0.07518797j, + 3.08270693+0.07518797j, 3.08270693+0.07518797j], + [ 3.08270693+0.07518797j, 3.08270693+0.07518797j, 3.08270693+0.07518797j, + 3.08270693+0.07518797j, 3.08270693+0.07518797j], + [ 3.08270693+0.07518797j, 3.08270693+0.07518797j, 3.08270693+0.07518797j, + 3.08270693+0.07518797j, 3.08270693+0.07518797j], + [ 3.08270693+0.07518797j, 3.08270693+0.07518797j, 3.08270693+0.07518797j, + 3.08270693+0.07518797j, 3.08270693+0.07518797j]]], dtype=COMPLEX_TYPE) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_array_equal(original_obj, + obj, + err_msg="The object has been updated when it shouldn't have been.") + np.testing.assert_array_equal(expected_probe, + probe, + err_msg="The probe has not been updated correctly.") + + def test_difference_map_overlap_update_test_order_of_updates_c(self): + ''' + This tests the order in which the object and probe are updated + ''' + + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = False + do_update_probe = False + # neither the probe or the object are updated + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr , exit_addr, dummy_addr, dummy_addr)) + + original_obj = deepcopy(obj) + original_probe = deepcopy(probe) + + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_array_equal(original_obj, + obj, + err_msg="The object has been updated when it shouldn't have been.") + np.testing.assert_array_equal(original_probe, + probe, + err_msg="The probe has been updated when it shouldn't have been.") + + def test_difference_map_overlap_update_test_order_of_updates_d(self): + ''' + This tests the order in which the object and probe are updated + ''' + + smooth_std = None # anything else currently not supported + max_iterations = 1 + update_object_first = True + do_update_probe = True + # both the object and the probe are updated + ocf = 1 # spam this for this test Not needed. + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like( + probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr, exit_addr, dummy_addr, dummy_addr)) + + expected_probe = np.array([[[ 0.34795576+0.32689944j, 0.34795576+0.32689944j, 0.34795576+0.32689944j, + 0.34795576+0.32689944j, 0.34795576+0.32689944j], + [ 0.35228869+0.48999104j, 0.35228869+0.48999104j, 0.35228869+0.48999104j, + 0.35228869+0.48999104j, 0.35228869+0.48999104j], + [ 0.35228869+0.48999104j, 0.35228869+0.48999104j, 0.35228869+0.48999104j, + 0.35228869+0.48999104j, 0.35228869+0.48999104j], + [ 0.35228869+0.48999104j, 0.35228869+0.48999104j, 0.35228869+0.48999104j, + 0.35228869+0.48999104j, 0.35228869+0.48999104j], + [-0.14553808-0.24420798j, -0.14553808-0.24420798j, -0.14553808-0.24420798j, + -0.14553808-0.24420798j, -0.14553808-0.24420798j]], + + [[ 2.74465489+1.76501989j, 2.74465489+1.76501989j, 2.74465489+1.76501989j, + 2.74465489+1.76501989j, 2.74465489+1.76501989j], + [ 2.46576023+1.78177691j, 2.46576023+1.78177691j, 2.46576023+1.78177691j, + 2.46576023+1.78177691j, 2.46576023+1.78177691j], + [ 2.46576023+1.78177691j, 2.46576023+1.78177691j, 2.46576023+1.78177691j, + 2.46576023+1.78177691j, 2.46576023+1.78177691j], + [ 2.46576023+1.78177691j, 2.46576023+1.78177691j, 2.46576023+1.78177691j, + 2.46576023+1.78177691j, 2.46576023+1.78177691j], + [ 2.47635674+1.60818517j, 2.47635674+1.60818517j, 2.47635674+1.60818517j, + 2.47635674+1.60818517j, 2.47635674+1.60818517j]]], dtype=COMPLEX_TYPE) + + expected_object = np.array([[[-5.00000000 + 5.j, -5.00000000 + 5.j, -5.00000000 + 5.j, + -5.00000000 + 5.j, -5.00000000 + 5.j, 1.00000000 + 1.j, 1.00000000 + 1.j], + [10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 1.00000000 + 1.j, 1.00000000 + 1.j], + [10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 1.00000000 + 1.j, 1.00000000 + 1.j], + [10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 1.00000000 + 1.j, 1.00000000 + 1.j], + [10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 10.33333397 - 1.66666675j, + 10.33333397 - 1.66666675j, 1.00000000 + 1.j, 1.00000000 + 1.j], + [-21.00000000 + 5.j, -21.00000000 + 5.j, -21.00000000 + 5.j, + -21.00000000 + 5.j, -21.00000000 + 5.j, 1.00000000 + 1.j, + 1.00000000 + 1.j], + [1.00000000 + 1.j, 1.00000000 + 1.j, 1.00000000 + 1.j, + 1.00000000 + 1.j, 1.00000000 + 1.j, 1.00000000 + 1.j, + 1.00000000 + 1.j]], + + [[4.00000000 + 4.j, 4.76923084 + 1.53846157j, + 4.76923084 + 1.53846157j, 4.76923084 + 1.53846157j, + 4.76923084 + 1.53846157j, 4.76923084 + 1.53846157j, 4.00000000 + 4.j], + [4.00000000 + 4.j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, 4.00000000 + 4.j], + [4.00000000 + 4.j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, 4.00000000 + 4.j], + [4.00000000 + 4.j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, 4.00000000 + 4.j], + [4.00000000 + 4.j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, + 5.71428585 + 0.95238096j, 5.71428585 + 0.95238096j, 4.00000000 + 4.j], + [4.00000000 + 4.j, 6.00000000 + 1.53846157j, + 6.00000000 + 1.53846157j, 6.00000000 + 1.53846157j, + 6.00000000 + 1.53846157j, 6.00000000 + 1.53846157j, 4.00000000 + 4.j], + [4.00000000 + 4.j, 4.00000000 + 4.j, 4.00000000 + 4.j, + 4.00000000 + 4.j, 4.00000000 + 4.j, 4.00000000 + 4.j, + 4.00000000 + 4.j]]], dtype=COMPLEX_TYPE) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + np.testing.assert_array_equal(expected_probe, + probe, + err_msg="The probe has been updated when it shouldn't have been.") + np.testing.assert_array_equal(expected_object, + obj, + err_msg="The object has not been updated correctly.") + + + + + def test_difference_map_overlap_update_break_when_in_tolerance(self): + ''' + This tests if the loop breaks according to the convergence criterion. + ''' + + + smooth_std = None # anything else currently not supported + max_iterations = 100 + update_object_first = False + do_update_probe = True + # both the object and the probe are updated + ocf = 4.2e-2 # chosen so that this should terminate on teh 6th iteration + + # create some inputs - I should really make this a utility... + B = 5 + C = 5 + + D = 2 + E = B + F = C + + npts_greater_than = 2 + G = 2 + H = B + npts_greater_than + I = C + npts_greater_than + + scan_pts = 2 + A = scan_pts ** 2 * G * D # this is a 16 point scan pattern (4x4 grid) over all the modes + + # shapes and types outlined here + exit_wave = np.empty(shape=(A, B, C), dtype=COMPLEX_TYPE) + for idx in range(A): + exit_wave[idx] = np.ones((B, C)) * (idx + 1) + 1j * np.ones((B, C)) * (idx + 1) + + exit_addr = np.empty(shape=(A, 3), dtype=int) + exit_addr[:, 0] = np.array(range(A)) + exit_addr[:, 1] = np.zeros((A,)) + exit_addr[:, 2] = np.zeros((A,)) + + probe = np.empty(shape=(D, E, F), dtype=COMPLEX_TYPE) + for idx in range(D): + probe[idx] = np.ones((E, F)) * (idx + 1) + 1j * np.ones((E, F)) * (idx + 1) + probe_addr = np.empty(shape=(A, 3), dtype=int) + probe_addr[:, 0] = np.array(range(D)).repeat(A / D) + probe_addr[:, 1] = np.zeros((A,)) + probe_addr[:, 2] = np.zeros((A,)) + + obj = np.empty(shape=(G, H, I), dtype=COMPLEX_TYPE) + for idx in range(G): + obj[idx] = np.ones((H, I)) * (3 * idx + 1) + 1j * np.ones((H, I)) * (3 * idx + 1) + obj_addr = np.empty(shape=(A, 3), dtype=int) + obj_addr[:, 0] = np.array(range(G)).repeat(A / G) + X, Y = np.meshgrid(range(scan_pts), range(scan_pts)) + X = X.reshape((scan_pts ** 2)) + Y = Y.reshape((scan_pts ** 2)) + for idx in range(G): + for idy in range(D): + index = idy + 2 * idx + obj_addr[index::scan_pts ** 2, 1] = X + obj_addr[index::scan_pts ** 2, 2] = Y + + obj_weights = np.empty(shape=(G,), dtype=FLOAT_TYPE) + obj_weights[:] = np.linspace(-1, 1, G) + + probe_weights = np.empty(shape=(D,), dtype=FLOAT_TYPE) + probe_weights[:] = np.linspace(-1, 1, D) + + cfact_object = np.empty_like(obj) + for idx in range(G): + cfact_object[idx] = np.ones((H, I)) * 10 * (idx + 1) + + cfact_probe = np.empty_like(probe) + for idx in range(G): + cfact_probe[idx] = np.ones((E, F)) * 5 * (idx + 1) + + dummy_addr = np.zeros_like(probe_addr) # these aren't used by the function, but are passed as a top level address book + addr_info = list(zip(probe_addr, obj_addr, exit_addr, dummy_addr, dummy_addr)) + + expected_probe = np.array([[[ 45.64985275-4.16102743j, 45.64985275-4.16102743j, + 45.64985275-4.16102743j, 45.64985275-4.16102743j, + 45.64985275-4.16102743j], + [ -9.70029163+0.79768848j, -9.70029163+0.79768848j, + -9.70029163+0.79768848j, -9.70029163+0.79768848j, + -9.70029163+0.79768848j], + [ 4.76249838-0.31339467j, 4.76249838-0.31339467j, + 4.76249838-0.31339467j, 4.76249838-0.31339467j, + 4.76249838-0.31339467j], + [ 3.71407413-0.28579614j, 3.71407413-0.28579614j, + 3.71407413-0.28579614j, 3.71407413-0.28579614j, + 3.71407413-0.28579614j], + [ 2.35006571-0.19345209j, 2.35006571-0.19345209j, + 2.35006571-0.19345209j, 2.35006571-0.19345209j, + 2.35006571-0.19345209j]], + + [[ 3.99001932+0.07404561j, 3.99001932+0.07404561j, + 3.99001932+0.07404561j, 3.99001932+0.07404561j, + 3.99001932+0.07404561j], + [ 3.36987257+0.06362584j, 3.36987257+0.06362584j, + 3.36987257+0.06362584j, 3.36987257+0.06362584j, + 3.36987257+0.06362584j], + [ 3.10962296+0.05934116j, 3.10962296+0.05934116j, + 3.10962296+0.05934116j, 3.10962296+0.05934116j, + 3.10962296+0.05934116j], + [ 2.90126610+0.05487255j, 2.90126610+0.05487255j, + 2.90126610+0.05487255j, 2.90126610+0.05487255j, + 2.90126610+0.05487255j], + [ 2.51116419+0.04660653j, 2.51116419+0.04660653j, + 2.51116419+0.04660653j, 2.51116419+0.04660653j, + 2.51116419+0.04660653j]]], dtype=COMPLEX_TYPE) + + expected_object=np.array([[[ 0.04918606+0.05905421j, 0.04918606+0.05905421j, 0.04918606+0.05905421j, + 0.04918606+0.05905421j, 0.04918606+0.05905421j, 1.00000000+1.j, 1.00000000+1.j], + [ 0.11200862+0.13464974j, 0.11200862+0.13464974j, 0.11200862+0.13464974j, + 0.11200862+0.13464974j, 0.11200862+0.13464974j, 1.00000000+1.j,1.00000000+1.j], + [-0.51355976-0.61082107j, -0.51355976-0.61082107j, -0.51355976-0.61082107j, + -0.51355976-0.61082107j, -0.51355976-0.61082107j, 1.00000000+1.j, 1.00000000+1.j], + [ 0.99391210+1.14208853j, 0.99391210+1.14208853j, 0.99391210+1.14208853j, + 0.99391210+1.14208853j, 0.99391210+1.14208853j, 1.00000000+1.j, 1.00000000+1.j], + [ 1.22169828+1.43459976j, 1.22169828+1.43459976j, 1.22169828+1.43459976j, + 1.22169828+1.43459976j, 1.22169828+1.43459976j, 1.00000000+1.j, 1.00000000+1.j], + [ 2.36522031+2.79235673j, 2.36522031+2.79235673j, 2.36522031+2.79235673j, + 2.36522031+2.79235673j, 2.36522031+2.79235673j, 1.00000000+1.j,1.00000000+1.j], + [ 1.00000000+1.j, 1.00000000+1.j, 1.00000000+1.j, + 1.00000000+1.j, 1.00000000+1.j, 1.00000000+1.j, 1.00000000+1.j ]], + [[ 4.00000000+4.j, 2.80242014+2.70098448j, 2.80242014+2.70098448j, + 2.80242014+2.70098448j, 2.80242014+2.70098448j, 2.80242014+2.70098448j, + 4.00000000+4.j], + [ 4.00000000+4.j, 3.59294462+3.46147537j, 3.59294462+3.46147537j, + 3.59294462+3.46147537j, 3.59294462+3.46147537j, 3.59294462+3.46147537j, + 4.00000000+4.j], + [ 4.00000000+4.j, 3.99926472+3.8498373j, 3.99926472+3.8498373j, + 3.99926472+3.8498373j, 3.99926472+3.8498373j, 3.99926472+3.8498373j, + 4.00000000+4.j], + [ 4.00000000+4.j, 4.21914482+4.06092405j, 4.21914482+4.06092405j, + 4.21914482+4.06092405j, 4.21914482+4.06092405j, 4.21914482+4.06092405j, + 4.00000000+4.j], + [ 4.00000000+4.j, 4.60679293+4.43693876j, 4.60679293+4.43693876j, + 4.60679293+4.43693876j, 4.60679293+4.43693876j, 4.60679293+4.43693876j, + 4.00000000+4.j], + [ 4.00000000+4.j, 5.59837151+5.39574909j, 5.59837151+5.39574909j, + 5.59837151+5.39574909j, 5.59837151+5.39574909j, 5.59837151+5.39574909j, + 4.00000000+4.j], + [ 4.00000000+4.j, 4.00000000+4.j, 4.00000000+4.j, + 4.00000000+4.j, 4.00000000+4.j, 4.00000000+4.j, + 4.00000000+4.j]]] ,dtype=COMPLEX_TYPE) + + opi.difference_map_overlap_update(addr_info=addr_info, + cfact_object=cfact_object, + cfact_probe=cfact_probe, + do_update_probe=do_update_probe, + exit_wave=exit_wave, + ob=obj, + object_weights=obj_weights, + probe=probe, + probe_support=None, + probe_weights=probe_weights, + max_iterations=max_iterations, + update_object_first=update_object_first, + obj_smooth_std=smooth_std, + overlap_converge_factor=ocf, + probe_center_tol=None, + clip_object=None) + + + + np.testing.assert_allclose(expected_probe, + probe, + err_msg="The probe has not been updated correctly") + + print(obj) + np.testing.assert_allclose(expected_object, + obj, + err_msg="The object has not been updated correctly.") + + + if __name__ == "__main__": + unittest.main() diff --git a/test/archive_tests/array_based_tests/object_probe_interaction_unity_test.py b/test/archive_tests/array_based_tests/object_probe_interaction_unity_test.py new file mode 100644 index 000000000..06ee40196 --- /dev/null +++ b/test/archive_tests/array_based_tests/object_probe_interaction_unity_test.py @@ -0,0 +1,44 @@ +''' +This is a unity test comparing to the pod based framework +''' + +import unittest +import numpy as np +from test.archive_tests.array_based_tests import utils as tu +from ptypy.accelerate.array_based import data_utils as du +from ptypy.accelerate.array_based import object_probe_interaction as opi +from collections import OrderedDict + + +class ObjectProbeInteractionUnityTest(unittest.TestCase): + + def test_difference_map_realspace_constraint_UNITY(self): + PtychoInstance = tu.get_ptycho_instance('pod_to_numpy_test') + a_ptycho_instance = tu.get_ptycho_instance('pod_to_numpy_test') + # now convert to arrays + vectorised_scan = du.pod_to_arrays(PtychoInstance, 'S0000') + addr_info = vectorised_scan['meta']['addr'] # probably want to extract these at a later date, but just to get stuff going... + probe = vectorised_scan['probe'] + obj = vectorised_scan['obj'] + exit_wave = vectorised_scan['exit wave'] + probe_and_object = opi.scan_and_multiply(probe, obj, exit_wave.shape, addr_info) + + ptypy_dm_constraint = self.ptypy_apply_difference_map(a_ptycho_instance) + numpy_dm_constraint = opi.difference_map_realspace_constraint(probe_and_object, + exit_wave, + alpha=1.0) + for idx, key in enumerate(ptypy_dm_constraint): + np.testing.assert_allclose(ptypy_dm_constraint[key], numpy_dm_constraint[idx]) + + def ptypy_apply_difference_map(self, a_ptycho_instance): + f = OrderedDict() + alpha = 1.0 + for dname, diff_view in a_ptycho_instance.diff.views.items(): + for name, pod in diff_view.pods.items(): + if not pod.active: + continue + f[name] = (1 + alpha) * pod.probe * pod.object - alpha * pod.exit + return f + + if __name__ == "__main__": + unittest.main() diff --git a/test/archive_tests/array_based_tests/utils.py b/test/archive_tests/array_based_tests/utils.py new file mode 100644 index 000000000..42d638a2a --- /dev/null +++ b/test/archive_tests/array_based_tests/utils.py @@ -0,0 +1,66 @@ +''' +Created on 4 Jan 2018 + +@author: clb02321 +''' +import unittest +from ptypy.core import Ptycho +from ptypy import utils as u + + +def get_ptycho_instance(label=None, num_modes=1, size=64, length=8): + ''' + new ptypy probably has a better way of doing this. + ''' + p = u.Param() + p.verbose_level = 0 + p.data_type = "single" + p.run = label + p.io = u.Param() + p.io.home = "/tmp/ptypy/" + p.io.interaction = u.Param(active=False) + p.io.autoplot = u.Param(active=False) + p.scans = u.Param() + p.scans.MF = u.Param() + p.scans.MF.name = 'Full' + p.scans.MF.propagation = 'farfield' + p.scans.MF.data = u.Param() + p.scans.MF.data.name = 'MoonFlowerScan' + p.scans.MF.data.positions_theory = None + p.scans.MF.data.auto_center = None + p.scans.MF.data.min_frames = 1 + p.scans.MF.data.orientation = None + p.scans.MF.data.num_frames =length + p.scans.MF.data.energy = 6.2 + p.scans.MF.data.shape = size + p.scans.MF.data.chunk_format = '.chunk%02d' + p.scans.MF.data.rebin = None + p.scans.MF.data.experimentID = None + p.scans.MF.data.label = None + p.scans.MF.data.version = 0.1 + p.scans.MF.data.dfile = None + p.scans.MF.data.psize = 0.000172 + p.scans.MF.data.load_parallel = None + p.scans.MF.data.distance = 7.0 + p.scans.MF.data.save = None + p.scans.MF.data.center = 'fftshift' + p.scans.MF.data.photons = 100000000.0 + p.scans.MF.data.psf = 0.0 + p.scans.MF.data.add_poisson_noise = False + p.scans.MF.data.density = 0.2 + p.scans.MF.illumination = u.Param() + p.scans.MF.illumination.model = None + p.scans.MF.illumination.aperture = u.Param() + p.scans.MF.illumination.aperture.diffuser = None + p.scans.MF.illumination.aperture.form = "circ" + p.scans.MF.illumination.aperture.size = 3e-6 + p.scans.MF.illumination.aperture.edge = 10 + p.scans.MF.coherence = u.Param() + p.scans.MF.coherence.num_probe_modes = num_modes + P = Ptycho(p, level=4) + P.di = P.diff + P.ma = P.mask + P.ex = P.exit + P.pr = P.probe + P.ob = P.obj + return P diff --git a/test/archive_tests/dls_tests/__init__.py b/test/archive_tests/dls_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/archive_tests/dls_tests/dls_auxiliary_wave_kernel_test.py b/test/archive_tests/dls_tests/dls_auxiliary_wave_kernel_test.py new file mode 100644 index 000000000..0d943c28e --- /dev/null +++ b/test/archive_tests/dls_tests/dls_auxiliary_wave_kernel_test.py @@ -0,0 +1,57 @@ +''' +Testing based on real data +''' +import h5py +import unittest +import numpy as np +from parameterized import parameterized +from .. import perfrun, PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import AuxiliaryWaveKernel +from ptypy.accelerate.base.kernels import AuxiliaryWaveKernel as BaseAuxiliaryWaveKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class DlsAuxiliaryWaveKernelTest(PyCudaTest): + + datadir = "/dls/science/users/iat69393/gpu-hackathon/test-data-%s/" + rtol = 1e-6 + atol = 1e-6 + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_build_aux_no_ex_noadd_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir % name + "build_aux_no_ex_%04d.h5" %iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + ob = f["ob"][:] + pr = f["pr"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + addr_dev = gpuarray.to_gpu(addr) + ob_dev = gpuarray.to_gpu(ob) + pr_dev = gpuarray.to_gpu(pr) + + # CPU kernel + BAWK = BaseAuxiliaryWaveKernel() + BAWK.allocate() + BAWK.build_aux_no_ex(aux, addr, ob, pr, add=False) + + ## GPU kernel + AWK = AuxiliaryWaveKernel(self.stream) + AWK.allocate() + AWK.build_aux_no_ex(aux_dev, addr_dev, ob_dev, pr_dev, add=False) + + ## Assert + np.testing.assert_allclose(aux_dev.get(), aux, rtol=self.rtol, atol=self.atol, + err_msg="The auxiliary_wave does not match the base kernel output") \ No newline at end of file diff --git a/test/archive_tests/dls_tests/dls_drpycuda_test.py b/test/archive_tests/dls_tests/dls_drpycuda_test.py new file mode 100644 index 000000000..57f62f9dd --- /dev/null +++ b/test/archive_tests/dls_tests/dls_drpycuda_test.py @@ -0,0 +1,83 @@ +''' +Testing on real data +''' + +import h5py +import unittest +import numpy as np +from parameterized import parameterized +from .. import PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import PoUpdateKernel +from ptypy.accelerate.base.kernels import PoUpdateKernel as BasePoUpdateKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class DlsDRpycudaTest(PyCudaTest): + + datadir = "/dls/science/users/iat69393/gpu-hackathon/test-data-dr/" + iter = 0 + rtol = 1e-6 + atol = 1e-6 + + def test_ob_update_local_UNITY(self): + + # Load data + with h5py.File(self.datadir + "ob_update_local_%04d.h5" %self.iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + ob = f["ob"][:] + pr = f["pr"][:] + ex = f["ex"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + ob_dev = gpuarray.to_gpu(ob) + pr_dev = gpuarray.to_gpu(pr) + ex_dev = gpuarray.to_gpu(ex) + addr_dev = gpuarray.to_gpu(addr) + + # CPU Kernel + BPOK = BasePoUpdateKernel() + BPOK.ob_update_local(addr, ob, pr, ex, aux) + + # GPU Kernel + POK = PoUpdateKernel() + POK.ob_update_local(addr_dev, ob_dev, pr_dev, ex_dev, aux_dev) + + ## Assert + np.testing.assert_allclose(ob_dev.get(), ob, atol=self.atol, rtol=self.rtol, verbose=False, + err_msg="The object array has not been updated as expected") + + def test_pr_update_local_UNITY(self): + + # Load data + with h5py.File(self.datadir + "pr_update_local_%04d.h5" %self.iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + ob = f["ob"][:] + pr = f["pr"][:] + ex = f["ex"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + ob_dev = gpuarray.to_gpu(ob) + pr_dev = gpuarray.to_gpu(pr) + ex_dev = gpuarray.to_gpu(ex) + addr_dev = gpuarray.to_gpu(addr) + + # CPU Kernel + BPOK = BasePoUpdateKernel() + BPOK.pr_update_local(addr, pr, ob, ex, aux) + + # GPU Kernel + POK = PoUpdateKernel() + POK.pr_update_local(addr_dev, pr_dev, ob_dev, ex_dev, aux_dev) + + ## Assert + np.testing.assert_allclose(pr_dev.get(), pr, atol=self.atol, rtol=self.rtol, verbose=False, + err_msg="The object array has not been updated as expected") diff --git a/test/archive_tests/dls_tests/dls_gradient_descent_kernel_test.py b/test/archive_tests/dls_tests/dls_gradient_descent_kernel_test.py new file mode 100644 index 000000000..f62834e2e --- /dev/null +++ b/test/archive_tests/dls_tests/dls_gradient_descent_kernel_test.py @@ -0,0 +1,261 @@ +''' +Testing on real data +''' + +import h5py +import unittest +import numpy as np +from parameterized import parameterized +from .. import perfrun, PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import GradientDescentKernel +from ptypy.accelerate.base.kernels import GradientDescentKernel as BaseGradientDescentKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class DlsGradientDescentKernelTest(PyCudaTest): + + datadir = "/dls/science/users/iat69393/gpu-hackathon/test-data-%s/" + rtol = 1e-6 + atol = 1e-6 + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_make_model_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir %name + "make_model_%04d.h5" %iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + addr_dev = gpuarray.to_gpu(addr) + + # CPU Kernel + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.make_model(aux, addr) + + # GPU kernel + GDK = GradientDescentKernel(aux_dev, addr.shape[1]) + GDK.allocate() + GDK.make_model(aux_dev, addr_dev) + + ## Assert + np.testing.assert_allclose(BGDK.npy.Imodel, GDK.gpu.Imodel.get(), atol=self.atol, rtol=self.rtol, + err_msg="`Imodel` buffer has not been updated as expected") + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_floating_intensity_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir %name + "floating_intensities_%04d.h5" %iter, "r") as f: + w = f["w"][:] + addr = f["addr"][:] + I = f["I"][:] + fic = f["fic"][:] + Imodel = f["Imodel"][:] + with h5py.File(self.datadir %name + "make_model_%04d.h5" %iter, "r") as f: + aux = f["aux"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + w_dev = gpuarray.to_gpu(w) + addr_dev = gpuarray.to_gpu(addr) + I_dev = gpuarray.to_gpu(I) + fic_dev = gpuarray.to_gpu(fic) + Imodel_dev = gpuarray.to_gpu(np.ascontiguousarray(Imodel)) + + # CPU Kernel + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.npy.Imodel = Imodel + BGDK.floating_intensity(addr, w, I, fic) + + # GPU kernel + GDK = GradientDescentKernel(aux_dev, addr.shape[1]) + GDK.allocate() + GDK.gpu.Imodel = Imodel_dev + GDK.floating_intensity(addr_dev, w_dev, I_dev, fic_dev) + + ## Assert + np.testing.assert_allclose(BGDK.npy.LLerr, GDK.gpu.LLerr.get(), atol=self.atol, rtol=self.rtol, + verbose=False, equal_nan=False, + err_msg="`LLerr` buffer has not been updated as expected") + np.testing.assert_allclose(BGDK.npy.LLden, GDK.gpu.LLden.get(), atol=self.atol, rtol=self.rtol, + verbose=False, equal_nan=False, + err_msg="`LLden` buffer has not been updated as expected") + np.testing.assert_allclose(BGDK.npy.fic_tmp, GDK.gpu.fic_tmp.get(), atol=self.atol, rtol=self.rtol, + verbose=False, equal_nan=False, + err_msg="`fic_tmp` buffer has not been updated as expected") + + np.testing.assert_allclose(fic, fic_dev.get(), atol=self.atol, rtol=self.rtol, + verbose=False, equal_nan=False, + err_msg="floating intensity coeff (fic) has not been updated as expected") + + np.testing.assert_allclose(BGDK.npy.Imodel, GDK.gpu.Imodel.get(), atol=self.atol, rtol=self.rtol, + verbose=False, equal_nan=False, + err_msg="`Imodel` buffer has not been updated as expected") + + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_main_and_error_reduce_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir %name + "main_%04d.h5" %iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + w = f["w"][:] + I = f["I"][:] + # Load data + with h5py.File(self.datadir %name + "error_reduce_%04d.h5" %iter, "r") as f: + err_phot = f["err_phot"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + w_dev = gpuarray.to_gpu(w) + addr_dev = gpuarray.to_gpu(addr) + I_dev = gpuarray.to_gpu(I) + err_phot_dev = gpuarray.to_gpu(err_phot) + + # CPU Kernel + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.main(aux, addr, w, I) + BGDK.error_reduce(addr, err_phot) + + # GPU kernel + GDK = GradientDescentKernel(aux_dev, addr.shape[1]) + GDK.allocate() + GDK.main(aux_dev, addr_dev, w_dev, I_dev) + GDK.error_reduce(addr_dev, err_phot_dev) + + ## Assert + np.testing.assert_allclose(aux, aux_dev.get(), atol=self.atol, rtol=self.rtol, + err_msg="Auxiliary has not been updated as expected") + np.testing.assert_allclose(BGDK.npy.LLerr, GDK.gpu.LLerr.get(), atol=self.atol, rtol=self.rtol, + err_msg="LogLikelihood error has not been updated as expected") + np.testing.assert_allclose(err_phot, err_phot_dev.get(), atol=self.atol, rtol=self.rtol, + err_msg="`err_phot` has not been updated as expected") + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_make_a012_UNITY(self, name, iter): + + # Reduce the array size to make the tests run faster + Nmax = 10 + Ymax = 128 + Xmax = 128 + + # Load data + with h5py.File(self.datadir %name + "make_a012_%04d.h5" %iter, "r") as g: + addr = g["addr"][:Nmax] + I = g["I"][:Nmax,:Ymax,:Xmax] + f = g["f"][:Nmax,:Ymax,:Xmax] + a = g["a"][:Nmax,:Ymax,:Xmax] + b = g["b"][:Nmax,:Ymax,:Xmax] + fic = g["fic"][:Nmax] + with h5py.File(self.datadir %name + "make_model_%04d.h5" %iter, "r") as h: + aux = h["aux"][:Nmax,:Ymax,:Xmax] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + addr_dev = gpuarray.to_gpu(addr) + I_dev = gpuarray.to_gpu(I) + f_dev = gpuarray.to_gpu(f) + a_dev = gpuarray.to_gpu(a) + b_dev = gpuarray.to_gpu(b) + fic_dev = gpuarray.to_gpu(fic) + + # CPU Kernel + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.make_a012(f, a, b, addr, I, fic) + + # GPU kernel + GDK = GradientDescentKernel(aux_dev, addr.shape[1], queue=self.stream) + GDK.allocate() + GDK.gpu.Imodel.fill(np.nan) + GDK.gpu.LLerr.fill(np.nan) + GDK.gpu.LLden.fill(np.nan) + GDK.make_a012(f_dev, a_dev, b_dev, addr_dev, I_dev, fic_dev) + + ## Assert + np.testing.assert_allclose(GDK.gpu.Imodel.get(), BGDK.npy.Imodel, atol=self.atol, rtol=self.rtol, + err_msg="Imodel error has not been updated as expected") + np.testing.assert_allclose(GDK.gpu.LLerr.get(), BGDK.npy.LLerr, atol=self.atol, rtol=self.rtol, + err_msg="LLerr error has not been updated as expected") + np.testing.assert_allclose(GDK.gpu.LLden.get(), BGDK.npy.LLden, atol=self.atol, rtol=self.rtol, + err_msg="LLden error has not been updated as expected") + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_fill_b_UNITY(self, name, iter): + + Nmax = 10 + Ymax = 128 + Xmax = 128 + + # Load data + with h5py.File(self.datadir %name + "fill_b_%04d.h5" %iter, "r") as f: + w = f["w"][:Nmax, :Ymax, :Xmax] + addr = f["addr"][:] + B = f["B"][:] + Brenorm = f["Brenorm"][...] + A0 = f["A0"][:Nmax, :Ymax, :Xmax] + A1 = f["A1"][:Nmax, :Ymax, :Xmax] + A2 = f["A2"][:Nmax, :Ymax, :Xmax] + with h5py.File(self.datadir %name + "make_model_%04d.h5" %iter, "r") as f: + aux = f["aux"][:Nmax, :Ymax, :Xmax] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + w_dev = gpuarray.to_gpu(w) + addr_dev = gpuarray.to_gpu(addr) + B_dev = gpuarray.to_gpu(B.astype(np.float32)) + A0_dev = gpuarray.to_gpu(A0) + A1_dev = gpuarray.to_gpu(A1) + A2_dev = gpuarray.to_gpu(A2) + + # CPU Kernel + BGDK = BaseGradientDescentKernel(aux, addr.shape[1]) + BGDK.allocate() + BGDK.npy.Imodel = A0 + BGDK.npy.LLerr = A1 + BGDK.npy.LLden = A2 + BGDK.fill_b(addr, Brenorm, w, B) + + # GPU kernel + GDK = GradientDescentKernel(aux_dev, addr.shape[1]) + GDK.allocate() + GDK.gpu.Imodel = A0_dev + GDK.gpu.LLerr = A1_dev + GDK.gpu.LLden = A2_dev + GDK.fill_b(addr_dev, Brenorm, w_dev, B_dev) + + ## Assert + np.testing.assert_allclose(B, B_dev.get(), rtol=self.rtol, atol=self.atol, + err_msg="`B` has not been updated as expected") + diff --git a/test/archive_tests/dls_tests/dls_po_update_kernel_test.py b/test/archive_tests/dls_tests/dls_po_update_kernel_test.py new file mode 100644 index 000000000..3b8ee0474 --- /dev/null +++ b/test/archive_tests/dls_tests/dls_po_update_kernel_test.py @@ -0,0 +1,106 @@ +''' +Testing on real data +''' + +import h5py +import unittest +import numpy as np +from parameterized import parameterized +from .. import PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import PoUpdateKernel +from ptypy.accelerate.base.kernels import PoUpdateKernel as BasePoUpdateKernel + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class DlsPoUpdateKernelTest(PyCudaTest): + + datadir = "/dls/science/users/iat69393/gpu-hackathon/test-data-%s/" + rtol = 1e-6 + atol = 1e-6 + + @parameterized.expand([ + ["base", 10, False], + ["regul", 50, False], + ["floating", 0, False], + ["base", 10, True], + ["regul", 50, True], + ["floating", 0, True], + ]) + def test_op_update_ml_UNITY(self, name, iter, atomics): + + # Load data + with h5py.File(self.datadir %name + "op_update_ml_%04d.h5" %iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + obg = f["obg"][:] + pr = f["pr"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + obg_dev = gpuarray.to_gpu(obg) + pr_dev = gpuarray.to_gpu(pr) + + # If not using atomics we need to change the addresses + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr, (2, 3, 0, 1))) + addr_dev = gpuarray.to_gpu(addr2) + else: + addr_dev = gpuarray.to_gpu(addr) + + # CPU Kernel + BPOK = BasePoUpdateKernel() + BPOK.ob_update_ML(addr, obg, pr, aux) + + # GPU Kernel + POK = PoUpdateKernel() + POK.ob_update_ML(addr_dev, obg_dev, pr_dev, aux_dev, atomics=atomics) + + ## Assert + np.testing.assert_allclose(obg_dev.get(), obg, atol=self.atol, rtol=self.rtol, verbose=False, + err_msg="The object array has not been updated as expected") + + @parameterized.expand([ + ["base", 10, False], + ["regul", 50, False], + ["floating", 0, False], + ["base", 10, True], + ["regul", 50, True], + ["floating", 0, True], + ]) + def test_pr_update_ml_UNITY(self, name, iter, atomics): + + # Load data + with h5py.File(self.datadir %name + "pr_update_ml_%04d.h5" %iter, "r") as f: + aux = f["aux"][:] + addr = f["addr"][:] + ob = f["ob"][:] + prg = f["prg"][:] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + ob_dev = gpuarray.to_gpu(ob) + prg_dev = gpuarray.to_gpu(prg) + + # If not using atomics we need to change the addresses + if not atomics: + addr2 = np.ascontiguousarray(np.transpose(addr, (2, 3, 0, 1))) + addr_dev = gpuarray.to_gpu(addr2) + else: + addr_dev = gpuarray.to_gpu(addr) + + # CPU Kernel + BPOK = BasePoUpdateKernel() + BPOK.pr_update_ML(addr, prg, ob, aux) + + # GPU Kernel + POK = PoUpdateKernel() + POK.pr_update_ML(addr_dev, prg_dev, ob_dev, aux_dev, atomics=atomics) + + ## Assert + np.testing.assert_allclose(prg, prg_dev.get(), atol=self.atol, rtol=self.rtol, verbose=False, + err_msg="The probe array has not been updated as expected") \ No newline at end of file diff --git a/test/archive_tests/dls_tests/dls_propagation_kernel_test.py b/test/archive_tests/dls_tests/dls_propagation_kernel_test.py new file mode 100644 index 000000000..ac9fa0402 --- /dev/null +++ b/test/archive_tests/dls_tests/dls_propagation_kernel_test.py @@ -0,0 +1,102 @@ +''' +testing on real data +''' + +import h5py +import unittest +import numpy as np +from parameterized import parameterized +from .. import PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.kernels import PropagationKernel + +import ptypy.utils as u +from ptypy.core import geometry +from ptypy.core import Base as theBase + +# subclass for dictionary access +Base = type('Base',(theBase,),{}) + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class DLsPropagationKernelTest(PyCudaTest): + + datadir = "/dls/science/users/iat69393/gpu-hackathon/test-data-%s/" + rtol = 1e-6 + atol = 1e-6 + + def set_up_farfield(self,shape): + P = Base() + P.CType = COMPLEX_TYPE + P.Ftype = FLOAT_TYPE + g = u.Param() + g.energy = None # u.keV2m(1.0)/6.32e-7 + g.lam = 5.32e-7 + g.distance = 15e-2 + g.psize = 24e-6 + g.shape = shape + g.propagation = "farfield" + G = geometry.Geo(owner=P, pars=g) + return G + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_forward_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir % name + "forward_%04d.h5" %iter, "r") as f: + aux = f["aux"][0] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + + # Geometry + geo = self.set_up_farfield(aux.shape) + + # CPU kernel + aux = geo.propagator.fw(aux) + + # GPU kernel + PropK = PropagationKernel(aux_dev, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.fw(aux_dev, aux_dev) + + ## Assert + np.testing.assert_allclose(aux, aux_dev.get(), atol=self.atol, rtol=self.rtol, + err_msg="Forward propagation was not as expected") + + @parameterized.expand([ + ["base", 10], + ["regul", 50], + ["floating", 0], + ]) + def test_backward_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir % name + "backward_%04d.h5" %iter, "r") as f: + aux = f["aux"][0] + + # Copy data to device + aux_dev = gpuarray.to_gpu(aux) + + # Geometry + geo = self.set_up_farfield(aux.shape) + + # CPU kernel + aux = geo.propagator.bw(aux) + + # GPU kernel + PropK = PropagationKernel(aux_dev, geo.propagator, queue_thread=self.stream) + PropK.allocate() + PropK.bw(aux_dev, aux_dev) + + ## Assert + np.testing.assert_allclose(aux, aux_dev.get(), atol=self.atol, rtol=self.rtol, + err_msg="Backward propagation was not as expected") \ No newline at end of file diff --git a/test/archive_tests/dls_tests/dls_regularizer_kernel_test.py b/test/archive_tests/dls_tests/dls_regularizer_kernel_test.py new file mode 100644 index 000000000..972648552 --- /dev/null +++ b/test/archive_tests/dls_tests/dls_regularizer_kernel_test.py @@ -0,0 +1,77 @@ +''' +Testing on real data +''' + +import h5py +import unittest +import numpy as np +from parameterized import parameterized +from .. import PyCudaTest, have_pycuda + +if have_pycuda(): + from pycuda import gpuarray + from ptypy.accelerate.cuda_pycuda.engines.ML_pycuda import Regul_del2_pycuda + import pycuda.driver as cuda +from ptypy.engines.ML import Regul_del2 + +COMPLEX_TYPE = np.complex64 +FLOAT_TYPE = np.float32 +INT_TYPE = np.int32 + +class DlsRegularizerTest(PyCudaTest): + + datadir = "/dls/science/users/iat69393/gpu-hackathon/test-data-%s/" + rtol = 1e-6 + atol = 1e-6 + + @parameterized.expand([ + ["regul", 50] + ]) + def test_regularizer_grad_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir %name + "regul_grad_%04d.h5" %iter, "r") as f: + ob = f["ob"][:] + + # Copy data to device + ob_dev = gpuarray.to_gpu(ob) + + # CPU Kernel + regul = Regul_del2(0.1) + obr = regul.grad(ob) + + # GPU Kernel + regul_pycuda = Regul_del2_pycuda(0.1, queue=self.stream, allocator=cuda.mem_alloc) + obr_dev = regul_pycuda.grad(ob_dev) + + ## Assert + np.testing.assert_allclose(obr, obr_dev.get(), atol=self.atol, rtol=self.rtol, + err_msg="The object array has not been updated as expected") + np.testing.assert_allclose(regul.LL, regul_pycuda.LL, atol=self.atol, rtol=self.rtol, + err_msg="The LL array has not been updated as expected") + + @parameterized.expand([ + ["regul", 50], + ]) + def test_regularizer_poly_line_ceoffs_UNITY(self, name, iter): + + # Load data + with h5py.File(self.datadir % name + "regul_poly_line_coeffs_%04d.h5" %iter, "r") as f: + ob = f["ob"][:] + obh = f["obh"][:] + + # Copy data to device + ob_dev = gpuarray.to_gpu(ob) + obh_dev = gpuarray.to_gpu(obh) + + # CPU Kernel + regul = Regul_del2(0.1) + res = regul.poly_line_coeffs(obh, ob) + + # GPU Kernel + regul_pycuda = Regul_del2_pycuda(0.1, queue=self.stream, allocator=cuda.mem_alloc) + res_pycuda = regul_pycuda.poly_line_coeffs(obh_dev, ob_dev) + + ## Assert + np.testing.assert_allclose(res, res_pycuda, atol=self.atol, rtol=self.rtol, + err_msg="The B array has not been updated as expected") diff --git a/test/core_tests/classes_test.py b/test/core_tests/classes_test.py index 7aae2856f..250ee7e2e 100644 --- a/test/core_tests/classes_test.py +++ b/test/core_tests/classes_test.py @@ -1308,7 +1308,7 @@ def test_init(self): self.assertTrue( np.array_equal( self.basic_view_dpt.psize, - np.ones(2, dtype=np.float) + np.ones(2, dtype=float) ), 'Assigning of instance attribute psize failed.' ) @@ -1316,7 +1316,7 @@ def test_init(self): self.assertTrue( np.array_equal( self.basic_view_dpt.shape, - np.ones(2, dtype=np.int) + np.ones(2, dtype=int) ), 'Assigning of instance attribute psize failed.' ) diff --git a/test/core_tests/import_ptypy_test.py b/test/core_tests/import_ptypy_test.py index 7bd592c7d..78080f194 100644 --- a/test/core_tests/import_ptypy_test.py +++ b/test/core_tests/import_ptypy_test.py @@ -2,12 +2,18 @@ So much stuff in the init files now, we better test the import. """ - import unittest class ImportPtypyTest(unittest.TestCase): def test_import(self): import ptypy -if __name__ == '__main__': - unittest.main() \ No newline at end of file +class PtypyLoaderTest(unittest.TestCase): + + def test_load_ptyscan_module(self): + import ptypy + ptypy.load_ptyscan_module("hdf5_loader") + + def test_load_all_ptyscan_modules(self): + import ptypy + ptypy.load_all_ptyscan_modules() diff --git a/test/engine_tests/DMOPR_test.py b/test/engine_tests/DMOPR_test.py index 4b7bbcae2..66c2fe3ab 100644 --- a/test/engine_tests/DMOPR_test.py +++ b/test/engine_tests/DMOPR_test.py @@ -10,17 +10,26 @@ from test import utils as tu from ptypy import utils as u from ptypy.core import Ptycho +import tempfile +import shutil +from ptypy.custom import DMOPR class DMOPRTest(unittest.TestCase): + def setUp(self): + self.outpath = tempfile.mkdtemp(suffix="DMOPR_test") + + def tearDown(self): + shutil.rmtree(self.outpath) + def test_DMOPR(self): p = u.Param() p.verbose_level = 3 p.io = u.Param() p.io.interaction = u.Param() p.io.interaction.active = False - p.io.home = './' - p.io.rfile = "./DMOPRTest.ptyr" + p.io.home = self.outpath + p.io.rfile = "DMOPRTest.ptyr" p.io.autosave = u.Param(active=False) p.io.autoplot = u.Param(active=False) p.ipython_kernel = False @@ -42,7 +51,7 @@ def test_DMOPR(self): p.scans.MF.data.experimentID = None p.scans.MF.data.label = None p.scans.MF.data.version = 0.1 - p.scans.MF.data.dfile = "./DMOPRTest.ptyd" + p.scans.MF.data.dfile = "DMOPRTest.ptyd" p.scans.MF.data.psize = 0.000172 p.scans.MF.data.load_parallel = None p.scans.MF.data.distance = 7.0 diff --git a/test/engine_tests/DM_simple_test.py b/test/engine_tests/DM_simple_test.py deleted file mode 100644 index 70e5082a2..000000000 --- a/test/engine_tests/DM_simple_test.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Test for the DM_simple engine. - -This file is part of the PTYPY package. - :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. - :license: GPLv2, see LICENSE for details. -""" - -import unittest -from test import utils as tu -from ptypy import utils as u - - - -class DMSimpleTest(unittest.TestCase): - @unittest.skip('Skipping because of a NotImplementedError in engine_prepare') - def test_DM_simple(self): - engine_params = u.Param() - engine_params.name = 'DM_simple' - engine_params.alpha = 1.0 - tu.EngineTestRunner(engine_params) -if __name__ == "__main__": - unittest.main() diff --git a/test/engine_tests/DM_test.py b/test/engine_tests/DM_test.py index 2a436577c..db10f8154 100644 --- a/test/engine_tests/DM_test.py +++ b/test/engine_tests/DM_test.py @@ -9,9 +9,17 @@ import unittest from test import utils as tu from ptypy import utils as u +import tempfile +import shutil class DMTest(unittest.TestCase): + def setUp(self): + self.outpath = tempfile.mkdtemp(suffix="DMOPR_test") + + def tearDown(self): + shutil.rmtree(self.outpath) + def test_DM_position_refinement(self): engine_params = u.Param() engine_params.name = 'DM' @@ -25,7 +33,7 @@ def test_DM_position_refinement(self): engine_params.fourier_relax_factor = 0.01 engine_params.obj_smooth_std = 20 engine_params.position_refinement = True - tu.EngineTestRunner(engine_params) + tu.EngineTestRunner(engine_params, output_path=self.outpath) def test_DM(self): engine_params = u.Param() @@ -39,7 +47,7 @@ def test_DM(self): engine_params.object_inertia = 0.1 engine_params.fourier_relax_factor = 0.01 engine_params.obj_smooth_std = 20 - tu.EngineTestRunner(engine_params) + tu.EngineTestRunner(engine_params, output_path=self.outpath) if __name__ == "__main__": unittest.main() diff --git a/test/engine_tests/MLOPR_test.py b/test/engine_tests/MLOPR_test.py index 7b4b64e49..1c0ff79af 100644 --- a/test/engine_tests/MLOPR_test.py +++ b/test/engine_tests/MLOPR_test.py @@ -10,8 +10,17 @@ from test import utils as tu from ptypy import utils as u from ptypy.core import Ptycho +import tempfile +import shutil + +from ptypy.custom import MLOPR class MLOPRTest(unittest.TestCase): + def setUp(self): + self.outpath = tempfile.mkdtemp(suffix="MLOPR_test") + + def tearDown(self): + shutil.rmtree(self.outpath) def test_MLOPR(self): @@ -20,8 +29,8 @@ def test_MLOPR(self): p.io = u.Param() p.io.interaction = u.Param() p.io.interaction.active = False - p.io.home = './' - p.io.rfile = "./MLOPRTest.ptyr" + p.io.home = self.outpath + p.io.rfile = "MLOPRTest.ptyr" p.io.autosave = u.Param(active=False) p.io.autoplot = u.Param(active=False) p.ipython_kernel = False @@ -43,7 +52,7 @@ def test_MLOPR(self): p.scans.MF.data.experimentID = None p.scans.MF.data.label = None p.scans.MF.data.version = 0.1 - p.scans.MF.data.dfile = "./MLOPRTest.ptyd" + p.scans.MF.data.dfile = "MLOPRTest.ptyd" p.scans.MF.data.psize = 0.000172 p.scans.MF.data.load_parallel = None p.scans.MF.data.distance = 7.0 diff --git a/test/engine_tests/ML_test.py b/test/engine_tests/ML_test.py index b7ae3525e..d06571d7d 100644 --- a/test/engine_tests/ML_test.py +++ b/test/engine_tests/ML_test.py @@ -9,23 +9,16 @@ import unittest from test import utils as tu from ptypy import utils as u +import tempfile +import shutil class MLTest(unittest.TestCase): - def test_ML_farfield_position_refinement(self): - engine_params = u.Param() - engine_params.name = 'ML' - engine_params.numiter = 5 - engine_params.probe_update_start = 2 - engine_params.floating_intensities = False - engine_params.intensity_renormalization = 1.0 - engine_params.reg_del2 =True - engine_params.reg_del2_amplitude = 0.01 - engine_params.smooth_gradient = 0.0 - engine_params.scale_precond =False - engine_params.probe_update_start = 0 - engine_params.position_refinement = True - tu.EngineTestRunner(engine_params) + def setUp(self): + self.outpath = tempfile.mkdtemp(suffix="DMOPR_test") + + def tearDown(self): + shutil.rmtree(self.outpath) def test_ML_farfield_floating_intensities(self): engine_params = u.Param() @@ -39,7 +32,7 @@ def test_ML_farfield_floating_intensities(self): engine_params.smooth_gradient = 0.0 engine_params.scale_precond =False engine_params.probe_update_start = 0 - tu.EngineTestRunner(engine_params) + tu.EngineTestRunner(engine_params, output_path=self.outpath) def test_ML_farfield(self): engine_params = u.Param() @@ -53,7 +46,7 @@ def test_ML_farfield(self): engine_params.smooth_gradient = 0.0 engine_params.scale_precond =False engine_params.probe_update_start = 0 - tu.EngineTestRunner(engine_params) + tu.EngineTestRunner(engine_params, output_path=self.outpath) def test_ML_nearfield(self): @@ -69,7 +62,7 @@ def test_ML_nearfield(self): engine_params.scale_precond =False engine_params.probe_update_start = 0 - tu.EngineTestRunner(engine_params, propagator='nearfield') + tu.EngineTestRunner(engine_params, propagator='nearfield', output_path=self.outpath) if __name__ == "__main__": unittest.main() diff --git a/test/engine_tests/engine_utils_test.py b/test/engine_tests/engine_utils_test.py new file mode 100644 index 000000000..4e8f977ce --- /dev/null +++ b/test/engine_tests/engine_utils_test.py @@ -0,0 +1,89 @@ +""" +Test for the DM engine. + +This file is part of the PTYPY package. + :copyright: Copyright 2014 by the PTYPY team, see AUTHORS. + :license: GPLv2, see LICENSE for details. +""" + +import unittest +from test import utils as tu +import numpy as np +from ptypy import utils as u +from ptypy.core import Ptycho +from ptypy.engines import utils as eu + +np.random.seed(1234) + +def get_ptycho(model='Full', base_dir='./'): + p = u.Param() + p.verbose_level = 3 + p.io = u.Param() + p.io.interaction = u.Param() + p.io.interaction.active = False + p.io.home = base_dir + p.io.rfile = base_dir + model + "_test.ptyr" + p.io.autosave = u.Param(active=False) + p.io.autoplot = u.Param(active=False) + p.ipython_kernel = False + p.scans = u.Param() + p.scans.MF = u.Param() + p.scans.MF.name = model + p.scans.MF.propagation = 'farfield' + p.scans.MF.data = u.Param() + p.scans.MF.data.name = 'MoonFlowerScan' + p.scans.MF.data.positions_theory = None + p.scans.MF.data.auto_center = None + p.scans.MF.data.min_frames = 1 + p.scans.MF.data.orientation = None + p.scans.MF.data.num_frames = 100 + p.scans.MF.data.energy = 6.2 + p.scans.MF.data.shape = 64 + p.scans.MF.data.chunk_format = '.chunk%02d' + p.scans.MF.data.rebin = None + p.scans.MF.data.experimentID = None + p.scans.MF.data.label = None + p.scans.MF.data.version = 0.1 + p.scans.MF.data.dfile = base_dir + model + "_test.ptyd" + p.scans.MF.data.psize = 0.000172 + p.scans.MF.data.load_parallel = None + p.scans.MF.data.distance = 7.0 + p.scans.MF.data.save = None + p.scans.MF.data.center = 'fftshift' + p.scans.MF.data.photons = 100000000.0 + p.scans.MF.data.psf = 0.0 + p.scans.MF.data.density = 0.2 + p.scans.MF.coherence = u.Param() + p.scans.MF.coherence.num_probe_modes = 1 # currently breaks when this is =2 + + # init data is level 2 + P = Ptycho(p, level=2) + + return P + +class FourierUpdateTest(unittest.TestCase): + def test_legacy_general_UNITY(self): + P = get_ptycho(model='Full', base_dir='./') + pod = list(P.pods.values())[0] + diff = pod.di_view + e = {} + pods = list(diff.pods.values()) + for p in pods: + print(pod.exit) + e[p.ex_view.ID] = pod.exit.copy() + error_LEGACY = eu.basic_fourier_update_LEGACY(diff, None, alpha=1.0, LL_error=False) + res = {} + for p in pods: + print(pod.exit) + res[p.ex_view.ID] = pod.exit.copy() + pod.exit = e[p.ex_view.ID] + + error = eu.basic_fourier_update(diff, None, alpha=1.0, LL_error=False) + for p in pods: + print(pod.exit) + #np.testing.assert_array_equal(res[p.ex_view.ID], pod.exit, + # "Exit wave data diverges after fourier update") + np.testing.assert_array_equal(error_LEGACY, error, "Error metrics diverge") + +if __name__ == "__main__": + unittest.main() diff --git a/test/ptyscan_tests/diamond_nexus_test.py b/test/ptyscan_tests/diamond_nexus_test.py index ed4a9c919..24634bcb6 100644 --- a/test/ptyscan_tests/diamond_nexus_test.py +++ b/test/ptyscan_tests/diamond_nexus_test.py @@ -34,29 +34,29 @@ def setUp(self): self.intensity_file = os.path.join(self.outdir, 'intensity.h5') self.intensity_key = 'entry_1/data/data' - create_file_and_dataset(path=self.intensity_file, key=self.intensity_key, data_type=np.float) + create_file_and_dataset(path=self.intensity_file, key=self.intensity_key, data_type=float) self.positions_file = os.path.join(self.outdir, 'positions.h5') self.positions_slow_key = 'entry_1/data/y' self.positions_fast_key = 'entry_1/data/x' create_file_and_dataset(path=self.positions_file, key=[self.positions_slow_key, self.positions_fast_key], - data_type=np.float) + data_type=float) self.mask_file = os.path.join(self.outdir, 'mask.h5') self.mask_key = 'entry/mask' - create_file_and_dataset(path=self.mask_file, key=self.mask_key, data_type=np.int) + create_file_and_dataset(path=self.mask_file, key=self.mask_key, data_type=int) self.dark_file = os.path.join(self.outdir, 'dark.h5') self.dark_key = 'entry_1/instrument_1/detector_1/darkfield' - create_file_and_dataset(path=self.dark_file, key=self.dark_key, data_type=np.float) + create_file_and_dataset(path=self.dark_file, key=self.dark_key, data_type=float) self.flat_file = os.path.join(self.outdir, 'flat.h5') self.flat_key = 'entry_1/instrument_1/detector_1/flatfield' - create_file_and_dataset(path=self.flat_file, key=self.flat_key, data_type=np.float) + create_file_and_dataset(path=self.flat_file, key=self.flat_key, data_type=float) self.normalisation_file = os.path.join(self.outdir, 'normalisation.h5') self.normalisation_key = 'entry_1/instrument_1/monitor/data' - create_file_and_dataset(path=self.normalisation_file, key=self.normalisation_key, data_type=np.float) + create_file_and_dataset(path=self.normalisation_file, key=self.normalisation_key, data_type=float) self.top_file = os.path.join(self.outdir, 'top_file.h5') self.recorded_energy_key = 'entry_1/instrument_1/beam_1/energy' @@ -251,7 +251,7 @@ def test_crop_load_works_case1(self): data = np.arange(k*frame_size_m*frame_size_n).reshape((k, frame_size_m, frame_size_n)) h5.File(self.intensity_file, 'w')[self.intensity_key] = data - mask = np.ones(data.shape[-2:], dtype=np.float) + mask = np.ones(data.shape[-2:], dtype=float) mask[::2] = 0 mask[:, ::2] = 0 h5.File(self.mask_file, 'w')[self.mask_key] = mask @@ -761,7 +761,7 @@ def test_mask_loaded(self): data = np.arange(k*frame_size_m*frame_size_n).reshape((k, frame_size_m, frame_size_n)) h5.File(self.intensity_file, 'w')[self.intensity_key] = data - mask = np.ones(data.shape[-2:], dtype=np.float) + mask = np.ones(data.shape[-2:], dtype=float) mask[::2] = 0 mask[:, ::2] = 0 h5.File(self.mask_file, 'w')[self.mask_key] = mask diff --git a/test/ptyscan_tests/hdf5_loader_test.py b/test/ptyscan_tests/hdf5_loader_test.py index b6188e071..9627fd316 100644 --- a/test/ptyscan_tests/hdf5_loader_test.py +++ b/test/ptyscan_tests/hdf5_loader_test.py @@ -34,33 +34,33 @@ def setUp(self): self.intensity_file = os.path.join(self.outdir, 'intensity.h5') self.intensity_key = 'entry/intensity' - create_file_and_dataset(path=self.intensity_file, key=self.intensity_key, data_type=np.float) + create_file_and_dataset(path=self.intensity_file, key=self.intensity_key, data_type=float) self.positions_file = os.path.join(self.outdir, 'positions.h5') self.positions_slow_key = 'entry/positions_slow' self.positions_fast_key = 'entry/positions_fast' create_file_and_dataset(path=self.positions_file, key=[self.positions_slow_key, self.positions_fast_key], - data_type=np.float) + data_type=float) self.mask_file = os.path.join(self.outdir, 'mask.h5') self.mask_key = 'entry/mask' - create_file_and_dataset(path=self.mask_file, key=self.mask_key, data_type=np.int) + create_file_and_dataset(path=self.mask_file, key=self.mask_key, data_type=int) self.dark_file = os.path.join(self.outdir, 'dark.h5') self.dark_key = 'entry/dark' - create_file_and_dataset(path=self.dark_file, key=self.dark_key, data_type=np.float) + create_file_and_dataset(path=self.dark_file, key=self.dark_key, data_type=float) self.flat_file = os.path.join(self.outdir, 'flat.h5') self.flat_key = 'entry/flat' - create_file_and_dataset(path=self.flat_file, key=self.flat_key, data_type=np.float) + create_file_and_dataset(path=self.flat_file, key=self.flat_key, data_type=float) self.normalisation_file = os.path.join(self.outdir, 'normalisation.h5') self.normalisation_key = 'entry/normalisation' - create_file_and_dataset(path=self.normalisation_file, key=self.normalisation_key, data_type=np.float) + create_file_and_dataset(path=self.normalisation_file, key=self.normalisation_key, data_type=float) self.framefilter_file = os.path.join(self.outdir, 'framefilter.h5') self.framefilter_key = 'entry/framefilter' - create_file_and_dataset(path=self.framefilter_file, key=self.framefilter_key, data_type=np.bool) + create_file_and_dataset(path=self.framefilter_file, key=self.framefilter_key, data_type=bool) self.top_file = os.path.join(self.outdir, 'top_file.h5') self.recorded_energy_key = 'entry/energy' @@ -370,7 +370,7 @@ def test_crop_load_works_case1(self): data = np.arange(k*frame_size_m*frame_size_n).reshape((k, frame_size_m, frame_size_n)) h5.File(self.intensity_file, 'w')[self.intensity_key] = data - mask = np.ones(data.shape[-2:], dtype=np.float) + mask = np.ones(data.shape[-2:], dtype=float) mask[::2] = 0 mask[:, ::2] = 0 h5.File(self.mask_file, 'w')[self.mask_key] = mask @@ -1267,7 +1267,7 @@ def test_mask_loaded(self): data = np.arange(k*frame_size_m*frame_size_n).reshape((k, frame_size_m, frame_size_n)) h5.File(self.intensity_file, 'w')[self.intensity_key] = data - mask = np.ones(data.shape[-2:], dtype=np.float) + mask = np.ones(data.shape[-2:], dtype=float) mask[::2] = 0 mask[:, ::2] = 0 h5.File(self.mask_file, 'w')[self.mask_key] = mask diff --git a/test/util_tests/derivatives_test.py b/test/util_tests/derivatives_test.py new file mode 100644 index 000000000..c2237d561 --- /dev/null +++ b/test/util_tests/derivatives_test.py @@ -0,0 +1,89 @@ +import unittest +import numpy as np +from ptypy.utils.math_utils import delxf, delxb + +class DerivativesTest(unittest.TestCase): + + def test_delxf_1dim(self): + inp = np.array([0, 1, 2, 4, 8, 0, 6], dtype=np.float32) + + outp = delxf(inp) + + exp = np.array([1, 1, 2, 4, -8, 6, 0], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_1dim_inplace(self): + inp = np.array([0, 1, 2, 4, 8, 0, 6], dtype=np.float32) + outp = np.zeros_like(inp) + + delxf(inp, out=outp) + + exp = np.array([1, 1, 2, 4, -8, 6, 0], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_2dim1(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + + outp = delxf(inp, axis=0) + + exp = np.array([ + [1, -6, -1], + [0, 0, 0] + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxf_2dim2(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + + outp = delxf(inp, axis=1) + + exp = np.array([ + [2, 4, 0], + [-5, 9, 0] + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + + def test_delxb_1dim(self): + inp = np.array([0, 1, 2, 4, 8, 0, 6], dtype=np.float32) + + outp = delxb(inp) + + exp = np.array([0, 1, 1, 2, 4, -8, 6], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxb_2dim1(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + + outp = delxb(inp, axis=0) + + exp = np.array([ + [0, 0, 0], + [1, -6, -1], + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + def test_delxb_2dim2(self): + inp = np.array([ + [0, 2, 6], + [1, -4, 5] + ], dtype=np.float32) + + outp = delxb(inp, axis=1) + + exp = np.array([ + [0, 2, 4], + [0, -5, 9] + ], dtype=np.float32) + np.testing.assert_array_equal(outp, exp) + + \ No newline at end of file diff --git a/test/utils.py b/test/utils.py index fa7e847c8..14ae745b3 100644 --- a/test/utils.py +++ b/test/utils.py @@ -13,6 +13,7 @@ import shutil import os import tempfile +import numpy as np from ptypy import utils as u from ptypy.core import Ptycho @@ -42,51 +43,125 @@ def PtyscanTestRunner(ptyscan_instance, data_params, save_type='append', auto_fr return out_dict -def EngineTestRunner(engine_params,propagator='farfield',output_path='./', output_file=None): - +def EngineTestRunner(engine_params,propagator='farfield',output_path='./', output_file=None, + autosave=True, scanmodel="Full", verbose_level="info", init_correct_probe=False): p = u.Param() - p.verbose_level = 3 + p.verbose_level = verbose_level p.io = u.Param() - p.io.interaction = u.Param() - p.io.interaction.active = False p.io.home = output_path p.io.rfile = "%s.ptyr" % output_file - p.io.autosave = u.Param(active=True) + p.io.interaction = u.Param() + p.io.interaction.active = False + p.io.autosave = u.Param(active=autosave) p.io.autoplot = u.Param(active=False) - p.ipython_kernel = False p.scans = u.Param() p.scans.MF = u.Param() - p.scans.MF.name = 'Full' + p.scans.MF.name = scanmodel p.scans.MF.propagation = propagator p.scans.MF.data = u.Param() p.scans.MF.data.name = 'MoonFlowerScan' - p.scans.MF.data.positions_theory = None - p.scans.MF.data.auto_center = None - p.scans.MF.data.min_frames = 1 - p.scans.MF.data.orientation = None - p.scans.MF.data.num_frames = 100 - p.scans.MF.data.energy = 6.2 + p.scans.MF.data.num_frames = 200 p.scans.MF.data.shape = 64 - p.scans.MF.data.chunk_format = '.chunk%02d' - p.scans.MF.data.rebin = None - p.scans.MF.data.experimentID = None - p.scans.MF.data.label = None - p.scans.MF.data.version = 0.1 - p.scans.MF.data.dfile = "%s.ptyd" % output_file - p.scans.MF.data.psize = 0.000172 - p.scans.MF.data.load_parallel = None - p.scans.MF.data.distance = 7.0 p.scans.MF.data.save = None - p.scans.MF.data.center = 'fftshift' - p.scans.MF.data.photons = 100000000.0 + p.scans.MF.data.photons = 1e8 p.scans.MF.data.psf = 0.0 p.scans.MF.data.density = 0.2 p.scans.MF.data.add_poisson_noise = False p.scans.MF.coherence = u.Param() - p.scans.MF.coherence.num_probe_modes = 1 # currently breaks when this is =2 + p.scans.MF.coherence.num_probe_modes = 1 p.engines = u.Param() p.engines.engine00 = engine_params - P = Ptycho(p, level=5) + P = Ptycho(p, level=4) + if init_correct_probe: + P.probe.S['SMFG00'].data[0] = P.model.scans['MF'].ptyscan.pr + P.run() return P + +def EngineTestRunner2(engine_params,propagator='farfield',output_path='./', output_file=None, + autosave=True, scanmodel="Full", verbose_level="info", init_correct_probe=False): + + p = u.Param() + p.verbose_level = verbose_level + p.io = u.Param() + p.io.home = output_path + p.io.rfile = "%s.ptyr" % output_file + p.io.interaction = u.Param() + p.io.interaction.active = False + p.io.autosave = u.Param(active=autosave) + p.io.autoplot = u.Param(active=False) + + # Simulation parameters + sim = u.Param() + sim.energy = 17.0 + sim.distance = 2.886 + sim.psize = 51e-6 + sim.shape = 128 + sim.xy = u.Param() + sim.xy.model = "round" + sim.xy.spacing = 250e-9 + sim.xy.steps = 30 + sim.xy.extent = 4e-6 + + sim.illumination = u.Param() + sim.illumination.model = None + sim.illumination.photons = 3e8 + sim.illumination.aperture = u.Param() + sim.illumination.aperture.diffuser = None + sim.illumination.aperture.form = "rect" + sim.illumination.aperture.size = 35e-6 + sim.illumination.aperture.central_stop = None + sim.illumination.propagation = u.Param() + sim.illumination.propagation.focussed = 0.08 + sim.illumination.propagation.parallel = 0.0014 + sim.illumination.propagation.spot_size = None + + sim.sample = u.Param() + sim.sample.model = u.xradia_star((1000,1000),minfeature=3,contrast=0.0) + sim.sample.process = u.Param() + sim.sample.process.offset = (100,100) + sim.sample.process.zoom = 1.0 + sim.sample.process.formula = "Au" + sim.sample.process.density = 19.3 + sim.sample.process.thickness = 2000e-9 + sim.sample.process.ref_index = None + sim.sample.process.smoothing = None + sim.sample.fill = 1.0+0.j + + sim.detector = 'GenericCCD32bit' + sim.verbose_level = 1 + sim.psf = 1. # emulates partial coherence + sim.plot = False + + # Scan model and initial value parameters + p.scans = u.Param() + p.scans.scan00 = u.Param() + p.scans.scan00.name = scanmodel + p.scans.scan00.coherence = u.Param() + p.scans.scan00.coherence.num_probe_modes = 1 + p.scans.scan00.coherence.num_object_modes = 1 + p.scans.scan00.sample = u.Param() + p.scans.scan00.sample.model = 'stxm' + p.scans.scan00.sample.process = None + p.scans.scan00.propagation = propagator + + # (copy the simulation illumination and change specific things) + p.scans.scan00.illumination = sim.illumination.copy(99) + if not init_correct_probe: + p.scans.scan00.illumination.aperture.form = 'circ' + p.scans.scan00.illumination.propagation.focussed = 0.06 + p.scans.scan00.illumination.diversity = u.Param() + p.scans.scan00.illumination.diversity.power = 0.1 + p.scans.scan00.illumination.diversity.noise = (np.pi,3.0) + + # Scan data (simulation) parameters + p.scans.scan00.data = u.Param() + p.scans.scan00.data.name = 'SimScan' + p.scans.scan00.data.update(sim) + p.scans.scan00.data.save = None + p.engines = u.Param() + p.engines.engine00 = engine_params + P = Ptycho(p, level=4) + P.run() + return P diff --git a/tutorial/minimal_script.py b/tutorial/minimal_script.py index 34492b115..32320083b 100644 --- a/tutorial/minimal_script.py +++ b/tutorial/minimal_script.py @@ -1,5 +1,5 @@ # This tutorial explains the minimal settings to get a reconstruction -# runnig in |ptypy|. A |ptypy| script consists of two parts: +# runnig in |ptypy|_. A |ptypy| script consists of two parts: # * Creation of a parameter tree with parameters # as listed in :ref:`parameters` and @@ -25,7 +25,7 @@ # We set the verbosity to a high level, in order to have information on the # reconstruction process printed to the terminal. # See :py:data:`~ptycho.verbose_level`. -p.verbose_level = 3 +p.verbose_level = "info" # We limit this reconstruction to single precision. The other choice is to # use double precision. diff --git a/tutorial/simupod.py b/tutorial/simupod.py index c1bab0ccc..a1026bc68 100644 --- a/tutorial/simupod.py +++ b/tutorial/simupod.py @@ -173,6 +173,7 @@ class Base2(Base): storage.fill(flower_obj(storage.shape[-2:])) fig = u.plot_storage(storage, 5) fig.savefig('%s_%d.png' % (scriptname, fig.number), dpi=300) +# Complex transmission function of the object. # Creating additional Views and the PODs # -------------------------------------- @@ -240,7 +241,7 @@ class Base2(Base): exit_storage = list(P.exit.storages.values())[0] fig = u.plot_storage(exit_storage, 6) fig.savefig('%s_%d.png' % (scriptname, fig.number), dpi=300) -# Simulated exit wave using a pod +# Simulated exit wave using a pod. # The diffraction plane is also conveniently accessible with pod.diff = np.abs(pod.fw(pod.exit))**2 @@ -249,7 +250,7 @@ class Base2(Base): diff_storage = list(P.diff.storages.values())[0] fig = u.plot_storage(diff_storage, 7, modulus='log') fig.savefig('%s_%d.png' % (scriptname, fig.number), dpi=300) - +# First simulated diffraction image (without noise) # Creating the rest of the pods is now straight-forward # since the data accesses are similar.